@akredit/acik-mcp 0.1.0 → 0.4.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 (29) 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 +27 -114
  4. package/dist/apps/acik-mcp/src/bologna.d.ts +209 -0
  5. package/dist/apps/acik-mcp/src/bologna.js +1321 -0
  6. package/dist/apps/acik-mcp/src/genel-ayristir.d.ts +83 -0
  7. package/dist/apps/acik-mcp/src/genel-ayristir.js +411 -0
  8. package/dist/apps/acik-mcp/src/http.js +4 -2
  9. package/dist/apps/acik-mcp/src/index.d.ts +6 -0
  10. package/dist/apps/acik-mcp/src/index.js +6 -0
  11. package/dist/apps/acik-mcp/src/kaynak-dogrula.d.ts +47 -0
  12. package/dist/apps/acik-mcp/src/kaynak-dogrula.js +319 -0
  13. package/dist/apps/acik-mcp/src/kaynak.d.ts +72 -0
  14. package/dist/apps/acik-mcp/src/kaynak.js +155 -0
  15. package/dist/apps/acik-mcp/src/kisisel-veri.d.ts +14 -2
  16. package/dist/apps/acik-mcp/src/kisisel-veri.js +63 -15
  17. package/dist/apps/acik-mcp/src/not-tablosu.d.ts +33 -0
  18. package/dist/apps/acik-mcp/src/not-tablosu.js +243 -0
  19. package/dist/apps/acik-mcp/src/promptlar.d.ts +25 -0
  20. package/dist/apps/acik-mcp/src/promptlar.js +253 -0
  21. package/dist/apps/acik-mcp/src/sunucu.d.ts +12 -4
  22. package/dist/apps/acik-mcp/src/sunucu.js +103 -24
  23. package/dist/apps/acik-mcp/src/temel.d.ts +54 -0
  24. package/dist/apps/acik-mcp/src/temel.js +10 -0
  25. package/dist/apps/acik-mcp/src/yardim.d.ts +56 -0
  26. package/dist/apps/acik-mcp/src/yardim.js +371 -0
  27. package/dist/packages/baglayicilar/src/bologna-proliz/ayristir.d.ts +29 -0
  28. package/dist/packages/baglayicilar/src/bologna-proliz/ayristir.js +200 -0
  29. package/package.json +4 -2
