@akredit/acik-mcp 0.6.0 → 0.8.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.
@@ -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,7 @@ 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 { AracHatasi, arac, KOD_KAYNAGI, PESIN_ONAY_ACIKLAMASI, pesinOnayVar, TARAMA_SECENEKLERI, } from "./temel.js";
18
19
  // ---------------------------------------------------------------- Alan modeli
19
20
  export const ALAN_KODLARI = [
20
21
  "amac",
@@ -40,7 +41,7 @@ export const ALANLAR = {
40
41
  ad: "Dersin amacı",
41
42
  sekme: DERS_BILGILERI,
42
43
  kurallar: [
43
- "K1: Ders adıyla tutarlı olsun; başka dersten kopya olmasın.",
44
+ "Akredit önerisi, araç denetlemez (hoca bakar): ders adıyla tutarlı olsun; başka dersten kopya olmasın.",
44
45
  "K2: 60-120 kelime. İngilizcesi tam çeviri olsun.",
45
46
  ],
46
47
  sinir: { enAzKelime: 60, enCokKelime: 120 },
@@ -65,7 +66,10 @@ export const ALANLAR = {
65
66
  yontem: {
66
67
  ad: "Yöntem ve teknikler",
67
68
  sekme: DERS_BILGILERI,
68
- kurallar: ["K2: 40-90 kelime.", "K11: Başka dersin yöntem metniyle birebir aynı olmasın."],
69
+ kurallar: [
70
+ "K2: 40-90 kelime.",
71
+ "Akredit önerisi, araç denetlemez (hoca bakar): başka dersin yöntem metniyle birebir aynı olmasın.",
72
+ ],
69
73
  sinir: { enAzKelime: 40, enCokKelime: 90 },
70
74
  kaliplar: ["Ders [anlatım, tartışma, uygulama, proje] ile yürütülür. [Haftalık etkinlik] konuyu pekiştirir."],
71
75
  yasaklar: [ORTAK_YASAK],
@@ -94,6 +98,7 @@ export const ALANLAR = {
94
98
  "K4: Her çıktı ölçülebilir bir eylem fiiliyle biter (açıklar, uygular, tasarlar). 'Bilgi sahibi olur', 'öğrenir', 'kavrar' olmaz.",
95
99
  "K4: '-ebilme/-abilme' (tasarlayabilme) ve '-ebilir' kalıpları kabul edilir. 'Sahip olur', 'farkında olur', 'haberdar olur', 'vakıf olur' ölçülemez.",
96
100
  "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.",
101
+ "K4 (uyarı): 'farkındalık' ifadesi hata sayılmaz ama ölçmesi zordur; gözlenebilir bir eylemle yazın.",
97
102
  "İngilizcesi tam çeviri olsun.",
98
103
  ],
99
104
  sinir: { enAzCikti: 4, enCokCikti: 10, onerilenEnAz: 5, onerilenEnCok: 8 },
@@ -102,7 +107,7 @@ export const ALANLAR = {
102
107
  "[Yöntem] kullanarak [problem türünü] çözer. (Beceri)",
103
108
  "[Ürün] tasarlar ve ekip içinde sunar. (Yetkinlik)",
104
109
  ],
105
- yasaklar: ["Ölçülemeyen fiil (bilir, anlar, öğrenir, farkındalık kazanır)."],
110
+ yasaklar: ["Ölçülemeyen fiil (bilir, anlar, öğrenir, kavrar, bilgi sahibi olur)."],
106
111
  mcpTasir: true,
107
112
  },
108
113
  degerlendirme: {
@@ -166,7 +171,7 @@ export const ALANLAR = {
166
171
  kurallar: [
167
172
  "K12: Her Ö satırı dolu; değerler 1-5 ya da boş.",
168
173
  "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.",
174
+ "K12: En az 10 dolu hücrenin %60'tan fazlası 5 olmasın (şişirilmiş matris); ayrıştırın.",
170
175
  "'Tüm' satırına dokunmayın.",
171
176
  ],
172
177
  sinir: { enAz: 1, enCok: 5 },
@@ -207,7 +212,7 @@ export const ALANLAR = {
207
212
  ad: "Dersin yetkilileri",
208
213
  sekme: "Dersin Yetkilileri",
209
214
  kurallar: [
210
- "K10: Koordinatör ve dersi veren hocanın kendisidir; başka hocanın adı yazılmaz.",
215
+ "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
216
  "Sayfada yayımlanmış görev bilgisi (Dersi Verenler, Yardımcılar, Koordinatör) olduğu gibi okunur ve korunur.",
212
217
  ],
213
218
  kaliplar: [],
@@ -228,7 +233,7 @@ const RESMI_DAYANAK = {
228
233
  durum: DOGRULANMADI_ISARETI,
229
234
  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
235
  };
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.";
236
+ 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
237
  const KAYNAK_KURALLAR = KOD_KAYNAGI("packages/cekirdek/src/bologna/taslak.ts", "Akredit Bologna taslak kuralları");
233
238
  const KAYNAK_SAYFA = KOD_KAYNAGI("packages/cekirdek/src/bologna/denetim.ts", "Akredit Bologna sayfa denetim kuralları");
234
239
  const KAYNAK_AYRISTIRICI = KOD_KAYNAGI("packages/baglayicilar/src/bologna-proliz/ayristir.ts", "Akredit Bologna (Proliz) sayfa ayrıştırıcısı");
@@ -377,6 +382,11 @@ const TaslakAlanlari = z.object({
377
382
  .enum(["sayfa", "hoca", "oneri"])
378
383
  .optional()
379
384
  .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."),
385
+ degerlendirmeKaniti: z
386
+ .string()
387
+ .max(2000)
388
+ .optional()
389
+ .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
390
  kaynaklar: z
381
391
  .array(KaynakOgesi)
382
392
  .max(30)
@@ -470,13 +480,11 @@ export function alanBulgulari(ders, alanlar, s) {
470
480
  aciklama: `${i + 1}. kaynakta yıl görünmüyor. Yılı hocaya sorun ya da kaynak_bicimle ile biçimlendirin.`,
471
481
  });
472
482
  }
473
- if (verilen.has("degerlendirme") &&
474
- alanlar.degerlendirmeDayanagi !== "sayfa" &&
475
- alanlar.degerlendirmeDayanagi !== "hoca")
483
+ if (verilen.has("degerlendirme") && !degerlendirmeTeyitli(alanlar))
476
484
  ekle("degerlendirme", {
477
485
  duzey: "uyari",
478
486
  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.",
487
+ 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
488
  kuralTuru: "Akredit önerisi",
481
489
  dayanak: "Değişmez ilke 2-3: değerlendirme oranları resmî taahhüttür, uydurulmaz; beyan hocadadır.",
482
490
  });
@@ -734,6 +742,99 @@ function alanDurumlari(d, sayfaBulgulari) {
734
742
  kural: ALANLAR[alan].kurallar.join(" "),
735
743
  }));
736
744
  }
745
+ const BolognaDenetleGirdisi = z.object({
746
+ ders: BolognaDersGirdisi,
747
+ programCiktilari: z
748
+ .array(z.object({ kod: z.string().max(20), metin: z.string().max(1000) }))
749
+ .max(40)
750
+ .optional()
751
+ .describe("Program çıktıları; verilirse program düzeyindeki kurallar da çalışır"),
752
+ matrisSatiri: z
753
+ .record(z.string().max(20), z.string().max(5))
754
+ .optional()
755
+ .describe("Program matrisinde bu dersin satırı: program çıktısı kodu ve değer"),
756
+ aktsSaat: z
757
+ .tuple([z.number().min(1), z.number().max(60)])
758
+ .optional()
759
+ .describe("AKTS başına saat aralığı; varsayılan [25, 30]"),
760
+ kurumBoleni: z
761
+ .number()
762
+ .int()
763
+ .min(20)
764
+ .max(40)
765
+ .optional()
766
+ .describe("Kurumun AKTS başına saat değeri; verilirse aralık yerine bu değer kullanılır (taslak denetimiyle aynı)"),
767
+ });
768
+ /** bologna_denetle hesabı. Ayrı işlevdir: bologna_ayristir aynı denetimi gömülü döndürür (tip dökümü gerekmez). */
769
+ const bolognaDenetimi = ({ ders, programCiktilari, matrisSatiri, aktsSaat: aralik, kurumBoleni }, b) => {
770
+ const aktsSaat = kurumBoleni ? [kurumBoleni, kurumBoleni] : aralik;
771
+ const { amac: _a, icerik: _i, yontem: _y, notlar: _n, kaynakSayisi: _k, kaynaklar: _kk, ...sayfaAlanlari } = ders;
772
+ const sayfa = {
773
+ ...sayfaAlanlari,
774
+ ad: ders.ad ?? ders.kod,
775
+ bilgiler: {},
776
+ };
777
+ const pkodlar = Object.keys(matrisSatiri ?? {});
778
+ const veri = {
779
+ adres: "kullanici-girdisi",
780
+ okundu: b.simdi().toISOString(),
781
+ programCiktilari: programCiktilari ?? [],
782
+ matris: {
783
+ pkodlar,
784
+ dersler: matrisSatiri
785
+ ? [
786
+ {
787
+ kod: ders.kod,
788
+ ad: ders.ad ?? ders.kod,
789
+ zorunlu: ders.zorunlu,
790
+ grup: null,
791
+ yariyil: ders.yariyil,
792
+ grupBasligi: false,
793
+ degerler: matrisSatiri,
794
+ },
795
+ ]
796
+ : [],
797
+ },
798
+ tyycIsaret: null,
799
+ dersler: [{ ...sayfa, zorunlu: ders.zorunlu, bos: false, kaynak: "kullanici-girdisi" }],
800
+ };
801
+ let bulgular = bolognaDenetle(veri, { simdi: b.simdi().getTime(), ...(aktsSaat ? { aktsSaat } : {}) }).map((x) => ({
802
+ ...x,
803
+ kuralTuru: x.kural === "akts-isyuku" && kurumBoleni ? "kurum" : "Akredit önerisi",
804
+ dayanak: x.kural === "akts-isyuku"
805
+ ? kurumBoleni
806
+ ? "Kurumun verdiği AKTS başına saat değeri."
807
+ : "AKTS başına 25-30 saat yaygın uygulamadır; resmî metinle DOĞRULANMADI."
808
+ : "Akredit iç kuralı (packages/cekirdek/src/bologna/denetim.ts); resmî dayanağı DOĞRULANMADI.",
809
+ }));
810
+ // Program düzeyi kuralları yalnız program çıktıları verildiyse anlamlıdır.
811
+ if (!programCiktilari)
812
+ bulgular = bulgular.filter((x) => x.konu !== "-");
813
+ const alanlar = alanDurumlari(ders, bulgular);
814
+ const doldurulacak = alanlar.filter((a) => a.durum === "bos" || a.durum === "zayif");
815
+ return {
816
+ sonuc: {
817
+ ders: ders.kod,
818
+ hataSayisi: bulgular.filter((x) => x.duzey === "hata").length,
819
+ uyariSayisi: bulgular.filter((x) => x.duzey === "uyari").length,
820
+ ozet: doldurulacak.length
821
+ ? `${doldurulacak.length} alan doldurulmalı ya da iyileştirilmeli: ${doldurulacak.map((a) => `${a.no}) ${a.ad}`).join(", ")}.`
822
+ : "Gönderilen alanlarda boş ya da zayıf alan bulunmadı.",
823
+ alanlar,
824
+ doldurulacak: doldurulacak.map((a) => a.no),
825
+ kalanAlanSayisi: doldurulacak.length,
826
+ dokunulmayacak: ["Dersin Yetkilileri sayfadaki hâliyle korunur; değiştirilmez."],
827
+ bulgular,
828
+ not: "Kurallar Akredit'in iç kurallarıdır, resmî ölçüt değildir. Düzeltme kararını ders sorumlusu ve komisyon verir.",
829
+ },
830
+ kaynak: [KAYNAK_SAYFA, KAYNAK_KURALLAR],
831
+ ...(doldurulacak.length
832
+ ? {}
833
+ : {
834
+ 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.",
835
+ }),
836
+ };
837
+ };
737
838
  export const bolognaDenetleAraci = arac({
738
839
  ad: "bologna_denetle",
739
840
  baslik: "Bologna ders paketini denetle",
@@ -741,96 +842,8 @@ export const bolognaDenetleAraci = arac({
741
842
  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
843
  kisiselVeriKapsami: BOLOGNA_KAPSAMI,
743
844
  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
- },
845
+ girdi: BolognaDenetleGirdisi,
846
+ calistir: bolognaDenetimi,
834
847
  });
835
848
  // ---------------------------------------------------------------- bologna_alan_rehberi
836
849
  const alanRehberi = arac({
@@ -1048,11 +1061,45 @@ export function ayristirOnAyikla(g) {
1048
1061
  ...ek,
1049
1062
  };
1050
1063
  }
1064
+ /**
1065
+ * Ayrıştırmanın hemen ardından denetim: tarama sırasında ders başına iki çağrı yerine bir çağrı yeter.
1066
+ * Girdi denetimin şemasından geçmezse (sayfa eksik okunduysa) denetim atlanır, asistan bologna_denetle'yi çağırır.
1067
+ */
1068
+ function gomuluDenetim(ders, b) {
1069
+ const g = BolognaDenetleGirdisi.safeParse({ ders });
1070
+ if (!g.success)
1071
+ return undefined;
1072
+ const s = bolognaDenetimi(g.data, b).sonuc;
1073
+ const d = g.data.ders;
1074
+ return {
1075
+ ozet: s.ozet,
1076
+ kalanAlanSayisi: s.kalanAlanSayisi,
1077
+ doldurulacak: s.alanlar
1078
+ .filter((a) => s.doldurulacak.includes(a.no))
1079
+ .map((a) => ({ no: a.no, ad: a.ad, durum: a.durum })),
1080
+ // Yalnız hocanın bilebileceği resmî bilgi sorulur; sayfada varsa sorulmaz.
1081
+ teyitSorulacak: [
1082
+ ...(d.degerlendirme.length ? [] : ["Değerlendirme yöntem ve yüzdeleri"]),
1083
+ ...(d.kaynaklar?.length || d.kaynakSayisi ? [] : ["Kullanılan kaynaklar"]),
1084
+ ],
1085
+ };
1086
+ }
1087
+ /** Ders sayfası okunduktan sonra tarayıcıda yapılacaklar; model bu turlarda sunucuya uğramaz. */
1088
+ const AYRISTIRMA_SONRASI = [
1089
+ "ADIM 1: Hocaya soru sormayın. Tarama planında ders kaldıysa ders listesine dönüp sıradakini açın.",
1090
+ "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.",
1091
+ "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ı.",
1092
+ "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.",
1093
+ ];
1094
+ const gomuluDenetimAlani = (ders, b) => {
1095
+ const d = gomuluDenetim(ders, b);
1096
+ return d ? { denetim: d } : {};
1097
+ };
1051
1098
  const ayristirAraci = arac({
1052
1099
  ad: "bologna_ayristir",
1053
1100
  baslik: "Bologna sayfasını ayrıştır",
1054
1101
  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.",
1102
+ sonrakiAdim: "Yanıttaki denetim alanı varsa bologna_denetle'yi ayrıca çağırmayın: eksikler (denetim.doldurulacak) ve teyit sorulacaklar (denetim.teyitSorulacak) hazırdır. Denetim alanı yoksa ders verisini bologna_denetle'ye verin (girdi hazır: bolognaDenetleGirdisi). Sonraki çağrılarda ingilizce değerini aynen verin. Sayfada değerlendirme yüzdeleri ya da kaynaklar yoksa yalnız bunları hocaya TEK mesajda sorun; yüzdeyi tahmin etmeyin. Uygulama/lab T+U+L'den okunur (U+L > 0 ise var), sorulmaz. Amaç ve içerik sayfada yoksa önden sorulmaz; 'öneri' olarak yazılıp son özette teyide sunulur.",
1056
1103
  kisiselVeriKapsami: BOLOGNA_KAPSAMI,
1057
1104
  kunyeAlanlari: BOLOGNA_KUNYE_ALANLARI,
1058
1105
  onAyikla: ayristirOnAyikla,
@@ -1072,7 +1119,7 @@ const ayristirAraci = arac({
1072
1119
  .optional()
1073
1120
  .describe("Sunucu doldurur; göndermeyin"),
1074
1121
  }),
1075
- calistir: ({ html, metin, kaynaklar = [], yetkililer = [], tur, sayfaIpucu }) => {
1122
+ calistir: ({ html, metin, kaynaklar = [], yetkililer = [], tur, sayfaIpucu }, b) => {
1076
1123
  if (!html && !metin)
1077
1124
  throw new AracHatasi("Sayfanın HTML'ini ya da metnini yapıştırın.");
1078
1125
  const kaynakAlani = {
@@ -1128,10 +1175,12 @@ const ayristirAraci = arac({
1128
1175
  guven: "yüksek",
1129
1176
  ingilizce: sayfaIpucu?.ingilizce ?? ingilizceAlanVar(html),
1130
1177
  bolognaDenetleGirdisi: { ders },
1178
+ ...gomuluDenetimAlani(ders, b),
1179
+ tarayiciEylemPlani: AYRISTIRMA_SONRASI,
1131
1180
  bulunamayan: bulunamayan.map((k) => GENEL_ALAN_ADLARI[k] ?? k),
1132
1181
  okunamayan: bulunamayan.length
1133
1182
  ? [
1134
- "Bu alanlar sayfada görünmedi. Hocaya tek soruyla sorun ya da tarayıcı aracınızla OBS düzenleme ekranından okuyun.",
1183
+ "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
1184
  ]
1136
1185
  : [],
1137
1186
  },
@@ -1155,13 +1204,15 @@ const ayristirAraci = arac({
1155
1204
  guven: g.guven,
1156
1205
  ingilizce: sayfaIpucu?.ingilizce ?? ingilizceAlanVar(ham),
1157
1206
  bolognaDenetleGirdisi: { ders },
1207
+ ...gomuluDenetimAlani(ders, b),
1208
+ tarayiciEylemPlani: AYRISTIRMA_SONRASI,
1158
1209
  bilgiler: g.bilgiler,
1159
1210
  haftaKonulari,
1160
1211
  bulunan: g.bulunan.map((k) => GENEL_ALAN_ADLARI[k] ?? k),
1161
1212
  bulunamayan: g.bulunamayan.map((k) => GENEL_ALAN_ADLARI[k] ?? k),
1162
1213
  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.",
1214
+ ? "Genel ayrıştırıcı alanların çoğunu buldu. Okunan değerleri son özette 'sayfa' dayanağıyla gösterin; hoca orada doğrular."
1215
+ : "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
1216
  },
1166
1217
  kaynak: [KOD_KAYNAGI("apps/acik-mcp/src/genel-ayristir.ts", "Akredit genel Bologna ayrıştırıcısı")],
1167
1218
  };
@@ -1183,8 +1234,8 @@ const ZORUNLU_ALANLAR = [
1183
1234
  const durumAraci = arac({
1184
1235
  ad: "bologna_durum",
1185
1236
  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.",
1237
+ 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.",
1238
+ 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
1239
  kisiselVeriKapsami: BOLOGNA_KAPSAMI,
1189
1240
  kunyeAlanlari: BOLOGNA_KUNYE_ALANLARI,
1190
1241
  girdi: z.object({
@@ -1212,9 +1263,7 @@ const durumAraci = arac({
1212
1263
  const kalan = satirlar.filter((s) => s.durum !== "tamam");
1213
1264
  const tamamlandi = kalan.length === 0 && genel.length === 0;
1214
1265
  const teyitBekleyen = [
1215
- ...(alanlar.degerlendirme && alanlar.degerlendirmeDayanagi !== "sayfa" && alanlar.degerlendirmeDayanagi !== "hoca"
1216
- ? ["Değerlendirme yöntem ve yüzdeleri"]
1217
- : []),
1266
+ ...(alanlar.degerlendirme && !degerlendirmeTeyitli(alanlar) ? ["Değerlendirme yöntem ve yüzdeleri"] : []),
1218
1267
  ];
1219
1268
  return {
1220
1269
  sonuc: {
@@ -1478,7 +1527,7 @@ const obsKarsilastirAraci = arac({
1478
1527
  ad: "obs_karsilastir",
1479
1528
  baslik: "OBS'ye yazılanı geri okuyup doğrula",
1480
1529
  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.",
1530
+ 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
1531
  kisiselVeriKapsami: BOLOGNA_KAPSAMI,
1483
1532
  kunyeAlanlari: ["beklenen"],
1484
1533
  onAyikla: (g) => {
@@ -1512,6 +1561,7 @@ const obsKarsilastirAraci = arac({
1512
1561
  return {
1513
1562
  sonuc,
1514
1563
  kaynak: [KOD_KAYNAGI("apps/acik-mcp/src/bologna.ts", "Akredit OBS geri okuma karşılaştırması")],
1564
+ hocayaSimdiSor: sonuc.dur,
1515
1565
  ...(sonuc.dur
1516
1566
  ? {
1517
1567
  sonrakiAdim: "Yazmayı durdurun: ders kodu ya da dönem beklenenden farklı. Hocaya nedenini söyleyin, başka alana yazmayın.",
@@ -1529,165 +1579,359 @@ const SEKME_SIRASI = [
1529
1579
  "Sür. Kalkınma Amaçları",
1530
1580
  "Diğer Kaynaklar",
1531
1581
  ];
1582
+ /**
1583
+ * Değerlendirme yüzdesi kod kapısı (ilke 2-3): dayanak 'sayfa' ya da 'hoca' olsa da her katkı yüzdesi verilen
1584
+ * kanıt metninde (sayfa tablosu ya da hocanın yanıtı) ayrı bir sayı olarak geçmelidir. Model beyanına güvenilmez.
1585
+ */
1586
+ export function degerlendirmeTeyitli(a) {
1587
+ if (a.degerlendirmeDayanagi !== "sayfa" && a.degerlendirmeDayanagi !== "hoca")
1588
+ return false;
1589
+ if (!a.degerlendirme?.length)
1590
+ return true;
1591
+ const sayilar = new Set((a.degerlendirmeKaniti ?? "").match(/\d+(?:[.,]\d+)?/gu)?.map((x) => Number(x.replace(",", "."))));
1592
+ return a.degerlendirme.every((x) => sayilar.has(x.katki));
1593
+ }
1594
+ /** Peşin onay varken yazma adımı: durak yok, sonda tek rapor. */
1595
+ 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.";
1596
+ const KopyaMetniGirdisi = z.object({
1597
+ ders: DersTemeli,
1598
+ alanlar: TaslakAlanlari,
1599
+ programCiktilari: z.array(z.string().max(10)).max(40).optional(),
1600
+ ingilizce: z.boolean().default(false).describe("bologna_ayristir'in ingilizce değeri"),
1601
+ oncekiDegerler: z
1602
+ .record(z.string().max(120), z.string().max(20_000))
1603
+ .optional()
1604
+ .describe("OBS'deki şimdiki değerler (alan adı, ör. 'Dersin Amacı (Türkçe)' → değer); önceki/yeni özeti için"),
1605
+ pesinOnay: z.string().max(500).optional().describe(PESIN_ONAY_ACIKLAMASI),
1606
+ });
1607
+ /** bologna_kopya_metni hesabı. Ayrı işlevdir: bologna_toplu_ozet her ders için aynı kapıları çalıştırır. */
1608
+ const kopyaPaketi = ({ ders, alanlar: gelen, programCiktilari, ingilizce, oncekiDegerler, pesinOnay, }) => {
1609
+ const pesin = pesinOnayVar(pesinOnay);
1610
+ // Yazma paketi kapısı: doğrulanmamış kaynak ve teyitsiz değerlendirme yüzdesi pakete giremez (ilke 1-3).
1611
+ const yazilamayan = [];
1612
+ const kaynaklar = gelen.kaynaklar?.filter((k, i) => {
1613
+ if (kaynakDurumu(k) !== "dogrulanmamis")
1614
+ return true;
1615
+ yazilamayan.push({
1616
+ alan: "Kaynaklar",
1617
+ neden: `${i + 1}. kaynak doğrulanmamış (katalogda bulunmadı ya da hoca beyanı yok); OBS'ye yazılmaz.`,
1618
+ });
1619
+ return false;
1620
+ });
1621
+ const teyitli = degerlendirmeTeyitli(gelen);
1622
+ if (gelen.degerlendirme && !teyitli)
1623
+ yazilamayan.push({
1624
+ alan: "Değerlendirme",
1625
+ 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.",
1626
+ });
1627
+ const { degerlendirme: _d, kaynaklar: _k, ...kalanAlanlar } = gelen;
1628
+ const alanlar = {
1629
+ ...kalanAlanlar,
1630
+ ...(gelen.degerlendirme && teyitli ? { degerlendirme: gelen.degerlendirme } : {}),
1631
+ ...(kaynaklar ? { kaynaklar } : {}),
1632
+ };
1633
+ const bolumler = [];
1634
+ const ekle = (sekme, alan, deger) => bolumler.push({ sira: bolumler.length + 1, sekme, alan, deger });
1635
+ const hesap = bolognaHesabi(ders, alanlar);
1636
+ const iki = (ad, m) => {
1637
+ if (!m)
1638
+ return;
1639
+ ekle(DERS_BILGILERI, `${ad} (Türkçe)`, m.tr.trim());
1640
+ if (m.en?.trim())
1641
+ ekle(DERS_BILGILERI, `${ad} (İngilizce)`, m.en.trim());
1642
+ };
1643
+ iki("Dersin Amacı", alanlar.amac);
1644
+ iki("Dersin İçeriği", alanlar.icerik);
1645
+ iki("Yöntem ve Teknikler", alanlar.yontem);
1646
+ iki("Ders Notları", alanlar.notlar);
1647
+ if (alanlar.onKosul !== undefined)
1648
+ ekle(DERS_BILGILERI, "Ön Koşul", alanlar.onKosul.trim());
1649
+ if (alanlar.degerlendirme)
1650
+ ekle(DERS_BILGILERI, "Değerlendirme (ad, sayı, katkı %)", [
1651
+ ...alanlar.degerlendirme.map((x) => `${x.ad}\t${x.sayi}\t${x.katki}`),
1652
+ `Toplam\t\t${hesap.degerlendirmeToplami}`,
1653
+ ].join("\n"));
1654
+ if (hesap.isyuku)
1655
+ ekle(DERS_BILGILERI, "AKTS İş Yükü (ad, sayı, süre, toplam saat)", [
1656
+ ...hesap.isyuku.satirlar.map((x) => `${x.ad}\t${x.sayi}\t${x.sure}\t${x.toplam}`),
1657
+ `Toplam İş Yükü\t\t\t${hesap.isyuku.toplam}`,
1658
+ ...(hesap.isyuku.hedef !== null ? [`AKTS\t\t\t${ders.akts}`] : []),
1659
+ ].join("\n"));
1660
+ if (alanlar.dersYapisi)
1661
+ ekle(DERS_BILGILERI, "Ders Yapısı (alan, %)", alanlar.dersYapisi.map((x) => `${x.alan}\t${x.yuzde}`).join("\n"));
1662
+ if (alanlar.ciktilar)
1663
+ 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"));
1664
+ if (alanlar.haftalar)
1665
+ ekle("Ders Akışı", "Haftalar (hafta, konu, İngilizce konu, ön hazırlık, dokümanlar)", alanlar.haftalar
1666
+ .map((h) => `${h.hafta}\t${h.konu.tr.trim()}\t${h.konu.en?.trim() ?? ""}\t${h.onHazirlik.trim()}\t${h.dokuman.trim()}`)
1667
+ .join("\n"));
1668
+ if (alanlar.katki) {
1669
+ const p = programCiktilari?.length
1670
+ ? programCiktilari
1671
+ : [...new Set(Object.values(alanlar.katki).flatMap((s) => Object.keys(s)))];
1672
+ ekle("Prg.Çıktısına Katkısı", "Katkı tablosu ('Tüm' satırına dokunmayın)", [
1673
+ `\t${p.join("\t")}`,
1674
+ ...Object.entries(alanlar.katki).map(([o, s]) => `${o}\t${p.map((k) => s[k] ?? "").join("\t")}`),
1675
+ ].join("\n"));
1676
+ }
1677
+ if (alanlar.ska)
1678
+ ekle("Sür. Kalkınma Amaçları", "Seçilecek amaç numaraları", alanlar.ska.join(", "));
1679
+ if (alanlar.kaynaklar?.length)
1680
+ ekle("Diğer Kaynaklar", "Kaynaklar (her satır bir künye)", alanlar.kaynaklar.map(kunyeMetni).join("\n"));
1681
+ if (!bolumler.length && !yazilamayan.length)
1682
+ throw new AracHatasi("Kopyalanacak alan yok. En az bir alan gönderin.");
1683
+ const b = alanBulgulari(ders, alanlar, { ingilizce, programCiktilari });
1684
+ const kalanEksikler = [...b.entries()].flatMap(([alan, xs]) => xs.filter((x) => x.duzey === "hata").map((x) => ({ alan, neden: x.aciklama })));
1685
+ const baslik = [ders.kod, ders.ad].filter(Boolean).join(" ") || "Ders";
1686
+ const sirali = [...bolumler].sort((x, y) => SEKME_SIRASI.indexOf(x.sekme) - SEKME_SIRASI.indexOf(y.sekme) || x.sira - y.sira);
1687
+ const metin = [
1688
+ `${baslik}: OBS'ye kopyalanacak alanlar`,
1689
+ "",
1690
+ ...sirali.flatMap((x, i) => [`${i + 1}. ${x.sekme} > ${x.alan}`, x.deger, ""]),
1691
+ ...(ders.yetkililer?.length
1692
+ ? [
1693
+ "Dersin Yetkilileri (sayfadaki hâliyle; değiştirmeyin, OBS'de zaten böyle kayıtlı):",
1694
+ ...ders.yetkililer.map((y) => `${y.rol}: ${y.deger}`),
1695
+ ]
1696
+ : ["Dersin Yetkilileri: bu metin onlara dokunmaz; OBS'deki hâliyle kalır."]),
1697
+ ...(kalanEksikler.length
1698
+ ? [
1699
+ "",
1700
+ "Kalan eksikler:",
1701
+ ...kalanEksikler.map((e) => `- ${ALANLAR[e.alan]?.ad ?? "Genel"}: ${e.neden}`),
1702
+ ]
1703
+ : []),
1704
+ ...(yazilamayan.length
1705
+ ? ["", "Yazma paketine alınmayanlar:", ...yazilamayan.map((y) => `- ${y.alan}: ${y.neden}`)]
1706
+ : []),
1707
+ ].join("\n");
1708
+ const numarali = sirali.map((x, i) => ({ ...x, sira: i + 1 }));
1709
+ const normal = (v) => v.replace(/\s+/gu, " ").trim();
1710
+ const degisiklikOzeti = numarali.map((x) => {
1711
+ const onceki = oncekiDegerler?.[x.alan];
1712
+ return {
1713
+ no: x.sira,
1714
+ sekme: x.sekme,
1715
+ alan: x.alan,
1716
+ onceki: onceki === undefined ? null : onceki,
1717
+ yeni: x.deger,
1718
+ degisim: onceki === undefined ? "bilinmiyor" : normal(onceki) === normal(x.deger) ? "aynı" : "değişecek",
1719
+ };
1720
+ });
1721
+ return {
1722
+ sonuc: {
1723
+ metin,
1724
+ bolumler: numarali,
1725
+ degisiklikOzeti,
1726
+ yazilamayan,
1727
+ hesap,
1728
+ kalanEksikler,
1729
+ obsKarsilastirGirdisi: {
1730
+ ...(ders.kod ? { dersKodu: ders.kod } : {}),
1731
+ ...(ders.donem ? { donem: ders.donem } : {}),
1732
+ beklenen: numarali.map((x) => ({ alan: x.alan, deger: x.deger })),
1733
+ },
1734
+ ...(pesin ? { pesinOnayKaydi: pesinOnay } : {}),
1735
+ not: `Biçim denetiminden geçti; içerik teyidi: hoca. ${pesin
1736
+ ? "Peşin onay var: degisiklikOzeti'ni sondaki tek raporda gösterin."
1737
+ : "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.`,
1738
+ },
1739
+ kaynak: [KAYNAK_KURALLAR],
1740
+ ...(pesin
1741
+ ? {
1742
+ hocayaSimdiSor: false,
1743
+ sonrakiAdim: PESIN_ONAY_YAZMA,
1744
+ }
1745
+ : pesinOnay
1746
+ ? {
1747
+ 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.",
1748
+ }
1749
+ : {}),
1750
+ ...(kalanEksikler.length && !pesin
1751
+ ? {
1752
+ 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.",
1753
+ }
1754
+ : {}),
1755
+ };
1756
+ };
1532
1757
  const kopyaMetniAraci = arac({
1533
1758
  ad: "bologna_kopya_metni",
1534
1759
  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.",
1760
+ hocayaSimdiSor: true,
1761
+ 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.",
1762
+ 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
1763
  kisiselVeriKapsami: BOLOGNA_KAPSAMI,
1538
1764
  kunyeAlanlari: BOLOGNA_KUNYE_ALANLARI,
1765
+ girdi: KopyaMetniGirdisi,
1766
+ calistir: kopyaPaketi,
1767
+ });
1768
+ /** OBS'de ders listesi açılınca: hocaya "hangi ders?" sordurmaz, tarama planını ve rapor biçimini verir. */
1769
+ const dersListesiAraci = arac({
1770
+ ad: "obs_ders_listesi",
1771
+ baslik: "OBS ders listesini tara",
1772
+ 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.",
1773
+ 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.",
1774
+ kisiselVeriKapsami: "tam",
1539
1775
  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"),
1776
+ dersler: z
1777
+ .array(z.object({ kod: z.string().trim().min(1).max(30), ad: z.string().trim().max(160).optional() }))
1778
+ .min(1)
1779
+ .max(60)
1780
+ .describe("Listede görünen dersler: kod ve ad (her çağrıda listenin tamamı)"),
1781
+ baslangic: z
1782
+ .number()
1783
+ .int()
1784
+ .min(0)
1785
+ .max(59)
1786
+ .default(0)
1787
+ .describe("Taranacak dilimin ilk dersinin sırası (0'dan). İlk çağrıda 0; sonraki dilimde yanıttaki sonrakiBaslangic."),
1788
+ pesinOnay: z.string().max(500).optional().describe(PESIN_ONAY_ACIKLAMASI),
1548
1789
  }),
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());
1790
+ calistir: ({ dersler, baslangic, pesinOnay }) => {
1791
+ const pesin = pesinOnayVar(pesinOnay);
1792
+ const DILIM = 15;
1793
+ const plan = dersler.slice(baslangic, baslangic + DILIM);
1794
+ const sonraki = baslangic + plan.length;
1795
+ const kalan = dersler.length - sonraki;
1796
+ const hepsi = baslangic === 0 && kalan === 0;
1797
+ return {
1798
+ sonuc: {
1799
+ hocayaGoster: pesin
1800
+ ? baslangic === 0
1801
+ ? `${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.`
1802
+ : ""
1803
+ : baslangic === 0
1804
+ ? `${dersler.length} dersiniz var. ${hepsi ? "Hepsini" : `${DILIM}'erli dilimlerle hepsini`} tarıyorum; eksikleri ve önerilerimi birazdan tek listede göstereceğim.`
1805
+ : `Sıradaki ${plan.length} dersi tarıyorum (${baslangic + 1}-${sonraki} / ${dersler.length}).`,
1806
+ taramaPlani: plan.map((d, i) => ({ sira: baslangic + i + 1, kod: d.kod, ad: d.ad ?? "" })),
1807
+ // Model tarayıcı turlarında sunucuya uğramaz; adımlar emir kipinde verilir ki ara soru doğmasın.
1808
+ tarayiciEylemPlani: [
1809
+ `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.`,
1810
+ "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.",
1811
+ "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.",
1812
+ pesin
1813
+ ? "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."
1814
+ : "ADIM 4: Listeye dönüp sıradaki derse geçin; bütün plan bitene kadar hocaya hiçbir şey yazmayın.",
1815
+ "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.",
1816
+ pesin
1817
+ ? "ADIM 6: Plan bitince sonrakiAdim'daki TEK raporu yazın; ara rapor ve seçenek yok."
1818
+ : "ADIM 6: Plan bitince raporBicimi'ne göre TEK mesaj yazın.",
1819
+ ],
1820
+ ...(kalan > 0 ? { taranmayan: kalan, sonrakiBaslangic: sonraki } : {}),
1821
+ // Peşin onayda rapor durağı yoktur; rapor biçimi yalnız olağan akışta döner.
1822
+ ...(pesin
1823
+ ? {}
1824
+ : {
1825
+ raporBicimi: [
1826
+ "Başlık: 'Şu eksiklikleri tespit ettim, önerilerim şunlar.'",
1827
+ "Her ders bir satır, en eksikten en tamama: ders kodu, eksik ya da kurala uymayan alan sayısı, en önemli 1-2 eksik.",
1828
+ "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).",
1829
+ "Sayfada olmayan değerlendirme yüzdeleri ve kaynakları aynı mesajda sor (tahmin etme).",
1830
+ `Altına: ${TARAMA_SECENEKLERI.join(" ")}`,
1831
+ ],
1832
+ }),
1833
+ },
1834
+ kaynak: [KAYNAK_KURALLAR],
1835
+ hocayaSimdiSor: false,
1836
+ ...(pesin
1837
+ ? {
1838
+ 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.`,
1839
+ }
1840
+ : kalan > 0
1841
+ ? {
1842
+ 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.`,
1843
+ }
1844
+ : {}),
1582
1845
  };
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];
1846
+ },
1847
+ });
1848
+ // ---------------------------------------------------------------- bologna_toplu_ozet
1849
+ /**
1850
+ * Toplu mod: birden çok dersin yazma paketi tek tabloda, tek onayla. Her ders için bologna_kopya_metni'nin kapıları
1851
+ * aynen çalışır (teyitsiz yüzde ve doğrulanmamış kaynak pakete girmez). Onay hakkı korunur: her satırda dayanak
1852
+ * etiketi görünür, hoca ders numarasıyla seçebilir, kayıt döngüsünde her dersten sonra obs_karsilastir çalışır.
1853
+ */
1854
+ const topluOzetAraci = arac({
1855
+ ad: "bologna_toplu_ozet",
1856
+ baslik: "Birden çok dersin yazma özeti",
1857
+ 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.",
1858
+ 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.",
1859
+ hocayaSimdiSor: true,
1860
+ kisiselVeriKapsami: BOLOGNA_KAPSAMI,
1861
+ kunyeAlanlari: BOLOGNA_KUNYE_ALANLARI,
1862
+ girdi: z.object({
1863
+ dersler: z.array(KopyaMetniGirdisi).min(1).max(15).describe("Her öğe bologna_kopya_metni girdisiyle aynı"),
1864
+ pesinOnay: z.string().max(500).optional().describe(PESIN_ONAY_ACIKLAMASI),
1865
+ }),
1866
+ calistir: (g) => {
1867
+ const pesin = pesinOnayVar(g.pesinOnay);
1868
+ const satirlar = g.dersler.map((d, i) => {
1869
+ const s = kopyaPaketi(d).sonuc;
1870
+ const dayanak = d.alanlar.degerlendirmeDayanagi;
1871
+ const degerlendirme = d.alanlar.degerlendirme
1872
+ ? degerlendirmeTeyitli(d.alanlar)
1873
+ ? `${d.alanlar.degerlendirme.map((x) => `${x.ad} %${x.katki}`).join(", ")} (${dayanak})`
1874
+ : "teyitsiz, yazılmaz"
1875
+ : "sayfadaki korunur";
1876
+ const kaynaklar = d.alanlar.kaynaklar ?? [];
1877
+ const kaynakOzeti = kaynaklar.length
1878
+ ? `${kaynaklar.length} (${kaynaklar.map((k) => kaynakDurumu(k)).join(", ")})`
1879
+ : "EKSİK";
1652
1880
  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",
1881
+ no: i + 1,
1882
+ kod: d.ders.kod,
1883
+ ad: d.ders.ad ?? "",
1884
+ degisecekAlanlar: s.degisiklikOzeti.filter((x) => x.degisim !== "aynı").map((x) => x.alan),
1885
+ degerlendirme,
1886
+ isYuku: s.hesap.isyuku
1887
+ ? `${s.hesap.isyuku.toplam} saat${d.ders.akts ? ` / ${d.ders.akts} AKTS` : ""}`
1888
+ : "EKSİK",
1889
+ kaynaklar: kaynakOzeti,
1890
+ yazilamayan: s.yazilamayan.map((y) => y.alan),
1891
+ kalanEksik: s.kalanEksikler.length,
1892
+ paket: s.obsKarsilastirGirdisi,
1659
1893
  };
1660
1894
  });
1895
+ const paketler = Object.fromEntries(satirlar.map((s) => [s.kod, s.paket]));
1896
+ const tablo = satirlar.map(({ paket: _p, ...kalan }) => kalan);
1897
+ const sorunlu = satirlar.filter((s) => s.yazilamayan.length || s.kalanEksik);
1661
1898
  return {
1662
1899
  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.",
1900
+ tablo,
1901
+ paketler,
1902
+ hocayaGoster: [
1903
+ `${satirlar.length} dersin değişiklik özeti yukarıda. Dayanak: sayfa = OBS'den okundu, hoca = siz söylediniz, öneri = ders adından üretildi.`,
1904
+ ...(sorunlu.length
1905
+ ? [
1906
+ `Pakete alınmayan ya da eksik kalan alanı olan dersler: ${sorunlu.map((s) => `${s.no}) ${s.kod}`).join(", ")}.`,
1907
+ ]
1908
+ : []),
1909
+ // Peşin onayda seçenek sorulmaz; özet sondaki raporda yer alır.
1910
+ ...(pesin
1911
+ ? []
1912
+ : [
1913
+ `1) ${satirlar.length} dersin tamamını OBS'ye yaz ve kaydet (önerilen; bu seçim 'kaydet' onayıdır)`,
1914
+ "2) Yalnız şu dersleri yaz: numaralarını yazın (ör. 1, 3)",
1915
+ "3) Bir dersin tam metnini göster: numarasını yazın (ör. detay 2)",
1916
+ ]),
1917
+ "Onaya Gönder'e siz basarsınız.",
1918
+ ].join("\n"),
1919
+ ...(pesin ? { pesinOnayKaydi: g.pesinOnay } : {}),
1920
+ not: "Biçim denetiminden geçti; içerik teyidi: hoca. Her ders kaydedildikten sonra geri okunup obs_karsilastir ile doğrulanır.",
1675
1921
  },
1676
1922
  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
- : {}),
1923
+ ...(pesin ? { hocayaSimdiSor: false, sonrakiAdim: PESIN_ONAY_YAZMA } : {}),
1682
1924
  };
1683
1925
  },
1684
1926
  });
1685
1927
  export const BOLOGNA_ARACLARI = [
1928
+ dersListesiAraci,
1686
1929
  ayristirAraci,
1687
1930
  alanRehberi,
1688
1931
  taslakDenetleAraci,
1689
1932
  isYukuOnerAraci,
1690
1933
  durumAraci,
1691
1934
  kopyaMetniAraci,
1935
+ topluOzetAraci,
1692
1936
  obsKarsilastirAraci,
1693
1937
  ];