@akredit/acik-mcp 0.6.0 → 0.8.1

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 (31) hide show
  1. package/README.md +41 -37
  2. package/dist/apps/acik-mcp/src/araclar.js +7 -2
  3. package/dist/apps/acik-mcp/src/bologna.d.ts +12 -0
  4. package/dist/apps/acik-mcp/src/bologna.js +580 -249
  5. package/dist/apps/acik-mcp/src/genel-ayristir.js +2 -2
  6. package/dist/apps/acik-mcp/src/http.js +2 -1
  7. package/dist/apps/acik-mcp/src/kaynak-dogrula.js +1 -1
  8. package/dist/apps/acik-mcp/src/kisisel-veri.d.ts +4 -3
  9. package/dist/apps/acik-mcp/src/kisisel-veri.js +5 -0
  10. package/dist/apps/acik-mcp/src/not-tablosu.d.ts +4 -1
  11. package/dist/apps/acik-mcp/src/not-tablosu.js +10 -4
  12. package/dist/apps/acik-mcp/src/proliz-metin.d.ts +60 -0
  13. package/dist/apps/acik-mcp/src/proliz-metin.js +271 -0
  14. package/dist/apps/acik-mcp/src/promptlar.d.ts +6 -3
  15. package/dist/apps/acik-mcp/src/promptlar.js +97 -48
  16. package/dist/apps/acik-mcp/src/sunucu.d.ts +3 -2
  17. package/dist/apps/acik-mcp/src/sunucu.js +16 -11
  18. package/dist/apps/acik-mcp/src/temel.d.ts +8 -0
  19. package/dist/apps/acik-mcp/src/temel.js +32 -0
  20. package/dist/apps/acik-mcp/src/yardim.js +46 -12
  21. package/dist/packages/baglayicilar/src/bologna-proliz/ayristir.js +4 -2
  22. package/dist/packages/cekirdek/src/bologna/taslak.js +8 -2
  23. package/dist/packages/cekirdek/src/karar/katman.d.ts +2 -2
  24. package/dist/packages/cekirdek/src/karar/katman.js +2 -2
  25. package/dist/packages/cekirdek/src/turkce.d.ts +0 -2
  26. package/dist/packages/cekirdek/src/turkce.js +0 -4
  27. package/dist/packages/olcme/src/girdi.d.ts +3 -0
  28. package/dist/packages/olcme/src/girdi.js +55 -0
  29. package/dist/packages/olcme/src/index.d.ts +1 -0
  30. package/dist/packages/olcme/src/index.js +1 -0
  31. package/package.json +9 -3
@@ -5,8 +5,9 @@
5
5
  // Akredit kurallarıyla denetler ve bütün sayıları (iş yükü, toplamlar) kendisi hesaplar (ilke 2).
6
6
  // Kurallar @akredit/cekirdek taslak ve sayfa denetiminden gelir; resmî ölçüt değildir, öyle etiketlenir.
7
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.
8
+ // Yetkili satırları (Dersi Verenler, Koordinatör) sayfada yayımlanmış görev bilgisidir: olduğu gibi okunur, taşınır
9
+ // ve korunur; araçlar onları değiştirmez. Kaynaklar yayımlanmış künyedir: yazar adı künye kuralıyla serbesttir.
10
+ // Öğrenci ve kimlik verisi (T.C. kimlik no, öğrenci numarası, IBAN) ile giriş bilgisi reddedilir.
10
11
  import { dersSayfasiCoz, programCiktilari, tablolar } from "../../../packages/baglayicilar/src/bologna-proliz/ayristir.js";
11
12
  import { bolognaDenetle, taslakDenetle, } from "../../../packages/cekirdek/src/index.js";
12
13
  import { DOGRULANMADI as DOGRULANMADI_ISARETI } from "../../../packages/cerceveler/src/index.js";
@@ -14,7 +15,8 @@ import { z } from "zod";
14
15
  import { GENEL_ALAN_ADLARI, genelAyikla, genelAyristir, ingilizceAlanVar, prolizMetniMi } from "./genel-ayristir.js";
15
16
  import { kaynakImzasiDogru } from "./kaynak-dogrula.js";
16
17
  import { girisBilgisiAyikla } from "./kisisel-veri.js";
17
- import { AracHatasi, arac, KOD_KAYNAGI } from "./temel.js";
18
+ import { prolizMetniCoz, prolizSayfaMetniMi } from "./proliz-metin.js";
19
+ import { AracHatasi, arac, KOD_KAYNAGI, PESIN_ONAY_ACIKLAMASI, pesinOnayVar, TARAMA_SECENEKLERI, } from "./temel.js";
18
20
  // ---------------------------------------------------------------- Alan modeli