@@ -0,0 +1,1321 @@
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 } from "./genel-ayristir.js";
15
+ import { girisBilgisiAyikla } from "./kisisel-veri.js";
16
+ import { AracHatasi, arac, KOD_KAYNAGI } from "./temel.js";
17
+ // ---------------------------------------------------------------- Alan modeli
18
+ export const ALAN_KODLARI = [
19
+ "amac",
20
+ "icerik",
21
+ "yontem",
22
+ "notlar",
23
+ "onKosul",
24
+ "ciktilar",
25
+ "degerlendirme",
26
+ "isyuku",
27
+ "dersYapisi",
28
+ "haftalar",
29
+ "katki",
30
+ "ska",
31
+ "kaynaklar",
32
+ "yetkililer",
33
+ ];
34
+ const DERS_BILGILERI = "Ders Bilgileri";
35
+ const ORTAK_YASAK = 'Alanı "-", ".", "Yok" ile geçiştirmeyin (ön koşul hariç). Türkçe karakterleri düşürmeyin.';
36
+ /** Akredit'in Bologna doldurma kuralları (packages/cekirdek/src/bologna/taslak.ts, apps/cli/skills/bologna-doldur). */
37
+ export const ALANLAR = {
38
+ amac: {
39
+ ad: "Dersin amacı",
40
+ sekme: DERS_BILGILERI,
41
+ kurallar: [
42
+ "K1: Ders adıyla tutarlı olsun; başka dersten kopya olmasın.",
43
+ "K2: 60-120 kelime. İngilizcesi tam çeviri olsun.",
44
+ ],
45
+ sinir: { enAzKelime: 60, enCokKelime: 120 },
46
+ kaliplar: [
47
+ "Bu dersin amacı, öğrencilere [alanın temel konuları] konusunda [kazanılacak yetkinlik] kazandırmaktır.",
48
+ "Öğrenciler [yöntem] yoluyla [beceri] geliştirir. Dönem sonunda [somut iş] yapabilmeleri hedeflenir.",
49
+ ],
50
+ yasaklar: [ORTAK_YASAK],
51
+ mcpTasir: true,
52
+ },
53
+ icerik: {
54
+ ad: "Dersin içeriği",
55
+ sekme: DERS_BILGILERI,
56
+ kurallar: ["K2: 120-250 kelime. Haftalık akışla aynı konuları kapsasın.", "İngilizcesi tam çeviri olsun."],
57
+ sinir: { enAzKelime: 120, enCokKelime: 250 },
58
+ kaliplar: [
59
+ "Ders, [giriş konusu] ile başlar. Ardından [ana konular] işlenir. [Uygulama ya da proje] ile tamamlanır.",
60
+ ],
61
+ yasaklar: [ORTAK_YASAK],
62
+ mcpTasir: true,
63
+ },
64
+ yontem: {
65
+ ad: "Yöntem ve teknikler",
66
+ sekme: DERS_BILGILERI,
67
+ kurallar: ["K2: 40-90 kelime.", "K11: Başka dersin yöntem metniyle birebir aynı olmasın."],
68
+ sinir: { enAzKelime: 40, enCokKelime: 90 },
69
+ kaliplar: ["Ders [anlatım, tartışma, uygulama, proje] ile yürütülür. [Haftalık etkinlik] konuyu pekiştirir."],
70
+ yasaklar: [ORTAK_YASAK],
71
+ mcpTasir: true,
72
+ },
73
+ notlar: {
74
+ ad: "Ders notları",
75
+ sekme: DERS_BILGILERI,
76
+ kurallar: ["K3: Boş bırakılmaz.", "Hocanın gerçekten kullandığı notları yazın."],
77
+ kaliplar: ["Haftalık ders sunumları; [uygulama föyleri]; [çevrim içi kaynak adı]."],
78
+ yasaklar: [ORTAK_YASAK, "Var olmayan not ya da kaynak uydurmayın."],
79
+ mcpTasir: true,
80
+ },
81
+ onKosul: {
82
+ ad: "Ön koşul",
83
+ sekme: DERS_BILGILERI,
84
+ kurallar: ['Ön koşul yoksa "Yok" yazılabilir.'],
85
+ kaliplar: ["Yok", "[Ders kodu] [Ders adı] dersini almış olmak."],
86
+ yasaklar: [],
87
+ mcpTasir: true,
88
+ },
89
+ ciktilar: {
90
+ ad: "Öğrenme çıktıları",
91
+ sekme: "Öğrenme Çıktıları",
92
+ kurallar: [
93
+ "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
+ "İngilizcesi tam çeviri olsun.",
96
+ ],
97
+ sinir: { enAzCikti: 8, enCokCikti: 12 },
98
+ kaliplar: [
99
+ "[Konu] kavramlarını örneklerle açıklar. (Bilgi)",
100
+ "[Yöntem] kullanarak [problem türünü] çözer. (Beceri)",
101
+ "[Ürün] tasarlar ve ekip içinde sunar. (Yetkinlik)",
102
+ ],
103
+ yasaklar: ["Ölçülemeyen fiil (bilir, anlar, öğrenir, farkındalık kazanır)."],
104
+ mcpTasir: true,
105
+ },
106
+ degerlendirme: {
107
+ ad: "Değerlendirme",
108
+ sekme: DERS_BILGILERI,
109
+ kurallar: [
110
+ "Katkılar toplamı tam %100.",
111
+ "Uygulamalı derste (U ya da L sıfırdan büyük) uygulama, proje, ödev ya da laboratuvar kalemi olur.",
112
+ ],
113
+ sinir: { toplamYuzde: 100 },
114
+ 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"],
115
+ yasaklar: [],
116
+ mcpTasir: true,
117
+ },
118
+ isyuku: {
119
+ ad: "AKTS iş yükü",
120
+ sekme: DERS_BILGILERI,
121
+ kurallar: [
122
+ "K8: Toplam iş yükü (sayı x süre toplamı) = AKTS x bölen (varsayılan 30), tam eşit.",
123
+ "AKTS'yi değiştirmeyin, saat dağılımını değiştirin.",
124
+ "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.",
126
+ ],
127
+ sinir: { varsayilanBolen: 30, sayfaAraligi: "25-30 saat/AKTS" },
128
+ kaliplar: [
129
+ "Ders Süresi 14 x (T+U+L)",
130
+ "Sınıf Dışı Ders Çalışma Süresi 14 x [saat]",
131
+ "Ara Sınav Hazırlık 1 x [saat]",
132
+ "Yarıyıl Sonu Sınavı Hazırlık 1 x [saat]",
133
+ ],
134
+ yasaklar: ["Toplamı kendiniz yazmayın; bologna_taslak_denetle hesaplar."],
135
+ mcpTasir: true,
136
+ },
137
+ dersYapisi: {
138
+ ad: "Ders yapısı (alan yüzdeleri)",
139
+ sekme: DERS_BILGILERI,
140
+ kurallar: ["K6: Yüzdeler toplamı tam %100 ve içerikle tutarlı."],
141
+ sinir: { toplamYuzde: 100 },
142
+ kaliplar: ["Alan Bilgisi %60, Temel Bilim %40"],
143
+ yasaklar: [],
144
+ mcpTasir: true,
145
+ },
146
+ haftalar: {
147
+ ad: "Haftalık ders akışı",
148
+ sekme: "Ders Akışı",
149
+ kurallar: [
150
+ "14-16 hafta. Her konu en az 8 kelime.",
151
+ "K9: Her haftada ön hazırlık ve doküman dolu.",
152
+ "Ara sınav haftası (çoğunlukla 8. hafta) ve son haftada yarıyıl sonu sınavı görünür.",
153
+ "İngilizce konu dolu.",
154
+ ],
155
+ sinir: { enAzHafta: 14, enCokHafta: 16, enAzKelime: 8 },
156
+ kaliplar: ["[Konu]: [alt başlık 1], [alt başlık 2] ve örnek uygulamalar"],
157
+ yasaklar: [ORTAK_YASAK],
158
+ mcpTasir: true,
159
+ },
160
+ katki: {
161
+ ad: "Program çıktısına katkı",
162
+ sekme: "Prg.Çıktısına Katkısı",
163
+ kurallar: [
164
+ "K12: Her Ö satırı dolu; değerler 1-5 ya da boş.",
165
+ "Yalnız programın resmî P kodları kullanılır; P kodlarını Bologna sayfasından alın, ezberden yazmayın.",
166
+ "Dolu hücrelerin çoğu 5 olmasın; ayrıştırın.",
167
+ "'Tüm' satırına dokunmayın.",
168
+ ],
169
+ sinir: { enAz: 1, enCok: 5 },
170
+ kaliplar: ["Ö1: P1=4, P3=2"],
171
+ yasaklar: ["Matrisi şişirmeyin."],
172
+ mcpTasir: true,
173
+ },
174
+ ska: {
175
+ ad: "Sürdürülebilir Kalkınma Amaçları",
176
+ sekme: "Sür. Kalkınma Amaçları",
177
+ kurallar: ["K7: 1-17 arasından gerçekten ilgili 2-4 amaç."],
178
+ sinir: { enAz: 2, enCok: 4 },
179
+ kaliplar: ["4 (Nitelikli eğitim) ve dersle doğrudan ilgili 1-3 amaç"],
180
+ yasaklar: [],
181
+ mcpTasir: true,
182
+ },
183
+ kaynaklar: {
184
+ ad: "Kaynaklar",
185
+ sekme: "Diğer Kaynaklar",
186
+ kurallar: [
187
+ "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.",
189
+ "Yazar adları yayımlanmış künyedir; bu alanda serbesttir. E-posta ve telefon yazılmaz.",
190
+ ],
191
+ sinir: { enAz: 2 },
192
+ kaliplar: [
193
+ "Soyad, A. (Yıl). Eser adı (Baskı). Yayınevi.",
194
+ "Soyad, A. (Yıl). Makale adı. Dergi, Cilt(Sayı), sayfa.",
195
+ ],
196
+ yasaklar: [
197
+ "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.",
198
+ "Emin olunmayan ISBN, yayınevi ya da yıl yazılmaz; bilinmeyen bilgi EKSİK kalır.",
199
+ ],
200
+ mcpTasir: true,
201
+ },
202
+ yetkililer: {
203
+ ad: "Dersin yetkilileri",
204
+ sekme: "Dersin Yetkilileri",
205
+ kurallar: [
206
+ "K10: Koordinatör ve dersi veren hocanın kendisidir; başka hocanın adı yazılmaz.",
207
+ "Sayfada yayımlanmış görev bilgisi (Dersi Verenler, Yardımcılar, Koordinatör) olduğu gibi okunur ve korunur.",
208
+ ],
209
+ kaliplar: [],
210
+ yasaklar: ["Bu satırlar değiştirilmez, kısaltılmaz, yeniden yazılmaz; OBS'ye geri yazılırken aynen kalır."],
211
+ mcpTasir: true,
212
+ },
213
+ };
214
+ /** Bloom'un yenilenmiş sınıflaması düzeylerine göre örnek fiiller. Akredit örneğidir, resmî liste değildir. */
215
+ export const BLOOM_FIILLERI = [
216
+ { duzey: "Hatırlama", fiiller: ["tanımlar", "listeler", "sayar", "adlandırır"] },
217
+ { duzey: "Anlama", fiiller: ["açıklar", "özetler", "sınıflandırır", "örneklendirir"] },
218
+ { duzey: "Uygulama", fiiller: ["uygular", "hesaplar", "kullanır", "çözer"] },
219
+ { duzey: "Çözümleme", fiiller: ["çözümler", "karşılaştırır", "ayırt eder", "ilişkilendirir"] },
220
+ { duzey: "Değerlendirme", fiiller: ["değerlendirir", "eleştirir", "gerekçelendirir", "savunur"] },
221
+ { duzey: "Yaratma", fiiller: ["tasarlar", "geliştirir", "oluşturur", "planlar"] },
222
+ ];
223
+ const RESMI_DAYANAK = {
224
+ durum: DOGRULANMADI_ISARETI,
225
+ 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.",
226
+ };
227
+ 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.";
228
+ const KAYNAK_KURALLAR = KOD_KAYNAGI("packages/cekirdek/src/bologna/taslak.ts", "Akredit Bologna taslak kuralları");
229
+ const KAYNAK_SAYFA = KOD_KAYNAGI("packages/cekirdek/src/bologna/denetim.ts", "Akredit Bologna sayfa denetim kuralları");
230
+ 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";
233
+ // ---------------------------------------------------------------- Ortak hesap ve eşleme
234
+ const TUL = z
235
+ .string()
236
+ .trim()
237
+ .regex(/^\d{1,2}\+\d{1,2}\+\d{1,2}$/u, "T+U+L '2+1+0' biçiminde olmalı.");
238
+ const tulSaati = (tul) => {
239
+ if (!tul || !/^\d+\+\d+\+\d+$/u.test(tul.trim()))
240
+ return null;
241
+ return tul
242
+ .split("+")
243
+ .map(Number)
244
+ .reduce((t, x) => t + x, 0);
245
+ };
246
+ const yuvarla = (x, b = 1) => Math.round(x * 10 ** b) / 10 ** b;
247
+ /** Bir taslak bulgusunun hangi alana ait olduğu. "genel" birden çok alana değen bulgudur. */
248
+ export function bulguAlani(b) {
249
+ switch (b.kural) {
250
+ case "Temel":
251
+ return "temel";
252
+ case "Değerlendirme":
253
+ return "degerlendirme";
254
+ case "K8":
255
+ return "isyuku";
256
+ case "K6":
257
+ return "dersYapisi";
258
+ case "K4":
259
+ case "K5":
260
+ return "ciktilar";
261
+ case "Ders akışı":
262
+ case "K9":
263
+ return "haftalar";
264
+ case "Kaynaklar":
265
+ return "kaynaklar";
266
+ case "K12":
267
+ return "katki";
268
+ case "K7":
269
+ return "ska";
270
+ case "K10":
271
+ return "yetkililer";
272
+ case "K2":
273
+ case "K3": {
274
+ const a = b.aciklama;
275
+ if (/^Amaç/u.test(a))
276
+ return "amac";
277
+ if (/^İçerik/u.test(a))
278
+ return "icerik";
279
+ if (/^Yöntem/u.test(a))
280
+ return "yontem";
281
+ if (/^Ders notları/u.test(a))
282
+ return "notlar";
283
+ if (/^Ö\d+/u.test(a))
284
+ return "ciktilar";
285
+ if (/^\d+\. hafta/u.test(a))
286
+ return "haftalar";
287
+ return "genel";
288
+ }
289
+ default:
290
+ return "genel";
291
+ }
292
+ }
293
+ // ---------------------------------------------------------------- Taslak alanları şeması
294
+ const IkiDilli = z.object({
295
+ tr: z.string().max(6000),
296
+ en: z.string().max(6000).optional(),
297
+ });
298
+ const TaslakAlanlari = z.object({
299
+ amac: IkiDilli.optional(),
300
+ icerik: IkiDilli.optional(),
301
+ yontem: IkiDilli.optional(),
302
+ notlar: IkiDilli.optional(),
303
+ onKosul: z.string().max(500).optional(),
304
+ ciktilar: z
305
+ .array(z.object({
306
+ kod: z.string().trim().max(10).describe("Ö1, Ö2, ..."),
307
+ tr: z.string().max(600),
308
+ en: z.string().max(600).optional(),
309
+ tur: z.enum(["Bilgi", "Beceri", "Yetkinlik"]),
310
+ }))
311
+ .max(20)
312
+ .optional(),
313
+ degerlendirme: z
314
+ .array(z.object({ ad: z.string().max(100), sayi: z.number().int().min(0).max(50), katki: z.number().min(0).max(100) }))
315
+ .max(20)
316
+ .optional(),
317
+ isyuku: z
318
+ .array(z.object({ ad: z.string().max(100), sayi: z.number().min(0).max(100), sure: z.number().min(0).max(200) }))
319
+ .max(20)
320
+ .optional()
321
+ .describe("Satır başına sayı ve süre; toplamı sunucu hesaplar"),
322
+ dersYapisi: z
323
+ .array(z.object({ alan: z.string().max(100), yuzde: z.number().min(0).max(100) }))
324
+ .max(10)
325
+ .optional(),
326
+ haftalar: z
327
+ .array(z.object({
328
+ hafta: z.number().int().min(1).max(20),
329
+ konu: IkiDilli,
330
+ onHazirlik: z.string().max(500),
331
+ dokuman: z.string().max(500),
332
+ }))
333
+ .max(20)
334
+ .optional(),
335
+ katki: z
336
+ .record(z.string().max(10), z.record(z.string().max(10), z.union([z.number().int().min(1).max(5), z.null(), z.literal("")])))
337
+ .optional()
338
+ .describe("Ö kodu, P kodu, 1-5 ya da boş"),
339
+ ska: z.array(z.number().int().min(1).max(17)).max(17).optional(),
340
+ kaynaklar: z
341
+ .array(z.string().trim().min(5).max(600))
342
+ .max(30)
343
+ .optional()
344
+ .describe("Künyeler, her biri tek satır (ör. kaynak_bicimle çıktısı). Yazar adı künyedir, serbesttir."),
345
+ kaynakSayisi: z
346
+ .number()
347
+ .int()
348
+ .min(0)
349
+ .max(50)
350
+ .optional()
351
+ .describe("Hocanın verdiği gerçek kaynak sayısı (yazar ve eser adı olan). Adlar gönderilmez."),
352
+ });
353
+ const YetkiliSemasi = z.object({ rol: z.string().max(80), deger: z.string().max(1000) });
354
+ const DersTemeli = z.object({
355
+ kod: z.string().trim().max(30).optional(),
356
+ ad: z.string().trim().max(200).optional(),
357
+ akts: z.number().min(0).max(60).optional(),
358
+ 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"),
360
+ yetkililer: z
361
+ .array(YetkiliSemasi)
362
+ .max(20)
363
+ .optional()
364
+ .describe("Sayfadaki görev satırları (Dersi Verenler, Koordinatör); olduğu gibi korunur"),
365
+ });
366
+ const ikiDil = (m, ingilizce) => m ? { tr: m.tr, en: m.en ?? (ingilizce ? "" : m.tr) } : undefined;
367
+ /** Taslak alanlarını cekirdek kurallarıyla denetler; yalnız verilen alanların bulgularını alan alan döndürür. */
368
+ export function alanBulgulari(ders, alanlar, s) {
369
+ const t = {
370
+ ...(ders.kod ? { kod: ders.kod } : {}),
371
+ ...(ders.ad ? { ad: ders.ad } : {}),
372
+ ...(ders.akts !== undefined ? { akts: ders.akts } : {}),
373
+ ...(ders.tul ? { tul: ders.tul } : {}),
374
+ bolen: ders.bolen,
375
+ ...(alanlar.amac ? { amac: ikiDil(alanlar.amac, s.ingilizce) } : {}),
376
+ ...(alanlar.icerik ? { icerik: ikiDil(alanlar.icerik, s.ingilizce) } : {}),
377
+ ...(alanlar.yontem ? { yontem: ikiDil(alanlar.yontem, s.ingilizce) } : {}),
378
+ ...(alanlar.notlar ? { notlar: ikiDil(alanlar.notlar, s.ingilizce) } : {}),
379
+ ...(alanlar.ciktilar
380
+ ? { ciktilar: alanlar.ciktilar.map((c) => ({ ...c, en: c.en ?? (s.ingilizce ? "" : c.tr) })) }
381
+ : {}),
382
+ ...(alanlar.degerlendirme ? { degerlendirme: alanlar.degerlendirme } : {}),
383
+ ...(alanlar.isyuku ? { isyuku: alanlar.isyuku } : {}),
384
+ ...(alanlar.dersYapisi ? { dersYapisi: alanlar.dersYapisi } : {}),
385
+ ...(alanlar.haftalar
386
+ ? {
387
+ haftalar: alanlar.haftalar.map((h) => ({
388
+ ...h,
389
+ konu: { tr: h.konu.tr, en: h.konu.en ?? (s.ingilizce ? "" : h.konu.tr) },
390
+ })),
391
+ }
392
+ : {}),
393
+ ...(alanlar.katki ? { katki: alanlar.katki } : {}),
394
+ ...(alanlar.ska ? { ska: alanlar.ska } : {}),
395
+ ...(alanlar.kaynaklar !== undefined || alanlar.kaynakSayisi !== undefined
396
+ ? {
397
+ kaynaklar: Array.from({ length: alanlar.kaynaklar?.length ?? alanlar.kaynakSayisi ?? 0 }, () => ({
398
+ yazar: "var",
399
+ eser: "var",
400
+ })),
401
+ }
402
+ : {}),
403
+ programCiktilari: s.programCiktilari ?? [],
404
+ };
405
+ const verilen = new Set(Object.keys(alanlar).filter((k) => alanlar[k] !== undefined));
406
+ if (verilen.has("kaynakSayisi"))
407
+ verilen.add("kaynaklar");
408
+ const sonuc = new Map();
409
+ const ekle = (a, b) => sonuc.set(a, [...(sonuc.get(a) ?? []), b]);
410
+ if (verilen.has("kaynaklar") && alanlar.kaynaklar)
411
+ for (const [i, k] of alanlar.kaynaklar.entries())
412
+ if (!KUNYE_YILI.test(k))
413
+ ekle("kaynaklar", {
414
+ duzey: "uyari",
415
+ kural: "Kaynaklar",
416
+ aciklama: `${i + 1}. kaynakta yıl görünmüyor. Yılı hocaya sorun ya da kaynak_bicimle ile biçimlendirin.`,
417
+ });
418
+ // İş yükü AKTS olmadan denetlenemez; cekirdek "akts boş" diye temel bulgu verir, burada alana bağlanır.
419
+ if (verilen.has("isyuku") && ders.akts === undefined)
420
+ ekle("isyuku", {
421
+ duzey: "hata",
422
+ kural: "K8",
423
+ aciklama: "Dersin AKTS değeri verilmedi; iş yükü hedefi hesaplanamaz.",
424
+ });
425
+ for (const b of taslakDenetle(t, { bolen: ders.bolen }).bulgular) {
426
+ const a = bulguAlani(b);
427
+ if (a === "temel" || a === "yetkililer")
428
+ continue;
429
+ if (a === "isyuku" && ders.akts === undefined)
430
+ continue;
431
+ if (!s.ingilizce && /İngilizce/u.test(b.aciklama))
432
+ continue;
433
+ if (a === "genel") {
434
+ if (verilen.size)
435
+ ekle("genel", b);
436
+ continue;
437
+ }
438
+ if (!verilen.has(a))
439
+ continue;
440
+ const temiz = b.kural === "K12"
441
+ ? {
442
+ ...b,
443
+ aciklama: b.aciklama.replace(" Önce bologna-al ile oku.", " Bologna sayfasındaki P kodlarını programCiktilari olarak verin."),
444
+ }
445
+ : b;
446
+ ekle(a, temiz);
447
+ }
448
+ return sonuc;
449
+ }
450
+ const KUNYE_YILI = /\((?:\d{4}[a-z]?|t\.y\.|n\.d\.|baskıda|in press)\)|\b(1[5-9]|20)\d{2}\b/iu;
451
+ /** Bütün sayılar burada hesaplanır; asistan toplam yazmaz (ilke 2). */
452
+ export function bolognaHesabi(ders, alanlar) {
453
+ const saat = tulSaati(ders.tul);
454
+ const hedef = ders.akts !== undefined ? ders.akts * ders.bolen : null;
455
+ const isyuku = alanlar.isyuku
456
+ ? (() => {
457
+ const satirlar = alanlar.isyuku.map((x) => ({
458
+ ad: x.ad,
459
+ sayi: x.sayi,
460
+ sure: x.sure,
461
+ toplam: yuvarla(x.sayi * x.sure, 2),
462
+ }));
463
+ const toplam = yuvarla(satirlar.reduce((t, x) => t + x.toplam, 0), 2);
464
+ const dersSuresi = alanlar.isyuku.find((x) => /ders süresi/iu.test(x.ad));
465
+ return {
466
+ satirlar,
467
+ toplam,
468
+ hedef,
469
+ fark: hedef === null ? null : yuvarla(hedef - toplam, 2),
470
+ aktsBasinaSaat: ders.akts ? yuvarla(toplam / ders.akts) : null,
471
+ dersSuresiHaftalikBeklenen: saat,
472
+ dersSuresiHaftalikVerilen: dersSuresi ? dersSuresi.sure : null,
473
+ aciklama: hedef === null
474
+ ? "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.`,
478
+ };
479
+ })()
480
+ : null;
481
+ const toplamYuzde = (xs) => xs
482
+ ? yuvarla(xs.reduce((t, x) => t + x.v, 0), 2)
483
+ : null;
484
+ return {
485
+ isyuku,
486
+ degerlendirmeToplami: toplamYuzde(alanlar.degerlendirme?.map((x) => ({ v: x.katki }))),
487
+ dersYapisiToplami: toplamYuzde(alanlar.dersYapisi?.map((x) => ({ v: x.yuzde }))),
488
+ ciktiSayisi: alanlar.ciktilar?.length ?? null,
489
+ haftaSayisi: alanlar.haftalar?.length ?? null,
490
+ };
491
+ }
492
+ // ---------------------------------------------------------------- bologna_denetle (genişletilmiş)
493
+ const sayiYaDaBos = z.number().nullable().default(null);
494
+ const BolognaDersGirdisi = z.object({
495
+ kod: z.string().trim().min(1).max(30),
496
+ ad: z.string().max(200).optional(),
497
+ zorunlu: z.boolean().default(true),
498
+ yariyil: z.number().int().min(1).max(16).nullable().default(null),
499
+ tul: z.string().max(20).optional().describe("Teori+Uygulama+Laboratuvar, ör. '2+1+0'"),
500
+ kredi: sayiYaDaBos,
501
+ akts: sayiYaDaBos,
502
+ guncelleme: z.string().max(20).optional().describe("Son güncelleme tarihi, GG.AA.YYYY"),
503
+ amac: z.string().max(6000).optional().describe("Dersin amacı (Türkçe)"),
504
+ icerik: z.string().max(6000).optional().describe("Dersin içeriği (Türkçe)"),
505
+ yontem: z.string().max(6000).optional().describe("Yöntem ve teknikler (Türkçe)"),
506
+ notlar: z.string().max(6000).optional().describe("Ders notları (Türkçe)"),
507
+ kaynakSayisi: z.number().int().min(0).max(50).optional().describe("Sayfadaki kaynak sayısı"),
508
+ kaynaklar: z
509
+ .array(z.string().max(600))
510
+ .max(30)
511
+ .optional()
512
+ .describe("Sayfadaki künyeler; yazar adları künyedir, serbesttir"),
513
+ yetkililer: z.array(YetkiliSemasi).max(20).optional().describe("Sayfadaki görev satırları, olduğu gibi"),
514
+ ciktilar: z
515
+ .array(z.object({ kod: z.string().max(20), metin: z.string().max(1000) }))
516
+ .max(40)
517
+ .default([]),
518
+ degerlendirme: z
519
+ .array(z.object({ ad: z.string().max(200), sayi: sayiYaDaBos, katki: sayiYaDaBos }))
520
+ .max(40)
521
+ .default([]),
522
+ isyuku: z
523
+ .array(z.object({ ad: z.string().max(200), sayi: sayiYaDaBos, sure: sayiYaDaBos, toplam: sayiYaDaBos }))
524
+ .max(40)
525
+ .default([]),
526
+ dersYapisi: z
527
+ .array(z.object({ alan: z.string().max(200), yuzde: sayiYaDaBos }))
528
+ .max(20)
529
+ .default([]),
530
+ haftalar: z.number().int().min(0).max(30).default(0).describe("Haftalık konu satırı sayısı"),
531
+ katki: z
532
+ .record(z.string().max(20), z.record(z.string().max(20), z.string().max(5)))
533
+ .default({})
534
+ .describe("Ders çıktısı ya da 'Tüm' satırı, program çıktısı kodu, katkı değeri ('1'-'5', '' ya da '-')"),
535
+ });
536
+ /** Yayımlanmış sayfa verisinden alan alan durum çıkarır (boş, zayıf, tamam). */
537
+ function alanDurumlari(d, sayfaBulgulari) {
538
+ const tr = (m) => (m?.trim() ? { tr: m } : undefined);
539
+ const alanlar = {
540
+ ...(tr(d.amac) ? { amac: tr(d.amac) } : {}),
541
+ ...(tr(d.icerik) ? { icerik: tr(d.icerik) } : {}),
542
+ ...(tr(d.yontem) ? { yontem: tr(d.yontem) } : {}),
543
+ ...(tr(d.notlar) ? { notlar: tr(d.notlar) } : {}),
544
+ ...(d.ciktilar.length
545
+ ? { ciktilar: d.ciktilar.map((c) => ({ kod: c.kod, tr: c.metin, tur: "Beceri" })) }
546
+ : {}),
547
+ ...(d.kaynakSayisi !== undefined || d.kaynaklar
548
+ ? { kaynakSayisi: d.kaynaklar?.length ?? d.kaynakSayisi ?? 0 }
549
+ : {}),
550
+ };
551
+ const b = alanBulgulari({ akts: d.akts ?? undefined, bolen: 30 }, alanlar, { ingilizce: false });
552
+ // Sayfada çıktı türü görünmez; türle ilgili kurallar burada denetlenmez.
553
+ const turKurali = /Yetkinlik|Beceri|türü/u;
554
+ const nedenler = (a) => (b.get(a) ?? []).filter((x) => x.duzey === "hata" && !turKurali.test(x.aciklama)).map((x) => x.aciklama);
555
+ const sayfa = (re) => sayfaBulgulari.filter((x) => re.test(x.kural)).map((x) => x.aciklama);
556
+ const metinAlan = (a, deger) => deger === undefined
557
+ ? ["bilinmiyor", ["Bu alan gönderilmedi."]]
558
+ : !deger.trim()
559
+ ? ["bos", []]
560
+ : nedenler(a).length
561
+ ? ["zayif", nedenler(a)]
562
+ : ["tamam", []];
563
+ const liste = [
564
+ ["amac", ...metinAlan("amac", d.amac)],
565
+ ["icerik", ...metinAlan("icerik", d.icerik)],
566
+ ["yontem", ...metinAlan("yontem", d.yontem)],
567
+ ["notlar", ...metinAlan("notlar", d.notlar)],
568
+ [
569
+ "ciktilar",
570
+ ...(() => {
571
+ if (!d.ciktilar.length)
572
+ return ["bos", []];
573
+ const n = [...nedenler("ciktilar"), ...sayfa(/^ders-ciktisi$/u)];
574
+ return n.length ? ["zayif", n] : ["tamam", []];
575
+ })(),
576
+ ],
577
+ [
578
+ "degerlendirme",
579
+ ...(() => {
580
+ if (!d.degerlendirme.length)
581
+ return ["bos", []];
582
+ const n = sayfa(/^degerlendirme$/u);
583
+ return n.length ? ["zayif", n] : ["tamam", []];
584
+ })(),
585
+ ],
586
+ [
587
+ "isyuku",
588
+ ...(() => {
589
+ if (!d.isyuku.length)
590
+ return ["bos", []];
591
+ const n = sayfa(/^akts-isyuku$/u);
592
+ return n.length ? ["zayif", n] : ["tamam", []];
593
+ })(),
594
+ ],
595
+ [
596
+ "dersYapisi",
597
+ ...(() => {
598
+ if (!d.dersYapisi.length)
599
+ return ["bos", []];
600
+ const n = sayfa(/^ders-yapisi$/u);
601
+ return n.length ? ["zayif", n] : ["tamam", []];
602
+ })(),
603
+ ],
604
+ [
605
+ "haftalar",
606
+ ...(() => {
607
+ if (!d.haftalar)
608
+ return ["bos", []];
609
+ if (d.haftalar < 14 || d.haftalar > 16)
610
+ return ["zayif", [`${d.haftalar} hafta var, 14-16 olmalı.`]];
611
+ return ["tamam", []];
612
+ })(),
613
+ ],
614
+ [
615
+ "katki",
616
+ ...(() => {
617
+ const satirlar = Object.keys(d.katki).filter((k) => /^Ö\d+$/u.test(k));
618
+ if (!satirlar.length)
619
+ return ["bos", []];
620
+ const bosSatir = satirlar.filter((k) => !Object.values(d.katki[k] ?? {}).some((v) => v && v !== "-"));
621
+ const n = [
622
+ ...(bosSatir.length ? [`${bosSatir.join(", ")} satırı tamamen boş.`] : []),
623
+ ...sayfa(/^(katki-destegi|matris-tutarlilik)$/u),
624
+ ];
625
+ return n.length ? ["zayif", n] : ["tamam", []];
626
+ })(),
627
+ ],
628
+ [
629
+ "kaynaklar",
630
+ ...(() => {
631
+ const n = d.kaynaklar?.length ?? d.kaynakSayisi;
632
+ if (n === undefined)
633
+ return ["bilinmiyor", ["Kaynaklar gönderilmedi."]];
634
+ if (n === 0)
635
+ return ["bos", []];
636
+ return n < 2 ? ["zayif", [`${n} kaynak var, en az 2 olmalı.`]] : ["tamam", []];
637
+ })(),
638
+ ],
639
+ ];
640
+ return liste.map(([alan, durum, n], i) => ({
641
+ no: i + 1,
642
+ alan,
643
+ ad: ALANLAR[alan].ad,
644
+ sekme: ALANLAR[alan].sekme,
645
+ durum,
646
+ nedenler: n,
647
+ kural: ALANLAR[alan].kurallar.join(" "),
648
+ }));
649
+ }
650
+ export const bolognaDenetleAraci = arac({
651
+ ad: "bologna_denetle",
652
+ baslik: "Bologna ders paketini denetle",
653
+ 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.",
655
+ kisiselVeriKapsami: BOLOGNA_KAPSAMI,
656
+ kunyeAlanlari: ["kaynaklar"],
657
+ girdi: z.object({
658
+ ders: BolognaDersGirdisi,
659
+ programCiktilari: z
660
+ .array(z.object({ kod: z.string().max(20), metin: z.string().max(1000) }))
661
+ .max(40)
662
+ .optional()
663
+ .describe("Program çıktıları; verilirse program düzeyindeki kurallar da çalışır"),
664
+ matrisSatiri: z
665
+ .record(z.string().max(20), z.string().max(5))
666
+ .optional()
667
+ .describe("Program matrisinde bu dersin satırı: program çıktısı kodu ve değer"),
668
+ aktsSaat: z
669
+ .tuple([z.number().min(1), z.number().max(60)])
670
+ .optional()
671
+ .describe("AKTS başına saat aralığı; varsayılan [25, 30]"),
672
+ }),
673
+ calistir: ({ ders, programCiktilari, matrisSatiri, aktsSaat }, b) => {
674
+ const { amac: _a, icerik: _i, yontem: _y, notlar: _n, kaynakSayisi: _k, kaynaklar: _kk, ...sayfaAlanlari } = ders;
675
+ const sayfa = {
676
+ ...sayfaAlanlari,
677
+ ad: ders.ad ?? ders.kod,
678
+ bilgiler: {},
679
+ };
680
+ const pkodlar = Object.keys(matrisSatiri ?? {});
681
+ const veri = {
682
+ adres: "kullanici-girdisi",
683
+ okundu: b.simdi().toISOString(),
684
+ programCiktilari: programCiktilari ?? [],
685
+ matris: {
686
+ pkodlar,
687
+ dersler: matrisSatiri
688
+ ? [
689
+ {
690
+ kod: ders.kod,
691
+ ad: ders.ad ?? ders.kod,
692
+ zorunlu: ders.zorunlu,
693
+ grup: null,
694
+ yariyil: ders.yariyil,
695
+ grupBasligi: false,
696
+ degerler: matrisSatiri,
697
+ },
698
+ ]
699
+ : [],
700
+ },
701
+ tyycIsaret: null,
702
+ dersler: [{ ...sayfa, zorunlu: ders.zorunlu, bos: false, kaynak: "kullanici-girdisi" }],
703
+ };
704
+ let bulgular = bolognaDenetle(veri, { simdi: b.simdi().getTime(), ...(aktsSaat ? { aktsSaat } : {}) });
705
+ // Program düzeyi kuralları yalnız program çıktıları verildiyse anlamlıdır.
706
+ if (!programCiktilari)
707
+ bulgular = bulgular.filter((x) => x.konu !== "-");
708
+ const alanlar = alanDurumlari(ders, bulgular);
709
+ const doldurulacak = alanlar.filter((a) => a.durum === "bos" || a.durum === "zayif");
710
+ return {
711
+ sonuc: {
712
+ ders: ders.kod,
713
+ hataSayisi: bulgular.filter((x) => x.duzey === "hata").length,
714
+ uyariSayisi: bulgular.filter((x) => x.duzey === "uyari").length,
715
+ ozet: doldurulacak.length
716
+ ? `${doldurulacak.length} alan doldurulmalı ya da iyileştirilmeli: ${doldurulacak.map((a) => `${a.no}) ${a.ad}`).join(", ")}.`
717
+ : "Gönderilen alanlarda boş ya da zayıf alan bulunmadı.",
718
+ alanlar,
719
+ doldurulacak: doldurulacak.map((a) => a.no),
720
+ kalanAlanSayisi: doldurulacak.length,
721
+ dokunulmayacak: ["Dersin Yetkilileri kişi adı içerir; hoca OBS'de kendisi yazar."],
722
+ bulgular,
723
+ not: "Kurallar Akredit'in iç kurallarıdır, resmî ölçüt değildir. Düzeltme kararını ders sorumlusu ve komisyon verir.",
724
+ },
725
+ kaynak: [KAYNAK_SAYFA, KAYNAK_KURALLAR],
726
+ ...(doldurulacak.length
727
+ ? {}
728
+ : { sonrakiAdim: "Hocaya sorun: 1) başka bir ders 2) OBS'ye kopya metni (bologna_kopya_metni) 3) bitir." }),
729
+ };
730
+ },
731
+ });
732
+ // ---------------------------------------------------------------- bologna_alan_rehberi
733
+ const alanRehberi = arac({
734
+ ad: "bologna_alan_rehberi",
735
+ baslik: "Bologna alanı nasıl doldurulur",
736
+ 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.",
738
+ girdi: z.object({
739
+ alan: z.enum([...ALAN_KODLARI, "hepsi"]).describe("Alan kodu ya da 'hepsi'"),
740
+ akts: z.number().min(0).max(60).optional(),
741
+ tul: TUL.optional().describe("ör. '2+1+0'"),
742
+ 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"),
744
+ }),
745
+ calistir: ({ alan, akts, tul, hafta, bolen }) => {
746
+ const secilen = alan === "hepsi" ? [...ALAN_KODLARI] : [alan];
747
+ const saat = tulSaati(tul);
748
+ const hesap = secilen.includes("isyuku") && akts !== undefined
749
+ ? (() => {
750
+ const hedef = akts * bolen;
751
+ const dersSuresi = saat === null ? null : hafta * saat;
752
+ return {
753
+ hedefSaat: hedef,
754
+ formul: `${akts} AKTS x ${bolen} = ${hedef} saat`,
755
+ dersSuresiSatiri: saat === null ? null : { sayi: hafta, sure: saat, toplam: dersSuresi },
756
+ kalanSaat: dersSuresi === null ? null : hedef - dersSuresi,
757
+ not: dersSuresi === null
758
+ ? "T+U+L verilmedi; ders süresi satırı hesaplanmadı."
759
+ : `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.`,
760
+ };
761
+ })()
762
+ : null;
763
+ return {
764
+ sonuc: {
765
+ alanlar: secilen.map((a) => ({
766
+ alan: a,
767
+ ...ALANLAR[a],
768
+ ...(a === "ciktilar" ? { bloomFiilleri: BLOOM_FIILLERI } : {}),
769
+ })),
770
+ ...(hesap ? { isyukuHesabi: hesap } : {}),
771
+ secenekUretimi: SECENEK_KURALI,
772
+ resmiDayanak: RESMI_DAYANAK,
773
+ },
774
+ kaynak: [
775
+ KAYNAK_KURALLAR,
776
+ KOD_KAYNAGI("apps/cli/skills/bologna-doldur/SKILL.md", "Akredit Bologna doldurma rehberi"),
777
+ ],
778
+ ...(secilen.length === 1 && secilen[0] === "yetkililer"
779
+ ? {
780
+ sonrakiAdim: "Bu alan sayfadaki görev bilgisidir; olduğu gibi korunur, değiştirmeyin. Sonraki alana geçin.",
781
+ }
782
+ : {}),
783
+ };
784
+ },
785
+ });
786
+ // ---------------------------------------------------------------- bologna_taslak_denetle
787
+ const taslakDenetleAraci = arac({
788
+ ad: "bologna_taslak_denetle",
789
+ 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.",
792
+ kisiselVeriKapsami: BOLOGNA_KAPSAMI,
793
+ kunyeAlanlari: ["kaynaklar"],
794
+ girdi: z.object({
795
+ ders: DersTemeli,
796
+ secenekler: z
797
+ .array(z.object({ etiket: z.string().trim().min(1).max(3).describe("A, B, C"), alanlar: TaslakAlanlari }))
798
+ .min(1)
799
+ .max(3),
800
+ 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"),
802
+ }),
803
+ calistir: ({ ders, secenekler, programCiktilari, ingilizce }) => {
804
+ const sonuclar = secenekler.map((s) => {
805
+ const b = alanBulgulari(ders, s.alanlar, { ingilizce, programCiktilari });
806
+ const hepsi = [...b.entries()].flatMap(([alan, xs]) => xs.map((x) => ({ alan, ...x })));
807
+ const eksikler = hepsi
808
+ .filter((x) => x.duzey === "hata")
809
+ .map((x) => ({ alan: x.alan, kural: x.kural, neden: x.aciklama }));
810
+ return {
811
+ etiket: s.etiket,
812
+ gecer: eksikler.length === 0,
813
+ eksikler,
814
+ uyarilar: hepsi
815
+ .filter((x) => x.duzey !== "hata")
816
+ .map((x) => ({ alan: x.alan, kural: x.kural, neden: x.aciklama })),
817
+ hesap: bolognaHesabi(ders, s.alanlar),
818
+ };
819
+ });
820
+ return {
821
+ sonuc: {
822
+ gecenler: sonuclar.filter((x) => x.gecer).map((x) => x.etiket),
823
+ secenekler: sonuclar,
824
+ not: "Geçer, Akredit kurallarına uyuyor demektir; içeriğin doğruluğuna ve derse uygunluğuna hoca karar verir.",
825
+ resmiDayanak: RESMI_DAYANAK,
826
+ },
827
+ kaynak: [KAYNAK_KURALLAR],
828
+ ...(sonuclar.some((x) => x.gecer)
829
+ ? {}
830
+ : {
831
+ sonrakiAdim: "Hiçbir seçenek geçmedi. Eksik nedenlerine göre düzeltip yeniden denetleyin; hocaya henüz göstermeyin.",
832
+ }),
833
+ };
834
+ },
835
+ });
836
+ // ---------------------------------------------------------------- bologna_ayristir
837
+ /** Bologna sayfasında görev bilgisi taşıyan satırlar (Dersi Verenler, Koordinatör...): ayrı alan olarak korunur. */
838
+ 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;
839
+ /** Kaynak satırı: ilk hücre bu etiketlerden biriyse satır künye olarak ayrılır. */
840
+ const KAYNAK_SATIRI = /^(Kaynaklar|Kaynak|Ders Kitabı|Önerilen Kaynaklar|Diğer Kaynaklar|Yardımcı Kaynaklar|Resources|References|Textbooks?|Recommended Sources|Reading List)(\s|:|$)/iu;
841
+ const hucreMetni = (h) => h
842
+ .replace(/<[^>]*>/gu, " ")
843
+ .replace(/&nbsp;/gu, " ")
844
+ .replace(/\s+/gu, " ")
845
+ .trim();
846
+ /** Proliz sayfası mı: üst bilgi tablosu ("Yarıyıl, Kodu") ya da program çıktıları tablosu. */
847
+ export function prolizMi(html) {
848
+ const t = tablolar(html);
849
+ return (t.some((x) => /Yarıyıl.*Kodu|Semester.*Code/iu.test((x[0] ?? []).join(" "))) || programCiktilari(html).length > 0);
850
+ }
851
+ /**
852
+ * Yapıştırılan HTML'den görev satırlarını (Dersi Verenler, Yardımcılar, Koordinatör) yetkililer listesine, kaynak
853
+ * satırlarını künye listesine ayırır. Değerler olduğu gibi korunur; hiçbir şey atılmaz.
854
+ */
855
+ export function bolognaSatirAyikla(html) {
856
+ const yetkililer = [];
857
+ const kaynaklar = [];
858
+ const h = html.replace(/<tr\b[\s\S]*?<\/tr>/giu, (tr) => {
859
+ const hucreler = [...tr.matchAll(/<t[dh]\b[^>]*>([\s\S]*?)<\/t[dh]>/giu)].map((m) => m[1] ?? "");
860
+ const etiket = hucreler[0] ? hucreMetni(hucreler[0]) : "";
861
+ if (YETKILI_SATIRI.test(etiket) && hucreler.length > 1) {
862
+ const deger = hucreler
863
+ .slice(1)
864
+ .map((x) => hucreMetni(x.replace(/<br\s*\/?>/giu, ", ")).replace(/(\s*,\s*)+$/u, ""))
865
+ .filter(Boolean)
866
+ .join(" ");
867
+ if (deger)
868
+ yetkililer.push({ rol: etiket.replace(/:$/u, "").slice(0, 80), deger: deger.slice(0, 1000) });
869
+ return "";
870
+ }
871
+ if (KAYNAK_SATIRI.test(etiket) && hucreler.length > 1) {
872
+ for (const hucre of hucreler.slice(1))
873
+ for (const parca of hucre.split(/<br\s*\/?>|<\/li>|<\/p>|\n|;\s+(?=\S)/iu)) {
874
+ const k = hucreMetni(parca).replace(/^\s*(\[\d+\]|\d{1,2}[.)-]|[-•*–])\s*/u, "");
875
+ if (k.length >= 5)
876
+ kaynaklar.push(k);
877
+ }
878
+ return "";
879
+ }
880
+ return tr;
881
+ });
882
+ return { html: h, yetkililer, kaynaklar };
883
+ }
884
+ /** bologna_ayristir ön ayıklaması: yalnız giriş bilgisi (oturum kimliği, parola kutusu, çerez) atılır. */
885
+ export function ayristirOnAyikla(g) {
886
+ if (g === null || typeof g !== "object")
887
+ return { girdi: g, atilan: [] };
888
+ const o = g;
889
+ const ham = typeof o.html === "string" ? o.html : typeof o.metin === "string" ? o.metin : null;
890
+ if (ham === null)
891
+ return { girdi: g, atilan: [] };
892
+ const giris = girisBilgisiAyikla(ham);
893
+ const atilan = giris.atildi ? ["oturum ve parola alanları"] : [];
894
+ const ileti = giris.atildi
895
+ ? "Sayfadaki oturum kimliği ve parola kutusu okunmadan atıldı, hiçbir yere yazılmadı. Ders bilgileri olduğu gibi okundu."
896
+ : undefined;
897
+ const onceki = Array.isArray(o.kaynaklar) ? o.kaynaklar.filter((x) => typeof x === "string") : [];
898
+ const oncekiY = Array.isArray(o.yetkililer) ? o.yetkililer : [];
899
+ const { html: _h, metin: _m, ...kalan } = o;
900
+ const satir = bolognaSatirAyikla(giris.metin);
901
+ const ek = ileti ? { ileti } : {};
902
+ if (prolizMi(giris.metin))
903
+ return {
904
+ girdi: {
905
+ ...kalan,
906
+ html: satir.html,
907
+ kaynaklar: [...onceki, ...satir.kaynaklar],
908
+ yetkililer: [...oncekiY, ...satir.yetkililer],
909
+ },
910
+ atilan,
911
+ ...ek,
912
+ };
913
+ // Proliz dışı: önce tablo satırları, sonra etiketli bölümler.
914
+ const r = genelAyikla(satir.html);
915
+ return {
916
+ girdi: {
917
+ ...kalan,
918
+ metin: r.metin,
919
+ kaynaklar: [...onceki, ...satir.kaynaklar, ...r.kaynaklar],
920
+ yetkililer: [...oncekiY, ...satir.yetkililer, ...r.yetkililer],
921
+ },
922
+ atilan,
923
+ ...ek,
924
+ };
925
+ }
926
+ const ayristirAraci = arac({
927
+ ad: "bologna_ayristir",
928
+ baslik: "Bologna sayfasını ayrıştır",
929
+ 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.",
931
+ kisiselVeriKapsami: BOLOGNA_KAPSAMI,
932
+ kunyeAlanlari: ["kaynaklar"],
933
+ onAyikla: ayristirOnAyikla,
934
+ girdi: z.object({
935
+ html: z
936
+ .string()
937
+ .min(20)
938
+ .max(400_000)
939
+ .optional()
940
+ .describe("Sayfanın HTML kaynağı ya da sayfadan kopyalanan metin (Ctrl+A, Ctrl+C)"),
941
+ metin: z.string().min(20).max(400_000).optional().describe("Sayfadan kopyalanan düz metin; html yerine"),
942
+ kaynaklar: z.array(z.string().max(600)).max(30).optional().describe("Ayrıca verilen künyeler"),
943
+ yetkililer: z.array(YetkiliSemasi).max(20).optional().describe("Sayfadaki görev satırları, olduğu gibi"),
944
+ tur: z.enum(["otomatik", "ders", "program_ciktilari"]).default("otomatik"),
945
+ }),
946
+ calistir: ({ html, metin, kaynaklar = [], yetkililer = [], tur }) => {
947
+ if (!html && !metin)
948
+ throw new AracHatasi("Sayfanın HTML'ini ya da metnini yapıştırın.");
949
+ const kaynakAlani = {
950
+ ...(kaynaklar.length ? { kaynaklar, kaynakSayisi: kaynaklar.length } : {}),
951
+ ...(yetkililer.length ? { yetkililer } : {}),
952
+ };
953
+ if (html && prolizMi(html)) {
954
+ const programMi = tur === "program_ciktilari" || (tur === "otomatik" && !/İş Yükü|Workload|Yarıyıl Çalışmaları/iu.test(html));
955
+ if (programMi) {
956
+ const pc = programCiktilari(html);
957
+ if (!pc.length)
958
+ throw new AracHatasi("Program çıktıları okunamadı. Sayfa biçimi tanınmadı.");
959
+ return {
960
+ sonuc: { tur: "program_ciktilari", bicim: "proliz", programCiktilari: pc, pKodlari: pc.map((p) => p.kod) },
961
+ kaynak: [KAYNAK_AYRISTIRICI],
962
+ sonrakiAdim: "P kodlarını bologna_denetle (programCiktilari) ve bologna_taslak_denetle (programCiktilari) çağrılarında kullanın.",
963
+ };
964
+ }
965
+ const d = dersSayfasiCoz(html);
966
+ // Proliz sayfasında görünmeyen metin alanları genel ayrıştırıcıdan tamamlanır.
967
+ const g = genelAyristir(html, kaynaklar.length);
968
+ const ders = {
969
+ kod: d.kod ?? g.ders.kod ?? "DERS",
970
+ ...(d.ad || g.ders.ad ? { ad: d.ad ?? g.ders.ad } : {}),
971
+ yariyil: d.yariyil,
972
+ ...(d.tul ? { tul: d.tul } : {}),
973
+ kredi: d.kredi,
974
+ akts: d.akts,
975
+ ...(d.guncelleme ? { guncelleme: d.guncelleme } : {}),
976
+ ...(g.ders.amac ? { amac: g.ders.amac } : {}),
977
+ ...(g.ders.icerik ? { icerik: g.ders.icerik } : {}),
978
+ ...(g.ders.yontem ? { yontem: g.ders.yontem } : {}),
979
+ ...(g.ders.notlar ? { notlar: g.ders.notlar } : {}),
980
+ ...kaynakAlani,
981
+ ciktilar: d.ciktilar ?? [],
982
+ degerlendirme: d.degerlendirme ?? [],
983
+ isyuku: d.isyuku ?? [],
984
+ dersYapisi: d.dersYapisi ?? [],
985
+ haftalar: d.haftalar ?? 0,
986
+ katki: d.katki ?? {},
987
+ };
988
+ const bulunamayan = [
989
+ ...(ders.amac ? [] : ["amac"]),
990
+ ...(ders.icerik ? [] : ["icerik"]),
991
+ ...(ders.yontem ? [] : ["yontem"]),
992
+ ...(ders.notlar ? [] : ["notlar"]),
993
+ ...(kaynaklar.length ? [] : ["kaynaklar"]),
994
+ ];
995
+ return {
996
+ sonuc: {
997
+ tur: "ders",
998
+ bicim: "proliz",
999
+ guven: "yüksek",
1000
+ bolognaDenetleGirdisi: { ders },
1001
+ bulunamayan: bulunamayan.map((k) => GENEL_ALAN_ADLARI[k] ?? k),
1002
+ okunamayan: bulunamayan.length
1003
+ ? [
1004
+ "Bu alanlar sayfada görünmedi. Hocaya tek soruyla sorun ya da tarayıcı aracınızla OBS düzenleme ekranından okuyun.",
1005
+ ]
1006
+ : [],
1007
+ },
1008
+ kaynak: [KAYNAK_AYRISTIRICI],
1009
+ };
1010
+ }
1011
+ const g = genelAyristir(html ?? metin ?? "", kaynaklar.length);
1012
+ if (g.bulunan.length < 2)
1013
+ 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
+ const { haftaKonulari, ...dersAlanlari } = g.ders;
1015
+ const ders = {
1016
+ ...dersAlanlari,
1017
+ kod: g.ders.kod ?? "DERS",
1018
+ ...kaynakAlani,
1019
+ };
1020
+ return {
1021
+ sonuc: {
1022
+ tur: "ders",
1023
+ bicim: "genel",
1024
+ guven: g.guven,
1025
+ bolognaDenetleGirdisi: { ders },
1026
+ bilgiler: g.bilgiler,
1027
+ haftaKonulari,
1028
+ bulunan: g.bulunan.map((k) => GENEL_ALAN_ADLARI[k] ?? k),
1029
+ bulunamayan: g.bulunamayan.map((k) => GENEL_ALAN_ADLARI[k] ?? k),
1030
+ not: g.guven === "yüksek"
1031
+ ? "Genel ayrıştırıcı alanların çoğunu buldu. Yine de hocaya kısaca gösterip doğrulatın."
1032
+ : "Genel ayrıştırıcı bazı alanları bulamadı. Bulunanları hocaya gösterip doğrulatın; bulunamayanları sorun.",
1033
+ },
1034
+ kaynak: [KOD_KAYNAGI("apps/acik-mcp/src/genel-ayristir.ts", "Akredit genel Bologna ayrıştırıcısı")],
1035
+ };
1036
+ },
1037
+ });
1038
+ // ---------------------------------------------------------------- bologna_durum
1039
+ /** Tamamlanmış sayılmak için dolu ve kurala uygun olması gereken alanlar. */
1040
+ const ZORUNLU_ALANLAR = [
1041
+ "amac",
1042
+ "icerik",
1043
+ "yontem",
1044
+ "notlar",
1045
+ "ciktilar",
1046
+ "degerlendirme",
1047
+ "isyuku",
1048
+ "haftalar",
1049
+ "kaynaklar",
1050
+ ];
1051
+ const durumAraci = arac({
1052
+ ad: "bologna_durum",
1053
+ baslik: "Bologna paketi ne kadar tamam",
1054
+ 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
+ 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
+ kisiselVeriKapsami: BOLOGNA_KAPSAMI,
1057
+ kunyeAlanlari: ["kaynaklar"],
1058
+ girdi: z.object({
1059
+ ders: DersTemeli,
1060
+ alanlar: TaslakAlanlari,
1061
+ programCiktilari: z
1062
+ .array(z.string().max(10))
1063
+ .max(40)
1064
+ .optional()
1065
+ .describe("Verilirse katkı tablosu da zorunlu alan sayılır"),
1066
+ ingilizce: z.boolean().default(true),
1067
+ }),
1068
+ calistir: ({ ders, alanlar, programCiktilari, ingilizce }) => {
1069
+ const zorunlu = [...ZORUNLU_ALANLAR, ...(programCiktilari?.length ? ["katki"] : [])];
1070
+ const b = alanBulgulari(ders, alanlar, { ingilizce, programCiktilari });
1071
+ const dolu = (a) => a === "kaynaklar"
1072
+ ? Boolean(alanlar.kaynaklar?.length ?? alanlar.kaynakSayisi)
1073
+ : alanlar[a] !== undefined;
1074
+ const satirlar = zorunlu.map((a, i) => {
1075
+ const hatalar = (b.get(a) ?? []).filter((x) => x.duzey === "hata").map((x) => x.aciklama);
1076
+ const durum = !dolu(a) ? "eksik" : hatalar.length ? "kurala_uymuyor" : "tamam";
1077
+ return { no: i + 1, alan: a, ad: ALANLAR[a].ad, durum, nedenler: hatalar };
1078
+ });
1079
+ const genel = (b.get("genel") ?? []).filter((x) => x.duzey === "hata").map((x) => x.aciklama);
1080
+ const kalan = satirlar.filter((s) => s.durum !== "tamam");
1081
+ const tamamlandi = kalan.length === 0 && genel.length === 0;
1082
+ return {
1083
+ sonuc: {
1084
+ tamamlandi,
1085
+ kalanAlanSayisi: kalan.length,
1086
+ ozet: tamamlandi
1087
+ ? "Tamamlandı: zorunlu alanların hepsi dolu ve kurallara uyuyor."
1088
+ : `${kalan.length} alan kaldı: ${kalan.map((s) => s.ad).join(", ")}.`,
1089
+ alanlar: satirlar,
1090
+ genelEksikler: genel,
1091
+ istegeBagli: ["Ders yapısı", "Sürdürülebilir Kalkınma Amaçları", "Ön koşul"].filter((ad) => !((ad === "Ders yapısı" && alanlar.dersYapisi) ||
1092
+ (ad === "Sürdürülebilir Kalkınma Amaçları" && alanlar.ska) ||
1093
+ (ad === "Ön koşul" && alanlar.onKosul !== undefined))),
1094
+ hesap: bolognaHesabi(ders, alanlar),
1095
+ hatirlatma: "Dersin Yetkilileri alanını hoca OBS'de kendisi doldurur. OBS'de Kaydet düğmesine hoca basar.",
1096
+ },
1097
+ kaynak: [KAYNAK_KURALLAR],
1098
+ ...(tamamlandi
1099
+ ? {
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.",
1101
+ }
1102
+ : {}),
1103
+ };
1104
+ },
1105
+ });
1106
+ // ---------------------------------------------------------------- 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. */
1108
+ const IS_YUKU_BICIMLERI = [
1109
+ {
1110
+ 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
+ ],
1117
+ },
1118
+ {
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
+ ],
1127
+ },
1128
+ {
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
+ ],
1136
+ },
1137
+ ];
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) {
1140
+ const saat = tulSaati(tul);
1141
+ if (saat === null)
1142
+ throw new AracHatasi("T+U+L '2+1+0' biçiminde olmalı.");
1143
+ const hedef = Math.round(akts * bolen);
1144
+ const dersSuresi = hafta * saat;
1145
+ const kalan = hedef - dersSuresi;
1146
+ 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
+ }
1165
+ });
1166
+ const toplam = satirlar.reduce((t, x) => t + x.sayi * x.sure, 0);
1167
+ return {
1168
+ secenek: i + 1,
1169
+ ad: b.ad,
1170
+ aciklama: b.aciklama,
1171
+ isyuku: satirlar,
1172
+ toplam,
1173
+ hedef,
1174
+ tamEsit: toplam === hedef,
1175
+ };
1176
+ });
1177
+ }
1178
+ const isYukuOnerAraci = arac({
1179
+ ad: "is_yuku_oner",
1180
+ 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.",
1183
+ girdi: z.object({
1184
+ akts: z.number().min(1).max(60),
1185
+ tul: TUL.describe("ör. '2+1+0'"),
1186
+ 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"),
1188
+ }),
1189
+ calistir: ({ akts, tul, hafta, bolen }) => {
1190
+ const secenekler = isYukuDagilimlari(akts, tul, hafta, bolen);
1191
+ return {
1192
+ sonuc: {
1193
+ hedef: `${akts} AKTS x ${bolen} = ${Math.round(akts * bolen)} saat`,
1194
+ 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.",
1196
+ resmiDayanak: RESMI_DAYANAK,
1197
+ },
1198
+ kaynak: [KAYNAK_KURALLAR],
1199
+ };
1200
+ },
1201
+ });
1202
+ // ---------------------------------------------------------------- bologna_kopya_metni
1203
+ const SEKME_SIRASI = [
1204
+ "Ders Bilgileri",
1205
+ "Öğrenme Çıktıları",
1206
+ "Ders Akışı",
1207
+ "Prg.Çıktısına Katkısı",
1208
+ "Sür. Kalkınma Amaçları",
1209
+ "Diğer Kaynaklar",
1210
+ ];
1211
+ const kopyaMetniAraci = arac({
1212
+ ad: "bologna_kopya_metni",
1213
+ baslik: "OBS'ye kopyalanacak metin",
1214
+ 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
+ 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
+ kisiselVeriKapsami: BOLOGNA_KAPSAMI,
1217
+ kunyeAlanlari: ["kaynaklar"],
1218
+ girdi: z.object({
1219
+ ders: DersTemeli,
1220
+ alanlar: TaslakAlanlari,
1221
+ programCiktilari: z.array(z.string().max(10)).max(40).optional(),
1222
+ ingilizce: z.boolean().default(true),
1223
+ }),
1224
+ calistir: ({ ders, alanlar, programCiktilari, ingilizce }) => {
1225
+ const bolumler = [];
1226
+ const ekle = (sekme, alan, deger) => bolumler.push({ sira: bolumler.length + 1, sekme, alan, deger });
1227
+ const hesap = bolognaHesabi(ders, alanlar);
1228
+ const iki = (ad, m) => {
1229
+ if (!m)
1230
+ return;
1231
+ ekle(DERS_BILGILERI, `${ad} (Türkçe)`, m.tr.trim());
1232
+ if (m.en?.trim())
1233
+ ekle(DERS_BILGILERI, `${ad} (İngilizce)`, m.en.trim());
1234
+ };
1235
+ iki("Dersin Amacı", alanlar.amac);
1236
+ iki("Dersin İçeriği", alanlar.icerik);
1237
+ iki("Yöntem ve Teknikler", alanlar.yontem);
1238
+ iki("Ders Notları", alanlar.notlar);
1239
+ if (alanlar.onKosul !== undefined)
1240
+ ekle(DERS_BILGILERI, "Ön Koşul", alanlar.onKosul.trim());
1241
+ if (alanlar.degerlendirme)
1242
+ ekle(DERS_BILGILERI, "Değerlendirme (ad, sayı, katkı %)", [
1243
+ ...alanlar.degerlendirme.map((x) => `${x.ad}\t${x.sayi}\t${x.katki}`),
1244
+ `Toplam\t\t${hesap.degerlendirmeToplami}`,
1245
+ ].join("\n"));
1246
+ if (hesap.isyuku)
1247
+ ekle(DERS_BILGILERI, "AKTS İş Yükü (ad, sayı, süre, toplam saat)", [
1248
+ ...hesap.isyuku.satirlar.map((x) => `${x.ad}\t${x.sayi}\t${x.sure}\t${x.toplam}`),
1249
+ `Toplam İş Yükü\t\t\t${hesap.isyuku.toplam}`,
1250
+ ...(hesap.isyuku.hedef !== null ? [`AKTS\t\t\t${ders.akts}`] : []),
1251
+ ].join("\n"));
1252
+ if (alanlar.dersYapisi)
1253
+ ekle(DERS_BILGILERI, "Ders Yapısı (alan, %)", alanlar.dersYapisi.map((x) => `${x.alan}\t${x.yuzde}`).join("\n"));
1254
+ if (alanlar.ciktilar)
1255
+ 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"));
1256
+ if (alanlar.haftalar)
1257
+ ekle("Ders Akışı", "Haftalar (hafta, konu, İngilizce konu, ön hazırlık, dokümanlar)", alanlar.haftalar
1258
+ .map((h) => `${h.hafta}\t${h.konu.tr.trim()}\t${h.konu.en?.trim() ?? ""}\t${h.onHazirlik.trim()}\t${h.dokuman.trim()}`)
1259
+ .join("\n"));
1260
+ if (alanlar.katki) {
1261
+ const p = programCiktilari?.length
1262
+ ? programCiktilari
1263
+ : [...new Set(Object.values(alanlar.katki).flatMap((s) => Object.keys(s)))];
1264
+ ekle("Prg.Çıktısına Katkısı", "Katkı tablosu ('Tüm' satırına dokunmayın)", [
1265
+ `\t${p.join("\t")}`,
1266
+ ...Object.entries(alanlar.katki).map(([o, s]) => `${o}\t${p.map((k) => s[k] ?? "").join("\t")}`),
1267
+ ].join("\n"));
1268
+ }
1269
+ if (alanlar.ska)
1270
+ ekle("Sür. Kalkınma Amaçları", "Seçilecek amaç numaraları", alanlar.ska.join(", "));
1271
+ 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)
1274
+ throw new AracHatasi("Kopyalanacak alan yok. En az bir alan gönderin.");
1275
+ const b = alanBulgulari(ders, alanlar, { ingilizce, programCiktilari });
1276
+ const kalanEksikler = [...b.entries()].flatMap(([alan, xs]) => xs.filter((x) => x.duzey === "hata").map((x) => ({ alan, neden: x.aciklama })));
1277
+ const baslik = [ders.kod, ders.ad].filter(Boolean).join(" ") || "Ders";
1278
+ const sirali = [...bolumler].sort((x, y) => SEKME_SIRASI.indexOf(x.sekme) - SEKME_SIRASI.indexOf(y.sekme) || x.sira - y.sira);
1279
+ const metin = [
1280
+ `${baslik}: OBS'ye kopyalanacak alanlar`,
1281
+ "",
1282
+ ...sirali.flatMap((x, i) => [`${i + 1}. ${x.sekme} > ${x.alan}`, x.deger, ""]),
1283
+ ...(ders.yetkililer?.length
1284
+ ? [
1285
+ "Dersin Yetkilileri (sayfadaki hâliyle; değiştirmeyin, OBS'de zaten böyle kayıtlı):",
1286
+ ...ders.yetkililer.map((y) => `${y.rol}: ${y.deger}`),
1287
+ ]
1288
+ : ["Dersin Yetkilileri: bu metin onlara dokunmaz; OBS'deki hâliyle kalır."]),
1289
+ ...(kalanEksikler.length
1290
+ ? [
1291
+ "",
1292
+ "Kalan eksikler:",
1293
+ ...kalanEksikler.map((e) => `- ${ALANLAR[e.alan]?.ad ?? "Genel"}: ${e.neden}`),
1294
+ ]
1295
+ : []),
1296
+ ].join("\n");
1297
+ return {
1298
+ sonuc: {
1299
+ metin,
1300
+ bolumler: sirali.map((x, i) => ({ ...x, sira: i + 1 })),
1301
+ hesap,
1302
+ kalanEksikler,
1303
+ not: "Alan adları OBS sürümüne göre küçük farklar gösterebilir. Kaydı ve son kontrolü hoca yapar.",
1304
+ },
1305
+ kaynak: [KAYNAK_KURALLAR],
1306
+ ...(kalanEksikler.length
1307
+ ? {
1308
+ sonrakiAdim: "Kalan eksikleri hocaya numaralı gösterin ve sorun: 1) eksikleri dolduralım 2) böyle kalsın, metni kopyalayacağım.",
1309
+ }
1310
+ : {}),
1311
+ };
1312
+ },
1313
+ });
1314
+ export const BOLOGNA_ARACLARI = [
1315
+ ayristirAraci,
1316
+ alanRehberi,
1317
+ taslakDenetleAraci,
1318
+ isYukuOnerAraci,
1319
+ durumAraci,
1320
+ kopyaMetniAraci,
1321
+ ];