@akredit/acik-mcp 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/apps/acik-mcp/src/araclar.js +7 -5
- package/dist/apps/acik-mcp/src/bologna.d.ts +91 -9
- package/dist/apps/acik-mcp/src/bologna.js +479 -107
- package/dist/apps/acik-mcp/src/genel-ayristir.d.ts +10 -1
- package/dist/apps/acik-mcp/src/genel-ayristir.js +85 -7
- package/dist/apps/acik-mcp/src/http.d.ts +31 -3
- package/dist/apps/acik-mcp/src/http.js +86 -15
- package/dist/apps/acik-mcp/src/index.d.ts +1 -0
- package/dist/apps/acik-mcp/src/index.js +1 -0
- package/dist/apps/acik-mcp/src/kaynak-dogrula.d.ts +51 -4
- package/dist/apps/acik-mcp/src/kaynak-dogrula.js +199 -58
- package/dist/apps/acik-mcp/src/kisisel-veri.d.ts +3 -1
- package/dist/apps/acik-mcp/src/kisisel-veri.js +24 -9
- package/dist/apps/acik-mcp/src/not-tablosu.d.ts +6 -0
- package/dist/apps/acik-mcp/src/not-tablosu.js +43 -18
- package/dist/apps/acik-mcp/src/promptlar.d.ts +44 -0
- package/dist/apps/acik-mcp/src/promptlar.js +144 -57
- package/dist/apps/acik-mcp/src/sunucu.d.ts +3 -1
- package/dist/apps/acik-mcp/src/sunucu.js +17 -5
- package/dist/apps/acik-mcp/src/temel.d.ts +5 -0
- package/dist/apps/acik-mcp/src/yardim.js +28 -18
- package/dist/packages/cekirdek/src/bologna/taslak.d.ts +19 -3
- package/dist/packages/cekirdek/src/bologna/taslak.js +70 -9
- package/dist/packages/cekirdek/src/turler.d.ts +8 -0
- package/package.json +3 -3
|
@@ -4,14 +4,65 @@
|
|
|
4
4
|
// adrese girer (SSRF yok). Her istekte zaman aşımı ve yanıt boyutu sınırı vardır, yanıt zod ile doğrulanır.
|
|
5
5
|
//
|
|
6
6
|
// Uydurma yok: künye yalnız katalog yanıtındaki alanlardan kurulur ve kaynak_bicimle biçimiyle yazılır. Başlık
|
|
7
|
-
// ve yazarla aramada eşleşme deterministik bir
|
|
8
|
-
//
|
|
7
|
+
// ve yazarla aramada eşleşme deterministik bir denetimden (başlık sözcükleri, tam soyad belirteci, kayıt türü,
|
|
8
|
+
// verilmişse yıl) geçmezse sonuç "bulunamadi"dır.
|
|
9
|
+
//
|
|
10
|
+
// Ayrım (dış inceleme 2026-11-05): "katalogda bulundu" yalnız eserin katalogda var olduğunu söyler; "ders kaynağı
|
|
11
|
+
// olarak uygun" kararı hocanındır. Türkçe kitapların çoğu bu kataloglarda yoktur: bulunamayan eser uydurma sayılmaz
|
|
12
|
+
// ama doğrulanmış da sayılmaz; yalnız hocanın verdiği bilgiyle "hoca_beyani" etiketiyle yazılabilir.
|
|
13
|
+
//
|
|
14
|
+
// Kötüye kullanım sınırı: çağrı başına en çok 5 kaynak, süreç genelinde en çok 4 eşzamanlı dış istek, yanıt akışla
|
|
15
|
+
// okunur ve sınırda kesilir. Adres başına ayrı kota HTTP katmanındadır (http.ts).
|
|
16
|
+
//
|
|
17
|
+
// İmza: bulunan her kayıt, sürecin gizli anahtarıyla künye+bağlantı üzerinden imzalanır. Bologna araçları kaynağı
|
|
18
|
+
// "doğrulanmış" saymak için bu imzayı arar; asistan imzayı kendisi üretemez.
|
|
19
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
9
20
|
import { z } from "zod";
|
|
10
21
|
import { kunyeBicimle } from "./kaynak.js";
|
|
11
22
|
import { AracHatasi, arac, KOD_KAYNAGI } from "./temel.js";
|
|
12
23
|
export const IZINLI_HOSTLAR = ["api.crossref.org", "openlibrary.org", "www.googleapis.com"];
|
|
13
24
|
export const ZAMAN_ASIMI_MS = 8000;
|
|
14
25
|
export const EN_BUYUK_YANIT = 1_000_000;
|
|
26
|
+
/** Çağrı başına en çok kaynak. */
|
|
27
|
+
export const EN_COK_KAYNAK = 5;
|
|
28
|
+
/** Süreç genelinde aynı anda en çok dış istek. */
|
|
29
|
+
export const ESZAMANLI_DIS_ISTEK = 4;
|
|
30
|
+
// ---------------------------------------------------------------- Eşzamanlılık sınırı (semafor)
|
|
31
|
+
let aktifIstek = 0;
|
|
32
|
+
const bekleyenler = [];
|
|
33
|
+
/** Süreç genelindeki dış istek semaforu. Sınır doluysa sıra bekler. */
|
|
34
|
+
export async function sinirli(is) {
|
|
35
|
+
if (aktifIstek >= ESZAMANLI_DIS_ISTEK)
|
|
36
|
+
await new Promise((coz) => bekleyenler.push(coz));
|
|
37
|
+
aktifIstek += 1;
|
|
38
|
+
try {
|
|
39
|
+
return await is();
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
aktifIstek -= 1;
|
|
43
|
+
bekleyenler.shift()?.();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// ---------------------------------------------------------------- İmza
|
|
47
|
+
// Anahtar ilk kullanımda üretilir: modül tarayıcı araçlarına da (/deneyin) girer ve orada imza hiç kullanılmaz.
|
|
48
|
+
let imzaAnahtari;
|
|
49
|
+
function anahtar() {
|
|
50
|
+
if (!imzaAnahtari) {
|
|
51
|
+
const ortam = typeof process !== "undefined" ? process.env.AKREDIT_MCP_IMZA_ANAHTARI : undefined;
|
|
52
|
+
imzaAnahtari = ortam && ortam.length >= 16 ? Buffer.from(ortam) : randomBytes(32);
|
|
53
|
+
}
|
|
54
|
+
return imzaAnahtari;
|
|
55
|
+
}
|
|
56
|
+
/** Katalog kaydının imzası: künye ve bağlantı üzerinden HMAC-SHA256 (ilk 32 onaltılık hane). */
|
|
57
|
+
export function kaynakImzasi(kunye, baglanti) {
|
|
58
|
+
return createHmac("sha256", anahtar()).update(`${kunye}\n${baglanti}`).digest("hex").slice(0, 32);
|
|
59
|
+
}
|
|
60
|
+
/** İmza bu sürecin kaynak_dogrula çıktısına mı ait? */
|
|
61
|
+
export function kaynakImzasiDogru(kunye, baglanti, imza) {
|
|
62
|
+
const beklenen = Buffer.from(kaynakImzasi(kunye, baglanti));
|
|
63
|
+
const verilen = Buffer.from(String(imza));
|
|
64
|
+
return beklenen.length === verilen.length && timingSafeEqual(beklenen, verilen);
|
|
65
|
+
}
|
|
15
66
|
const DOI_DESENI = /^10\.\d{4,9}\/[^\s]{1,200}$/u;
|
|
16
67
|
/** "https://doi.org/10.x/y", "doi:10.x/y" ya da "10.x/y" → "10.x/y". Geçersizse null. */
|
|
17
68
|
export function doiTemizle(d) {
|
|
@@ -47,22 +98,33 @@ export function sadele(s) {
|
|
|
47
98
|
.split(/\s+/u)
|
|
48
99
|
.filter((x) => x.length > 1);
|
|
49
100
|
}
|
|
50
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Sorgu başlığının sözcüklerinin en az %80'i bulunan başlıkta var mı, ve bulunanın en az %60'ı sorguda var mı.
|
|
103
|
+
* Kısa (en çok iki sözcüklü) başlıklarda ("Veri Yapıları", "Genel Kimya") sözcük kümeleri birebir aynı olmalı.
|
|
104
|
+
*/
|
|
51
105
|
export function baslikEslesir(sorgu, bulunan) {
|
|
52
|
-
const a = sadele(sorgu);
|
|
106
|
+
const a = [...new Set(sadele(sorgu))];
|
|
53
107
|
const b = new Set(sadele(bulunan));
|
|
54
108
|
if (!a.length || !b.size)
|
|
55
109
|
return false;
|
|
56
110
|
const ortak = a.filter((x) => b.has(x)).length;
|
|
111
|
+
if (a.length <= 2)
|
|
112
|
+
return ortak === a.length && b.size === a.length;
|
|
57
113
|
return ortak / a.length >= 0.8 && ortak / b.size >= 0.6;
|
|
58
114
|
}
|
|
59
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* Sorgudaki yazar soyadı (son sözcük ya da virgülden önceki kısım) bulunan yazarlardan birinde TAM belirteç olarak
|
|
117
|
+
* geçiyor mu. Alt dize yetmez: "Yılmaz" sorgusu "Yılmazer" ile eşleşmez.
|
|
118
|
+
*/
|
|
60
119
|
export function yazarEslesir(sorgu, yazarlar) {
|
|
61
120
|
const soyad = sorgu.includes(",") ? (sorgu.split(",")[0] ?? "") : (sorgu.trim().split(/\s+/u).at(-1) ?? "");
|
|
62
|
-
const s = sadele(soyad)
|
|
63
|
-
if (!s)
|
|
121
|
+
const s = sadele(soyad);
|
|
122
|
+
if (!s.length)
|
|
64
123
|
return false;
|
|
65
|
-
return yazarlar.some((y) =>
|
|
124
|
+
return yazarlar.some((y) => {
|
|
125
|
+
const b = new Set(sadele(y));
|
|
126
|
+
return s.every((x) => b.has(x));
|
|
127
|
+
});
|
|
66
128
|
}
|
|
67
129
|
// ---------------------------------------------------------------- Güvenli istek
|
|
68
130
|
async function getir(f, adres) {
|
|
@@ -70,31 +132,58 @@ async function getir(f, adres) {
|
|
|
70
132
|
throw new Error("İzinsiz host");
|
|
71
133
|
if (adres.hostname === "www.googleapis.com" && !adres.pathname.startsWith("/books/"))
|
|
72
134
|
throw new Error("İzinsiz yol");
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
135
|
+
return sinirli(async () => {
|
|
136
|
+
let y;
|
|
137
|
+
try {
|
|
138
|
+
y = await f(adres.toString(), {
|
|
139
|
+
signal: AbortSignal.timeout(ZAMAN_ASIMI_MS),
|
|
140
|
+
redirect: "error",
|
|
141
|
+
headers: { accept: "application/json", "user-agent": "akredit-acik-mcp (https://akredit.org)" },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
if (!y.ok)
|
|
148
|
+
return null;
|
|
149
|
+
const uzunluk = Number(y.headers.get("content-length") ?? "0");
|
|
150
|
+
if (uzunluk > EN_BUYUK_YANIT)
|
|
151
|
+
return null;
|
|
152
|
+
const metin = await sinirliMetin(y, EN_BUYUK_YANIT);
|
|
153
|
+
if (metin === null)
|
|
154
|
+
return null;
|
|
155
|
+
try {
|
|
156
|
+
return JSON.parse(metin);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/** Yanıt gövdesini bayt sayarak okur; sınır aşılırsa akışı iptal edip null döner. */
|
|
164
|
+
export async function sinirliMetin(y, enBuyuk) {
|
|
165
|
+
if (!y.body)
|
|
166
|
+
return "";
|
|
167
|
+
const okuyucu = y.body.getReader();
|
|
168
|
+
const parcalar = [];
|
|
169
|
+
let toplam = 0;
|
|
92
170
|
try {
|
|
93
|
-
|
|
171
|
+
for (;;) {
|
|
172
|
+
const { done, value } = await okuyucu.read();
|
|
173
|
+
if (done)
|
|
174
|
+
break;
|
|
175
|
+
toplam += value.byteLength;
|
|
176
|
+
if (toplam > enBuyuk) {
|
|
177
|
+
await okuyucu.cancel().catch(() => { });
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
parcalar.push(value);
|
|
181
|
+
}
|
|
94
182
|
}
|
|
95
183
|
catch {
|
|
96
184
|
return null;
|
|
97
185
|
}
|
|
186
|
+
return Buffer.concat(parcalar).toString("utf8");
|
|
98
187
|
}
|
|
99
188
|
// ---------------------------------------------------------------- Katalog yanıtları (zod)
|
|
100
189
|
const CrossrefEser = z.object({
|
|
@@ -134,24 +223,39 @@ const GoogleCilt = z.object({
|
|
|
134
223
|
}),
|
|
135
224
|
});
|
|
136
225
|
const GoogleYanit = z.object({ totalItems: z.number().optional(), items: z.array(GoogleCilt).optional() });
|
|
226
|
+
const HOCA_BEYANI = "Katalogda bulunamadı. Hoca bu eseri kendisi kullanıyorsa künyesini hocanın verdiği bilgiyle 'hoca_beyani' etiketiyle yazın; asistan künye alanı uydurmaz.";
|
|
227
|
+
const bulunamadi = (neden) => ({ durum: "bulunamadi", neden, hocaBeyani: HOCA_BEYANI });
|
|
137
228
|
const yilAl = (s) => {
|
|
138
229
|
const m = s?.match(/\b(1[4-9]\d{2}|20\d{2}|2100)\b/u);
|
|
139
230
|
return m ? Number(m[1]) : undefined;
|
|
140
231
|
};
|
|
141
|
-
const kur = (k, baglanti, katalog) => {
|
|
232
|
+
const kur = (k, baglanti, katalog, kayitTuru = k.tur === "makale" ? "makale" : k.tur === "kitap_bolumu" ? "kitap_bolumu" : "kitap") => {
|
|
142
233
|
const g = { ...k, kurumYazar: false };
|
|
143
234
|
const { kunye, eksikler } = kunyeBicimle(g);
|
|
144
235
|
return {
|
|
145
|
-
durum: "
|
|
236
|
+
durum: "katalogda_bulundu",
|
|
146
237
|
kunye,
|
|
147
238
|
eksikler,
|
|
148
239
|
baglanti,
|
|
149
240
|
katalog,
|
|
241
|
+
kayitTuru,
|
|
150
242
|
baslik: k.baslik,
|
|
151
243
|
yazarlar: k.yazarlar,
|
|
152
244
|
...(k.yil !== undefined ? { yil: k.yil } : {}),
|
|
245
|
+
kimlik_eslesmesi: { yontem: "baslik_yazar", denetlenen: [] },
|
|
246
|
+
bibliyografik_tamlik: eksikler.length ? "eksik" : "tam",
|
|
247
|
+
ders_kaynagi_uygunlugu: "hoca karar verir",
|
|
248
|
+
imza: kaynakImzasi(kunye, baglanti),
|
|
153
249
|
};
|
|
154
250
|
};
|
|
251
|
+
const CROSSREF_KITAP = new Set(["book", "monograph", "edited-book", "reference-book", "book-set", "book-series"]);
|
|
252
|
+
const crossrefTuru = (t) => t === "journal-article"
|
|
253
|
+
? "makale"
|
|
254
|
+
: t === "book-chapter" || t === "book-section" || t === "book-part"
|
|
255
|
+
? "kitap_bolumu"
|
|
256
|
+
: t && CROSSREF_KITAP.has(t)
|
|
257
|
+
? "kitap"
|
|
258
|
+
: "diger";
|
|
155
259
|
function crossrefKunye(e) {
|
|
156
260
|
const baslik = e.title?.[0]?.trim();
|
|
157
261
|
if (!baslik)
|
|
@@ -161,7 +265,8 @@ function crossrefKunye(e) {
|
|
|
161
265
|
.filter(Boolean);
|
|
162
266
|
const yil = e.issued?.["date-parts"]?.[0]?.[0] ?? undefined;
|
|
163
267
|
const kap = e["container-title"]?.[0];
|
|
164
|
-
const
|
|
268
|
+
const kayit = crossrefTuru(e.type);
|
|
269
|
+
const tur = kayit === "makale" ? "makale" : kayit === "kitap_bolumu" ? "kitap_bolumu" : "kitap";
|
|
165
270
|
const doi = e.DOI;
|
|
166
271
|
return kur({
|
|
167
272
|
tur,
|
|
@@ -180,7 +285,7 @@ function crossrefKunye(e) {
|
|
|
180
285
|
...(e.issue ? { sayi: e.issue } : {}),
|
|
181
286
|
...(e.page ? { sayfalar: e.page } : {}),
|
|
182
287
|
doi,
|
|
183
|
-
}, `https://doi.org/${doi}`, "Crossref");
|
|
288
|
+
}, `https://doi.org/${doi}`, "Crossref", kayit);
|
|
184
289
|
}
|
|
185
290
|
function googleKunye(c) {
|
|
186
291
|
const v = c.volumeInfo;
|
|
@@ -197,10 +302,41 @@ function googleKunye(c) {
|
|
|
197
302
|
const CROSSREF = "https://api.crossref.org/works";
|
|
198
303
|
const OPENLIBRARY = "https://openlibrary.org/api/books";
|
|
199
304
|
const GOOGLE = "https://www.googleapis.com/books/v1/volumes";
|
|
305
|
+
const KAYIT_ADI = {
|
|
306
|
+
kitap: "kitap",
|
|
307
|
+
makale: "dergi makalesi",
|
|
308
|
+
kitap_bolumu: "kitap bölümü",
|
|
309
|
+
diger: "başka türde bir kayıt (bildiri, rapor...)",
|
|
310
|
+
};
|
|
311
|
+
/** Aranan tür ve yıl verildiyse kayıtla uyuşuyor mu; uyuşmuyorsa nedeni. */
|
|
312
|
+
function ekDenetim(k, s) {
|
|
313
|
+
if (s.tur && s.tur !== "herhangi" && k.kayitTuru !== s.tur)
|
|
314
|
+
return `Katalogdaki kayıt ${KAYIT_ADI[k.kayitTuru]}; aranan ${KAYIT_ADI[s.tur]}. Aynı eser sayılmadı.`;
|
|
315
|
+
if (s.yil !== undefined) {
|
|
316
|
+
const y = typeof k.yil === "number" ? k.yil : yilAl(String(k.yil ?? ""));
|
|
317
|
+
if (y === undefined)
|
|
318
|
+
return "Katalog kaydında yıl yok; verilen yılla karşılaştırılamadı.";
|
|
319
|
+
if (y !== s.yil)
|
|
320
|
+
return `Katalogdaki yıl ${y}, verilen yıl ${s.yil}. Aynı baskı sayılmadı.`;
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
function sonla(k, yontem, s, bulunamadiNedeni) {
|
|
325
|
+
if (!k)
|
|
326
|
+
return bulunamadi(bulunamadiNedeni);
|
|
327
|
+
const sorun = ekDenetim(k, s);
|
|
328
|
+
if (sorun)
|
|
329
|
+
return bulunamadi(sorun);
|
|
330
|
+
const denetlenen = [
|
|
331
|
+
yontem === "doi" ? "DOI" : yontem === "isbn" ? "ISBN (sağlama basamağı geçerli)" : "başlık sözcükleri, tam soyad",
|
|
332
|
+
...(s.tur && s.tur !== "herhangi" ? ["kayıt türü"] : []),
|
|
333
|
+
...(s.yil !== undefined ? ["yıl"] : []),
|
|
334
|
+
];
|
|
335
|
+
return { ...k, kimlik_eslesmesi: { yontem, denetlenen } };
|
|
336
|
+
}
|
|
200
337
|
async function doiIle(f, doi) {
|
|
201
338
|
const y = CrossrefTek.safeParse(await getir(f, new URL(`${CROSSREF}/${encodeURIComponent(doi)}`)));
|
|
202
|
-
|
|
203
|
-
return k ?? { durum: "dogrulanamadi", neden: "Bu DOI Crossref kataloğunda bulunamadı." };
|
|
339
|
+
return y.success ? crossrefKunye(y.data.message) : null;
|
|
204
340
|
}
|
|
205
341
|
async function isbnIle(f, isbn) {
|
|
206
342
|
const ol = new URL(OPENLIBRARY);
|
|
@@ -228,18 +364,20 @@ async function isbnIle(f, isbn) {
|
|
|
228
364
|
const c = gy.success
|
|
229
365
|
? gy.data.items?.find((x) => (x.volumeInfo.industryIdentifiers ?? []).some((i) => i.identifier.replace(/-/gu, "") === isbn))
|
|
230
366
|
: undefined;
|
|
231
|
-
return c ? googleKunye(c) :
|
|
367
|
+
return c ? googleKunye(c) : null;
|
|
232
368
|
}
|
|
233
|
-
async function baslikIle(f, baslik, yazar) {
|
|
369
|
+
async function baslikIle(f, baslik, yazar, s) {
|
|
370
|
+
const uygun = (k) => baslikEslesir(baslik, k.baslik) && yazarEslesir(yazar, k.yazarlar) && ekDenetim(k, s) === null;
|
|
234
371
|
const g = new URL(GOOGLE);
|
|
235
372
|
g.searchParams.set("q", `intitle:${baslik} inauthor:${yazar}`);
|
|
236
373
|
g.searchParams.set("maxResults", "5");
|
|
237
374
|
const gy = GoogleYanit.safeParse(await getir(f, g));
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
375
|
+
if (gy.success)
|
|
376
|
+
for (const x of gy.data.items ?? []) {
|
|
377
|
+
const k = googleKunye(x);
|
|
378
|
+
if (baslikEslesir(baslik, x.volumeInfo.title) && uygun(k))
|
|
379
|
+
return k;
|
|
380
|
+
}
|
|
243
381
|
const cr = new URL(CROSSREF);
|
|
244
382
|
cr.searchParams.set("query.bibliographic", baslik);
|
|
245
383
|
cr.searchParams.set("query.author", yazar);
|
|
@@ -248,13 +386,10 @@ async function baslikIle(f, baslik, yazar) {
|
|
|
248
386
|
if (cy.success)
|
|
249
387
|
for (const e of cy.data.message.items) {
|
|
250
388
|
const k = crossrefKunye(e);
|
|
251
|
-
if (k &&
|
|
389
|
+
if (k && uygun(k))
|
|
252
390
|
return k;
|
|
253
391
|
}
|
|
254
|
-
return
|
|
255
|
-
durum: "dogrulanamadi",
|
|
256
|
-
neden: "Bu başlık ve yazarla eşleşen eser Google Books ve Crossref kataloglarında bulunamadı.",
|
|
257
|
-
};
|
|
392
|
+
return null;
|
|
258
393
|
}
|
|
259
394
|
const KaynakSorgusu = z
|
|
260
395
|
.object({
|
|
@@ -262,25 +397,31 @@ const KaynakSorgusu = z
|
|
|
262
397
|
isbn: z.string().trim().max(30).optional(),
|
|
263
398
|
baslik: z.string().trim().min(2).max(300).optional(),
|
|
264
399
|
yazar: z.string().trim().min(2).max(120).optional(),
|
|
400
|
+
tur: z
|
|
401
|
+
.enum(["kitap", "makale", "kitap_bolumu", "herhangi"])
|
|
402
|
+
.optional()
|
|
403
|
+
.describe("Aranan kayıt türü. Kitap aranıyorsa dergi makalesi kabul edilmez. ISBN verilirse varsayılan kitap."),
|
|
404
|
+
yil: z.number().int().min(1400).max(2100).optional().describe("Biliniyorsa yayın yılı; kayıtla birebir uymalı"),
|
|
265
405
|
})
|
|
266
406
|
.refine((k) => k.doi || k.isbn || (k.baslik && k.yazar), {
|
|
267
407
|
message: "DOI, ISBN ya da başlık ile yazar verin.",
|
|
268
408
|
});
|
|
269
409
|
/** Tek kaynağı doğrular. DOI önce, sonra ISBN, sonra başlık+yazar. */
|
|
270
410
|
export async function kaynakDogrula(f, k) {
|
|
411
|
+
const ek = { tur: k.tur, yil: k.yil };
|
|
271
412
|
if (k.doi) {
|
|
272
413
|
const d = doiTemizle(k.doi);
|
|
273
414
|
if (!d)
|
|
274
|
-
return
|
|
275
|
-
return doiIle(f, d);
|
|
415
|
+
return bulunamadi("DOI biçimi geçersiz (10.xxxx/... olmalı).");
|
|
416
|
+
return sonla(await doiIle(f, d), "doi", ek, "Bu DOI Crossref kataloğunda bulunamadı.");
|
|
276
417
|
}
|
|
277
418
|
if (k.isbn) {
|
|
278
419
|
const i = isbnTemizle(k.isbn);
|
|
279
420
|
if (!i)
|
|
280
|
-
return
|
|
281
|
-
return isbnIle(f, i);
|
|
421
|
+
return bulunamadi("ISBN geçersiz (sağlama basamağı tutmuyor).");
|
|
422
|
+
return sonla(await isbnIle(f, i), "isbn", { ...ek, tur: k.tur ?? "kitap" }, "Bu ISBN Open Library ve Google Books'ta bulunamadı.");
|
|
282
423
|
}
|
|
283
|
-
return baslikIle(f, k.baslik ?? "", k.yazar ?? "");
|
|
424
|
+
return sonla(await baslikIle(f, k.baslik ?? "", k.yazar ?? "", ek), "baslik_yazar", ek, "Bu başlık ve yazarla (tam soyad, verildiyse tür ve yıl) eşleşen eser Google Books ve Crossref kataloglarında bulunamadı. Türkçe kitapların çoğu bu kataloglarda yoktur.");
|
|
284
425
|
}
|
|
285
426
|
const KATALOG_KAYNAKLARI = [
|
|
286
427
|
{ ad: "Crossref REST API", adres: "https://api.crossref.org" },
|
|
@@ -290,14 +431,14 @@ const KATALOG_KAYNAKLARI = [
|
|
|
290
431
|
export const kaynakDogrulaAraci = arac({
|
|
291
432
|
ad: "kaynak_dogrula",
|
|
292
433
|
baslik: "Kaynağın gerçekten var olduğunu doğrula",
|
|
293
|
-
aciklama: "DOI, ISBN ya da başlık ile yazar verilen eserleri açık kataloglarda (Crossref, Open Library, Google Books) arar. Bulursa katalogdaki bilgiden künye ve
|
|
294
|
-
sonrakiAdim: "Yalnız
|
|
434
|
+
aciklama: "DOI, ISBN ya da başlık ile yazar verilen eserleri (çağrı başına en çok 5) açık kataloglarda (Crossref, Open Library, Google Books) arar. Eşleşme için tam soyad, başlık sözcükleri, verildiyse kayıt türü ve yıl denetlenir. Bulursa katalogdaki bilgiden künye, bağlantı ve imza döndürür ('katalogda_bulundu'); bulamazsa 'bulunamadi' der. Katalogda bulunmak ders kaynağı olarak uygunluk demek değildir; ona hoca karar verir.",
|
|
435
|
+
sonrakiAdim: "Yalnız 'katalogda_bulundu' olan kaynakları hocaya künyesi ve bağlantısıyla gösterin; uygunluğuna hoca karar verir. Bologna araçlarına kaynağı { kunye, dogrulama: { durum, baglanti, imza } } olarak aynen verin. 'bulunamadi' olan kaynağı asla kendiniz önermeyin; hoca o eseri kullandığını söylerse yalnız hocanın verdiği bilgiyle dogrulama: 'hoca_beyani' olarak yazın.",
|
|
295
436
|
kunyeAlanlari: ["kaynaklar"],
|
|
296
437
|
disAg: true,
|
|
297
438
|
// Künye ve katalog numaraları kişisel veri değildir; ISBN gibi uzun sayılar numara deseniyle karışmasın.
|
|
298
439
|
kisiselVeriKapsami: "yok",
|
|
299
440
|
girdi: z.object({
|
|
300
|
-
kaynaklar: z.array(KaynakSorgusu).min(1).max(
|
|
441
|
+
kaynaklar: z.array(KaynakSorgusu).min(1).max(EN_COK_KAYNAK),
|
|
301
442
|
}),
|
|
302
443
|
calistir: async ({ kaynaklar }, b) => {
|
|
303
444
|
const f = b.fetch ?? globalThis.fetch;
|
|
@@ -305,13 +446,13 @@ export const kaynakDogrulaAraci = arac({
|
|
|
305
446
|
throw new AracHatasi("Bu ortamda dış katalog sorgusu yapılamıyor. Kaynakları şimdilik önermeyin.");
|
|
306
447
|
const sonuclar = await Promise.all(kaynaklar.map((k) => kaynakDogrula(f, k)));
|
|
307
448
|
const liste = sonuclar.map((s, i) => ({ no: i + 1, ...s }));
|
|
308
|
-
const
|
|
449
|
+
const bulunan = liste.filter((s) => s.durum === "katalogda_bulundu").length;
|
|
309
450
|
return {
|
|
310
451
|
sonuc: {
|
|
311
452
|
kaynaklar: liste,
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
kural: "
|
|
453
|
+
katalogdaBulunan: bulunan,
|
|
454
|
+
bulunamayan: liste.length - bulunan,
|
|
455
|
+
kural: "Katalogda bulunmayan kaynak asistanca önerilmez; hoca kullandığını söylerse 'hoca_beyani' etiketiyle yazılır. Künye katalog bilgisinden kurulur; katalogda olmayan alan [EKSİK] kalır. Ders kaynağı olarak uygunluğa hoca karar verir.",
|
|
315
456
|
},
|
|
316
457
|
kaynak: [KOD_KAYNAGI("apps/acik-mcp/src/kaynak-dogrula.ts", "Akredit kaynak doğrulayıcı"), ...KATALOG_KAYNAKLARI],
|
|
317
458
|
};
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* `kunyeAnahtarlari`: altındaki metinler künye kuralıyla (yazar adı serbest) değerlendirilen alan adları.
|
|
4
4
|
*/
|
|
5
5
|
export type KisiselVeriKapsami = "yok" | "ogrenci" | "tam";
|
|
6
|
+
/** HTML ise yalnız görünen metni bırakır (betik, stil, yorum, etiket ve öznitelikler atılır). Değilse aynen döner. */
|
|
7
|
+
export declare function gorunenMetin(x: string): string;
|
|
6
8
|
export declare function kisiselVeriTurleri(girdi: unknown, kunyeAnahtarlari?: readonly string[], kapsam?: KisiselVeriKapsami): string[];
|
|
7
9
|
export declare function kisiselVeriIletisi(turler: readonly string[]): string;
|
|
8
10
|
/** Girdide giriş bilgisi var mı? Yalnız evet ya da hayır döner, değer döndürmez. */
|
|
@@ -12,4 +14,4 @@ export declare function girisBilgisiAyikla(metin: string): {
|
|
|
12
14
|
metin: string;
|
|
13
15
|
atildi: boolean;
|
|
14
16
|
};
|
|
15
|
-
export declare const GIRIS_BILGISI_ILETISI = "G\u00F6nderdi\u011Finiz veride parola, do\u011Frulama kodu ya da oturum \u00E7erezi olabilecek bir ifade var.
|
|
17
|
+
export declare const GIRIS_BILGISI_ILETISI = "G\u00F6nderdi\u011Finiz veride parola, do\u011Frulama kodu ya da oturum \u00E7erezi olabilecek bir ifade var. Akredit bu bilgileri istemez; g\u00F6r\u00FClen istek reddedildi ve kaydedilmedi. Bunlar\u0131 yaln\u0131z OBS'nin kendi giri\u015F ekran\u0131na siz yazars\u0131n\u0131z. O k\u0131sm\u0131 silip yeniden g\u00F6nderin. Parolan\u0131z\u0131 bir sohbete yazd\u0131ysan\u0131z OBS'de de\u011Fi\u015Ftirmeniz iyi olur.";
|
|
@@ -3,16 +3,20 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Kapsam (İP-49): hocanın kendi sayfasında yayımlanmış görev bilgisi (Dersi Verenler, Koordinatör, iletişim)
|
|
5
5
|
// kişisel veri sayılıp atılmaz; olduğu gibi okunur ve korunur. Araçlar kapsamı kendisi seçer:
|
|
6
|
-
// - "yok": yalnız giriş bilgisi denetlenir (
|
|
7
|
-
// - "ogrenci" (varsayılan): öğrenci ve kimlik verisi (T.C. kimlik no, öğrenci
|
|
6
|
+
// - "yok": yalnız giriş bilgisi denetlenir (kaynak doğrulama: künye ve katalog numaraları).
|
|
7
|
+
// - "ogrenci" (varsayılan; Bologna araçları da bunu kullanır): öğrenci ve kimlik verisi (T.C. kimlik no, öğrenci
|
|
8
|
+
// numarası, IBAN) reddedilir. Yayımlanmış öğretim elemanı adları serbesttir.
|
|
8
9
|
// - "tam": bütün desenler (not tablosu ve ÇK/PÇ: öğrenci adı ve numarası takma adla gelmelidir).
|
|
9
10
|
//
|
|
10
11
|
// Künye kuralı: kaynakçadaki yazar adları yayımlanmış künyedir, kişisel veri sayılmaz. Bu yüzden künye alanlarında
|
|
11
|
-
// (araç bunları adıyla bildirir) kişi adı deseni aranmaz; e-posta,
|
|
12
|
-
// yine aranır.
|
|
12
|
+
// (araç bunları adıyla bildirir) kişi adı ve etiketsiz uzun sayı (ISBN, katalog no) deseni aranmaz; e-posta,
|
|
13
|
+
// T.C. kimlik no, IBAN gibi öbür desenler yine aranır.
|
|
13
14
|
//
|
|
14
|
-
// Giriş bilgisi (ilke 8): parola, doğrulama kodu ve oturum çerezi
|
|
15
|
-
// çağrı reddedilir
|
|
15
|
+
// Giriş bilgisi (ilke 8): parola, doğrulama kodu ve oturum çerezi istenmez. Yine de gönderilirse (sunucu isteği
|
|
16
|
+
// almadan bunu engelleyemez) desen görülür görülmez çağrı reddedilir, değer kaydedilmez ve yanıtta tekrarlanmaz.
|
|
17
|
+
//
|
|
18
|
+
// Başarım (dış inceleme 2026-11-05): HTML girdide desenler bütün kaynağa değil, yalnız görünen metne uygulanır
|
|
19
|
+
// (betik, stil, etiket ve öznitelikler önce atılır). Bağlantı adreslerindeki sayılar da böylece numara sayılmaz.
|
|
16
20
|
import { maskele } from "../../../packages/yz/src/index.js";
|
|
17
21
|
const TUR_ADI = {
|
|
18
22
|
KİŞİ: "kişi adı",
|
|
@@ -41,17 +45,28 @@ function metinler(deger, kunyeAnahtarlari, cikti = { genel: [], kunye: [] }, kun
|
|
|
41
45
|
/** "ogrenci" kapsamında aranan türler; öbürleri (ad, e-posta, telefon, adres, imza) kapalıdır. */
|
|
42
46
|
const OGRENCI_TURLERI = ["TCKN", "ÖĞRENCİ-NO", "IBAN"];
|
|
43
47
|
const TUM_TURLER = Object.keys(TUR_ADI);
|
|
48
|
+
/** HTML ise yalnız görünen metni bırakır (betik, stil, yorum, etiket ve öznitelikler atılır). Değilse aynen döner. */
|
|
49
|
+
export function gorunenMetin(x) {
|
|
50
|
+
if (!/<\/?[a-z][\w-]*[\s>/]/iu.test(x))
|
|
51
|
+
return x;
|
|
52
|
+
return x
|
|
53
|
+
.replace(/<!--[\s\S]*?-->/gu, " ")
|
|
54
|
+
.replace(/<(script|style|noscript|template)\b[\s\S]*?<\/\1\s*>/giu, " ")
|
|
55
|
+
.replace(/<[^>]{0,2000}>/gu, " ")
|
|
56
|
+
.replace(/ /gu, " ");
|
|
57
|
+
}
|
|
44
58
|
export function kisiselVeriTurleri(girdi, kunyeAnahtarlari = [], kapsam = "tam") {
|
|
45
59
|
if (kapsam === "yok")
|
|
46
60
|
return [];
|
|
47
61
|
const kapali = kapsam === "ogrenci" ? TUM_TURLER.filter((t) => !OGRENCI_TURLERI.includes(t)) : [];
|
|
62
|
+
const kunyeKapali = [...new Set([...kapali, "KİŞİ", "ÖĞRENCİ-NO"])];
|
|
48
63
|
const turler = new Set();
|
|
49
64
|
const m = metinler(girdi, new Set(kunyeAnahtarlari));
|
|
50
65
|
for (const x of m.genel)
|
|
51
|
-
for (const e of maskele(x, { kapali }).eslemeler)
|
|
66
|
+
for (const e of maskele(gorunenMetin(x), { kapali }).eslemeler)
|
|
52
67
|
turler.add(e.tur);
|
|
53
68
|
for (const x of m.kunye)
|
|
54
|
-
for (const e of maskele(x, { kapali:
|
|
69
|
+
for (const e of maskele(gorunenMetin(x), { kapali: kunyeKapali }).eslemeler)
|
|
55
70
|
turler.add(e.tur);
|
|
56
71
|
return [...turler].map((t) => TUR_ADI[t]);
|
|
57
72
|
}
|
|
@@ -85,4 +100,4 @@ export function girisBilgisiAyikla(metin) {
|
|
|
85
100
|
.replace(/\b(set-cookie|cookie)\s*:[^\n]*/giu, at);
|
|
86
101
|
return { metin: yeni, atildi };
|
|
87
102
|
}
|
|
88
|
-
export const GIRIS_BILGISI_ILETISI = "Gönderdiğiniz veride parola, doğrulama kodu ya da oturum çerezi olabilecek bir ifade var.
|
|
103
|
+
export const GIRIS_BILGISI_ILETISI = "Gönderdiğiniz veride parola, doğrulama kodu ya da oturum çerezi olabilecek bir ifade var. Akredit bu bilgileri istemez; görülen istek reddedildi ve kaydedilmedi. Bunları yalnız OBS'nin kendi giriş ekranına siz yazarsınız. O kısmı silip yeniden gönderin. Parolanızı bir sohbete yazdıysanız OBS'de değiştirmeniz iyi olur.";
|
|
@@ -8,6 +8,8 @@ export declare function tabloOku(metin: string): {
|
|
|
8
8
|
};
|
|
9
9
|
/** Kimlik sütunları: başlıktan ya da içerikten (çoğunluğu harfli metin ya da 7+ haneli sayı). */
|
|
10
10
|
export declare function kimlikSutunlari(satirlar: string[][]): number[];
|
|
11
|
+
/** Kimlik sütunu olmayan tablonun başına o1, o2 takma ad sütunu ekler. Başlıkta zaten takma ad varsa dokunmaz. */
|
|
12
|
+
export declare function takmaAdEkle(metin: string): string;
|
|
11
13
|
/** Tablodan kimlik sütunlarını atar, başa o1, o2 takma adı koyar. Yalnız bellekte çalışır. */
|
|
12
14
|
export declare function notTablosuAyikla(metin: string): {
|
|
13
15
|
tablo: string;
|
|
@@ -24,6 +26,10 @@ export interface Sutun {
|
|
|
24
26
|
}
|
|
25
27
|
/** Sütun başlığından araç, soru, tam puan ve ÇK eşlemesi sezgisi. "Vize S1 (ÇK2) /20" gibi. */
|
|
26
28
|
export declare function sutunCoz(baslik: string): Sutun | null;
|
|
29
|
+
/** Kişisel veri görülünce verilen ileti (aynen). */
|
|
30
|
+
export declare const NOT_TABLOSU_KISISEL_ILETI = "Ki\u015Fisel veriyi yapay zek\u00E2 sohbetine yap\u0131\u015Ft\u0131rmay\u0131n: Excel'de Ad, Soyad, Numara s\u00FCtunlar\u0131n\u0131 silip yaln\u0131z not s\u00FCtunlar\u0131n\u0131 yap\u0131\u015Ft\u0131r\u0131n ya da verinin taray\u0131c\u0131n\u0131zdan \u00E7\u0131kmad\u0131\u011F\u0131 hesaplay\u0131c\u0131y\u0131 kullan\u0131n: https://akredit.org/deneyin";
|
|
31
|
+
/** Tabloda kimlik sütunu (ad, soyad, numara...) ya da 9-11 haneli numara var mı? Varsa red iletisi döner. */
|
|
32
|
+
export declare function notTablosuKisiselVeri(metin: string): string | null;
|
|
27
33
|
export declare const SUTUN_ESLEME_REHBERI: string[];
|
|
28
34
|
export declare const notTablosuAyristirAraci: import("./temel.ts").AcikArac<z.ZodObject<{
|
|
29
35
|
tablo: z.ZodString;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
// Excel ya da CSV'den yapıştırılan not tablosunu ck_pc_hesapla girdisine hazırlar.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Excel ya da CSV'den yapıştırılan not tablosunu ck_pc_hesapla girdisine hazırlar. Öğrenciler satır sırasına göre
|
|
2
|
+
// o1, o2, ... takma adı alır.
|
|
3
|
+
//
|
|
4
|
+
// Kişisel veri (dış inceleme 2026-11-05): tablo sohbete yapıştırıldığı anda yapay zekâ sağlayıcısına gitmiştir;
|
|
5
|
+
// sunucunun sonradan sütun atması bunu geri almaz. Bu yüzden ad, soyad ya da öğrenci numarası sütunu ya da 9-11
|
|
6
|
+
// haneli bir numara görülürse tablo işlenmez; hoca bu sütunları Excel'de silip yalnız not sütunlarını yapıştırır ya
|
|
7
|
+
// da verinin tarayıcıdan çıkmadığı hesaplayıcıyı kullanır. Değerler hiçbir yere yazılmaz. Sütun başlıklarından ölçme aracı, soru numarası, tam puan ve ders çıktısı (ÇK)
|
|
4
8
|
// eşlemesi sezilir; sezilemeyen bilgi tahmin edilmez, eksikler listesine girer. Sayılar yalnız tablodan okunur.
|
|
5
9
|
import { z } from "zod";
|
|
6
10
|
import { arac, KOD_KAYNAGI } from "./temel.js";
|
|
@@ -59,6 +63,13 @@ export function kimlikSutunlari(satirlar) {
|
|
|
59
63
|
});
|
|
60
64
|
return sonuc;
|
|
61
65
|
}
|
|
66
|
+
/** Kimlik sütunu olmayan tablonun başına o1, o2 takma ad sütunu ekler. Başlıkta zaten takma ad varsa dokunmaz. */
|
|
67
|
+
export function takmaAdEkle(metin) {
|
|
68
|
+
const { satirlar } = tabloOku(metin);
|
|
69
|
+
if (satirlar.length < 2 || /^takma ad$/iu.test((satirlar[0]?.[0] ?? "").trim()))
|
|
70
|
+
return metin;
|
|
71
|
+
return satirlar.map((r, i) => [i === 0 ? "Takma ad" : `o${i}`, ...r].join("\t")).join("\n");
|
|
72
|
+
}
|
|
62
73
|
/** Tablodan kimlik sütunlarını atar, başa o1, o2 takma adı koyar. Yalnız bellekte çalışır. */
|
|
63
74
|
export function notTablosuAyikla(metin) {
|
|
64
75
|
const { satirlar } = tabloOku(metin);
|
|
@@ -110,11 +121,23 @@ const puanOku = (s, ayirac) => {
|
|
|
110
121
|
const n = Number(ayirac === "," ? t : t.replace(",", "."));
|
|
111
122
|
return Number.isFinite(n) ? n : null;
|
|
112
123
|
};
|
|
124
|
+
/** Kişisel veri görülünce verilen ileti (aynen). */
|
|
125
|
+
export const NOT_TABLOSU_KISISEL_ILETI = "Kişisel veriyi yapay zekâ sohbetine yapıştırmayın: Excel'de Ad, Soyad, Numara sütunlarını silip yalnız not sütunlarını yapıştırın ya da verinin tarayıcınızdan çıkmadığı hesaplayıcıyı kullanın: https://akredit.org/deneyin";
|
|
126
|
+
const UZUN_NUMARA = /(?<![\d.,])\d{9,11}(?![\d.,]\d)/u;
|
|
127
|
+
/** Tabloda kimlik sütunu (ad, soyad, numara...) ya da 9-11 haneli numara var mı? Varsa red iletisi döner. */
|
|
128
|
+
export function notTablosuKisiselVeri(metin) {
|
|
129
|
+
const { satirlar } = tabloOku(metin);
|
|
130
|
+
if (satirlar.length >= 2 && kimlikSutunlari(satirlar).length)
|
|
131
|
+
return NOT_TABLOSU_KISISEL_ILETI;
|
|
132
|
+
if (satirlar.some((r) => r.some((h) => UZUN_NUMARA.test(h.replace(/\s/gu, "")))))
|
|
133
|
+
return NOT_TABLOSU_KISISEL_ILETI;
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
113
136
|
export const SUTUN_ESLEME_REHBERI = [
|
|
114
137
|
"Excel'de ilk satır başlık olsun: her soru ya da ölçme için bir sütun.",
|
|
115
138
|
"Başlık örnekleri: 'Vize S1 (ÇK1) /20', 'Vize S2 (ÇK2) /30', 'Final S1 ÇK3 /50', 'Ödev ÇK4 /100'.",
|
|
116
139
|
"Başlıkta sınavın adı (Vize, Final, Ödev, Proje, Quiz), soru numarası (S1, Soru 2), ölçtüğü ders çıktısı (ÇK1 ya da Ö1) ve tam puanı (/20 ya da (20p)) olsun.",
|
|
117
|
-
"Ad,
|
|
140
|
+
"Ad, Soyad ve Numara sütunlarını yapıştırmadan önce Excel'de silin: yalnız not sütunları gelsin. Kimlik sütunu ya da numara görülürse tablo işlenmez. Öğrenciler o1, o2 diye takma ad alır.",
|
|
118
141
|
"Toplam, Ortalama, Harf sütunları yok sayılır.",
|
|
119
142
|
"Boş, '-' ya da 'G' (girmedi) hücreleri boş puan sayılır.",
|
|
120
143
|
"Tabloyu Excel'de seçip Ctrl+C ile kopyalayın, sohbete Ctrl+V ile yapıştırın.",
|
|
@@ -122,25 +145,27 @@ export const SUTUN_ESLEME_REHBERI = [
|
|
|
122
145
|
export const notTablosuAyristirAraci = arac({
|
|
123
146
|
ad: "not_tablosu_ayristir",
|
|
124
147
|
baslik: "Not tablosunu ayrıştır",
|
|
125
|
-
aciklama: "Excel ya da CSV'den yapıştırılan not tablosunu ÇK/PÇ hesabına hazırlar.
|
|
126
|
-
sonrakiAdim: "
|
|
127
|
-
kisiselVeriIpucu:
|
|
148
|
+
aciklama: "Excel ya da CSV'den yapıştırılan not tablosunu ÇK/PÇ hesabına hazırlar. Tabloda yalnız not sütunları olmalı: ad, soyad ya da numara sütunu ya da 9-11 haneli numara görülürse tablo işlenmez. Öğrencilere o1, o2 takma adı verir. Başlıklardan sınav, soru, tam puan ve ders çıktısı eşlemesini sezer; eksikleri listeler.",
|
|
149
|
+
sonrakiAdim: "Eksik yoksa hocaya soru sormadan ckPcHesaplaTaslagi'ni hemen ck_pc_hesapla'ya verin. Eksik varsa (tam puan, soru-çıktı eşlemesi, sınav ağırlığı) hepsini TEK mesajda, numaralı sorun; tahmin etmeyin, sütun sütun ayrı mesaj açmayın.",
|
|
150
|
+
kisiselVeriIpucu: NOT_TABLOSU_KISISEL_ILETI,
|
|
128
151
|
kisiselVeriKapsami: "tam",
|
|
152
|
+
onDenetim: (g) => {
|
|
153
|
+
const t = g?.tablo;
|
|
154
|
+
return typeof t === "string" ? notTablosuKisiselVeri(t) : null;
|
|
155
|
+
},
|
|
129
156
|
onAyikla: (g) => {
|
|
157
|
+
// Kimlik sütunu yoksa tablo yalnız not sütunlarıdır; başa o1, o2 takma adı eklenir (sütun atılmaz).
|
|
130
158
|
const t = g?.tablo;
|
|
131
159
|
if (typeof t !== "string")
|
|
132
160
|
return { girdi: g, atilan: [] };
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
girdi: { ...g, tablo: r.tablo },
|
|
136
|
-
atilan: r.atilan,
|
|
137
|
-
ileti: r.atilan.length
|
|
138
|
-
? `Öğrenci kimliği taşıyan sütunlar hesaplamadan önce atıldı, öğrencilere o1, o2 takma adı verildi; hiçbir yere yazılmadı: ${r.atilan.join(", ")}.`
|
|
139
|
-
: undefined,
|
|
140
|
-
};
|
|
161
|
+
return { girdi: { ...g, tablo: takmaAdEkle(t) }, atilan: [] };
|
|
141
162
|
},
|
|
142
163
|
girdi: z.object({
|
|
143
|
-
tablo: z
|
|
164
|
+
tablo: z
|
|
165
|
+
.string()
|
|
166
|
+
.min(5)
|
|
167
|
+
.max(400_000)
|
|
168
|
+
.describe("Excel ya da CSV'den yapıştırılan, yalnız not sütunlarından oluşan tablo; ilk satır başlık"),
|
|
144
169
|
tamPuanlar: z
|
|
145
170
|
.record(z.string().max(80), z.number().positive().max(1000))
|
|
146
171
|
.optional()
|
|
@@ -228,14 +253,14 @@ export const notTablosuAyristirAraci = arac({
|
|
|
228
253
|
ciktilar: ciktilar.map((kod) => ({ kod })),
|
|
229
254
|
araclar,
|
|
230
255
|
puanlar,
|
|
231
|
-
not: "dersKodu ve donem
|
|
256
|
+
not: "dersKodu ve donem biliniyorsa ekleyin; bilinmiyorsa bunun için hocayı durdurmayın, çıktıda EKSİK yazın. Eksik kalmayınca ck_pc_hesapla'ya verin.",
|
|
232
257
|
},
|
|
233
258
|
sutunEslemeRehberi: SUTUN_ESLEME_REHBERI,
|
|
234
259
|
},
|
|
235
260
|
kaynak: [KOD_KAYNAGI("apps/acik-mcp/src/not-tablosu.ts", "Akredit not tablosu ayrıştırıcısı")],
|
|
236
261
|
...(eksikler.length
|
|
237
262
|
? {
|
|
238
|
-
sonrakiAdim: "
|
|
263
|
+
sonrakiAdim: "Bütün eksikleri TEK mesajda, numaralı sorun (tahmin etmeyin); yanıtları tamPuanlar, eslemeler ve agirliklar olarak verip aracı yeniden çağırın. Tabloyu yeniden yapıştırtmayın, aynı tabloyu kullanın.",
|
|
239
264
|
}
|
|
240
265
|
: {}),
|
|
241
266
|
};
|