@akredit/acik-mcp 0.1.0 → 0.5.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.
Files changed (33) hide show
  1. package/README.md +94 -8
  2. package/dist/apps/acik-mcp/src/araclar.d.ts +2 -28
  3. package/dist/apps/acik-mcp/src/araclar.js +29 -114
  4. package/dist/apps/acik-mcp/src/bologna.d.ts +291 -0
  5. package/dist/apps/acik-mcp/src/bologna.js +1693 -0
  6. package/dist/apps/acik-mcp/src/genel-ayristir.d.ts +92 -0
  7. package/dist/apps/acik-mcp/src/genel-ayristir.js +489 -0
  8. package/dist/apps/acik-mcp/src/http.d.ts +31 -3
  9. package/dist/apps/acik-mcp/src/http.js +90 -17
  10. package/dist/apps/acik-mcp/src/index.d.ts +7 -0
  11. package/dist/apps/acik-mcp/src/index.js +7 -0
  12. package/dist/apps/acik-mcp/src/kaynak-dogrula.d.ts +94 -0
  13. package/dist/apps/acik-mcp/src/kaynak-dogrula.js +460 -0
  14. package/dist/apps/acik-mcp/src/kaynak.d.ts +72 -0
  15. package/dist/apps/acik-mcp/src/kaynak.js +155 -0
  16. package/dist/apps/acik-mcp/src/kisisel-veri.d.ts +16 -2
  17. package/dist/apps/acik-mcp/src/kisisel-veri.js +78 -15
  18. package/dist/apps/acik-mcp/src/not-tablosu.d.ts +39 -0
  19. package/dist/apps/acik-mcp/src/not-tablosu.js +268 -0
  20. package/dist/apps/acik-mcp/src/promptlar.d.ts +48 -0
  21. package/dist/apps/acik-mcp/src/promptlar.js +323 -0
  22. package/dist/apps/acik-mcp/src/sunucu.d.ts +12 -4
  23. package/dist/apps/acik-mcp/src/sunucu.js +108 -24
  24. package/dist/apps/acik-mcp/src/temel.d.ts +59 -0
  25. package/dist/apps/acik-mcp/src/temel.js +10 -0
  26. package/dist/apps/acik-mcp/src/yardim.d.ts +56 -0
  27. package/dist/apps/acik-mcp/src/yardim.js +381 -0
  28. package/dist/packages/baglayicilar/src/bologna-proliz/ayristir.d.ts +29 -0
  29. package/dist/packages/baglayicilar/src/bologna-proliz/ayristir.js +200 -0
  30. package/dist/packages/cekirdek/src/bologna/taslak.d.ts +19 -3
  31. package/dist/packages/cekirdek/src/bologna/taslak.js +70 -9
  32. package/dist/packages/cekirdek/src/turler.d.ts +8 -0
  33. package/package.json +5 -3