19
21
  export const ALAN_KODLARI = [
20
22
  "amac",
@@ -40,7 +42,7 @@ export const ALANLAR = {
40
42
  ad: "Dersin amacı",
41
43
  sekme: DERS_BILGILERI,
42
44
  kurallar: [
43
- "K1: Ders adıyla tutarlı olsun; başka dersten kopya olmasın.",
45
+ "Akredit önerisi, araç denetlemez (hoca bakar): ders adıyla tutarlı olsun; başka dersten kopya olmasın.",
44
46
  "K2: 60-120 kelime. İngilizcesi tam çeviri olsun.",
45
47
  ],
46
48
  sinir: { enAzKelime: 60, enCokKelime: 120 },
@@ -65,7 +67,10 @@ export const ALANLAR = {
65
67
  yontem: {
66
68
  ad: "Yöntem ve teknikler",
67
69
  sekme: DERS_BILGILERI,
68
- kurallar: ["K2: 40-90 kelime.", "K11: Başka dersin yöntem metniyle birebir aynı olmasın."],
70
+ kurallar: [
71
+ "K2: 40-90 kelime.",
72
+ "Akredit önerisi, araç denetlemez (hoca bakar): başka dersin yöntem metniyle birebir aynı olmasın.",
73
+ ],
69
74
  sinir: { enAzKelime: 40, enCokKelime: 90 },
70
75
  kaliplar: ["Ders [anlatım, tartışma, uygulama, proje] ile yürütülür. [Haftalık etkinlik] konuyu pekiştirir."],
71
76
  yasaklar: [ORTAK_YASAK],
@@ -94,6 +99,7 @@ export const ALANLAR = {
94
99
  "K4: Her çıktı ölçülebilir bir eylem fiiliyle biter (açıklar, uygular, tasarlar). 'Bilgi sahibi olur', 'öğrenir', 'kavrar' olmaz.",
95
100
  "K4: '-ebilme/-abilme' (tasarlayabilme) ve '-ebilir' kalıpları kabul edilir. 'Sahip olur', 'farkında olur', 'haberdar olur', 'vakıf olur' ölçülemez.",
96
101
  "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.",
102
+ "K4 (uyarı): 'farkındalık' ifadesi hata sayılmaz ama ölçmesi zordur; gözlenebilir bir eylemle yazın.",
97
103
  "İngilizcesi tam çeviri olsun.",
98
104
  ],
99
105
  sinir: { enAzCikti: 4, enCokCikti: 10, onerilenEnAz: 5, onerilenEnCok: 8 },
@@ -102,7 +108,7 @@ export const ALANLAR = {
102
108
  "[Yöntem] kullanarak [problem türünü] çözer. (Beceri)",
103
109
  "[Ürün] tasarlar ve ekip içinde sunar. (Yetkinlik)",
104
110
  ],
105
- yasaklar: ["Ölçülemeyen fiil (bilir, anlar, öğrenir, farkındalık kazanır)."],
111
+ yasaklar: ["Ölçülemeyen fiil (bilir, anlar, öğrenir, kavrar, bilgi sahibi olur)."],
106
112
  mcpTasir: true,
107
113
  },
108
114
  degerlendirme: {
@@ -166,7 +172,7 @@ export const ALANLAR = {
166
172
  kurallar: [
167
173
  "K12: Her Ö satırı dolu; değerler 1-5 ya da boş.",
168
174
  "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.",
175
+ "K12: En az 10 dolu hücrenin %60'tan fazlası 5 olmasın (şişirilmiş matris); ayrıştırın.",
170
176
  "'Tüm' satırına dokunmayın.",
171
177
  ],
172
178
  sinir: { enAz: 1, enCok: 5 },
@@ -207,7 +213,7 @@ export const ALANLAR = {
207
213
  ad: "Dersin yetkilileri",
208
214
  sekme: "Dersin Yetkilileri",
209
215
  kurallar: [
210
- "K10: Koordinatör ve dersi veren hocanın kendisidir; başka hocanın adı yazılmaz.",
216
+ "Araç denetlemez (kişisel veri taşınmaz; hoca bakar): koordinatör ve dersi veren hocanın kendisidir; başka hocanın adı yazılmaz.",
211
217
  "Sayfada yayımlanmış görev bilgisi (Dersi Verenler, Yardımcılar, Koordinatör) olduğu gibi okunur ve korunur.",
212
218
  ],
213
219
  kaliplar: [],
@@ -228,7 +234,7 @@ const RESMI_DAYANAK = {
228
234
  durum: DOGRULANMADI_ISARETI,
229
235
  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
236
  };
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.";
237
+ const SECENEK_KURALI = "Sunucu metin üretmez. Taslağı asistan yazar: isterseniz 2-3 kısa, birbirinden gerçekten farklı aday (A, B, C) yazıp bologna_taslak_denetle ile denetleyin ve geçenlerden en iyisini kendiniz seçin; hocaya alan alan seçenek sormayın, seçileni son özette gösterin. Hocanın vermediği bilgi (kitap, araç, proje adı) uydurulmaz.";
232
238
  const KAYNAK_KURALLAR = KOD_KAYNAGI("packages/cekirdek/src/bologna/taslak.ts", "Akredit Bologna taslak kuralları");
233
239
  const KAYNAK_SAYFA = KOD_KAYNAGI("packages/cekirdek/src/bologna/denetim.ts", "Akredit Bologna sayfa denetim kuralları");
234
240
  const KAYNAK_AYRISTIRICI = KOD_KAYNAGI("packages/baglayicilar/src/bologna-proliz/ayristir.ts", "Akredit Bologna (Proliz) sayfa ayrıştırıcısı");
@@ -377,6 +383,11 @@ const TaslakAlanlari = z.object({
377
383
  .enum(["sayfa", "hoca", "oneri"])
378
384
  .optional()
379
385
  .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."),
386
+ degerlendirmeKaniti: z
387
+ .string()
388
+ .max(2000)
389
+ .optional()
390
+ .describe("'sayfa' ya da 'hoca' dayanağının kanıtı: sayfadaki değerlendirme tablosunun metni ya da hocanın sohbete yazdığı yanıt, olduğu gibi (ör. 'vize 40 final 60'). Her katkı yüzdesi bu metinde geçmelidir; geçmezse yüzde teyitsiz sayılır."),
380
391
  kaynaklar: z
381
392
  .array(KaynakOgesi)
382
393
  .max(30)
@@ -470,13 +481,11 @@ export function alanBulgulari(ders, alanlar, s) {
470
481
  aciklama: `${i + 1}. kaynakta yıl görünmüyor. Yılı hocaya sorun ya da kaynak_bicimle ile biçimlendirin.`,
471
482
  });
472
483
  }
473
- if (verilen.has("degerlendirme") &&
474
- alanlar.degerlendirmeDayanagi !== "sayfa" &&
475
- alanlar.degerlendirmeDayanagi !== "hoca")
484
+ if (verilen.has("degerlendirme") && !degerlendirmeTeyitli(alanlar))
476
485
  ekle("degerlendirme", {
477
486
  duzey: "uyari",
478
487
  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.",
488
+ aciklama: "Değerlendirme yöntem ve yüzdeleri teyitsiz: dayanak 'sayfa' ya da 'hoca' değil ya da yüzdeler degerlendirmeKaniti metninde geçmiyor. Hocaya tek mesajda sorun, yanıtını degerlendirmeKaniti'ne olduğu gibi yazın; teyitsiz yüzde OBS'ye yazılmaz.",
480
489
  kuralTuru: "Akredit önerisi",
481
490
  dayanak: "Değişmez ilke 2-3: değerlendirme oranları resmî taahhüttür, uydurulmaz; beyan hocadadır.",
482
491
  });
@@ -734,6 +743,99 @@ function alanDurumlari(d, sayfaBulgulari) {
734
743
  kural: ALANLAR[alan].kurallar.join(" "),
735
744
  }));
736
745
  }
746
+ const BolognaDenetleGirdisi = z.object({
747
+ ders: BolognaDersGirdisi,
748
+ programCiktilari: z
749
+ .array(z.object({ kod: z.string().max(20), metin: z.string().max(1000) }))
750
+ .max(40)
751
+ .optional()
752
+ .describe("Program çıktıları; verilirse program düzeyindeki kurallar da çalışır"),
753
+ matrisSatiri: z
754
+ .record(z.string().max(20), z.string().max(5))
755
+ .optional()
756
+ .describe("Program matrisinde bu dersin satırı: program çıktısı kodu ve değer"),
757
+ aktsSaat: z
758
+ .tuple([z.number().min(1), z.number().max(60)])
759
+ .optional()
760
+ .describe("AKTS başına saat aralığı; varsayılan [25, 30]"),
761
+ kurumBoleni: z
762
+ .number()
763
+ .int()
764
+ .min(20)
765
+ .max(40)
766
+ .optional()
767
+ .describe("Kurumun AKTS başına saat değeri; verilirse aralık yerine bu değer kullanılır (taslak denetimiyle aynı)"),
768
+ });
769
+ /** bologna_denetle hesabı. Ayrı işlevdir: bologna_ayristir aynı denetimi gömülü döndürür (tip dökümü gerekmez). */
770
+ const bolognaDenetimi = ({ ders, programCiktilari, matrisSatiri, aktsSaat: aralik, kurumBoleni }, b) => {
771
+ const aktsSaat = kurumBoleni ? [kurumBoleni, kurumBoleni] : aralik;
772
+ const { amac: _a, icerik: _i, yontem: _y, notlar: _n, kaynakSayisi: _k, kaynaklar: _kk, ...sayfaAlanlari } = ders;
773
+ const sayfa = {
774
+ ...sayfaAlanlari,
775
+ ad: ders.ad ?? ders.kod,
776
+ bilgiler: {},
777
+ };
778
+ const pkodlar = Object.keys(matrisSatiri ?? {});
779
+ const veri = {
780
+ adres: "kullanici-girdisi",
781
+ okundu: b.simdi().toISOString(),
782
+ programCiktilari: programCiktilari ?? [],
783
+ matris: {
784
+ pkodlar,
785
+ dersler: matrisSatiri
786
+ ? [
787
+ {
788
+ kod: ders.kod,
789
+ ad: ders.ad ?? ders.kod,
790
+ zorunlu: ders.zorunlu,
791
+ grup: null,
792
+ yariyil: ders.yariyil,
793
+ grupBasligi: false,
794
+ degerler: matrisSatiri,
795
+ },
796
+ ]
797
+ : [],
798
+ },
799
+ tyycIsaret: null,
800
+ dersler: [{ ...sayfa, zorunlu: ders.zorunlu, bos: false, kaynak: "kullanici-girdisi" }],
801
+ };
802
+ let bulgular = bolognaDenetle(veri, { simdi: b.simdi().getTime(), ...(aktsSaat ? { aktsSaat } : {}) }).map((x) => ({
803
+ ...x,
804
+ kuralTuru: x.kural === "akts-isyuku" && kurumBoleni ? "kurum" : "Akredit önerisi",
805
+ dayanak: x.kural === "akts-isyuku"
806
+ ? kurumBoleni
807
+ ? "Kurumun verdiği AKTS başına saat değeri."
808
+ : "AKTS başına 25-30 saat yaygın uygulamadır; resmî metinle DOĞRULANMADI."
809
+ : "Akredit iç kuralı (packages/cekirdek/src/bologna/denetim.ts); resmî dayanağı DOĞRULANMADI.",
810
+ }));
811
+ // Program düzeyi kuralları yalnız program çıktıları verildiyse anlamlıdır.
812
+ if (!programCiktilari)
813
+ bulgular = bulgular.filter((x) => x.konu !== "-");
814
+ const alanlar = alanDurumlari(ders, bulgular);
815
+ const doldurulacak = alanlar.filter((a) => a.durum === "bos" || a.durum === "zayif");
816
+ return {
817
+ sonuc: {
818
+ ders: ders.kod,
819
+ hataSayisi: bulgular.filter((x) => x.duzey === "hata").length,
820
+ uyariSayisi: bulgular.filter((x) => x.duzey === "uyari").length,
821
+ ozet: doldurulacak.length
822
+ ? `${doldurulacak.length} alan doldurulmalı ya da iyileştirilmeli: ${doldurulacak.map((a) => `${a.no}) ${a.ad}`).join(", ")}.`
823
+ : "Gönderilen alanlarda boş ya da zayıf alan bulunmadı.",
824
+ alanlar,
825
+ doldurulacak: doldurulacak.map((a) => a.no),
826
+ kalanAlanSayisi: doldurulacak.length,
827
+ dokunulmayacak: ["Dersin Yetkilileri sayfadaki hâliyle korunur; değiştirilmez."],
828
+ bulgular,
829
+ not: "Kurallar Akredit'in iç kurallarıdır, resmî ölçüt değildir. Düzeltme kararını ders sorumlusu ve komisyon verir.",
830
+ },
831
+ kaynak: [KAYNAK_SAYFA, KAYNAK_KURALLAR],
832
+ ...(doldurulacak.length
833
+ ? {}
834
+ : {
835
+ sonrakiAdim: "Eksik görünmüyor. Bunu tek cümleyle söyleyin ve sormadan kaliteyi artıracak 2-4 somut öneri verin: zayıf ya da kısa alanlar, öğrenme çıktılarında Bloom düzeyi ve ölçülebilirlik, iş yükü ile AKTS uyumu, kaynakların güncelliği, İngilizce alanlar. Altına: 1) önerileri uygulayalım (önerilen) 2) sıradaki derse geç 3) bitir.",
836
+ }),
837
+ };
838
+ };
737
839
  export const bolognaDenetleAraci = arac({
738
840
  ad: "bologna_denetle",
739
841
  baslik: "Bologna ders paketini denetle",
@@ -741,96 +843,8 @@ export const bolognaDenetleAraci = arac({
741
843
  sonrakiAdim: "Nasıl doldurulacağını hocaya sormayın; doğrudan akıllı doldurun. Sayfada değerlendirme yüzdeleri ya da kaynaklar yoksa yalnız bunları TEK mesajda sorun (yüzdeyi tahmin etmeyin). Gerisini sormadan bologna_alan_rehberi(alan='hepsi'), is_yuku_oner ve bologna_taslak_denetle ile doldurup denetleyin; sonunda bologna_kopya_metni özetini tek seferde gösterin.",
742
844
  kisiselVeriKapsami: BOLOGNA_KAPSAMI,
743
845
  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
- },
846
+ girdi: BolognaDenetleGirdisi,
847
+ calistir: bolognaDenetimi,
834
848
  });
835
849
  // ---------------------------------------------------------------- bologna_alan_rehberi
836
850
  const alanRehberi = arac({
@@ -1034,6 +1048,19 @@ export function ayristirOnAyikla(g) {
1034
1048
  atilan,
1035
1049
  ...ek,
1036
1050
  };
1051
+ // Proliz ders sayfasının düz metni: bölüm bölüm ayrıştırıcı ham metni okur (görev ve kaynak satırları dahil).
1052
+ if (prolizSayfaMetniMi(giris.metin))
1053
+ return {
1054
+ girdi: {
1055
+ ...kalan,
1056
+ sayfaIpucu: { ...sayfaIpucu, prolizMetni: true },
1057
+ metin: giris.metin,
1058
+ ...(onceki.length ? { kaynaklar: onceki } : {}),
1059
+ ...(oncekiY.length ? { yetkililer: oncekiY } : {}),
1060
+ },
1061
+ atilan,
1062
+ ...ek,
1063
+ };
1037
1064
  // Proliz dışı: önce tablo satırları, sonra etiketli bölümler.
1038
1065
  const r = genelAyikla(satir.html);
1039
1066
  return {
@@ -1048,11 +1075,108 @@ export function ayristirOnAyikla(g) {
1048
1075
  ...ek,
1049
1076
  };
1050
1077
  }
1078
+ /**
1079
+ * Ayrıştırmanın hemen ardından denetim: tarama sırasında ders başına iki çağrı yerine bir çağrı yeter.
1080
+ * Girdi denetimin şemasından geçmezse (sayfa eksik okunduysa) denetim atlanır, asistan bologna_denetle'yi çağırır.
1081
+ */
1082
+ function gomuluDenetim(ders, b) {
1083
+ const g = BolognaDenetleGirdisi.safeParse({ ders });
1084
+ if (!g.success)
1085
+ return undefined;
1086
+ const s = bolognaDenetimi(g.data, b).sonuc;
1087
+ const d = g.data.ders;
1088
+ return {
1089
+ ozet: s.ozet,
1090
+ kalanAlanSayisi: s.kalanAlanSayisi,
1091
+ doldurulacak: s.alanlar
1092
+ .filter((a) => s.doldurulacak.includes(a.no))
1093
+ .map((a) => ({ no: a.no, ad: a.ad, durum: a.durum })),
1094
+ // Yalnız hocanın bilebileceği resmî bilgi sorulur; sayfada varsa sorulmaz.
1095
+ teyitSorulacak: [
1096
+ ...(d.degerlendirme.length ? [] : ["Değerlendirme yöntem ve yüzdeleri"]),
1097
+ ...(d.kaynaklar?.length || d.kaynakSayisi ? [] : ["Kullanılan kaynaklar"]),
1098
+ ],
1099
+ };
1100
+ }
1101
+ /** Ders sayfası okunduktan sonra tarayıcıda yapılacaklar; model bu turlarda sunucuya uğramaz. */
1102
+ const AYRISTIRMA_SONRASI = [
1103
+ "ADIM 1: Hocaya soru sormayın. Tarama planında ders kaldıysa ders listesine dönüp sıradakini açın.",
1104
+ "ADIM 2: Plan bittiyse bütün derslerin denetim özetinden TEK rapor yazın (obs_ders_listesi'nin raporBicimi); sayfada olmayan değerlendirme yüzdeleri ve kaynakları aynı mesajda sorun.",
1105
+ "ADIM 3: Hoca seçince taslakları bologna_alan_rehberi ve bologna_taslak_denetle ile sormadan hazırlayın; sonra bologna_kopya_metni (birden çok derste bologna_toplu_ozet) ile tek özet ve tek kayıt onayı.",
1106
+ "Peşin onay varsa ADIM 1-3 yerine obs_ders_listesi'nin peşin onay adımlarını izleyin: rapor durağı ve teyit sorusu yoktur.",
1107
+ ];
1108
+ const gomuluDenetimAlani = (ders, b) => {
1109
+ const d = gomuluDenetim(ders, b);
1110
+ return d ? { denetim: d } : {};
1111
+ };
1112
+ /** Proliz ders sayfasının düz metninden ayrıştırma sonucu (dolu ya da boş paket). */
1113
+ function prolizMetniSonucu(ham, ek, b) {
1114
+ const p = prolizMetniCoz(ham);
1115
+ const d = p.ders;
1116
+ const kaynaklar = [...ek.kaynaklar, ...p.kaynaklar];
1117
+ const yetkililer = [...ek.yetkililer, ...p.yetkililer];
1118
+ const ad = d.ad ?? ek.dersIpucu?.ad;
1119
+ const ders = {
1120
+ kod: d.kod ?? ek.dersIpucu?.kod ?? "DERS",
1121
+ ...(ad ? { ad } : {}),
1122
+ yariyil: d.yariyil,
1123
+ ...(d.tul ? { tul: d.tul } : {}),
1124
+ kredi: d.kredi,
1125
+ akts: d.akts ?? ek.dersIpucu?.akts ?? null,
1126
+ ...(d.guncelleme ? { guncelleme: d.guncelleme } : {}),
1127
+ amac: d.amac,
1128
+ icerik: d.icerik,
1129
+ yontem: d.yontem,
1130
+ notlar: d.notlar,
1131
+ kaynaklar,
1132
+ kaynakSayisi: kaynaklar.length,
1133
+ ...(yetkililer.length ? { yetkililer } : {}),
1134
+ ciktilar: d.ciktilar,
1135
+ degerlendirme: d.degerlendirme,
1136
+ isyuku: d.isyuku,
1137
+ dersYapisi: d.dersYapisi,
1138
+ haftalar: d.haftaKonulari.length,
1139
+ // Denetim şeması metin değer bekler; sayısal matris ayrıca katkiMatrisi'nde.
1140
+ katki: Object.fromEntries(Object.entries(d.katki).map(([o, s]) => [
1141
+ o,
1142
+ Object.fromEntries(p.ders.pKodlari.map((k) => [k, s[k] === undefined ? "" : String(s[k])])),
1143
+ ])),
1144
+ };
1145
+ const bosPaket = p.bulunan.length === 0 || (!d.amac && !d.icerik && !d.ciktilar.length && !d.degerlendirme.length);
1146
+ const sonrakiAdim = bosPaket
1147
+ ? "Boş paket: sayfadaki bütün alanlar boş (denetim.doldurulacak). Hepsini öneriyle doldurun (bologna_alan_rehberi, bologna_taslak_denetle) ve 'öneri' etiketiyle son özette gösterin. Değerlendirme yöntem ve yüzdeleri ile kullanılan kaynaklar hocaya TEK mesajda sorulur; peşin onayda sorulmaz, EKSİK bırakılır. Uydurmayın."
1148
+ : d.degerlendirme.length
1149
+ ? "Yanıttaki denetim alanı hazırdır; bologna_denetle'yi ayrıca çağırmayın. Değerlendirme sayfadan okundu: hocaya sormayın; taslakta degerlendirmeDayanagi: 'sayfa' ve degerlendirmeKaniti olarak bu yanıttaki degerlendirmeKaniti metnini aynen verin. Yalnız denetim.teyitSorulacak'taki bilgiler hocaya TEK mesajda sorulur. Sonraki çağrılarda ingilizce değerini aynen verin."
1150
+ : "Yanıttaki denetim alanı hazırdır; bologna_denetle'yi ayrıca çağırmayın. Değerlendirme yöntem ve yüzdeleri sayfada yok: hocaya TEK mesajda sorun (peşin onayda EKSİK bırakın), tahmin etmeyin. Sonraki çağrılarda ingilizce değerini aynen verin.";
1151
+ return {
1152
+ sonuc: {
1153
+ tur: "ders",
1154
+ bicim: "proliz_duz_metin",
1155
+ guven: bosPaket ? "düşük" : p.bulunamayan.length <= 3 ? "yüksek" : "orta",
1156
+ bosPaket,
1157
+ ingilizce: ingilizceAlanVar(ham),
1158
+ bolognaDenetleGirdisi: { ders },
1159
+ ...gomuluDenetimAlani(ders, b),
1160
+ tarayiciEylemPlani: AYRISTIRMA_SONRASI,
1161
+ bilgiler: p.bilgiler,
1162
+ haftaKonulari: d.haftaKonulari,
1163
+ ...(d.degerlendirmeKaniti ? { degerlendirmeDayanagi: "sayfa", degerlendirmeKaniti: d.degerlendirmeKaniti } : {}),
1164
+ katkiMatrisi: d.katki,
1165
+ ...(Object.keys(d.katkiTum).length ? { katkiTum: d.katkiTum } : {}),
1166
+ ...(d.pKodlari.length ? { pKodlari: d.pKodlari } : {}),
1167
+ ...(!d.kod && ek.dersIpucu ? { dersKimligi: "dersIpucu" } : {}),
1168
+ bulunan: p.bulunan.map((k) => GENEL_ALAN_ADLARI[k] ?? k),
1169
+ bulunamayan: p.bulunamayan.map((k) => GENEL_ALAN_ADLARI[k] ?? k),
1170
+ },
1171
+ kaynak: [KOD_KAYNAGI("apps/acik-mcp/src/proliz-metin.ts", "Akredit Proliz düz metin ayrıştırıcısı")],
1172
+ sonrakiAdim,
1173
+ };
1174
+ }
1051
1175
  const ayristirAraci = arac({
1052
1176
  ad: "bologna_ayristir",
1053
1177
  baslik: "Bologna sayfasını ayrıştır",
1054
1178
  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. Sayfada değerlendirme yüzdeleri ya da kaynaklar yoksa yalnız bunları hocaya TEK mesajda sorun; yüzdeyi tahmin etmeyin. Uygulama/lab T+U+L'den okunur (U+L > 0 ise var), sorulmaz. Amaç ve içerik sayfada yoksa önden sorulmaz; 'öneri' olarak yazılıp son özette teyide sunulur.",
1179
+ sonrakiAdim: "Yanıttaki denetim alanı varsa bologna_denetle'yi ayrıca çağırmayın: eksikler (denetim.doldurulacak) ve teyit sorulacaklar (denetim.teyitSorulacak) hazırdır. Denetim alanı yoksa ders verisini bologna_denetle'ye verin (girdi hazır: bolognaDenetleGirdisi). Sonraki çağrılarda ingilizce değerini aynen verin. Sayfada değerlendirme yüzdeleri ya da kaynaklar yoksa yalnız bunları hocaya TEK mesajda sorun; yüzdeyi tahmin etmeyin. Uygulama/lab T+U+L'den okunur (U+L > 0 ise var), sorulmaz. Amaç ve içerik sayfada yoksa önden sorulmaz; 'öneri' olarak yazılıp son özette teyide sunulur.",
1056
1180
  kisiselVeriKapsami: BOLOGNA_KAPSAMI,
1057
1181
  kunyeAlanlari: BOLOGNA_KUNYE_ALANLARI,
1058
1182
  onAyikla: ayristirOnAyikla,
@@ -1067,12 +1191,20 @@ const ayristirAraci = arac({
1067
1191
  kaynaklar: z.array(z.string().max(600)).max(30).optional().describe("Ayrıca verilen künyeler"),
1068
1192
  yetkililer: z.array(YetkiliSemasi).max(20).optional().describe("Sayfadaki görev satırları, olduğu gibi"),
1069
1193
  tur: z.enum(["otomatik", "ders", "program_ciktilari"]).default("otomatik"),
1194
+ dersIpucu: z
1195
+ .object({
1196
+ kod: z.string().trim().min(1).max(30),
1197
+ ad: z.string().trim().max(200).optional(),
1198
+ akts: z.number().min(0).max(60).optional(),
1199
+ })
1200
+ .optional()
1201
+ .describe("Ders listesinden bilinen kod, ad ve AKTS (obs_ders_listesi taramaPlani). Sayfada yoksa bunlar kullanılır; boş pakette ders kodu ve adı sayfada görünmez."),
1070
1202
  sayfaIpucu: z
1071
1203
  .object({ prolizMetni: z.boolean(), ingilizce: z.boolean() })
1072
1204
  .optional()
1073
1205
  .describe("Sunucu doldurur; göndermeyin"),
1074
1206
  }),
1075
- calistir: ({ html, metin, kaynaklar = [], yetkililer = [], tur, sayfaIpucu }) => {
1207
+ calistir: ({ html, metin, kaynaklar = [], yetkililer = [], tur, dersIpucu, sayfaIpucu }, b) => {
1076
1208
  if (!html && !metin)
1077
1209
  throw new AracHatasi("Sayfanın HTML'ini ya da metnini yapıştırın.");
1078
1210
  const kaynakAlani = {
@@ -1128,10 +1260,12 @@ const ayristirAraci = arac({
1128
1260
  guven: "yüksek",
1129
1261
  ingilizce: sayfaIpucu?.ingilizce ?? ingilizceAlanVar(html),
1130
1262
  bolognaDenetleGirdisi: { ders },
1263
+ ...gomuluDenetimAlani(ders, b),
1264
+ tarayiciEylemPlani: AYRISTIRMA_SONRASI,
1131
1265
  bulunamayan: bulunamayan.map((k) => GENEL_ALAN_ADLARI[k] ?? k),
1132
1266
  okunamayan: bulunamayan.length
1133
1267
  ? [
1134
- "Bu alanlar sayfada görünmedi. Hocaya tek soruyla sorun ya da tarayıcı aracınızla OBS düzenleme ekranından okuyun.",
1268
+ "Bu alanlar sayfada görünmedi. Tarayıcı aracınız varsa OBS düzenleme ekranından okuyun. Yoksa önden sormayın: kaynaklar teyit sorusuyla birlikte sorulur, öbürleri 'öneri' olarak yazılıp son özette teyide sunulur.",
1135
1269
  ]
1136
1270
  : [],
1137
1271
  },
@@ -1139,6 +1273,8 @@ const ayristirAraci = arac({
1139
1273
  };
1140
1274
  }
1141
1275
  const ham = html ?? metin ?? "";
1276
+ if (prolizSayfaMetniMi(ham))
1277
+ return prolizMetniSonucu(ham, { kaynaklar, yetkililer, dersIpucu }, b);
1142
1278
  const g = genelAyristir(ham, kaynaklar.length);
1143
1279
  if (g.bulunan.length < 2)
1144
1280
  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.");
@@ -1155,13 +1291,15 @@ const ayristirAraci = arac({
1155
1291
  guven: g.guven,
1156
1292
  ingilizce: sayfaIpucu?.ingilizce ?? ingilizceAlanVar(ham),
1157
1293
  bolognaDenetleGirdisi: { ders },
1294
+ ...gomuluDenetimAlani(ders, b),
1295
+ tarayiciEylemPlani: AYRISTIRMA_SONRASI,
1158
1296
  bilgiler: g.bilgiler,
1159
1297
  haftaKonulari,
1160
1298
  bulunan: g.bulunan.map((k) => GENEL_ALAN_ADLARI[k] ?? k),
1161
1299
  bulunamayan: g.bulunamayan.map((k) => GENEL_ALAN_ADLARI[k] ?? k),
1162
1300
  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.",
1301
+ ? "Genel ayrıştırıcı alanların çoğunu buldu. Okunan değerleri son özette 'sayfa' dayanağıyla gösterin; hoca orada doğrular."
1302
+ : "Genel ayrıştırıcı bazı alanları bulamadı. Bulunanları son özette gösterin; bulunamayanları tarayıcıyla OBS düzenleme ekranından okuyun ya da 'öneri' olarak yazıp son özette teyide sunun. Yalnız değerlendirme ve kaynaklar hocaya sorulur.",
1165
1303
  },
1166
1304
  kaynak: [KOD_KAYNAGI("apps/acik-mcp/src/genel-ayristir.ts", "Akredit genel Bologna ayrıştırıcısı")],
1167
1305
  };
@@ -1183,8 +1321,8 @@ const ZORUNLU_ALANLAR = [
1183
1321
  const durumAraci = arac({
1184
1322
  ad: "bologna_durum",
1185
1323
  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.",
1324
+ aciklama: "Hazırlanan taslak alanların bütününe bakar: hangi zorunlu alan tamam, hangisi eksik ya da kurala uymuyor, kaç alan kaldı. Eksik kalmadıysa biçim denetiminden geçti der. Bütün alanlar yazılınca, OBS'ye yazmadan önce çağrılır.",
1325
+ sonrakiAdim: "Kalan alanları hocaya sormadan tamamlayın ya da düzeltin (bologna_alan_rehberi, bologna_taslak_denetle) ve yeniden denetleyin; ara rapor vermeyin. Yalnız hocanın bilebileceği resmî bilgi (değerlendirme, kullanılan kaynak) eksikse onu TEK mesajda sorun. Kalan yoksa OBS'ye yazma adımına geçin.",
1188
1326
  kisiselVeriKapsami: BOLOGNA_KAPSAMI,
1189
1327
  kunyeAlanlari: BOLOGNA_KUNYE_ALANLARI,
1190
1328
  girdi: z.object({
@@ -1212,9 +1350,7 @@ const durumAraci = arac({
1212
1350
  const kalan = satirlar.filter((s) => s.durum !== "tamam");
1213
1351
  const tamamlandi = kalan.length === 0 && genel.length === 0;
1214
1352
  const teyitBekleyen = [
1215
- ...(alanlar.degerlendirme && alanlar.degerlendirmeDayanagi !== "sayfa" && alanlar.degerlendirmeDayanagi !== "hoca"
1216
- ? ["Değerlendirme yöntem ve yüzdeleri"]
1217
- : []),
1353
+ ...(alanlar.degerlendirme && !degerlendirmeTeyitli(alanlar) ? ["Değerlendirme yöntem ve yüzdeleri"] : []),
1218
1354
  ];
1219
1355
  return {
1220
1356
  sonuc: {
@@ -1478,7 +1614,7 @@ const obsKarsilastirAraci = arac({
1478
1614
  ad: "obs_karsilastir",
1479
1615
  baslik: "OBS'ye yazılanı geri okuyup doğrula",
1480
1616
  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.",
1617
+ sonrakiAdim: "dur true ise yazmayı hemen bırakın, toplu kayıttaysa sonraki derslere de geçmeyin ve hocaya nedenini söyleyin. Farklı alan varsa önce sormadan bir kez toparlayın: o sekmenin Kaydet'ine basılmamışsa basın ya da onaylanan AYNI değeri yeniden yazın, sonra geri okuyup yeniden karşılaştırın. Metni kısaltmak ya da değiştirmek gerekiyorsa bu yeni içeriktir: hocaya alan alan gösterip onay alın. Hepsi eşleştiyse 'Kaydedildi ve geri okunarak doğrulandı' deyin ve toplu kayıttaysa sormadan sonraki derse geçin. Onaya Gönder'e hoca basar.",
1482
1618
  kisiselVeriKapsami: BOLOGNA_KAPSAMI,
1483
1619
  kunyeAlanlari: ["beklenen"],
1484
1620
  onAyikla: (g) => {
@@ -1512,6 +1648,7 @@ const obsKarsilastirAraci = arac({
1512
1648
  return {
1513
1649
  sonuc,
1514
1650
  kaynak: [KOD_KAYNAGI("apps/acik-mcp/src/bologna.ts", "Akredit OBS geri okuma karşılaştırması")],
1651
+ hocayaSimdiSor: sonuc.dur,
1515
1652
  ...(sonuc.dur
1516
1653
  ? {
1517
1654
  sonrakiAdim: "Yazmayı durdurun: ders kodu ya da dönem beklenenden farklı. Hocaya nedenini söyleyin, başka alana yazmayın.",
@@ -1529,165 +1666,359 @@ const SEKME_SIRASI = [
1529
1666
  "Sür. Kalkınma Amaçları",
1530
1667
  "Diğer Kaynaklar",
1531
1668
  ];
1669
+ /**
1670
+ * Değerlendirme yüzdesi kod kapısı (ilke 2-3): dayanak 'sayfa' ya da 'hoca' olsa da her katkı yüzdesi verilen
1671
+ * kanıt metninde (sayfa tablosu ya da hocanın yanıtı) ayrı bir sayı olarak geçmelidir. Model beyanına güvenilmez.
1672
+ */
1673
+ export function degerlendirmeTeyitli(a) {
1674
+ if (a.degerlendirmeDayanagi !== "sayfa" && a.degerlendirmeDayanagi !== "hoca")
1675
+ return false;
1676
+ if (!a.degerlendirme?.length)
1677
+ return true;
1678
+ const sayilar = new Set((a.degerlendirmeKaniti ?? "").match(/\d+(?:[.,]\d+)?/gu)?.map((x) => Number(x.replace(",", "."))));
1679
+ return a.degerlendirme.every((x) => sayilar.has(x.katki));
1680
+ }
1681
+ /** Peşin onay varken yazma adımı: durak yok, sonda tek rapor. */
1682
+ const PESIN_ONAY_YAZMA = "Peşin onay var (pesinOnayKaydi): hocaya sormadan yalnız bu paketteki alanları OBS'ye yazın, Kaydet'e basın, sayfayı geri okuyup obs_karsilastir ile doğrulayın. yazilamayan ve kalanEksikler'i uydurmayın, boş bırakın. Başka ders kaldıysa sormadan ona geçin. Hepsi bitince TEK rapor: ders ders yazılan alanlar, 'EKSİK' bırakılanlar (değerlendirme yüzdesi, kaynak), geri okuma sonucu, ve 'Onaya Gönder'e siz basarsınız; itirazınız olan alanı OBS'de düzeltin ya da bana numarasını yazın.' dur/fark çıkarsa o derste durun, rapora yazın.";
1683
+ const KopyaMetniGirdisi = z.object({
1684
+ ders: DersTemeli,
1685
+ alanlar: TaslakAlanlari,
1686
+ programCiktilari: z.array(z.string().max(10)).max(40).optional(),
1687
+ ingilizce: z.boolean().default(false).describe("bologna_ayristir'in ingilizce değeri"),
1688
+ oncekiDegerler: z
1689
+ .record(z.string().max(120), z.string().max(20_000))
1690
+ .optional()
1691
+ .describe("OBS'deki şimdiki değerler (alan adı, ör. 'Dersin Amacı (Türkçe)' → değer); önceki/yeni özeti için"),
1692
+ pesinOnay: z.string().max(500).optional().describe(PESIN_ONAY_ACIKLAMASI),
1693
+ });
1694
+ /** bologna_kopya_metni hesabı. Ayrı işlevdir: bologna_toplu_ozet her ders için aynı kapıları çalıştırır. */
1695
+ const kopyaPaketi = ({ ders, alanlar: gelen, programCiktilari, ingilizce, oncekiDegerler, pesinOnay, }) => {
1696
+ const pesin = pesinOnayVar(pesinOnay);
1697
+ // Yazma paketi kapısı: doğrulanmamış kaynak ve teyitsiz değerlendirme yüzdesi pakete giremez (ilke 1-3).
1698
+ const yazilamayan = [];
1699
+ const kaynaklar = gelen.kaynaklar?.filter((k, i) => {
1700
+ if (kaynakDurumu(k) !== "dogrulanmamis")
1701
+ return true;
1702
+ yazilamayan.push({
1703
+ alan: "Kaynaklar",
1704
+ neden: `${i + 1}. kaynak doğrulanmamış (katalogda bulunmadı ya da hoca beyanı yok); OBS'ye yazılmaz.`,
1705
+ });
1706
+ return false;
1707
+ });
1708
+ const teyitli = degerlendirmeTeyitli(gelen);
1709
+ if (gelen.degerlendirme && !teyitli)
1710
+ yazilamayan.push({
1711
+ alan: "Değerlendirme",
1712
+ neden: "Değerlendirme yöntem ve yüzdeleri teyitsiz (sayfadan okunmadı, hoca söylemedi ya da yüzdeler degerlendirmeKaniti metninde yok); OBS'ye yazılmaz. Hocaya sorun.",
1713
+ });
1714
+ const { degerlendirme: _d, kaynaklar: _k, ...kalanAlanlar } = gelen;
1715
+ const alanlar = {
1716
+ ...kalanAlanlar,
1717
+ ...(gelen.degerlendirme && teyitli ? { degerlendirme: gelen.degerlendirme } : {}),
1718
+ ...(kaynaklar ? { kaynaklar } : {}),
1719
+ };
1720
+ const bolumler = [];
1721
+ const ekle = (sekme, alan, deger) => bolumler.push({ sira: bolumler.length + 1, sekme, alan, deger });
1722
+ const hesap = bolognaHesabi(ders, alanlar);
1723
+ const iki = (ad, m) => {
1724
+ if (!m)
1725
+ return;
1726
+ ekle(DERS_BILGILERI, `${ad} (Türkçe)`, m.tr.trim());
1727
+ if (m.en?.trim())
1728
+ ekle(DERS_BILGILERI, `${ad} (İngilizce)`, m.en.trim());
1729
+ };
1730
+ iki("Dersin Amacı", alanlar.amac);
1731
+ iki("Dersin İçeriği", alanlar.icerik);
1732
+ iki("Yöntem ve Teknikler", alanlar.yontem);
1733
+ iki("Ders Notları", alanlar.notlar);
1734
+ if (alanlar.onKosul !== undefined)
1735
+ ekle(DERS_BILGILERI, "Ön Koşul", alanlar.onKosul.trim());
1736
+ if (alanlar.degerlendirme)
1737
+ ekle(DERS_BILGILERI, "Değerlendirme (ad, sayı, katkı %)", [
1738
+ ...alanlar.degerlendirme.map((x) => `${x.ad}\t${x.sayi}\t${x.katki}`),
1739
+ `Toplam\t\t${hesap.degerlendirmeToplami}`,
1740
+ ].join("\n"));
1741
+ if (hesap.isyuku)
1742
+ ekle(DERS_BILGILERI, "AKTS İş Yükü (ad, sayı, süre, toplam saat)", [
1743
+ ...hesap.isyuku.satirlar.map((x) => `${x.ad}\t${x.sayi}\t${x.sure}\t${x.toplam}`),
1744
+ `Toplam İş Yükü\t\t\t${hesap.isyuku.toplam}`,
1745
+ ...(hesap.isyuku.hedef !== null ? [`AKTS\t\t\t${ders.akts}`] : []),
1746
+ ].join("\n"));
1747
+ if (alanlar.dersYapisi)
1748
+ ekle(DERS_BILGILERI, "Ders Yapısı (alan, %)", alanlar.dersYapisi.map((x) => `${x.alan}\t${x.yuzde}`).join("\n"));
1749
+ if (alanlar.ciktilar)
1750
+ 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"));
1751
+ if (alanlar.haftalar)
1752
+ ekle("Ders Akışı", "Haftalar (hafta, konu, İngilizce konu, ön hazırlık, dokümanlar)", alanlar.haftalar
1753
+ .map((h) => `${h.hafta}\t${h.konu.tr.trim()}\t${h.konu.en?.trim() ?? ""}\t${h.onHazirlik.trim()}\t${h.dokuman.trim()}`)
1754
+ .join("\n"));
1755
+ if (alanlar.katki) {
1756
+ const p = programCiktilari?.length
1757
+ ? programCiktilari
1758
+ : [...new Set(Object.values(alanlar.katki).flatMap((s) => Object.keys(s)))];
1759
+ ekle("Prg.Çıktısına Katkısı", "Katkı tablosu ('Tüm' satırına dokunmayın)", [
1760
+ `\t${p.join("\t")}`,
1761
+ ...Object.entries(alanlar.katki).map(([o, s]) => `${o}\t${p.map((k) => s[k] ?? "").join("\t")}`),
1762
+ ].join("\n"));
1763
+ }
1764
+ if (alanlar.ska)
1765
+ ekle("Sür. Kalkınma Amaçları", "Seçilecek amaç numaraları", alanlar.ska.join(", "));
1766
+ if (alanlar.kaynaklar?.length)
1767
+ ekle("Diğer Kaynaklar", "Kaynaklar (her satır bir künye)", alanlar.kaynaklar.map(kunyeMetni).join("\n"));
1768
+ if (!bolumler.length && !yazilamayan.length)
1769
+ throw new AracHatasi("Kopyalanacak alan yok. En az bir alan gönderin.");
1770
+ const b = alanBulgulari(ders, alanlar, { ingilizce, programCiktilari });
1771
+ const kalanEksikler = [...b.entries()].flatMap(([alan, xs]) => xs.filter((x) => x.duzey === "hata").map((x) => ({ alan, neden: x.aciklama })));
1772
+ const baslik = [ders.kod, ders.ad].filter(Boolean).join(" ") || "Ders";
1773
+ const sirali = [...bolumler].sort((x, y) => SEKME_SIRASI.indexOf(x.sekme) - SEKME_SIRASI.indexOf(y.sekme) || x.sira - y.sira);
1774
+ const metin = [
1775
+ `${baslik}: OBS'ye kopyalanacak alanlar`,
1776
+ "",
1777
+ ...sirali.flatMap((x, i) => [`${i + 1}. ${x.sekme} > ${x.alan}`, x.deger, ""]),
1778
+ ...(ders.yetkililer?.length
1779
+ ? [
1780
+ "Dersin Yetkilileri (sayfadaki hâliyle; değiştirmeyin, OBS'de zaten böyle kayıtlı):",
1781
+ ...ders.yetkililer.map((y) => `${y.rol}: ${y.deger}`),
1782
+ ]
1783
+ : ["Dersin Yetkilileri: bu metin onlara dokunmaz; OBS'deki hâliyle kalır."]),
1784
+ ...(kalanEksikler.length
1785
+ ? [
1786
+ "",
1787
+ "Kalan eksikler:",
1788
+ ...kalanEksikler.map((e) => `- ${ALANLAR[e.alan]?.ad ?? "Genel"}: ${e.neden}`),
1789
+ ]
1790
+ : []),
1791
+ ...(yazilamayan.length
1792
+ ? ["", "Yazma paketine alınmayanlar:", ...yazilamayan.map((y) => `- ${y.alan}: ${y.neden}`)]
1793
+ : []),
1794
+ ].join("\n");
1795
+ const numarali = sirali.map((x, i) => ({ ...x, sira: i + 1 }));
1796
+ const normal = (v) => v.replace(/\s+/gu, " ").trim();
1797
+ const degisiklikOzeti = numarali.map((x) => {
1798
+ const onceki = oncekiDegerler?.[x.alan];
1799
+ return {
1800
+ no: x.sira,
1801
+ sekme: x.sekme,
1802
+ alan: x.alan,
1803
+ onceki: onceki === undefined ? null : onceki,
1804
+ yeni: x.deger,
1805
+ degisim: onceki === undefined ? "bilinmiyor" : normal(onceki) === normal(x.deger) ? "aynı" : "değişecek",
1806
+ };
1807
+ });
1808
+ return {
1809
+ sonuc: {
1810
+ metin,
1811
+ bolumler: numarali,
1812
+ degisiklikOzeti,
1813
+ yazilamayan,
1814
+ hesap,
1815
+ kalanEksikler,
1816
+ obsKarsilastirGirdisi: {
1817
+ ...(ders.kod ? { dersKodu: ders.kod } : {}),
1818
+ ...(ders.donem ? { donem: ders.donem } : {}),
1819
+ beklenen: numarali.map((x) => ({ alan: x.alan, deger: x.deger })),
1820
+ },
1821
+ ...(pesin ? { pesinOnayKaydi: pesinOnay } : {}),
1822
+ not: `Biçim denetiminden geçti; içerik teyidi: hoca. ${pesin
1823
+ ? "Peşin onay var: degisiklikOzeti'ni sondaki tek raporda gösterin."
1824
+ : "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.`,
1825
+ },
1826
+ kaynak: [KAYNAK_KURALLAR],
1827
+ ...(pesin
1828
+ ? {
1829
+ hocayaSimdiSor: false,
1830
+ sonrakiAdim: PESIN_ONAY_YAZMA,
1831
+ }
1832
+ : pesinOnay
1833
+ ? {
1834
+ sonrakiAdim: "pesinOnay metninde kayıt sözü yok ya da söz olumsuz; peşin onay sayılmadı. degisiklikOzeti'ni gösterip kayıt onayını isteyin: 1) OBS'ye yaz ve kaydet 2) formu doldur, Kaydet'e ben basayım 3) kopya metni.",
1835
+ }
1836
+ : {}),
1837
+ ...(kalanEksikler.length && !pesin
1838
+ ? {
1839
+ sonrakiAdim: "Kalan eksikleri sormadan doldurmaya dönün (bologna_alan_rehberi, bologna_taslak_denetle); yalnız hocanın bilebileceği resmî bilgi (değerlendirme, kullanılan kaynak) eksikse onu TEK mesajda, numaralı sorun. Sonra degisiklikOzeti ile kayıt onayını aynı mesajda isteyin.",
1840
+ }
1841
+ : {}),
1842
+ };
1843
+ };
1532
1844
  const kopyaMetniAraci = arac({
1533
1845
  ad: "bologna_kopya_metni",
1534
1846
  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.",
1847
+ hocayaSimdiSor: true,
1848
+ 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. Doğrulanmamış kaynağı ve teyitsiz değerlendirme yüzdesini pakete almaz. Sunucu OBS'ye yazmaz.",
1849
+ sonrakiAdim: "Metnin tamamını basmayın. Yalnız degisiklikOzeti'ni tablo olarak gösterin (Alan | Önceki | Yeni, ilk cümle | Dayanak: sayfa/hoca/öneri) ve AYNI mesajda sorun: 1) OBS'ye yaz ve kaydet (önerilen; bu seçim özete 'kaydet' onayıdır) 2) formu doldur, Kaydet'e ben basayım 3) alan alan kopya metnini ver. Değiştirmek istediği alan varsa numarasını yazar. Tarayıcı aracınız yoksa yalnız 3'ü uygulayın ve metni kod bloğunda verin. Birden çok ders taranıyorsa tüm derslerin özetini tek tabloda gösterip tek onay alın.",
1537
1850
  kisiselVeriKapsami: BOLOGNA_KAPSAMI,
1538
1851
  kunyeAlanlari: BOLOGNA_KUNYE_ALANLARI,
1852
+ girdi: KopyaMetniGirdisi,
1853
+ calistir: kopyaPaketi,
1854
+ });
1855
+ /** OBS'de ders listesi açılınca: hocaya "hangi ders?" sordurmaz, tarama planını ve rapor biçimini verir. */
1856
+ const dersListesiAraci = arac({
1857
+ ad: "obs_ders_listesi",
1858
+ baslik: "OBS ders listesini tara",
1859
+ aciklama: "OBS ya da Bologna sisteminde hocanın ders listesi açılınca, hocaya 'hangi ders?' diye SORMADAN İLK bu aracı çağır: listedeki ders kodlarını ve adlarını ver. Tarama sırasını, hocaya gösterilecek kısa bilgi cümlesini ve sonuç raporunun biçimini döndürür. OBS'ye yazmaz, adres açmaz.",
1860
+ sonrakiAdim: "Hocaya hocayaGoster cümlesini yazın ve hiçbir şey sormadan tarayiciEylemPlani'nı uygulayın: taramaPlani sırasıyla her dersin Bologna sayfasını açıp bologna_ayristir'a verin (yanıttaki denetim eksikleri sayar). Bitince raporBicimi'ne göre TEK mesaj yazın.",
1861
+ kisiselVeriKapsami: "tam",
1539
1862
  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"),
1863
+ dersler: z
1864
+ .array(z.object({ kod: z.string().trim().min(1).max(30), ad: z.string().trim().max(160).optional() }))
1865
+ .min(1)
1866
+ .max(60)
1867
+ .describe("Listede görünen dersler: kod ve ad (her çağrıda listenin tamamı)"),
1868
+ baslangic: z
1869
+ .number()
1870
+ .int()
1871
+ .min(0)
1872
+ .max(59)
1873
+ .default(0)
1874
+ .describe("Taranacak dilimin ilk dersinin sırası (0'dan). İlk çağrıda 0; sonraki dilimde yanıttaki sonrakiBaslangic."),
1875
+ pesinOnay: z.string().max(500).optional().describe(PESIN_ONAY_ACIKLAMASI),
1548
1876
  }),
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());
1877
+ calistir: ({ dersler, baslangic, pesinOnay }) => {
1878
+ const pesin = pesinOnayVar(pesinOnay);
1879
+ const DILIM = 15;
1880
+ const plan = dersler.slice(baslangic, baslangic + DILIM);
1881
+ const sonraki = baslangic + plan.length;
1882
+ const kalan = dersler.length - sonraki;
1883
+ const hepsi = baslangic === 0 && kalan === 0;
1884
+ return {
1885
+ sonuc: {
1886
+ hocayaGoster: pesin
1887
+ ? baslangic === 0
1888
+ ? `${dersler.length} dersiniz var. Hepsini tarayıp eksikleri dolduracak ve OBS'ye kaydedeceğim; bilmediğim resmî bilgiyi boş bırakacağım. Bitince tek rapor vereceğim. Vazgeçmek için 'dur' yazın.`
1889
+ : ""
1890
+ : baslangic === 0
1891
+ ? `${dersler.length} dersiniz var. ${hepsi ? "Hepsini" : `${DILIM}'erli dilimlerle hepsini`} tarıyorum; eksikleri ve önerilerimi birazdan tek listede göstereceğim.`
1892
+ : `Sıradaki ${plan.length} dersi tarıyorum (${baslangic + 1}-${sonraki} / ${dersler.length}).`,
1893
+ taramaPlani: plan.map((d, i) => ({ sira: baslangic + i + 1, kod: d.kod, ad: d.ad ?? "" })),
1894
+ // Model tarayıcı turlarında sunucuya uğramaz; adımlar emir kipinde verilir ki ara soru doğmasın.
1895
+ tarayiciEylemPlani: [
1896
+ `ADIM 1: Hocaya soru sormayın. Listede '${plan[0]?.kod ?? ""}' satırının düzenleme/ders bilgi paketi bağlantısına tıklayın.`,
1897
+ "ADIM 2: Açılan sayfanın tamamını (metin ya da HTML) okuyup bologna_ayristir'a verin; yanıttaki 'denetim' alanı eksikleri sayar, bologna_denetle'yi ayrıca çağırmayın.",
1898
+ "ADIM 3: Sayfa iframe içindeyse iframe'in src adresine gidin. Alanlar ayrı sekmelerdeyse (Ders Bilgileri, Öğrenme Çıktıları, Ders Akışı, Katkı) sırayla açıp metinleri birleştirin.",
1899
+ pesin
1900
+ ? "ADIM 4: Bu dersin eksiklerini doldurup kaydedin (sonrakiAdim'daki sıra), sonra listeye dönüp sıradaki derse geçin; bütün plan bitene kadar hocaya hiçbir şey yazmayın."
1901
+ : "ADIM 4: Listeye dönüp sıradaki derse geçin; bütün plan bitene kadar hocaya hiçbir şey yazmayın.",
1902
+ "ADIM 5: Giriş ekranına düşerseniz durun: 'Oturum düştü; OBS sekmesinde yeniden girip girdim yazın' deyin. Parola ve doğrulama kodu istemeyin.",
1903
+ pesin
1904
+ ? "ADIM 6: Plan bitince sonrakiAdim'daki TEK raporu yazın; ara rapor ve seçenek yok."
1905
+ : "ADIM 6: Plan bitince raporBicimi'ne göre TEK mesaj yazın.",
1906
+ ],
1907
+ ...(kalan > 0 ? { taranmayan: kalan, sonrakiBaslangic: sonraki } : {}),
1908
+ // Peşin onayda rapor durağı yoktur; rapor biçimi yalnız olağan akışta döner.
1909
+ ...(pesin
1910
+ ? {}
1911
+ : {
1912
+ raporBicimi: [
1913
+ "Başlık: 'Şu eksiklikleri tespit ettim, önerilerim şunlar.'",
1914
+ "Her ders bir satır, en eksikten en tamama: ders kodu, eksik ya da kurala uymayan alan sayısı, en önemli 1-2 eksik.",
1915
+ "Eksiği olmayan dersler için: 'eksik görünmüyor' ve kaliteyi artıracak 1-2 somut öneri (zayıf alan, Bloom düzeyi, iş yükü ile AKTS uyumu, kaynak güncelliği, İngilizce alanlar).",
1916
+ "Sayfada olmayan değerlendirme yüzdeleri ve kaynakları aynı mesajda sor (tahmin etme).",
1917
+ `Altına: ${TARAMA_SECENEKLERI.join(" ")}`,
1918
+ ],
1919
+ }),
1920
+ },
1921
+ kaynak: [KAYNAK_KURALLAR],
1922
+ hocayaSimdiSor: false,
1923
+ ...(pesin
1924
+ ? {
1925
+ sonrakiAdim: `Peşin onay var: rapor durağı yok. Her ders için sırayla: bologna_ayristir → eksik alanları bologna_alan_rehberi ve bologna_taslak_denetle ile hazırla → bologna_kopya_metni'ni pesinOnay ile çağır → yaz, Kaydet, geri oku, obs_karsilastir. Sayfada olmayan değerlendirme yüzdesi ve kaynağı sorma, EKSİK bırak.${kalan > 0 ? ` Dilim bitince obs_ders_listesi'ni baslangic=${sonraki} ve aynı pesinOnay ile çağır.` : ""} Hepsi bitince TEK rapor: ders ders yazılanlar, EKSİK kalanlar, geri okuma sonucu; Onaya Gönder hocada.`,
1926
+ }
1927
+ : kalan > 0
1928
+ ? {
1929
+ sonrakiAdim: `Bu dilimi sormadan tarayın ve raporunu verin. Hoca 1'i seçip bu dilim kapanınca 'devam edeyim mi' diye sormadan obs_ders_listesi'ni baslangic=${sonraki} ile yeniden çağırıp sonraki dilime geçin.`,
1930
+ }
1931
+ : {}),
1582
1932
  };
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];
1933
+ },
1934
+ });
1935
+ // ---------------------------------------------------------------- bologna_toplu_ozet
1936
+ /**
1937
+ * Toplu mod: birden çok dersin yazma paketi tek tabloda, tek onayla. Her ders için bologna_kopya_metni'nin kapıları
1938
+ * aynen çalışır (teyitsiz yüzde ve doğrulanmamış kaynak pakete girmez). Onay hakkı korunur: her satırda dayanak
1939
+ * etiketi görünür, hoca ders numarasıyla seçebilir, kayıt döngüsünde her dersten sonra obs_karsilastir çalışır.
1940
+ */
1941
+ const topluOzetAraci = arac({
1942
+ ad: "bologna_toplu_ozet",
1943
+ baslik: "Birden çok dersin yazma özeti",
1944
+ aciklama: "Birden çok dersin (1-15) hazır taslaklarını tek çağrıda yazma paketine çevirir ve hocaya gösterilecek tek özet tablosunu üretir: ders, değişecek alanlar, değerlendirme dayanağı, iş yükü, kaynak sayısı, pakete alınmayanlar. Her ders için bologna_kopya_metni kuralları aynen uygulanır. OBS'ye yazmaz.",
1945
+ sonrakiAdim: "tablo'yu olduğu gibi ve hocayaGoster seçenekleriyle TEK mesajda gösterin; tek onay alın. Hoca 1 derse kayıt döngüsü: her dersi sırayla yazın, Kaydet'e basın, geri okuyup o dersin paketi (paketler[kod]) ile obs_karsilastir çağırın; dur true ya da ikinci farkta bütün döngüyü durdurup hocaya söyleyin. Onaya Gönder'e hiçbir derste basmayın.",
1946
+ hocayaSimdiSor: true,
1947
+ kisiselVeriKapsami: BOLOGNA_KAPSAMI,
1948
+ kunyeAlanlari: BOLOGNA_KUNYE_ALANLARI,
1949
+ girdi: z.object({
1950
+ dersler: z.array(KopyaMetniGirdisi).min(1).max(15).describe("Her öğe bologna_kopya_metni girdisiyle aynı"),
1951
+ pesinOnay: z.string().max(500).optional().describe(PESIN_ONAY_ACIKLAMASI),
1952
+ }),
1953
+ calistir: (g) => {
1954
+ const pesin = pesinOnayVar(g.pesinOnay);
1955
+ const satirlar = g.dersler.map((d, i) => {
1956
+ const s = kopyaPaketi(d).sonuc;
1957
+ const dayanak = d.alanlar.degerlendirmeDayanagi;
1958
+ const degerlendirme = d.alanlar.degerlendirme
1959
+ ? degerlendirmeTeyitli(d.alanlar)
1960
+ ? `${d.alanlar.degerlendirme.map((x) => `${x.ad} %${x.katki}`).join(", ")} (${dayanak})`
1961
+ : "teyitsiz, yazılmaz"
1962
+ : "sayfadaki korunur";
1963
+ const kaynaklar = d.alanlar.kaynaklar ?? [];
1964
+ const kaynakOzeti = kaynaklar.length
1965
+ ? `${kaynaklar.length} (${kaynaklar.map((k) => kaynakDurumu(k)).join(", ")})`
1966
+ : "EKSİK";
1652
1967
  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",
1968
+ no: i + 1,
1969
+ kod: d.ders.kod,
1970
+ ad: d.ders.ad ?? "",
1971
+ degisecekAlanlar: s.degisiklikOzeti.filter((x) => x.degisim !== "aynı").map((x) => x.alan),
1972
+ degerlendirme,
1973
+ isYuku: s.hesap.isyuku
1974
+ ? `${s.hesap.isyuku.toplam} saat${d.ders.akts ? ` / ${d.ders.akts} AKTS` : ""}`
1975
+ : "EKSİK",
1976
+ kaynaklar: kaynakOzeti,
1977
+ yazilamayan: s.yazilamayan.map((y) => y.alan),
1978
+ kalanEksik: s.kalanEksikler.length,
1979
+ paket: s.obsKarsilastirGirdisi,
1659
1980
  };
1660
1981
  });
1982
+ const paketler = Object.fromEntries(satirlar.map((s) => [s.kod, s.paket]));
1983
+ const tablo = satirlar.map(({ paket: _p, ...kalan }) => kalan);
1984
+ const sorunlu = satirlar.filter((s) => s.yazilamayan.length || s.kalanEksik);
1661
1985
  return {
1662
1986
  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.",
1987
+ tablo,
1988
+ paketler,
1989
+ hocayaGoster: [
1990
+ `${satirlar.length} dersin değişiklik özeti yukarıda. Dayanak: sayfa = OBS'den okundu, hoca = siz söylediniz, öneri = ders adından üretildi.`,
1991
+ ...(sorunlu.length
1992
+ ? [
1993
+ `Pakete alınmayan ya da eksik kalan alanı olan dersler: ${sorunlu.map((s) => `${s.no}) ${s.kod}`).join(", ")}.`,
1994
+ ]
1995
+ : []),
1996
+ // Peşin onayda seçenek sorulmaz; özet sondaki raporda yer alır.
1997
+ ...(pesin
1998
+ ? []
1999
+ : [
2000
+ `1) ${satirlar.length} dersin tamamını OBS'ye yaz ve kaydet (önerilen; bu seçim 'kaydet' onayıdır)`,
2001
+ "2) Yalnız şu dersleri yaz: numaralarını yazın (ör. 1, 3)",
2002
+ "3) Bir dersin tam metnini göster: numarasını yazın (ör. detay 2)",
2003
+ ]),
2004
+ "Onaya Gönder'e siz basarsınız.",
2005
+ ].join("\n"),
2006
+ ...(pesin ? { pesinOnayKaydi: g.pesinOnay } : {}),
2007
+ not: "Biçim denetiminden geçti; içerik teyidi: hoca. Her ders kaydedildikten sonra geri okunup obs_karsilastir ile doğrulanır.",
1675
2008
  },
1676
2009
  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
- : {}),
2010
+ ...(pesin ? { hocayaSimdiSor: false, sonrakiAdim: PESIN_ONAY_YAZMA } : {}),
1682
2011
  };
1683
2012
  },
1684
2013
  });
1685
2014
  export const BOLOGNA_ARACLARI = [
2015
+ dersListesiAraci,
1686
2016
  ayristirAraci,
1687
2017
  alanRehberi,
1688
2018
  taslakDenetleAraci,
1689
2019
  isYukuOnerAraci,
1690
2020
  durumAraci,
1691
2021
  kopyaMetniAraci,
2022
+ topluOzetAraci,
1692
2023
  obsKarsilastirAraci,
1693
2024
  ];