@@ -0,0 +1,1693 @@
1
+ // Bologna ders bilgi paketi araçları: denetim, alan rehberi, taslak denetimi, sayfa ayrıştırma, kopya metni.
2
+ //
3
+ // Tasarım: sunucu model çağırmaz, bu yüzden doldurma seçeneklerini (amaç metni, çıktı cümleleri, haftalık
4
+ // konular) üretemez. Seçenekleri hocanın asistanı üretir; sunucu kural, sınır ve kalıp verir, üretileni
5
+ // Akredit kurallarıyla denetler ve bütün sayıları (iş yükü, toplamlar) kendisi hesaplar (ilke 2).
6
+ // Kurallar @akredit/cekirdek taslak ve sayfa denetiminden gelir; resmî ölçüt değildir, öyle etiketlenir.
7
+ // Sunucu adres çekmez (SSRF yok): Bologna sayfası yapıştırılan HTML olarak gelir, ağsız ayrıştırılır.
8
+ // Yetkili alanı kişi adı içerir; bu araçlar onu taşımaz (ilke 7), hoca OBS'ye kendisi yazar. Kaynaklar ise
9
+ // yayımlanmış künyedir: yazar adı künye kuralıyla serbesttir, e-posta ve telefon yine reddedilir.
10
+ import { dersSayfasiCoz, programCiktilari, tablolar } from "../../../packages/baglayicilar/src/bologna-proliz/ayristir.js";
11
+ import { bolognaDenetle, taslakDenetle, } from "../../../packages/cekirdek/src/index.js";
12
+ import { DOGRULANMADI as DOGRULANMADI_ISARETI } from "../../../packages/cerceveler/src/index.js";
13
+ import { z } from "zod";
14
+ import { GENEL_ALAN_ADLARI, genelAyikla, genelAyristir, ingilizceAlanVar, prolizMetniMi } from "./genel-ayristir.js";
15
+ import { kaynakImzasiDogru } from "./kaynak-dogrula.js";
16
+ import { girisBilgisiAyikla } from "./kisisel-veri.js";
17
+ import { AracHatasi, arac, KOD_KAYNAGI } from "./temel.js";
18
+ // ---------------------------------------------------------------- Alan modeli
19
+ export const ALAN_KODLARI = [
20
+ "amac",
21
+ "icerik",
22
+ "yontem",
23
+ "notlar",
24
+ "onKosul",
25
+ "ciktilar",
26
+ "degerlendirme",
27
+ "isyuku",
28
+ "dersYapisi",
29
+ "haftalar",
30
+ "katki",
31
+ "ska",
32
+ "kaynaklar",
33
+ "yetkililer",
34
+ ];
35
+ const DERS_BILGILERI = "Ders Bilgileri";
36
+ const ORTAK_YASAK = 'Alanı "-", ".", "Yok" ile geçiştirmeyin (ön koşul hariç). Türkçe karakterleri düşürmeyin.';
37
+ /** Akredit'in Bologna doldurma kuralları (packages/cekirdek/src/bologna/taslak.ts, apps/cli/skills/bologna-doldur). */
38
+ export const ALANLAR = {
39
+ amac: {
40
+ ad: "Dersin amacı",
41
+ sekme: DERS_BILGILERI,
42
+ kurallar: [
43
+ "K1: Ders adıyla tutarlı olsun; başka dersten kopya olmasın.",
44
+ "K2: 60-120 kelime. İngilizcesi tam çeviri olsun.",
45
+ ],
46
+ sinir: { enAzKelime: 60, enCokKelime: 120 },
47
+ kaliplar: [
48
+ "Bu dersin amacı, öğrencilere [alanın temel konuları] konusunda [kazanılacak yetkinlik] kazandırmaktır.",
49
+ "Öğrenciler [yöntem] yoluyla [beceri] geliştirir. Dönem sonunda [somut iş] yapabilmeleri hedeflenir.",
50
+ ],
51
+ yasaklar: [ORTAK_YASAK],
52
+ mcpTasir: true,
53
+ },
54
+ icerik: {
55
+ ad: "Dersin içeriği",
56
+ sekme: DERS_BILGILERI,
57
+ kurallar: ["K2: 120-250 kelime. Haftalık akışla aynı konuları kapsasın.", "İngilizcesi tam çeviri olsun."],
58
+ sinir: { enAzKelime: 120, enCokKelime: 250 },
59
+ kaliplar: [
60
+ "Ders, [giriş konusu] ile başlar. Ardından [ana konular] işlenir. [Uygulama ya da proje] ile tamamlanır.",
61
+ ],
62
+ yasaklar: [ORTAK_YASAK],
63
+ mcpTasir: true,
64
+ },
65
+ yontem: {
66
+ ad: "Yöntem ve teknikler",
67
+ sekme: DERS_BILGILERI,
68
+ kurallar: ["K2: 40-90 kelime.", "K11: Başka dersin yöntem metniyle birebir aynı olmasın."],
69
+ sinir: { enAzKelime: 40, enCokKelime: 90 },
70
+ kaliplar: ["Ders [anlatım, tartışma, uygulama, proje] ile yürütülür. [Haftalık etkinlik] konuyu pekiştirir."],
71
+ yasaklar: [ORTAK_YASAK],
72
+ mcpTasir: true,
73
+ },
74
+ notlar: {
75
+ ad: "Ders notları",
76
+ sekme: DERS_BILGILERI,
77
+ kurallar: ["K3: Boş bırakılmaz.", "Hocanın gerçekten kullandığı notları yazın."],
78
+ kaliplar: ["Haftalık ders sunumları; [uygulama föyleri]; [çevrim içi kaynak adı]."],
79
+ yasaklar: [ORTAK_YASAK, "Var olmayan not ya da kaynak uydurmayın."],
80
+ mcpTasir: true,
81
+ },
82
+ onKosul: {
83
+ ad: "Ön koşul",
84
+ sekme: DERS_BILGILERI,
85
+ kurallar: ['Ön koşul yoksa "Yok" yazılabilir.'],
86
+ kaliplar: ["Yok", "[Ders kodu] [Ders adı] dersini almış olmak."],
87
+ yasaklar: [],
88
+ mcpTasir: true,
89
+ },
90
+ ciktilar: {
91
+ ad: "Öğrenme çıktıları",
92
+ sekme: "Öğrenme Çıktıları",
93
+ kurallar: [
94
+ "K4: Her çıktı ölçülebilir bir eylem fiiliyle biter (açıklar, uygular, tasarlar). 'Bilgi sahibi olur', 'öğrenir', 'kavrar' olmaz.",
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.",
97
+ "İngilizcesi tam çeviri olsun.",
98
+ ],
99
+ sinir: { enAzCikti: 4, enCokCikti: 10, onerilenEnAz: 5, onerilenEnCok: 8 },
100
+ kaliplar: [
101
+ "[Konu] kavramlarını örneklerle açıklar. (Bilgi)",
102
+ "[Yöntem] kullanarak [problem türünü] çözer. (Beceri)",
103
+ "[Ürün] tasarlar ve ekip içinde sunar. (Yetkinlik)",
104
+ ],
105
+ yasaklar: ["Ölçülemeyen fiil (bilir, anlar, öğrenir, farkındalık kazanır)."],
106
+ mcpTasir: true,
107
+ },
108
+ degerlendirme: {
109
+ ad: "Değerlendirme",
110
+ sekme: DERS_BILGILERI,
111
+ kurallar: [
112
+ "Katkılar toplamı tam %100.",
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.",
115
+ ],
116
+ sinir: { toplamYuzde: 100 },
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"],
118
+ yasaklar: [],
119
+ mcpTasir: true,
120
+ },
121
+ isyuku: {
122
+ ad: "AKTS iş yükü",
123
+ sekme: DERS_BILGILERI,
124
+ kurallar: [
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.",
126
+ "AKTS'yi değiştirmeyin, saat dağılımını değiştirin.",
127
+ "Ders Süresi satırının haftalık süresi T+U+L toplamına eşit olur.",
128
+ "Yalnız derste gerçekten olan etkinliklere saat verilir (ödev yoksa ödev satırı yok).",
129
+ ],
130
+ sinir: { aralik: "25-30 saat/AKTS", kurumDegeri: "verilirse tam eşitlik" },
131
+ kaliplar: [
132
+ "Ders Süresi 14 x (T+U+L)",
133
+ "Sınıf Dışı Ders Çalışma Süresi 14 x [saat]",
134
+ "Ara Sınav Hazırlık 1 x [saat]",
135
+ "Yarıyıl Sonu Sınavı Hazırlık 1 x [saat]",
136
+ ],
137
+ yasaklar: ["Toplamı kendiniz yazmayın; bologna_taslak_denetle hesaplar."],
138
+ mcpTasir: true,
139
+ },
140
+ dersYapisi: {
141
+ ad: "Ders yapısı (alan yüzdeleri)",
142
+ sekme: DERS_BILGILERI,
143
+ kurallar: ["K6: Yüzdeler toplamı tam %100 ve içerikle tutarlı."],
144
+ sinir: { toplamYuzde: 100 },
145
+ kaliplar: ["Alan Bilgisi %60, Temel Bilim %40"],
146
+ yasaklar: [],
147
+ mcpTasir: true,
148
+ },
149
+ haftalar: {
150
+ ad: "Haftalık ders akışı",
151
+ sekme: "Ders Akışı",
152
+ kurallar: [
153
+ "14-16 hafta. Her konu en az 8 kelime.",
154
+ "K9: Her haftada ön hazırlık ve doküman dolu.",
155
+ "Ara sınav haftası (çoğunlukla 8. hafta) ve son haftada yarıyıl sonu sınavı görünür.",
156
+ "İngilizce konu dolu.",
157
+ ],
158
+ sinir: { enAzHafta: 14, enCokHafta: 16, enAzKelime: 8 },
159
+ kaliplar: ["[Konu]: [alt başlık 1], [alt başlık 2] ve örnek uygulamalar"],
160
+ yasaklar: [ORTAK_YASAK],
161
+ mcpTasir: true,
162
+ },
163
+ katki: {
164
+ ad: "Program çıktısına katkı",
165
+ sekme: "Prg.Çıktısına Katkısı",
166
+ kurallar: [
167
+ "K12: Her Ö satırı dolu; değerler 1-5 ya da boş.",
168
+ "Yalnız programın resmî P kodları kullanılır; P kodlarını Bologna sayfasından alın, ezberden yazmayın.",
169
+ "Dolu hücrelerin çoğu 5 olmasın; ayrıştırın.",
170
+ "'Tüm' satırına dokunmayın.",
171
+ ],
172
+ sinir: { enAz: 1, enCok: 5 },
173
+ kaliplar: ["Ö1: P1=4, P3=2"],
174
+ yasaklar: ["Matrisi şişirmeyin."],
175
+ mcpTasir: true,
176
+ },
177
+ ska: {
178
+ ad: "Sürdürülebilir Kalkınma Amaçları",
179
+ sekme: "Sür. Kalkınma Amaçları",
180
+ kurallar: ["K7: 1-17 arasından gerçekten ilgili 2-4 amaç."],
181
+ sinir: { enAz: 2, enCok: 4 },
182
+ kaliplar: ["4 (Nitelikli eğitim) ve dersle doğrudan ilgili 1-3 amaç"],
183
+ yasaklar: [],
184
+ mcpTasir: true,
185
+ },
186
+ kaynaklar: {
187
+ ad: "Kaynaklar",
188
+ sekme: "Diğer Kaynaklar",
189
+ kurallar: [
190
+ "En az 2 gerçek kaynak (yazar, yıl ve eser adı).",
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.",
193
+ "Yazar adları yayımlanmış künyedir; bu alanda serbesttir. E-posta ve telefon yazılmaz.",
194
+ ],
195
+ sinir: { enAz: 2 },
196
+ kaliplar: [
197
+ "Soyad, A. (Yıl). Eser adı (Baskı). Yayınevi.",
198
+ "Soyad, A. (Yıl). Makale adı. Dergi, Cilt(Sayı), sayfa.",
199
+ ],
200
+ yasaklar: [
201
+ "Kaynak uydurulmaz: asistan web aramasıyla aday bulur, her adayı kaynak_dogrula'dan geçirir; yalnız doğrulanan eser bağlantısıyla önerilir.",
202
+ "Emin olunmayan ISBN, yayınevi ya da yıl yazılmaz; bilinmeyen bilgi EKSİK kalır.",
203
+ ],
204
+ mcpTasir: true,
205
+ },
206
+ yetkililer: {
207
+ ad: "Dersin yetkilileri",
208
+ sekme: "Dersin Yetkilileri",
209
+ kurallar: [
210
+ "K10: Koordinatör ve dersi veren hocanın kendisidir; başka hocanın adı yazılmaz.",
211
+ "Sayfada yayımlanmış görev bilgisi (Dersi Verenler, Yardımcılar, Koordinatör) olduğu gibi okunur ve korunur.",
212
+ ],
213
+ kaliplar: [],
214
+ yasaklar: ["Bu satırlar değiştirilmez, kısaltılmaz, yeniden yazılmaz; OBS'ye geri yazılırken aynen kalır."],
215
+ mcpTasir: true,
216
+ },
217
+ };
218
+ /** Bloom'un yenilenmiş sınıflaması düzeylerine göre örnek fiiller. Akredit örneğidir, resmî liste değildir. */
219
+ export const BLOOM_FIILLERI = [
220
+ { duzey: "Hatırlama", fiiller: ["tanımlar", "listeler", "sayar", "adlandırır"] },
221
+ { duzey: "Anlama", fiiller: ["açıklar", "özetler", "sınıflandırır", "örneklendirir"] },
222
+ { duzey: "Uygulama", fiiller: ["uygular", "hesaplar", "kullanır", "çözer"] },
223
+ { duzey: "Çözümleme", fiiller: ["çözümler", "karşılaştırır", "ayırt eder", "ilişkilendirir"] },
224
+ { duzey: "Değerlendirme", fiiller: ["değerlendirir", "eleştirir", "gerekçelendirir", "savunur"] },
225
+ { duzey: "Yaratma", fiiller: ["tasarlar", "geliştirir", "oluşturur", "planlar"] },
226
+ ];
227
+ const RESMI_DAYANAK = {
228
+ durum: DOGRULANMADI_ISARETI,
229
+ not: "Bu kurallar Akredit'in iç kurallarıdır, resmî ölçüt değildir. Kurumunuzun Bologna ya da AKTS yönergesi farklı sınır koyabilir; o yönerge önceliklidir.",
230
+ };
231
+ const SECENEK_KURALI = "Sunucu metin üretmez. Seçenekleri asistan üretir: 2-3 kısa, birbirinden gerçekten farklı seçenek (A, B, C), ilki önerilen. Her seçeneği bologna_taslak_denetle ile denetleyin, yalnız geçenleri gösterin. Hocanın vermediği bilgi (kitap, araç, proje adı) uydurulmaz.";
232
+ const KAYNAK_KURALLAR = KOD_KAYNAGI("packages/cekirdek/src/bologna/taslak.ts", "Akredit Bologna taslak kuralları");
233
+ const KAYNAK_SAYFA = KOD_KAYNAGI("packages/cekirdek/src/bologna/denetim.ts", "Akredit Bologna sayfa denetim kuralları");
234
+ const KAYNAK_AYRISTIRICI = KOD_KAYNAGI("packages/baglayicilar/src/bologna-proliz/ayristir.ts", "Akredit Bologna (Proliz) sayfa ayrıştırıcısı");
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"];
242
+ // ---------------------------------------------------------------- Ortak hesap ve eşleme
243
+ const TUL = z
244
+ .string()
245
+ .trim()
246
+ .regex(/^\d{1,2}\+\d{1,2}\+\d{1,2}$/u, "T+U+L '2+1+0' biçiminde olmalı.");
247
+ const tulSaati = (tul) => {
248
+ if (!tul || !/^\d+\+\d+\+\d+$/u.test(tul.trim()))
249
+ return null;
250
+ return tul
251
+ .split("+")
252
+ .map(Number)
253
+ .reduce((t, x) => t + x, 0);
254
+ };
255
+ const yuvarla = (x, b = 1) => Math.round(x * 10 ** b) / 10 ** b;
256
+ /** Bir taslak bulgusunun hangi alana ait olduğu. "genel" birden çok alana değen bulgudur. */
257
+ export function bulguAlani(b) {
258
+ switch (b.kural) {
259
+ case "Temel":
260
+ return "temel";
261
+ case "Değerlendirme":
262
+ return "degerlendirme";
263
+ case "K8":
264
+ return "isyuku";
265
+ case "K6":
266
+ return "dersYapisi";
267
+ case "K4":
268
+ case "K5":
269
+ return "ciktilar";
270
+ case "Ders akışı":
271
+ case "K9":
272
+ return "haftalar";
273
+ case "Kaynaklar":
274
+ return "kaynaklar";
275
+ case "K12":
276
+ return "katki";
277
+ case "K7":
278
+ return "ska";
279
+ case "K10":
280
+ return "yetkililer";
281
+ case "K2":
282
+ case "K3": {
283
+ const a = b.aciklama;
284
+ if (/^Amaç/u.test(a))
285
+ return "amac";
286
+ if (/^İçerik/u.test(a))
287
+ return "icerik";
288
+ if (/^Yöntem/u.test(a))
289
+ return "yontem";
290
+ if (/^Ders notları/u.test(a))
291
+ return "notlar";
292
+ if (/^Ö\d+/u.test(a))
293
+ return "ciktilar";
294
+ if (/^\d+\. hafta/u.test(a))
295
+ return "haftalar";
296
+ return "genel";
297
+ }
298
+ default:
299
+ return "genel";
300
+ }
301
+ }
302
+ // ---------------------------------------------------------------- Taslak alanları şeması
303
+ const IkiDilli = z.object({
304
+ tr: z.string().max(6000),
305
+ en: z.string().max(6000).optional(),
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();
334
+ const TaslakAlanlari = z.object({
335
+ amac: IkiDilli.optional(),
336
+ icerik: IkiDilli.optional(),
337
+ yontem: IkiDilli.optional(),
338
+ notlar: IkiDilli.optional(),
339
+ onKosul: z.string().max(500).optional(),
340
+ ciktilar: z
341
+ .array(z.object({
342
+ kod: z.string().trim().max(10).describe("Ö1, Ö2, ..."),
343
+ tr: z.string().max(600),
344
+ en: z.string().max(600).optional(),
345
+ tur: z.enum(["Bilgi", "Beceri", "Yetkinlik"]),
346
+ }))
347
+ .max(20)
348
+ .optional(),
349
+ degerlendirme: z
350
+ .array(z.object({ ad: z.string().max(100), sayi: z.number().int().min(0).max(50), katki: z.number().min(0).max(100) }))
351
+ .max(20)
352
+ .optional(),
353
+ isyuku: z
354
+ .array(z.object({ ad: z.string().max(100), sayi: z.number().min(0).max(100), sure: z.number().min(0).max(200) }))
355
+ .max(20)
356
+ .optional()
357
+ .describe("Satır başına sayı ve süre; toplamı sunucu hesaplar"),
358
+ dersYapisi: z
359
+ .array(z.object({ alan: z.string().max(100), yuzde: z.number().min(0).max(100) }))
360
+ .max(10)
361
+ .optional(),
362
+ haftalar: z
363
+ .array(z.object({
364
+ hafta: z.number().int().min(1).max(20),
365
+ konu: IkiDilli,
366
+ onHazirlik: z.string().max(500),
367
+ dokuman: z.string().max(500),
368
+ }))
369
+ .max(20)
370
+ .optional(),
371
+ katki: z
372
+ .record(z.string().max(10), z.record(z.string().max(10), z.union([z.number().int().min(1).max(5), z.null(), z.literal("")])))
373
+ .optional()
374
+ .describe("Ö kodu, P kodu, 1-5 ya da boş"),
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."),
380
+ kaynaklar: z
381
+ .array(KaynakOgesi)
382
+ .max(30)
383
+ .optional()
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."),
385
+ kaynakSayisi: z
386
+ .number()
387
+ .int()
388
+ .min(0)
389
+ .max(50)
390
+ .optional()
391
+ .describe("Hocanın verdiği gerçek kaynak sayısı (yazar ve eser adı olan). Adlar gönderilmez."),
392
+ });
393
+ const YetkiliSemasi = z.object({ rol: z.string().max(80), deger: z.string().max(1000) });
394
+ const DersTemeli = z.object({
395
+ kod: z.string().trim().max(30).optional(),
396
+ ad: z.string().trim().max(200).optional(),
397
+ akts: z.number().min(0).max(60).optional(),
398
+ tul: TUL.optional().describe("Teori+Uygulama+Laboratuvar, ör. '2+1+0'"),
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"),
407
+ yetkililer: z
408
+ .array(YetkiliSemasi)
409
+ .max(20)
410
+ .optional()
411
+ .describe("Sayfadaki görev satırları (Dersi Verenler, Koordinatör); olduğu gibi korunur"),
412
+ });
413
+ const ikiDil = (m, ingilizce) => m ? { tr: m.tr, en: m.en ?? (ingilizce ? "" : m.tr) } : undefined;
414
+ /** Taslak alanlarını cekirdek kurallarıyla denetler; yalnız verilen alanların bulgularını alan alan döndürür. */
415
+ export function alanBulgulari(ders, alanlar, s) {
416
+ const t = {
417
+ ...(ders.kod ? { kod: ders.kod } : {}),
418
+ ...(ders.ad ? { ad: ders.ad } : {}),
419
+ ...(ders.akts !== undefined ? { akts: ders.akts } : {}),
420
+ ...(ders.tul ? { tul: ders.tul } : {}),
421
+ ...(alanlar.amac ? { amac: ikiDil(alanlar.amac, s.ingilizce) } : {}),
422
+ ...(alanlar.icerik ? { icerik: ikiDil(alanlar.icerik, s.ingilizce) } : {}),
423
+ ...(alanlar.yontem ? { yontem: ikiDil(alanlar.yontem, s.ingilizce) } : {}),
424
+ ...(alanlar.notlar ? { notlar: ikiDil(alanlar.notlar, s.ingilizce) } : {}),
425
+ ...(alanlar.ciktilar
426
+ ? { ciktilar: alanlar.ciktilar.map((c) => ({ ...c, en: c.en ?? (s.ingilizce ? "" : c.tr) })) }
427
+ : {}),
428
+ ...(alanlar.degerlendirme ? { degerlendirme: alanlar.degerlendirme } : {}),
429
+ ...(alanlar.isyuku ? { isyuku: alanlar.isyuku } : {}),
430
+ ...(alanlar.dersYapisi ? { dersYapisi: alanlar.dersYapisi } : {}),
431
+ ...(alanlar.haftalar
432
+ ? {
433
+ haftalar: alanlar.haftalar.map((h) => ({
434
+ ...h,
435
+ konu: { tr: h.konu.tr, en: h.konu.en ?? (s.ingilizce ? "" : h.konu.tr) },
436
+ })),
437
+ }
438
+ : {}),
439
+ ...(alanlar.katki ? { katki: alanlar.katki } : {}),
440
+ ...(alanlar.ska ? { ska: alanlar.ska } : {}),
441
+ ...(alanlar.kaynaklar !== undefined || alanlar.kaynakSayisi !== undefined
442
+ ? {
443
+ kaynaklar: Array.from({ length: alanlar.kaynaklar?.length ?? alanlar.kaynakSayisi ?? 0 }, () => ({
444
+ yazar: "var",
445
+ eser: "var",
446
+ })),
447
+ }
448
+ : {}),
449
+ programCiktilari: s.programCiktilari ?? [],
450
+ };
451
+ const verilen = new Set(Object.keys(alanlar).filter((k) => alanlar[k] !== undefined));
452
+ if (verilen.has("kaynakSayisi"))
453
+ verilen.add("kaynaklar");
454
+ const sonuc = new Map();
455
+ const ekle = (a, b) => sonuc.set(a, [...(sonuc.get(a) ?? []), b]);
456
+ if (verilen.has("kaynaklar") && alanlar.kaynaklar)
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)))
467
+ ekle("kaynaklar", {
468
+ duzey: "uyari",
469
+ kural: "Kaynaklar",
470
+ aciklama: `${i + 1}. kaynakta yıl görünmüyor. Yılı hocaya sorun ya da kaynak_bicimle ile biçimlendirin.`,
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
+ });
483
+ // İş yükü AKTS olmadan denetlenemez; cekirdek "akts boş" diye temel bulgu verir, burada alana bağlanır.
484
+ if (verilen.has("isyuku") && ders.akts === undefined)
485
+ ekle("isyuku", {
486
+ duzey: "hata",
487
+ kural: "K8",
488
+ aciklama: "Dersin AKTS değeri verilmedi; iş yükü hedefi hesaplanamaz.",
489
+ });
490
+ for (const b of taslakDenetle(t, { kuralSeti: "guncel", kurumBoleni: ders.bolen }).bulgular) {
491
+ const a = bulguAlani(b);
492
+ if (a === "temel" || a === "yetkililer")
493
+ continue;
494
+ if (a === "isyuku" && ders.akts === undefined)
495
+ continue;
496
+ if (!s.ingilizce && /İngilizce/u.test(b.aciklama))
497
+ continue;
498
+ if (a === "genel") {
499
+ if (verilen.size)
500
+ ekle("genel", b);
501
+ continue;
502
+ }
503
+ if (!verilen.has(a))
504
+ continue;
505
+ const temiz = b.kural === "K12"
506
+ ? {
507
+ ...b,
508
+ aciklama: b.aciklama.replace(" Önce bologna-al ile oku.", " Bologna sayfasındaki P kodlarını programCiktilari olarak verin."),
509
+ }
510
+ : b;
511
+ ekle(a, temiz);
512
+ }
513
+ return sonuc;
514
+ }
515
+ const KUNYE_YILI = /\((?:\d{4}[a-z]?|t\.y\.|n\.d\.|baskıda|in press)\)|\b(1[5-9]|20)\d{2}\b/iu;
516
+ /** Bütün sayılar burada hesaplanır; asistan toplam yazmaz (ilke 2). */
517
+ export function bolognaHesabi(ders, alanlar) {
518
+ const saat = tulSaati(ders.tul);
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)`;
530
+ const isyuku = alanlar.isyuku
531
+ ? (() => {
532
+ const satirlar = alanlar.isyuku.map((x) => ({
533
+ ad: x.ad,
534
+ sayi: x.sayi,
535
+ sure: x.sure,
536
+ toplam: yuvarla(x.sayi * x.sure, 2),
537
+ }));
538
+ const toplam = yuvarla(satirlar.reduce((t, x) => t + x.toplam, 0), 2);
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;
548
+ return {
549
+ satirlar,
550
+ toplam,
551
+ hedef,
552
+ hedefAraligi,
553
+ aralikta,
554
+ fark,
555
+ aktsBasinaSaat: ders.akts ? yuvarla(toplam / ders.akts) : null,
556
+ dersSuresiHaftalikBeklenen: saat,
557
+ dersSuresiHaftalikVerilen: dersSuresi ? dersSuresi.sure : null,
558
+ aciklama: hedefAraligi === null
559
+ ? "AKTS verilmediği için hedef hesaplanmadı."
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.`,
563
+ };
564
+ })()
565
+ : null;
566
+ const toplamYuzde = (xs) => xs
567
+ ? yuvarla(xs.reduce((t, x) => t + x.v, 0), 2)
568
+ : null;
569
+ return {
570
+ isyuku,
571
+ degerlendirmeToplami: toplamYuzde(alanlar.degerlendirme?.map((x) => ({ v: x.katki }))),
572
+ dersYapisiToplami: toplamYuzde(alanlar.dersYapisi?.map((x) => ({ v: x.yuzde }))),
573
+ ciktiSayisi: alanlar.ciktilar?.length ?? null,
574
+ haftaSayisi: alanlar.haftalar?.length ?? null,
575
+ };
576
+ }
577
+ // ---------------------------------------------------------------- bologna_denetle (genişletilmiş)
578
+ const sayiYaDaBos = z.number().nullable().default(null);
579
+ const BolognaDersGirdisi = z.object({
580
+ kod: z.string().trim().min(1).max(30),
581
+ ad: z.string().max(200).optional(),
582
+ zorunlu: z.boolean().default(true),
583
+ yariyil: z.number().int().min(1).max(16).nullable().default(null),
584
+ tul: z.string().max(20).optional().describe("Teori+Uygulama+Laboratuvar, ör. '2+1+0'"),
585
+ kredi: sayiYaDaBos,
586
+ akts: sayiYaDaBos,
587
+ guncelleme: z.string().max(20).optional().describe("Son güncelleme tarihi, GG.AA.YYYY"),
588
+ amac: z.string().max(6000).optional().describe("Dersin amacı (Türkçe)"),
589
+ icerik: z.string().max(6000).optional().describe("Dersin içeriği (Türkçe)"),
590
+ yontem: z.string().max(6000).optional().describe("Yöntem ve teknikler (Türkçe)"),
591
+ notlar: z.string().max(6000).optional().describe("Ders notları (Türkçe)"),
592
+ kaynakSayisi: z.number().int().min(0).max(50).optional().describe("Sayfadaki kaynak sayısı"),
593
+ kaynaklar: z
594
+ .array(z.string().max(600))
595
+ .max(30)
596
+ .optional()
597
+ .describe("Sayfadaki künyeler; yazar adları künyedir, serbesttir"),
598
+ yetkililer: z.array(YetkiliSemasi).max(20).optional().describe("Sayfadaki görev satırları, olduğu gibi"),
599
+ ciktilar: z
600
+ .array(z.object({ kod: z.string().max(20), metin: z.string().max(1000) }))
601
+ .max(40)
602
+ .default([]),
603
+ degerlendirme: z
604
+ .array(z.object({ ad: z.string().max(200), sayi: sayiYaDaBos, katki: sayiYaDaBos }))
605
+ .max(40)
606
+ .default([]),
607
+ isyuku: z
608
+ .array(z.object({ ad: z.string().max(200), sayi: sayiYaDaBos, sure: sayiYaDaBos, toplam: sayiYaDaBos }))
609
+ .max(40)
610
+ .default([]),
611
+ dersYapisi: z
612
+ .array(z.object({ alan: z.string().max(200), yuzde: sayiYaDaBos }))
613
+ .max(20)
614
+ .default([]),
615
+ haftalar: z.number().int().min(0).max(30).default(0).describe("Haftalık konu satırı sayısı"),
616
+ katki: z
617
+ .record(z.string().max(20), z.record(z.string().max(20), z.string().max(5)))
618
+ .default({})
619
+ .describe("Ders çıktısı ya da 'Tüm' satırı, program çıktısı kodu, katkı değeri ('1'-'5', '' ya da '-')"),
620
+ });
621
+ /** Yayımlanmış sayfa verisinden alan alan durum çıkarır (boş, zayıf, tamam). */
622
+ function alanDurumlari(d, sayfaBulgulari) {
623
+ const tr = (m) => (m?.trim() ? { tr: m } : undefined);
624
+ const alanlar = {
625
+ ...(tr(d.amac) ? { amac: tr(d.amac) } : {}),
626
+ ...(tr(d.icerik) ? { icerik: tr(d.icerik) } : {}),
627
+ ...(tr(d.yontem) ? { yontem: tr(d.yontem) } : {}),
628
+ ...(tr(d.notlar) ? { notlar: tr(d.notlar) } : {}),
629
+ ...(d.ciktilar.length
630
+ ? { ciktilar: d.ciktilar.map((c) => ({ kod: c.kod, tr: c.metin, tur: "Beceri" })) }
631
+ : {}),
632
+ ...(d.kaynakSayisi !== undefined || d.kaynaklar
633
+ ? { kaynakSayisi: d.kaynaklar?.length ?? d.kaynakSayisi ?? 0 }
634
+ : {}),
635
+ // Sayfadaki değerlendirme yayımlanmış hâlidir; teyitsiz uyarısı burada anlamsızdır.
636
+ degerlendirmeDayanagi: "sayfa",
637
+ };
638
+ const b = alanBulgulari({ akts: d.akts ?? undefined }, alanlar, { ingilizce: false });
639
+ // Sayfada çıktı türü görünmez; türle ilgili kurallar burada denetlenmez.
640
+ const turKurali = /Yetkinlik|Beceri|türü/u;
641
+ const nedenler = (a) => (b.get(a) ?? []).filter((x) => x.duzey === "hata" && !turKurali.test(x.aciklama)).map((x) => x.aciklama);
642
+ const sayfa = (re) => sayfaBulgulari.filter((x) => re.test(x.kural)).map((x) => x.aciklama);
643
+ const metinAlan = (a, deger) => deger === undefined
644
+ ? ["bilinmiyor", ["Bu alan gönderilmedi."]]
645
+ : !deger.trim()
646
+ ? ["bos", []]
647
+ : nedenler(a).length
648
+ ? ["zayif", nedenler(a)]
649
+ : ["tamam", []];
650
+ const liste = [
651
+ ["amac", ...metinAlan("amac", d.amac)],
652
+ ["icerik", ...metinAlan("icerik", d.icerik)],
653
+ ["yontem", ...metinAlan("yontem", d.yontem)],
654
+ ["notlar", ...metinAlan("notlar", d.notlar)],
655
+ [
656
+ "ciktilar",
657
+ ...(() => {
658
+ if (!d.ciktilar.length)
659
+ return ["bos", []];
660
+ const n = [...nedenler("ciktilar"), ...sayfa(/^ders-ciktisi$/u)];
661
+ return n.length ? ["zayif", n] : ["tamam", []];
662
+ })(),
663
+ ],
664
+ [
665
+ "degerlendirme",
666
+ ...(() => {
667
+ if (!d.degerlendirme.length)
668
+ return ["bos", []];
669
+ const n = sayfa(/^degerlendirme$/u);
670
+ return n.length ? ["zayif", n] : ["tamam", []];
671
+ })(),
672
+ ],
673
+ [
674
+ "isyuku",
675
+ ...(() => {
676
+ if (!d.isyuku.length)
677
+ return ["bos", []];
678
+ const n = sayfa(/^akts-isyuku$/u);
679
+ return n.length ? ["zayif", n] : ["tamam", []];
680
+ })(),
681
+ ],
682
+ [
683
+ "dersYapisi",
684
+ ...(() => {
685
+ if (!d.dersYapisi.length)
686
+ return ["bos", []];
687
+ const n = sayfa(/^ders-yapisi$/u);
688
+ return n.length ? ["zayif", n] : ["tamam", []];
689
+ })(),
690
+ ],
691
+ [
692
+ "haftalar",
693
+ ...(() => {
694
+ if (!d.haftalar)
695
+ return ["bos", []];
696
+ if (d.haftalar < 14 || d.haftalar > 16)
697
+ return ["zayif", [`${d.haftalar} hafta var, 14-16 olmalı.`]];
698
+ return ["tamam", []];
699
+ })(),
700
+ ],
701
+ [
702
+ "katki",
703
+ ...(() => {
704
+ const satirlar = Object.keys(d.katki).filter((k) => /^Ö\d+$/u.test(k));
705
+ if (!satirlar.length)
706
+ return ["bos", []];
707
+ const bosSatir = satirlar.filter((k) => !Object.values(d.katki[k] ?? {}).some((v) => v && v !== "-"));
708
+ const n = [
709
+ ...(bosSatir.length ? [`${bosSatir.join(", ")} satırı tamamen boş.`] : []),
710
+ ...sayfa(/^(katki-destegi|matris-tutarlilik)$/u),
711
+ ];
712
+ return n.length ? ["zayif", n] : ["tamam", []];
713
+ })(),
714
+ ],
715
+ [
716
+ "kaynaklar",
717
+ ...(() => {
718
+ const n = d.kaynaklar?.length ?? d.kaynakSayisi;
719
+ if (n === undefined)
720
+ return ["bilinmiyor", ["Kaynaklar gönderilmedi."]];
721
+ if (n === 0)
722
+ return ["bos", []];
723
+ return n < 2 ? ["zayif", [`${n} kaynak var, en az 2 olmalı.`]] : ["tamam", []];
724
+ })(),
725
+ ],
726
+ ];
727
+ return liste.map(([alan, durum, n], i) => ({
728
+ no: i + 1,
729
+ alan,
730
+ ad: ALANLAR[alan].ad,
731
+ sekme: ALANLAR[alan].sekme,
732
+ durum,
733
+ nedenler: n,
734
+ kural: ALANLAR[alan].kurallar.join(" "),
735
+ }));
736
+ }
737
+ export const bolognaDenetleAraci = arac({
738
+ ad: "bologna_denetle",
739
+ baslik: "Bologna ders paketini denetle",
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.",
741
+ 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.",
742
+ kisiselVeriKapsami: BOLOGNA_KAPSAMI,
743
+ kunyeAlanlari: BOLOGNA_KUNYE_ALANLARI,
744
+ girdi: z.object({
745
+ ders: BolognaDersGirdisi,
746
+ programCiktilari: z
747
+ .array(z.object({ kod: z.string().max(20), metin: z.string().max(1000) }))
748
+ .max(40)
749
+ .optional()
750
+ .describe("Program çıktıları; verilirse program düzeyindeki kurallar da çalışır"),
751
+ matrisSatiri: z
752
+ .record(z.string().max(20), z.string().max(5))
753
+ .optional()
754
+ .describe("Program matrisinde bu dersin satırı: program çıktısı kodu ve değer"),
755
+ aktsSaat: z
756
+ .tuple([z.number().min(1), z.number().max(60)])
757
+ .optional()
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ı)"),
766
+ }),
767
+ calistir: ({ ders, programCiktilari, matrisSatiri, aktsSaat: aralik, kurumBoleni }, b) => {
768
+ const aktsSaat = kurumBoleni ? [kurumBoleni, kurumBoleni] : aralik;
769
+ const { amac: _a, icerik: _i, yontem: _y, notlar: _n, kaynakSayisi: _k, kaynaklar: _kk, ...sayfaAlanlari } = ders;
770
+ const sayfa = {
771
+ ...sayfaAlanlari,
772
+ ad: ders.ad ?? ders.kod,
773
+ bilgiler: {},
774
+ };
775
+ const pkodlar = Object.keys(matrisSatiri ?? {});
776
+ const veri = {
777
+ adres: "kullanici-girdisi",
778
+ okundu: b.simdi().toISOString(),
779
+ programCiktilari: programCiktilari ?? [],
780
+ matris: {
781
+ pkodlar,
782
+ dersler: matrisSatiri
783
+ ? [
784
+ {
785
+ kod: ders.kod,
786
+ ad: ders.ad ?? ders.kod,
787
+ zorunlu: ders.zorunlu,
788
+ grup: null,
789
+ yariyil: ders.yariyil,
790
+ grupBasligi: false,
791
+ degerler: matrisSatiri,
792
+ },
793
+ ]
794
+ : [],
795
+ },
796
+ tyycIsaret: null,
797
+ dersler: [{ ...sayfa, zorunlu: ders.zorunlu, bos: false, kaynak: "kullanici-girdisi" }],
798
+ };
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
+ }));
808
+ // Program düzeyi kuralları yalnız program çıktıları verildiyse anlamlıdır.
809
+ if (!programCiktilari)
810
+ bulgular = bulgular.filter((x) => x.konu !== "-");
811
+ const alanlar = alanDurumlari(ders, bulgular);
812
+ const doldurulacak = alanlar.filter((a) => a.durum === "bos" || a.durum === "zayif");
813
+ return {
814
+ sonuc: {
815
+ ders: ders.kod,
816
+ hataSayisi: bulgular.filter((x) => x.duzey === "hata").length,
817
+ uyariSayisi: bulgular.filter((x) => x.duzey === "uyari").length,
818
+ ozet: doldurulacak.length
819
+ ? `${doldurulacak.length} alan doldurulmalı ya da iyileştirilmeli: ${doldurulacak.map((a) => `${a.no}) ${a.ad}`).join(", ")}.`
820
+ : "Gönderilen alanlarda boş ya da zayıf alan bulunmadı.",
821
+ alanlar,
822
+ doldurulacak: doldurulacak.map((a) => a.no),
823
+ kalanAlanSayisi: doldurulacak.length,
824
+ dokunulmayacak: ["Dersin Yetkilileri kişi adı içerir; hoca OBS'de kendisi yazar."],
825
+ bulgular,
826
+ not: "Kurallar Akredit'in iç kurallarıdır, resmî ölçüt değildir. Düzeltme kararını ders sorumlusu ve komisyon verir.",
827
+ },
828
+ kaynak: [KAYNAK_SAYFA, KAYNAK_KURALLAR],
829
+ ...(doldurulacak.length
830
+ ? {}
831
+ : { sonrakiAdim: "Hocaya sorun: 1) başka bir ders 2) OBS'ye kopya metni (bologna_kopya_metni) 3) bitir." }),
832
+ };
833
+ },
834
+ });
835
+ // ---------------------------------------------------------------- bologna_alan_rehberi
836
+ const alanRehberi = arac({
837
+ ad: "bologna_alan_rehberi",
838
+ baslik: "Bologna alanı nasıl doldurulur",
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.",
840
+ 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.",
841
+ girdi: z.object({
842
+ alan: z.enum([...ALAN_KODLARI, "hepsi"]).describe("Alan kodu ya da 'hepsi'"),
843
+ akts: z.number().min(0).max(60).optional(),
844
+ tul: TUL.optional().describe("ör. '2+1+0'"),
845
+ hafta: z.number().int().min(10).max(20).default(14).describe("Ders haftası sayısı; varsayılan 14"),
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"),
853
+ }),
854
+ calistir: ({ alan, akts, tul, hafta, bolen: kurum }) => {
855
+ const bolen = kurum ?? 30;
856
+ const secilen = alan === "hepsi" ? [...ALAN_KODLARI] : [alan];
857
+ const saat = tulSaati(tul);
858
+ const hesap = secilen.includes("isyuku") && akts !== undefined
859
+ ? (() => {
860
+ const hedef = akts * bolen;
861
+ const dersSuresi = saat === null ? null : hafta * saat;
862
+ return {
863
+ hedefSaat: hedef,
864
+ formul: `${akts} AKTS x ${bolen} = ${hedef} saat`,
865
+ kabulAraligi: kurum ? [hedef, hedef] : [akts * 25, akts * 30],
866
+ dersSuresiSatiri: saat === null ? null : { sayi: hafta, sure: saat, toplam: dersSuresi },
867
+ kalanSaat: dersSuresi === null ? null : hedef - dersSuresi,
868
+ not: dersSuresi === null
869
+ ? "T+U+L verilmedi; ders süresi satırı hesaplanmadı."
870
+ : `Ders süresi ${hafta} x ${saat} = ${dersSuresi} saat. Kalan ${hedef - dersSuresi} saati sınıf dışı çalışma, ödev, proje ve sınav hazırlığına dağıtın.`,
871
+ };
872
+ })()
873
+ : null;
874
+ return {
875
+ sonuc: {
876
+ alanlar: secilen.map((a) => ({
877
+ alan: a,
878
+ ...ALANLAR[a],
879
+ ...(a === "ciktilar" ? { bloomFiilleri: BLOOM_FIILLERI } : {}),
880
+ })),
881
+ ...(hesap ? { isyukuHesabi: hesap } : {}),
882
+ secenekUretimi: SECENEK_KURALI,
883
+ resmiDayanak: RESMI_DAYANAK,
884
+ },
885
+ kaynak: [
886
+ KAYNAK_KURALLAR,
887
+ KOD_KAYNAGI("apps/cli/skills/bologna-doldur/SKILL.md", "Akredit Bologna doldurma rehberi"),
888
+ ],
889
+ ...(secilen.length === 1 && secilen[0] === "yetkililer"
890
+ ? {
891
+ sonrakiAdim: "Bu alan sayfadaki görev bilgisidir; olduğu gibi korunur, değiştirmeyin. Sonraki alana geçin.",
892
+ }
893
+ : {}),
894
+ };
895
+ },
896
+ });
897
+ // ---------------------------------------------------------------- bologna_taslak_denetle
898
+ const taslakDenetleAraci = arac({
899
+ ad: "bologna_taslak_denetle",
900
+ baslik: "Bologna taslağını denetle",
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: "Biçim denetiminden geçen seçenekleri hocaya 'öneri' etiketiyle kısa gösterin (A, B, C; ilki önerilen) ve içeriği hocanın teyit edeceğini söyleyin. Geçmeyeni düzeltip yeniden denetleyin, hocaya göstermeyin. Seçim bitince bologna_kopya_metni ile OBS metnini verin.",
903
+ kisiselVeriKapsami: BOLOGNA_KAPSAMI,
904
+ kunyeAlanlari: BOLOGNA_KUNYE_ALANLARI,
905
+ girdi: z.object({
906
+ ders: DersTemeli,
907
+ secenekler: z
908
+ .array(z.object({ etiket: z.string().trim().min(1).max(3).describe("A, B, C"), alanlar: TaslakAlanlari }))
909
+ .min(1)
910
+ .max(3),
911
+ programCiktilari: z.array(z.string().max(10)).max(40).optional().describe("Programın P kodları: P1, P2, ..."),
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)."),
916
+ }),
917
+ calistir: ({ ders, secenekler, programCiktilari, ingilizce }) => {
918
+ const sonuclar = secenekler.map((s) => {
919
+ const b = alanBulgulari(ders, s.alanlar, { ingilizce, programCiktilari });
920
+ const hepsi = [...b.entries()].flatMap(([alan, xs]) => xs.map((x) => ({ alan, ...x })));
921
+ const eksikler = hepsi
922
+ .filter((x) => x.duzey === "hata")
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
+ }));
930
+ return {
931
+ etiket: s.etiket,
932
+ bicimDenetimi: eksikler.length === 0 ? "geçti" : "geçmedi",
933
+ icerikTeyidi: "hoca",
934
+ eksikler,
935
+ uyarilar: hepsi
936
+ .filter((x) => x.duzey !== "hata")
937
+ .map((x) => ({ alan: x.alan, kural: x.kural, neden: x.aciklama })),
938
+ hesap: bolognaHesabi(ders, s.alanlar),
939
+ };
940
+ });
941
+ return {
942
+ sonuc: {
943
+ bicimdenGecenler: sonuclar.filter((x) => x.bicimDenetimi === "geçti").map((x) => x.etiket),
944
+ secenekler: sonuclar,
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.",
946
+ resmiDayanak: RESMI_DAYANAK,
947
+ },
948
+ kaynak: [KAYNAK_KURALLAR],
949
+ ...(sonuclar.some((x) => x.bicimDenetimi === "geçti")
950
+ ? {}
951
+ : {
952
+ sonrakiAdim: "Hiçbir seçenek geçmedi. Eksik nedenlerine göre düzeltip yeniden denetleyin; hocaya henüz göstermeyin.",
953
+ }),
954
+ };
955
+ },
956
+ });
957
+ // ---------------------------------------------------------------- bologna_ayristir
958
+ /** Bologna sayfasında görev bilgisi taşıyan satırlar (Dersi Verenler, Koordinatör...): ayrı alan olarak korunur. */
959
+ const YETKILI_SATIRI = /^(Dersi Veren(ler)?|Dersi Veren Öğretim Elemanı|Dersin Yardımcıları|Dersin Koordinatörü|Koordinatör|Dersin Yetkilileri|Yetkililer|Sorumlu|Öğretim Elemanı|Öğretim Elemanları|Instructors?|Lecturers?|Assistants?|Coordinator|Course Coordinator|İletişim|E-?posta|Telefon|Contact)(\s|:|$)/iu;
960
+ /** Kaynak satırı: ilk hücre bu etiketlerden biriyse satır künye olarak ayrılır. */
961
+ const KAYNAK_SATIRI = /^(Kaynaklar|Kaynak|Ders Kitabı|Önerilen Kaynaklar|Diğer Kaynaklar|Yardımcı Kaynaklar|Resources|References|Textbooks?|Recommended Sources|Reading List)(\s|:|$)/iu;
962
+ const hucreMetni = (h) => h
963
+ .replace(/<[^>]*>/gu, " ")
964
+ .replace(/&nbsp;/gu, " ")
965
+ .replace(/\s+/gu, " ")
966
+ .trim();
967
+ /** Proliz sayfası mı: üst bilgi tablosu ("Yarıyıl, Kodu") ya da program çıktıları tablosu. */
968
+ export function prolizMi(html) {
969
+ const t = tablolar(html);
970
+ return (t.some((x) => /Yarıyıl.*Kodu|Semester.*Code/iu.test((x[0] ?? []).join(" "))) || programCiktilari(html).length > 0);
971
+ }
972
+ /**
973
+ * Yapıştırılan HTML'den görev satırlarını (Dersi Verenler, Yardımcılar, Koordinatör) yetkililer listesine, kaynak
974
+ * satırlarını künye listesine ayırır. Değerler olduğu gibi korunur; hiçbir şey atılmaz.
975
+ */
976
+ export function bolognaSatirAyikla(html) {
977
+ const yetkililer = [];
978
+ const kaynaklar = [];
979
+ const h = html.replace(/<tr\b[\s\S]*?<\/tr>/giu, (tr) => {
980
+ const hucreler = [...tr.matchAll(/<t[dh]\b[^>]*>([\s\S]*?)<\/t[dh]>/giu)].map((m) => m[1] ?? "");
981
+ const etiket = hucreler[0] ? hucreMetni(hucreler[0]) : "";
982
+ if (YETKILI_SATIRI.test(etiket) && hucreler.length > 1) {
983
+ const deger = hucreler
984
+ .slice(1)
985
+ .map((x) => hucreMetni(x.replace(/<br\s*\/?>/giu, ", ")).replace(/(\s*,\s*)+$/u, ""))
986
+ .filter(Boolean)
987
+ .join(" ");
988
+ if (deger)
989
+ yetkililer.push({ rol: etiket.replace(/:$/u, "").slice(0, 80), deger: deger.slice(0, 1000) });
990
+ return "";
991
+ }
992
+ if (KAYNAK_SATIRI.test(etiket) && hucreler.length > 1) {
993
+ for (const hucre of hucreler.slice(1))
994
+ for (const parca of hucre.split(/<br\s*\/?>|<\/li>|<\/p>|\n|;\s+(?=\S)/iu)) {
995
+ const k = hucreMetni(parca).replace(/^\s*(\[\d+\]|\d{1,2}[.)-]|[-•*–])\s*/u, "");
996
+ if (k.length >= 5)
997
+ kaynaklar.push(k);
998
+ }
999
+ return "";
1000
+ }
1001
+ return tr;
1002
+ });
1003
+ return { html: h, yetkililer, kaynaklar };
1004
+ }
1005
+ /** bologna_ayristir ön ayıklaması: yalnız giriş bilgisi (oturum kimliği, parola kutusu, çerez) atılır. */
1006
+ export function ayristirOnAyikla(g) {
1007
+ if (g === null || typeof g !== "object")
1008
+ return { girdi: g, atilan: [] };
1009
+ const o = g;
1010
+ const ham = typeof o.html === "string" ? o.html : typeof o.metin === "string" ? o.metin : null;
1011
+ if (ham === null)
1012
+ return { girdi: g, atilan: [] };
1013
+ const giris = girisBilgisiAyikla(ham);
1014
+ const atilan = giris.atildi ? ["oturum ve parola alanları"] : [];
1015
+ const ileti = giris.atildi
1016
+ ? "Sayfadaki oturum kimliği ve parola kutusu okunmadan atıldı, hiçbir yere yazılmadı. Ders bilgileri olduğu gibi okundu."
1017
+ : undefined;
1018
+ const onceki = Array.isArray(o.kaynaklar) ? o.kaynaklar.filter((x) => typeof x === "string") : [];
1019
+ const oncekiY = Array.isArray(o.yetkililer) ? o.yetkililer : [];
1020
+ const { html: _h, metin: _m, ...kalan } = o;
1021
+ const satir = bolognaSatirAyikla(giris.metin);
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) };
1025
+ if (prolizMi(giris.metin))
1026
+ return {
1027
+ girdi: {
1028
+ ...kalan,
1029
+ sayfaIpucu,
1030
+ html: satir.html,
1031
+ kaynaklar: [...onceki, ...satir.kaynaklar],
1032
+ yetkililer: [...oncekiY, ...satir.yetkililer],
1033
+ },
1034
+ atilan,
1035
+ ...ek,
1036
+ };
1037
+ // Proliz dışı: önce tablo satırları, sonra etiketli bölümler.
1038
+ const r = genelAyikla(satir.html);
1039
+ return {
1040
+ girdi: {
1041
+ ...kalan,
1042
+ sayfaIpucu,
1043
+ metin: r.metin,
1044
+ kaynaklar: [...onceki, ...satir.kaynaklar, ...r.kaynaklar],
1045
+ yetkililer: [...oncekiY, ...satir.yetkililer, ...r.yetkililer],
1046
+ },
1047
+ atilan,
1048
+ ...ek,
1049
+ };
1050
+ }
1051
+ const ayristirAraci = arac({
1052
+ ad: "bologna_ayristir",
1053
+ baslik: "Bologna sayfasını ayrıştır",
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.",
1055
+ sonrakiAdim: "Çıkan ders verisini bologna_denetle'ye verin (girdi hazır: bolognaDenetleGirdisi). Sonraki çağrılarda ingilizce değerini aynen verin. Değerlendirme yöntem ve yüzdeleri, uygulama/lab, kaynaklar, amaç ve içerik sayfada yoksa bunları hocaya TEK mesajda, numaralı ve varsayılan seçenekli sorun; uydurmayın.",
1056
+ kisiselVeriKapsami: BOLOGNA_KAPSAMI,
1057
+ kunyeAlanlari: BOLOGNA_KUNYE_ALANLARI,
1058
+ onAyikla: ayristirOnAyikla,
1059
+ girdi: z.object({
1060
+ html: z
1061
+ .string()
1062
+ .min(20)
1063
+ .max(400_000)
1064
+ .optional()
1065
+ .describe("Sayfanın HTML kaynağı ya da sayfadan kopyalanan metin (Ctrl+A, Ctrl+C)"),
1066
+ metin: z.string().min(20).max(400_000).optional().describe("Sayfadan kopyalanan düz metin; html yerine"),
1067
+ kaynaklar: z.array(z.string().max(600)).max(30).optional().describe("Ayrıca verilen künyeler"),
1068
+ yetkililer: z.array(YetkiliSemasi).max(20).optional().describe("Sayfadaki görev satırları, olduğu gibi"),
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"),
1074
+ }),
1075
+ calistir: ({ html, metin, kaynaklar = [], yetkililer = [], tur, sayfaIpucu }) => {
1076
+ if (!html && !metin)
1077
+ throw new AracHatasi("Sayfanın HTML'ini ya da metnini yapıştırın.");
1078
+ const kaynakAlani = {
1079
+ ...(kaynaklar.length ? { kaynaklar, kaynakSayisi: kaynaklar.length } : {}),
1080
+ ...(yetkililer.length ? { yetkililer } : {}),
1081
+ };
1082
+ if (html && prolizMi(html)) {
1083
+ const programMi = tur === "program_ciktilari" || (tur === "otomatik" && !/İş Yükü|Workload|Yarıyıl Çalışmaları/iu.test(html));
1084
+ if (programMi) {
1085
+ const pc = programCiktilari(html);
1086
+ if (!pc.length)
1087
+ throw new AracHatasi("Program çıktıları okunamadı. Sayfa biçimi tanınmadı.");
1088
+ return {
1089
+ sonuc: { tur: "program_ciktilari", bicim: "proliz", programCiktilari: pc, pKodlari: pc.map((p) => p.kod) },
1090
+ kaynak: [KAYNAK_AYRISTIRICI],
1091
+ sonrakiAdim: "P kodlarını bologna_denetle (programCiktilari) ve bologna_taslak_denetle (programCiktilari) çağrılarında kullanın.",
1092
+ };
1093
+ }
1094
+ const d = dersSayfasiCoz(html);
1095
+ // Proliz sayfasında görünmeyen metin alanları genel ayrıştırıcıdan tamamlanır.
1096
+ const g = genelAyristir(html, kaynaklar.length);
1097
+ const ders = {
1098
+ kod: d.kod ?? g.ders.kod ?? "DERS",
1099
+ ...(d.ad || g.ders.ad ? { ad: d.ad ?? g.ders.ad } : {}),
1100
+ yariyil: d.yariyil,
1101
+ ...(d.tul ? { tul: d.tul } : {}),
1102
+ kredi: d.kredi,
1103
+ akts: d.akts,
1104
+ ...(d.guncelleme ? { guncelleme: d.guncelleme } : {}),
1105
+ ...(g.ders.amac ? { amac: g.ders.amac } : {}),
1106
+ ...(g.ders.icerik ? { icerik: g.ders.icerik } : {}),
1107
+ ...(g.ders.yontem ? { yontem: g.ders.yontem } : {}),
1108
+ ...(g.ders.notlar ? { notlar: g.ders.notlar } : {}),
1109
+ ...kaynakAlani,
1110
+ ciktilar: d.ciktilar ?? [],
1111
+ degerlendirme: d.degerlendirme ?? [],
1112
+ isyuku: d.isyuku ?? [],
1113
+ dersYapisi: d.dersYapisi ?? [],
1114
+ haftalar: d.haftalar ?? 0,
1115
+ katki: d.katki ?? {},
1116
+ };
1117
+ const bulunamayan = [
1118
+ ...(ders.amac ? [] : ["amac"]),
1119
+ ...(ders.icerik ? [] : ["icerik"]),
1120
+ ...(ders.yontem ? [] : ["yontem"]),
1121
+ ...(ders.notlar ? [] : ["notlar"]),
1122
+ ...(kaynaklar.length ? [] : ["kaynaklar"]),
1123
+ ];
1124
+ return {
1125
+ sonuc: {
1126
+ tur: "ders",
1127
+ bicim: "proliz",
1128
+ guven: "yüksek",
1129
+ ingilizce: sayfaIpucu?.ingilizce ?? ingilizceAlanVar(html),
1130
+ bolognaDenetleGirdisi: { ders },
1131
+ bulunamayan: bulunamayan.map((k) => GENEL_ALAN_ADLARI[k] ?? k),
1132
+ okunamayan: bulunamayan.length
1133
+ ? [
1134
+ "Bu alanlar sayfada görünmedi. Hocaya tek soruyla sorun ya da tarayıcı aracınızla OBS düzenleme ekranından okuyun.",
1135
+ ]
1136
+ : [],
1137
+ },
1138
+ kaynak: [KAYNAK_AYRISTIRICI],
1139
+ };
1140
+ }
1141
+ const ham = html ?? metin ?? "";
1142
+ const g = genelAyristir(ham, kaynaklar.length);
1143
+ if (g.bulunan.length < 2)
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.");
1145
+ const { haftaKonulari, ...dersAlanlari } = g.ders;
1146
+ const ders = {
1147
+ ...dersAlanlari,
1148
+ kod: g.ders.kod ?? "DERS",
1149
+ ...kaynakAlani,
1150
+ };
1151
+ return {
1152
+ sonuc: {
1153
+ tur: "ders",
1154
+ bicim: (sayfaIpucu?.prolizMetni ?? prolizMetniMi(ham)) ? "proliz_duz_metin" : "genel",
1155
+ guven: g.guven,
1156
+ ingilizce: sayfaIpucu?.ingilizce ?? ingilizceAlanVar(ham),
1157
+ bolognaDenetleGirdisi: { ders },
1158
+ bilgiler: g.bilgiler,
1159
+ haftaKonulari,
1160
+ bulunan: g.bulunan.map((k) => GENEL_ALAN_ADLARI[k] ?? k),
1161
+ bulunamayan: g.bulunamayan.map((k) => GENEL_ALAN_ADLARI[k] ?? k),
1162
+ not: g.guven === "yüksek"
1163
+ ? "Genel ayrıştırıcı alanların çoğunu buldu. Yine de hocaya kısaca gösterip doğrulatın."
1164
+ : "Genel ayrıştırıcı bazı alanları bulamadı. Bulunanları hocaya gösterip doğrulatın; bulunamayanları sorun.",
1165
+ },
1166
+ kaynak: [KOD_KAYNAGI("apps/acik-mcp/src/genel-ayristir.ts", "Akredit genel Bologna ayrıştırıcısı")],
1167
+ };
1168
+ },
1169
+ });
1170
+ // ---------------------------------------------------------------- bologna_durum
1171
+ /** Tamamlanmış sayılmak için dolu ve kurala uygun olması gereken alanlar. */
1172
+ const ZORUNLU_ALANLAR = [
1173
+ "amac",
1174
+ "icerik",
1175
+ "yontem",
1176
+ "notlar",
1177
+ "ciktilar",
1178
+ "degerlendirme",
1179
+ "isyuku",
1180
+ "haftalar",
1181
+ "kaynaklar",
1182
+ ];
1183
+ const durumAraci = arac({
1184
+ ad: "bologna_durum",
1185
+ baslik: "Bologna paketi ne kadar tamam",
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.",
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.",
1188
+ kisiselVeriKapsami: BOLOGNA_KAPSAMI,
1189
+ kunyeAlanlari: BOLOGNA_KUNYE_ALANLARI,
1190
+ girdi: z.object({
1191
+ ders: DersTemeli,
1192
+ alanlar: TaslakAlanlari,
1193
+ programCiktilari: z
1194
+ .array(z.string().max(10))
1195
+ .max(40)
1196
+ .optional()
1197
+ .describe("Verilirse katkı tablosu da zorunlu alan sayılır"),
1198
+ ingilizce: z.boolean().default(false).describe("bologna_ayristir'in ingilizce değeri"),
1199
+ }),
1200
+ calistir: ({ ders, alanlar, programCiktilari, ingilizce }) => {
1201
+ const zorunlu = [...ZORUNLU_ALANLAR, ...(programCiktilari?.length ? ["katki"] : [])];
1202
+ const b = alanBulgulari(ders, alanlar, { ingilizce, programCiktilari });
1203
+ const dolu = (a) => a === "kaynaklar"
1204
+ ? Boolean(alanlar.kaynaklar?.length ?? alanlar.kaynakSayisi)
1205
+ : alanlar[a] !== undefined;
1206
+ const satirlar = zorunlu.map((a, i) => {
1207
+ const hatalar = (b.get(a) ?? []).filter((x) => x.duzey === "hata").map((x) => x.aciklama);
1208
+ const durum = !dolu(a) ? "eksik" : hatalar.length ? "kurala_uymuyor" : "tamam";
1209
+ return { no: i + 1, alan: a, ad: ALANLAR[a].ad, durum, nedenler: hatalar };
1210
+ });
1211
+ const genel = (b.get("genel") ?? []).filter((x) => x.duzey === "hata").map((x) => x.aciklama);
1212
+ const kalan = satirlar.filter((s) => s.durum !== "tamam");
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
+ ];
1219
+ return {
1220
+ sonuc: {
1221
+ bicimDenetimi: tamamlandi ? "geçti" : "geçmedi",
1222
+ icerikTeyidi: "hoca",
1223
+ kalanAlanSayisi: kalan.length,
1224
+ teyitBekleyen,
1225
+ ozet: tamamlandi
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(", ")}.` : ""}`
1227
+ : `${kalan.length} alan kaldı: ${kalan.map((s) => s.ad).join(", ")}.`,
1228
+ alanlar: satirlar,
1229
+ genelEksikler: genel,
1230
+ istegeBagli: ["Ders yapısı", "Sürdürülebilir Kalkınma Amaçları", "Ön koşul"].filter((ad) => !((ad === "Ders yapısı" && alanlar.dersYapisi) ||
1231
+ (ad === "Sürdürülebilir Kalkınma Amaçları" && alanlar.ska) ||
1232
+ (ad === "Ön koşul" && alanlar.onKosul !== undefined))),
1233
+ hesap: bolognaHesabi(ders, alanlar),
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.",
1235
+ },
1236
+ kaynak: [KAYNAK_KURALLAR],
1237
+ ...(tamamlandi
1238
+ ? {
1239
+ sonrakiAdim: "bologna_kopya_metni ile yazma paketini alın (sayfadaki önceki değerleri oncekiDegerler olarak verin). Hocaya alan alan önceki/yeni değer özetini gösterip sorun: 1) formu doldurayım, Kaydet'e siz basın 2) formu ben doldurup kaydedeyim (bir kez 'kaydet' demeniz yeter) 3) alan alan kopyalayın 4) bitir.",
1240
+ }
1241
+ : {}),
1242
+ };
1243
+ },
1244
+ });
1245
+ // ---------------------------------------------------------------- is_yuku_oner
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
+ }
1280
+ const IS_YUKU_BICIMLERI = [
1281
+ {
1282
+ ad: "Dengeli",
1283
+ aciklama: "Kalan saatin %60'ı haftalık çalışmaya, kalanı değerlendirme etkinliklerine.",
1284
+ haftalikPay: 0.6,
1285
+ },
1286
+ {
1287
+ ad: "Etkinlik ağırlıklı",
1288
+ aciklama: "Kalan saatin %45'i haftalık çalışmaya, kalanı değerlendirme etkinliklerine.",
1289
+ haftalikPay: 0.45,
1290
+ },
1291
+ {
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,
1295
+ },
1296
+ ];
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) {
1303
+ const saat = tulSaati(tul);
1304
+ if (saat === null)
1305
+ throw new AracHatasi("T+U+L '2+1+0' biçiminde olmalı.");
1306
+ const hedef = Math.round(akts * (bolen ?? 30));
1307
+ const aralik = bolen ? [hedef, hedef] : [Math.round(akts * 25), hedef];
1308
+ const dersSuresi = hafta * saat;
1309
+ const kalan = hedef - dersSuresi;
1310
+ if (kalan < 0)
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;
1352
+ });
1353
+ const toplam = satirlar.reduce((t, x) => t + x.sayi * x.sure, 0);
1354
+ return {
1355
+ secenek: i + 1,
1356
+ ad: b.ad,
1357
+ aciklama: b.aciklama,
1358
+ etiket: "öneri",
1359
+ isyuku: satirlar,
1360
+ toplam,
1361
+ hedef,
1362
+ kabulAraligi: aralik,
1363
+ tamEsit: toplam === hedef,
1364
+ aralikta: toplam >= aralik[0] && toplam <= aralik[1],
1365
+ };
1366
+ });
1367
+ }
1368
+ const isYukuOnerAraci = arac({
1369
+ ad: "is_yuku_oner",
1370
+ baslik: "AKTS iş yükü önerisi",
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: "Önce sayfadan okunan iş yükü varsa onu gösterin. Sonra önerileri 'öneri' etiketiyle 1, 2, 3 diye kısa gösterin (1 önerilen), satırların kaynak ve varsayımını yazın. Değerlendirme kalemleri bilinmiyorsa önce hocaya sorun. Seçileni bologna_taslak_denetle ile denetleyin.",
1373
+ kisiselVeriKapsami: BOLOGNA_KAPSAMI,
1374
+ girdi: z.object({
1375
+ akts: z.number().min(1).max(60),
1376
+ tul: TUL.describe("ör. '2+1+0'"),
1377
+ hafta: z.number().int().min(10).max(20).default(14),
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"),
1399
+ }),
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;
1408
+ return {
1409
+ sonuc: {
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,
1414
+ secenekler,
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.",
1421
+ resmiDayanak: RESMI_DAYANAK,
1422
+ },
1423
+ kaynak: [KAYNAK_KURALLAR],
1424
+ };
1425
+ },
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
+ });
1523
+ // ---------------------------------------------------------------- bologna_kopya_metni
1524
+ const SEKME_SIRASI = [
1525
+ "Ders Bilgileri",
1526
+ "Öğrenme Çıktıları",
1527
+ "Ders Akışı",
1528
+ "Prg.Çıktısına Katkısı",
1529
+ "Sür. Kalkınma Amaçları",
1530
+ "Diğer Kaynaklar",
1531
+ ];
1532
+ const kopyaMetniAraci = arac({
1533
+ ad: "bologna_kopya_metni",
1534
+ baslik: "OBS'ye kopyalanacak metin",
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.",
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.",
1537
+ kisiselVeriKapsami: BOLOGNA_KAPSAMI,
1538
+ kunyeAlanlari: BOLOGNA_KUNYE_ALANLARI,
1539
+ girdi: z.object({
1540
+ ders: DersTemeli,
1541
+ alanlar: TaslakAlanlari,
1542
+ programCiktilari: z.array(z.string().max(10)).max(40).optional(),
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"),
1548
+ }),
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
+ };
1573
+ const bolumler = [];
1574
+ const ekle = (sekme, alan, deger) => bolumler.push({ sira: bolumler.length + 1, sekme, alan, deger });
1575
+ const hesap = bolognaHesabi(ders, alanlar);
1576
+ const iki = (ad, m) => {
1577
+ if (!m)
1578
+ return;
1579
+ ekle(DERS_BILGILERI, `${ad} (Türkçe)`, m.tr.trim());
1580
+ if (m.en?.trim())
1581
+ ekle(DERS_BILGILERI, `${ad} (İngilizce)`, m.en.trim());
1582
+ };
1583
+ iki("Dersin Amacı", alanlar.amac);
1584
+ iki("Dersin İçeriği", alanlar.icerik);
1585
+ iki("Yöntem ve Teknikler", alanlar.yontem);
1586
+ iki("Ders Notları", alanlar.notlar);
1587
+ if (alanlar.onKosul !== undefined)
1588
+ ekle(DERS_BILGILERI, "Ön Koşul", alanlar.onKosul.trim());
1589
+ if (alanlar.degerlendirme)
1590
+ ekle(DERS_BILGILERI, "Değerlendirme (ad, sayı, katkı %)", [
1591
+ ...alanlar.degerlendirme.map((x) => `${x.ad}\t${x.sayi}\t${x.katki}`),
1592
+ `Toplam\t\t${hesap.degerlendirmeToplami}`,
1593
+ ].join("\n"));
1594
+ if (hesap.isyuku)
1595
+ ekle(DERS_BILGILERI, "AKTS İş Yükü (ad, sayı, süre, toplam saat)", [
1596
+ ...hesap.isyuku.satirlar.map((x) => `${x.ad}\t${x.sayi}\t${x.sure}\t${x.toplam}`),
1597
+ `Toplam İş Yükü\t\t\t${hesap.isyuku.toplam}`,
1598
+ ...(hesap.isyuku.hedef !== null ? [`AKTS\t\t\t${ders.akts}`] : []),
1599
+ ].join("\n"));
1600
+ if (alanlar.dersYapisi)
1601
+ ekle(DERS_BILGILERI, "Ders Yapısı (alan, %)", alanlar.dersYapisi.map((x) => `${x.alan}\t${x.yuzde}`).join("\n"));
1602
+ if (alanlar.ciktilar)
1603
+ ekle("Öğrenme Çıktıları", "Öğrenme çıktıları (kod, tür, Türkçe, İngilizce)", alanlar.ciktilar.map((c) => `${c.kod}\t${c.tur}\t${c.tr.trim()}\t${c.en?.trim() ?? ""}`).join("\n"));
1604
+ if (alanlar.haftalar)
1605
+ ekle("Ders Akışı", "Haftalar (hafta, konu, İngilizce konu, ön hazırlık, dokümanlar)", alanlar.haftalar
1606
+ .map((h) => `${h.hafta}\t${h.konu.tr.trim()}\t${h.konu.en?.trim() ?? ""}\t${h.onHazirlik.trim()}\t${h.dokuman.trim()}`)
1607
+ .join("\n"));
1608
+ if (alanlar.katki) {
1609
+ const p = programCiktilari?.length
1610
+ ? programCiktilari
1611
+ : [...new Set(Object.values(alanlar.katki).flatMap((s) => Object.keys(s)))];
1612
+ ekle("Prg.Çıktısına Katkısı", "Katkı tablosu ('Tüm' satırına dokunmayın)", [
1613
+ `\t${p.join("\t")}`,
1614
+ ...Object.entries(alanlar.katki).map(([o, s]) => `${o}\t${p.map((k) => s[k] ?? "").join("\t")}`),
1615
+ ].join("\n"));
1616
+ }
1617
+ if (alanlar.ska)
1618
+ ekle("Sür. Kalkınma Amaçları", "Seçilecek amaç numaraları", alanlar.ska.join(", "));
1619
+ if (alanlar.kaynaklar?.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)
1622
+ throw new AracHatasi("Kopyalanacak alan yok. En az bir alan gönderin.");
1623
+ const b = alanBulgulari(ders, alanlar, { ingilizce, programCiktilari });
1624
+ const kalanEksikler = [...b.entries()].flatMap(([alan, xs]) => xs.filter((x) => x.duzey === "hata").map((x) => ({ alan, neden: x.aciklama })));
1625
+ const baslik = [ders.kod, ders.ad].filter(Boolean).join(" ") || "Ders";
1626
+ const sirali = [...bolumler].sort((x, y) => SEKME_SIRASI.indexOf(x.sekme) - SEKME_SIRASI.indexOf(y.sekme) || x.sira - y.sira);
1627
+ const metin = [
1628
+ `${baslik}: OBS'ye kopyalanacak alanlar`,
1629
+ "",
1630
+ ...sirali.flatMap((x, i) => [`${i + 1}. ${x.sekme} > ${x.alan}`, x.deger, ""]),
1631
+ ...(ders.yetkililer?.length
1632
+ ? [
1633
+ "Dersin Yetkilileri (sayfadaki hâliyle; değiştirmeyin, OBS'de zaten böyle kayıtlı):",
1634
+ ...ders.yetkililer.map((y) => `${y.rol}: ${y.deger}`),
1635
+ ]
1636
+ : ["Dersin Yetkilileri: bu metin onlara dokunmaz; OBS'deki hâliyle kalır."]),
1637
+ ...(kalanEksikler.length
1638
+ ? [
1639
+ "",
1640
+ "Kalan eksikler:",
1641
+ ...kalanEksikler.map((e) => `- ${ALANLAR[e.alan]?.ad ?? "Genel"}: ${e.neden}`),
1642
+ ]
1643
+ : []),
1644
+ ...(yazilamayan.length
1645
+ ? ["", "Yazma paketine alınmayanlar:", ...yazilamayan.map((y) => `- ${y.alan}: ${y.neden}`)]
1646
+ : []),
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
+ });
1661
+ return {
1662
+ sonuc: {
1663
+ metin,
1664
+ bolumler: numarali,
1665
+ degisiklikOzeti,
1666
+ yazilamayan,
1667
+ hesap,
1668
+ kalanEksikler,
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.",
1675
+ },
1676
+ kaynak: [KAYNAK_KURALLAR],
1677
+ ...(kalanEksikler.length
1678
+ ? {
1679
+ sonrakiAdim: "Kalan eksikleri hocaya numaralı gösterin ve sorun: 1) eksikleri dolduralım 2) böyle kalsın, metni kopyalayacağım.",
1680
+ }
1681
+ : {}),
1682
+ };
1683
+ },
1684
+ });
1685
+ export const BOLOGNA_ARACLARI = [
1686
+ ayristirAraci,
1687
+ alanRehberi,
1688
+ taslakDenetleAraci,
1689
+ isYukuOnerAraci,
1690
+ durumAraci,
1691
+ kopyaMetniAraci,
1692
+ obsKarsilastirAraci,
1693
+ ];