@akredit/acik-mcp 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +94 -8
  2. package/dist/apps/acik-mcp/src/araclar.d.ts +2 -28
  3. package/dist/apps/acik-mcp/src/araclar.js +27 -114
  4. package/dist/apps/acik-mcp/src/bologna.d.ts +209 -0
  5. package/dist/apps/acik-mcp/src/bologna.js +1321 -0
  6. package/dist/apps/acik-mcp/src/genel-ayristir.d.ts +83 -0
  7. package/dist/apps/acik-mcp/src/genel-ayristir.js +411 -0
  8. package/dist/apps/acik-mcp/src/http.js +4 -2
  9. package/dist/apps/acik-mcp/src/index.d.ts +6 -0
  10. package/dist/apps/acik-mcp/src/index.js +6 -0
  11. package/dist/apps/acik-mcp/src/kaynak-dogrula.d.ts +47 -0
  12. package/dist/apps/acik-mcp/src/kaynak-dogrula.js +319 -0
  13. package/dist/apps/acik-mcp/src/kaynak.d.ts +72 -0
  14. package/dist/apps/acik-mcp/src/kaynak.js +155 -0
  15. package/dist/apps/acik-mcp/src/kisisel-veri.d.ts +14 -2
  16. package/dist/apps/acik-mcp/src/kisisel-veri.js +63 -15
  17. package/dist/apps/acik-mcp/src/not-tablosu.d.ts +33 -0
  18. package/dist/apps/acik-mcp/src/not-tablosu.js +243 -0
  19. package/dist/apps/acik-mcp/src/promptlar.d.ts +25 -0
  20. package/dist/apps/acik-mcp/src/promptlar.js +253 -0
  21. package/dist/apps/acik-mcp/src/sunucu.d.ts +12 -4
  22. package/dist/apps/acik-mcp/src/sunucu.js +103 -24
  23. package/dist/apps/acik-mcp/src/temel.d.ts +54 -0
  24. package/dist/apps/acik-mcp/src/temel.js +10 -0
  25. package/dist/apps/acik-mcp/src/yardim.d.ts +56 -0
  26. package/dist/apps/acik-mcp/src/yardim.js +371 -0
  27. package/dist/packages/baglayicilar/src/bologna-proliz/ayristir.d.ts +29 -0
  28. package/dist/packages/baglayicilar/src/bologna-proliz/ayristir.js +200 -0
  29. package/package.json +4 -2
@@ -0,0 +1,319 @@
1
+ // Kaynak doğrulama: DOI, ISBN ya da başlık+yazar ile eserin gerçekten var olduğunu açık kataloglardan denetler.
2
+ // Yalnız sabit, izinli hostlara sorar (api.crossref.org, openlibrary.org, www.googleapis.com/books). Ağ adresi
3
+ // kullanıcıdan alınmaz; kullanıcı girdisi yalnız doğrulanmış DOI, ISBN ya da sorgu parametresi olarak, kodlanarak
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
+ //
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 benzerlik denetiminden geçmezse sonuç "doğrulanamadı"dır; istem o
8
+ // eserin asla önerilmemesini söyler.
9
+ import { z } from "zod";
10
+ import { kunyeBicimle } from "./kaynak.js";
11
+ import { AracHatasi, arac, KOD_KAYNAGI } from "./temel.js";
12
+ export const IZINLI_HOSTLAR = ["api.crossref.org", "openlibrary.org", "www.googleapis.com"];
13
+ export const ZAMAN_ASIMI_MS = 8000;
14
+ export const EN_BUYUK_YANIT = 1_000_000;
15
+ const DOI_DESENI = /^10\.\d{4,9}\/[^\s]{1,200}$/u;
16
+ /** "https://doi.org/10.x/y", "doi:10.x/y" ya da "10.x/y" → "10.x/y". Geçersizse null. */
17
+ export function doiTemizle(d) {
18
+ const t = d
19
+ .trim()
20
+ .replace(/^https?:\/\/(dx\.)?doi\.org\//iu, "")
21
+ .replace(/^doi:\s*/iu, "");
22
+ return DOI_DESENI.test(t) ? t : null;
23
+ }
24
+ /** ISBN-10 ya da ISBN-13; tire ve boşluk atılır, denetim basamağı sınanır. Geçersizse null. */
25
+ export function isbnTemizle(i) {
26
+ const t = i.replace(/[\s-]/gu, "").toUpperCase();
27
+ if (/^\d{13}$/u.test(t)) {
28
+ // ISBN-13: basamaklar sırayla 1 ve 3 ile çarpılır, toplam 10'un katı olmalı.
29
+ const top = [...t].reduce((s, c, k) => s + Number(c) * (k % 2 ? 3 : 1), 0);
30
+ return top % 10 === 0 ? t : null;
31
+ }
32
+ if (/^\d{9}[\dX]$/u.test(t)) {
33
+ // ISBN-10: basamaklar 10'dan 1'e ağırlıkla çarpılır (X = 10), toplam 11'in katı olmalı.
34
+ const top = [...t].reduce((s, c, k) => s + (c === "X" ? 10 : Number(c)) * (10 - k), 0);
35
+ return top % 11 === 0 ? t : null;
36
+ }
37
+ return null;
38
+ }
39
+ /** Karşılaştırma için sadeleştirme: küçük harf, Türkçe harfler düz, noktalama yok. */
40
+ export function sadele(s) {
41
+ return s
42
+ .toLocaleLowerCase("tr")
43
+ .normalize("NFD")
44
+ .replace(/\p{M}/gu, "")
45
+ .replace(/ı/gu, "i")
46
+ .replace(/[^\p{L}\p{N}\s]/gu, " ")
47
+ .split(/\s+/u)
48
+ .filter((x) => x.length > 1);
49
+ }
50
+ /** 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ı. */
51
+ export function baslikEslesir(sorgu, bulunan) {
52
+ const a = sadele(sorgu);
53
+ const b = new Set(sadele(bulunan));
54
+ if (!a.length || !b.size)
55
+ return false;
56
+ const ortak = a.filter((x) => b.has(x)).length;
57
+ return ortak / a.length >= 0.8 && ortak / b.size >= 0.6;
58
+ }
59
+ /** Sorgudaki yazar soyadı (son sözcük ya da virgülden önceki kısım) bulunan yazarlardan birinde geçiyor mu. */
60
+ export function yazarEslesir(sorgu, yazarlar) {
61
+ const soyad = sorgu.includes(",") ? (sorgu.split(",")[0] ?? "") : (sorgu.trim().split(/\s+/u).at(-1) ?? "");
62
+ const s = sadele(soyad).join(" ");
63
+ if (!s)
64
+ return false;
65
+ return yazarlar.some((y) => sadele(y).join(" ").includes(s));
66
+ }
67
+ // ---------------------------------------------------------------- Güvenli istek
68
+ async function getir(f, adres) {
69
+ if (adres.protocol !== "https:" || !IZINLI_HOSTLAR.includes(adres.hostname))
70
+ throw new Error("İzinsiz host");
71
+ if (adres.hostname === "www.googleapis.com" && !adres.pathname.startsWith("/books/"))
72
+ throw new Error("İzinsiz yol");
73
+ let y;
74
+ try {
75
+ y = await f(adres.toString(), {
76
+ signal: AbortSignal.timeout(ZAMAN_ASIMI_MS),
77
+ redirect: "error",
78
+ headers: { accept: "application/json", "user-agent": "akredit-acik-mcp (https://akredit.org)" },
79
+ });
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ if (!y.ok)
85
+ return null;
86
+ const uzunluk = Number(y.headers.get("content-length") ?? "0");
87
+ if (uzunluk > EN_BUYUK_YANIT)
88
+ return null;
89
+ const metin = await y.text();
90
+ if (metin.length > EN_BUYUK_YANIT)
91
+ return null;
92
+ try {
93
+ return JSON.parse(metin);
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ }
99
+ // ---------------------------------------------------------------- Katalog yanıtları (zod)
100
+ const CrossrefEser = z.object({
101
+ DOI: z.string(),
102
+ type: z.string().optional(),
103
+ title: z.array(z.string()).optional(),
104
+ author: z
105
+ .array(z.object({ given: z.string().optional(), family: z.string().optional(), name: z.string().optional() }))
106
+ .optional(),
107
+ editor: z.array(z.object({ given: z.string().optional(), family: z.string().optional() })).optional(),
108
+ "container-title": z.array(z.string()).optional(),
109
+ publisher: z.string().optional(),
110
+ volume: z.string().optional(),
111
+ issue: z.string().optional(),
112
+ page: z.string().optional(),
113
+ issued: z.object({ "date-parts": z.array(z.array(z.number().nullable())).optional() }).optional(),
114
+ });
115
+ const CrossrefTek = z.object({ message: CrossrefEser });
116
+ const CrossrefListe = z.object({ message: z.object({ items: z.array(CrossrefEser) }) });
117
+ const OpenLibraryKitap = z.object({
118
+ title: z.string(),
119
+ url: z.string().optional(),
120
+ authors: z.array(z.object({ name: z.string() })).optional(),
121
+ publishers: z.array(z.object({ name: z.string() })).optional(),
122
+ publish_date: z.string().optional(),
123
+ });
124
+ const OpenLibraryYanit = z.record(z.string(), OpenLibraryKitap);
125
+ const GoogleCilt = z.object({
126
+ id: z.string(),
127
+ volumeInfo: z.object({
128
+ title: z.string(),
129
+ subtitle: z.string().optional(),
130
+ authors: z.array(z.string()).optional(),
131
+ publisher: z.string().optional(),
132
+ publishedDate: z.string().optional(),
133
+ industryIdentifiers: z.array(z.object({ type: z.string(), identifier: z.string() })).optional(),
134
+ }),
135
+ });
136
+ const GoogleYanit = z.object({ totalItems: z.number().optional(), items: z.array(GoogleCilt).optional() });
137
+ const yilAl = (s) => {
138
+ const m = s?.match(/\b(1[4-9]\d{2}|20\d{2}|2100)\b/u);
139
+ return m ? Number(m[1]) : undefined;
140
+ };
141
+ const kur = (k, baglanti, katalog) => {
142
+ const g = { ...k, kurumYazar: false };
143
+ const { kunye, eksikler } = kunyeBicimle(g);
144
+ return {
145
+ durum: "dogrulandi",
146
+ kunye,
147
+ eksikler,
148
+ baglanti,
149
+ katalog,
150
+ baslik: k.baslik,
151
+ yazarlar: k.yazarlar,
152
+ ...(k.yil !== undefined ? { yil: k.yil } : {}),
153
+ };
154
+ };
155
+ function crossrefKunye(e) {
156
+ const baslik = e.title?.[0]?.trim();
157
+ if (!baslik)
158
+ return null;
159
+ const yazarlar = (e.author ?? [])
160
+ .map((a) => (a.family ? [a.family, a.given].filter(Boolean).join(", ") : (a.name ?? "")))
161
+ .filter(Boolean);
162
+ const yil = e.issued?.["date-parts"]?.[0]?.[0] ?? undefined;
163
+ const kap = e["container-title"]?.[0];
164
+ const tur = e.type === "journal-article" ? "makale" : e.type === "book-chapter" ? "kitap_bolumu" : "kitap";
165
+ const doi = e.DOI;
166
+ return kur({
167
+ tur,
168
+ yazarlar,
169
+ baslik,
170
+ ...(yil ? { yil } : {}),
171
+ ...(tur === "makale" && kap ? { dergi: kap } : {}),
172
+ ...(tur === "kitap_bolumu" && kap ? { kitapBasligi: kap } : {}),
173
+ ...(tur !== "makale" && e.publisher ? { yayinevi: e.publisher } : {}),
174
+ ...(tur === "kitap_bolumu" && e.editor?.length
175
+ ? {
176
+ editorler: e.editor.map((x) => [x.family, x.given].filter(Boolean).join(", ")).filter((x) => x.length >= 2),
177
+ }
178
+ : {}),
179
+ ...(e.volume ? { cilt: e.volume } : {}),
180
+ ...(e.issue ? { sayi: e.issue } : {}),
181
+ ...(e.page ? { sayfalar: e.page } : {}),
182
+ doi,
183
+ }, `https://doi.org/${doi}`, "Crossref");
184
+ }
185
+ function googleKunye(c) {
186
+ const v = c.volumeInfo;
187
+ const yil = yilAl(v.publishedDate);
188
+ return kur({
189
+ tur: "kitap",
190
+ yazarlar: v.authors ?? [],
191
+ baslik: v.subtitle ? `${v.title}: ${v.subtitle}` : v.title,
192
+ ...(yil ? { yil } : {}),
193
+ ...(v.publisher ? { yayinevi: v.publisher } : {}),
194
+ }, `https://books.google.com/books?id=${encodeURIComponent(c.id)}`, "Google Books");
195
+ }
196
+ // ---------------------------------------------------------------- Sorgular
197
+ const CROSSREF = "https://api.crossref.org/works";
198
+ const OPENLIBRARY = "https://openlibrary.org/api/books";
199
+ const GOOGLE = "https://www.googleapis.com/books/v1/volumes";
200
+ async function doiIle(f, doi) {
201
+ const y = CrossrefTek.safeParse(await getir(f, new URL(`${CROSSREF}/${encodeURIComponent(doi)}`)));
202
+ const k = y.success ? crossrefKunye(y.data.message) : null;
203
+ return k ?? { durum: "dogrulanamadi", neden: "Bu DOI Crossref kataloğunda bulunamadı." };
204
+ }
205
+ async function isbnIle(f, isbn) {
206
+ const ol = new URL(OPENLIBRARY);
207
+ ol.searchParams.set("bibkeys", `ISBN:${isbn}`);
208
+ ol.searchParams.set("format", "json");
209
+ ol.searchParams.set("jscmd", "data");
210
+ const o = OpenLibraryYanit.safeParse(await getir(f, ol));
211
+ const kitap = o.success ? o.data[`ISBN:${isbn}`] : undefined;
212
+ if (kitap) {
213
+ const yil = yilAl(kitap.publish_date);
214
+ const baglanti = kitap.url && /^https:\/\/openlibrary\.org\//u.test(kitap.url)
215
+ ? kitap.url
216
+ : `https://openlibrary.org/isbn/${isbn}`;
217
+ return kur({
218
+ tur: "kitap",
219
+ yazarlar: (kitap.authors ?? []).map((a) => a.name),
220
+ baslik: kitap.title,
221
+ ...(yil ? { yil } : {}),
222
+ ...(kitap.publishers?.[0] ? { yayinevi: kitap.publishers[0].name } : {}),
223
+ }, baglanti, "Open Library");
224
+ }
225
+ const g = new URL(GOOGLE);
226
+ g.searchParams.set("q", `isbn:${isbn}`);
227
+ const gy = GoogleYanit.safeParse(await getir(f, g));
228
+ const c = gy.success
229
+ ? gy.data.items?.find((x) => (x.volumeInfo.industryIdentifiers ?? []).some((i) => i.identifier.replace(/-/gu, "") === isbn))
230
+ : undefined;
231
+ return c ? googleKunye(c) : { durum: "dogrulanamadi", neden: "Bu ISBN Open Library ve Google Books'ta bulunamadı." };
232
+ }
233
+ async function baslikIle(f, baslik, yazar) {
234
+ const g = new URL(GOOGLE);
235
+ g.searchParams.set("q", `intitle:${baslik} inauthor:${yazar}`);
236
+ g.searchParams.set("maxResults", "5");
237
+ const gy = GoogleYanit.safeParse(await getir(f, g));
238
+ const c = gy.success
239
+ ? gy.data.items?.find((x) => baslikEslesir(baslik, x.volumeInfo.title) && yazarEslesir(yazar, x.volumeInfo.authors ?? []))
240
+ : undefined;
241
+ if (c)
242
+ return googleKunye(c);
243
+ const cr = new URL(CROSSREF);
244
+ cr.searchParams.set("query.bibliographic", baslik);
245
+ cr.searchParams.set("query.author", yazar);
246
+ cr.searchParams.set("rows", "5");
247
+ const cy = CrossrefListe.safeParse(await getir(f, cr));
248
+ if (cy.success)
249
+ for (const e of cy.data.message.items) {
250
+ const k = crossrefKunye(e);
251
+ if (k && baslikEslesir(baslik, k.baslik) && yazarEslesir(yazar, k.yazarlar))
252
+ return k;
253
+ }
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
+ };
258
+ }
259
+ const KaynakSorgusu = z
260
+ .object({
261
+ doi: z.string().trim().max(250).optional(),
262
+ isbn: z.string().trim().max(30).optional(),
263
+ baslik: z.string().trim().min(2).max(300).optional(),
264
+ yazar: z.string().trim().min(2).max(120).optional(),
265
+ })
266
+ .refine((k) => k.doi || k.isbn || (k.baslik && k.yazar), {
267
+ message: "DOI, ISBN ya da başlık ile yazar verin.",
268
+ });
269
+ /** Tek kaynağı doğrular. DOI önce, sonra ISBN, sonra başlık+yazar. */
270
+ export async function kaynakDogrula(f, k) {
271
+ if (k.doi) {
272
+ const d = doiTemizle(k.doi);
273
+ if (!d)
274
+ return { durum: "dogrulanamadi", neden: "DOI biçimi geçersiz (10.xxxx/... olmalı)." };
275
+ return doiIle(f, d);
276
+ }
277
+ if (k.isbn) {
278
+ const i = isbnTemizle(k.isbn);
279
+ if (!i)
280
+ return { durum: "dogrulanamadi", neden: "ISBN geçersiz (denetim basamağı tutmuyor)." };
281
+ return isbnIle(f, i);
282
+ }
283
+ return baslikIle(f, k.baslik ?? "", k.yazar ?? "");
284
+ }
285
+ const KATALOG_KAYNAKLARI = [
286
+ { ad: "Crossref REST API", adres: "https://api.crossref.org" },
287
+ { ad: "Open Library Books API", adres: "https://openlibrary.org/developers/api" },
288
+ { ad: "Google Books API", adres: "https://developers.google.com/books" },
289
+ ];
290
+ export const kaynakDogrulaAraci = arac({
291
+ ad: "kaynak_dogrula",
292
+ 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 açılabilir bağlantı döndürür; bulamazsa 'doğrulanamadı' der. Önerilecek her kaynak önce bu araçtan geçer.",
294
+ sonrakiAdim: "Yalnız durumu 'dogrulandi' olan kaynakları hocaya künyesi ve bağlantısıyla numaralı seçenek olarak gösterin. 'dogrulanamadi' olan kaynağı asla önermeyin, künyesini yazmayın; yerine başka bir aday arayıp yeniden doğrulayın.",
295
+ kunyeAlanlari: ["kaynaklar"],
296
+ disAg: true,
297
+ // Künye ve katalog numaraları kişisel veri değildir; ISBN gibi uzun sayılar numara deseniyle karışmasın.
298
+ kisiselVeriKapsami: "yok",
299
+ girdi: z.object({
300
+ kaynaklar: z.array(KaynakSorgusu).min(1).max(10),
301
+ }),
302
+ calistir: async ({ kaynaklar }, b) => {
303
+ const f = b.fetch ?? globalThis.fetch;
304
+ if (!f)
305
+ throw new AracHatasi("Bu ortamda dış katalog sorgusu yapılamıyor. Kaynakları şimdilik önermeyin.");
306
+ const sonuclar = await Promise.all(kaynaklar.map((k) => kaynakDogrula(f, k)));
307
+ const liste = sonuclar.map((s, i) => ({ no: i + 1, ...s }));
308
+ const dogrulanan = liste.filter((s) => s.durum === "dogrulandi").length;
309
+ return {
310
+ sonuc: {
311
+ kaynaklar: liste,
312
+ dogrulanan,
313
+ dogrulanamayan: liste.length - dogrulanan,
314
+ kural: "Doğrulanamayan kaynak önerilmez. Künye katalog bilgisinden kurulur; katalogda olmayan alan [EKSİK] kalır.",
315
+ },
316
+ kaynak: [KOD_KAYNAGI("apps/acik-mcp/src/kaynak-dogrula.ts", "Akredit kaynak doğrulayıcı"), ...KATALOG_KAYNAKLARI],
317
+ };
318
+ },
319
+ });
@@ -0,0 +1,72 @@
1
+ import { z } from "zod";
2
+ declare const Kaynak: z.ZodObject<{
3
+ tur: z.ZodDefault<z.ZodEnum<{
4
+ web: "web";
5
+ kitap: "kitap";
6
+ makale: "makale";
7
+ kitap_bolumu: "kitap_bolumu";
8
+ tez: "tez";
9
+ }>>;
10
+ yazarlar: z.ZodDefault<z.ZodArray<z.ZodString>>;
11
+ kurumYazar: z.ZodDefault<z.ZodBoolean>;
12
+ yil: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>;
13
+ baslik: z.ZodString;
14
+ baski: z.ZodOptional<z.ZodNumber>;
15
+ yayinevi: z.ZodOptional<z.ZodString>;
16
+ dergi: z.ZodOptional<z.ZodString>;
17
+ cilt: z.ZodOptional<z.ZodString>;
18
+ sayi: z.ZodOptional<z.ZodString>;
19
+ sayfalar: z.ZodOptional<z.ZodString>;
20
+ editorler: z.ZodOptional<z.ZodArray<z.ZodString>>;
21
+ kitapBasligi: z.ZodOptional<z.ZodString>;
22
+ kurum: z.ZodOptional<z.ZodString>;
23
+ tezTuru: z.ZodOptional<z.ZodEnum<{
24
+ yuksek_lisans: "yuksek_lisans";
25
+ doktora: "doktora";
26
+ }>>;
27
+ site: z.ZodOptional<z.ZodString>;
28
+ doi: z.ZodOptional<z.ZodString>;
29
+ url: z.ZodOptional<z.ZodString>;
30
+ }, z.core.$strip>;
31
+ type KaynakT = z.infer<typeof Kaynak>;
32
+ /** "Soyad, Ad İkinci" ya da "Ad İkinci Soyad" → "Soyad, A. İ." */
33
+ export declare function yazarBicimle(ad: string, kurum?: boolean): string;
34
+ /** APA 7: 1 yazar "A"; 2 yazar "A, & B"; 3-20 yazar virgülle, sonuncudan önce "&"; 21+ ilk 19, "...", son. */
35
+ export declare function yazarListesi(yazarlar: string[], kurum?: boolean): string;
36
+ /** Tek künye. Eksik zorunlu bilgi [EKSİK: ...] olarak yazılır ve ayrıca listelenir. */
37
+ export declare function kunyeBicimle(k: KaynakT): {
38
+ kunye: string;
39
+ eksikler: string[];
40
+ };
41
+ export declare const kaynakBicimleAraci: import("./temel.ts").AcikArac<z.ZodObject<{
42
+ kaynaklar: z.ZodArray<z.ZodObject<{
43
+ tur: z.ZodDefault<z.ZodEnum<{
44
+ web: "web";
45
+ kitap: "kitap";
46
+ makale: "makale";
47
+ kitap_bolumu: "kitap_bolumu";
48
+ tez: "tez";
49
+ }>>;
50
+ yazarlar: z.ZodDefault<z.ZodArray<z.ZodString>>;
51
+ kurumYazar: z.ZodDefault<z.ZodBoolean>;
52
+ yil: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>;
53
+ baslik: z.ZodString;
54
+ baski: z.ZodOptional<z.ZodNumber>;
55
+ yayinevi: z.ZodOptional<z.ZodString>;
56
+ dergi: z.ZodOptional<z.ZodString>;
57
+ cilt: z.ZodOptional<z.ZodString>;
58
+ sayi: z.ZodOptional<z.ZodString>;
59
+ sayfalar: z.ZodOptional<z.ZodString>;
60
+ editorler: z.ZodOptional<z.ZodArray<z.ZodString>>;
61
+ kitapBasligi: z.ZodOptional<z.ZodString>;
62
+ kurum: z.ZodOptional<z.ZodString>;
63
+ tezTuru: z.ZodOptional<z.ZodEnum<{
64
+ yuksek_lisans: "yuksek_lisans";
65
+ doktora: "doktora";
66
+ }>>;
67
+ site: z.ZodOptional<z.ZodString>;
68
+ doi: z.ZodOptional<z.ZodString>;
69
+ url: z.ZodOptional<z.ZodString>;
70
+ }, z.core.$strip>>;
71
+ }, z.core.$strip>>;
72
+ export {};
@@ -0,0 +1,155 @@
1
+ // Kaynak künyesi biçimlendirme: APA 7'ye benzer, Türkçe kısaltmalı, deterministik. Yalnız verilen bilgiyi dizer;
2
+ // eksik bilgi tahmin edilmez, künyede [EKSİK: ...] olarak görünür. Yazar adları yayımlanmış künyedir; bu araçta
3
+ // kişi adı deseni aranmaz (künye kuralı), e-posta ve telefon yine reddedilir.
4
+ import { z } from "zod";
5
+ import { arac, KOD_KAYNAGI } from "./temel.js";
6
+ const Kaynak = z.object({
7
+ tur: z.enum(["kitap", "makale", "kitap_bolumu", "web", "tez"]).default("kitap"),
8
+ yazarlar: z
9
+ .array(z.string().trim().min(2).max(120))
10
+ .max(25)
11
+ .default([])
12
+ .describe("'Soyad, Ad' ya da 'Ad Soyad'; kurum adı da olabilir"),
13
+ kurumYazar: z.boolean().default(false).describe("Yazar bir kurumsa (ör. Kurgu Bakanlığı) true"),
14
+ yil: z.union([z.number().int().min(1400).max(2100), z.string().trim().max(20)]).optional(),
15
+ baslik: z.string().trim().min(2).max(400),
16
+ baski: z.number().int().min(2).max(50).optional(),
17
+ yayinevi: z.string().trim().max(200).optional(),
18
+ dergi: z.string().trim().max(200).optional(),
19
+ cilt: z.string().trim().max(20).optional(),
20
+ sayi: z.string().trim().max(20).optional(),
21
+ sayfalar: z.string().trim().max(30).optional().describe("ör. '12-30'"),
22
+ editorler: z.array(z.string().trim().min(2).max(120)).max(10).optional(),
23
+ kitapBasligi: z.string().trim().max(400).optional(),
24
+ kurum: z.string().trim().max(200).optional().describe("Tezde üniversite"),
25
+ tezTuru: z.enum(["yuksek_lisans", "doktora"]).optional(),
26
+ site: z.string().trim().max(200).optional(),
27
+ doi: z.string().trim().max(200).optional(),
28
+ url: z.string().trim().max(500).optional(),
29
+ });
30
+ const bas = (s) => {
31
+ const h = [...s.trim()][0] ?? "";
32
+ return h ? `${h.toLocaleUpperCase("tr")}.` : "";
33
+ };
34
+ /** "Soyad, Ad İkinci" ya da "Ad İkinci Soyad" → "Soyad, A. İ." */
35
+ export function yazarBicimle(ad, kurum = false) {
36
+ const a = ad.trim().replace(/\s+/gu, " ");
37
+ if (kurum)
38
+ return a;
39
+ if (a.includes(",")) {
40
+ const [soyad, ...ilk] = a.split(",");
41
+ const adlar = ilk
42
+ .join(" ")
43
+ .trim()
44
+ .split(/[\s-]+/u)
45
+ .filter(Boolean);
46
+ return [soyad?.trim(), adlar.map(bas).join(" ")].filter(Boolean).join(", ");
47
+ }
48
+ const p = a.split(" ");
49
+ if (p.length === 1)
50
+ return a;
51
+ const soyad = p.at(-1) ?? "";
52
+ return `${soyad}, ${p.slice(0, -1).map(bas).join(" ")}`;
53
+ }
54
+ /** APA 7: 1 yazar "A"; 2 yazar "A, & B"; 3-20 yazar virgülle, sonuncudan önce "&"; 21+ ilk 19, "...", son. */
55
+ export function yazarListesi(yazarlar, kurum = false) {
56
+ const b = yazarlar.map((y) => yazarBicimle(y, kurum));
57
+ if (b.length === 0)
58
+ return "";
59
+ if (b.length === 1)
60
+ return b[0] ?? "";
61
+ if (b.length <= 20)
62
+ return `${b.slice(0, -1).join(", ")}, & ${b.at(-1)}`;
63
+ return `${b.slice(0, 19).join(", ")}, ... ${b.at(-1)}`;
64
+ }
65
+ const nokta = (s) => (/[.?!]$/u.test(s) ? s : `${s}.`);
66
+ const doiBagi = (d) => (/^https?:\/\//u.test(d) ? d : `https://doi.org/${d.replace(/^doi:\s*/iu, "")}`);
67
+ const editorKisa = (e) => {
68
+ const b = yazarBicimle(e);
69
+ const [soyad, bas] = b.split(", ");
70
+ return bas ? `${bas} ${soyad}` : b;
71
+ };
72
+ /** Tek künye. Eksik zorunlu bilgi [EKSİK: ...] olarak yazılır ve ayrıca listelenir. */
73
+ export function kunyeBicimle(k) {
74
+ const eksikler = [];
75
+ const eksik = (ad) => {
76
+ eksikler.push(ad);
77
+ return `[EKSİK: ${ad}]`;
78
+ };
79
+ const yazar = k.yazarlar.length ? yazarListesi(k.yazarlar, k.kurumYazar) : eksik("yazar");
80
+ const yil = k.yil === undefined || k.yil === "" ? "t.y." : String(k.yil);
81
+ if (yil === "t.y.")
82
+ eksikler.push("yıl (t.y. yazıldı)");
83
+ const parcalar = [`${nokta(yazar)} (${yil}).`];
84
+ const baski = k.baski ? ` (${k.baski}. bs.)` : "";
85
+ switch (k.tur) {
86
+ case "kitap":
87
+ parcalar.push(nokta(`${k.baslik}${baski}`), nokta(k.yayinevi || eksik("yayınevi")));
88
+ break;
89
+ case "makale": {
90
+ parcalar.push(nokta(k.baslik));
91
+ const dergi = k.dergi || eksik("dergi adı");
92
+ const cilt = k.cilt ? `, ${k.cilt}${k.sayi ? `(${k.sayi})` : ""}` : "";
93
+ const sayfa = k.sayfalar ? `, ${k.sayfalar}` : "";
94
+ parcalar.push(nokta(`${dergi}${cilt}${sayfa}`));
95
+ if (!k.cilt)
96
+ eksikler.push("cilt");
97
+ if (!k.sayfalar)
98
+ eksikler.push("sayfalar");
99
+ break;
100
+ }
101
+ case "kitap_bolumu": {
102
+ parcalar.push(nokta(k.baslik));
103
+ const ed = k.editorler?.length
104
+ ? `${k.editorler.map(editorKisa).join(", ")} (${k.editorler.length > 1 ? "Ed'ler" : "Ed."}), `
105
+ : "";
106
+ const kitap = k.kitapBasligi || eksik("kitap adı");
107
+ const ss = k.sayfalar ? ` (ss. ${k.sayfalar})` : "";
108
+ parcalar.push(nokta(`İçinde ${ed}${kitap}${baski}${ss}`), nokta(k.yayinevi || eksik("yayınevi")));
109
+ break;
110
+ }
111
+ case "tez": {
112
+ const tur = k.tezTuru === "doktora"
113
+ ? "Doktora tezi"
114
+ : k.tezTuru === "yuksek_lisans"
115
+ ? "Yüksek lisans tezi"
116
+ : eksik("tez türü");
117
+ parcalar.push(`${k.baslik} [${tur}, ${k.kurum || eksik("üniversite")}].`);
118
+ break;
119
+ }
120
+ case "web":
121
+ parcalar.push(nokta(k.baslik));
122
+ if (k.site)
123
+ parcalar.push(nokta(k.site));
124
+ if (!k.url)
125
+ parcalar.push(eksik("adres"));
126
+ break;
127
+ }
128
+ if (k.doi)
129
+ parcalar.push(doiBagi(k.doi));
130
+ else if (k.url)
131
+ parcalar.push(k.url);
132
+ return { kunye: parcalar.join(" ").replace(/\s+/gu, " ").trim(), eksikler };
133
+ }
134
+ export const kaynakBicimleAraci = arac({
135
+ ad: "kaynak_bicimle",
136
+ baslik: "Kaynak künyesi biçimle",
137
+ aciklama: "Hocanın verdiği kitap, makale, kitap bölümü, tez ya da web kaynağını APA 7'ye benzer Türkçe künyeye çevirir. Yalnız verilen bilgiyi dizer; eksik bilgi [EKSİK] olarak görünür, uydurulmaz.",
138
+ sonrakiAdim: "Künyeleri hocaya gösterin. Eserin varlığını kaynak_dogrula ile doğrulayın; doğrulanamayanı yazmayın. EKSİK bilgiyi tahmin etmeyin. Sonra künyeleri kaynaklar alanına koyun.",
139
+ kunyeAlanlari: ["kaynaklar"],
140
+ girdi: z.object({
141
+ kaynaklar: z.array(Kaynak).min(1).max(30),
142
+ }),
143
+ calistir: ({ kaynaklar }) => {
144
+ const sonuclar = kaynaklar.map((k, i) => ({ no: i + 1, ...kunyeBicimle(k) }));
145
+ return {
146
+ sonuc: {
147
+ kunyeler: sonuclar,
148
+ tamamlanan: sonuclar.filter((s) => !s.eksikler.length).length,
149
+ not: "Biçim APA 7'ye benzer bir Akredit biçimidir; kurumunuzun yazım kılavuzu farklıysa o önceliklidir. Eser bilgisinin doğruluğunu hoca denetler.",
150
+ uydurmaYok: "Bu araç yalnız verilen bilgiyi biçimlendirir. Asistan, kaynak_dogrula ile doğrulanmamış bir eseri önermez.",
151
+ },
152
+ kaynak: [KOD_KAYNAGI("apps/acik-mcp/src/kaynak.ts", "Akredit künye biçimleyici")],
153
+ };
154
+ },
155
+ });
@@ -1,3 +1,15 @@
1
- /** Girdide kişisel veri olabilecek türlerin adları (değerler değil). Boşsa temiz. */
2
- export declare function kisiselVeriTurleri(girdi: unknown): string[];
1
+ /**
2
+ * Girdide kişisel veri olabilecek türlerin adları (değerler değil). Boşsa temiz.
3
+ * `kunyeAnahtarlari`: altındaki metinler künye kuralıyla (yazar adı serbest) değerlendirilen alan adları.
4
+ */
5
+ export type KisiselVeriKapsami = "yok" | "ogrenci" | "tam";
6
+ export declare function kisiselVeriTurleri(girdi: unknown, kunyeAnahtarlari?: readonly string[], kapsam?: KisiselVeriKapsami): string[];
3
7
  export declare function kisiselVeriIletisi(turler: readonly string[]): string;
8
+ /** Girdide giriş bilgisi var mı? Yalnız evet ya da hayır döner, değer döndürmez. */
9
+ export declare function girisBilgisiVar(girdi: unknown): boolean;
10
+ /** Yapıştırılan sayfadan oturum kimliklerini, parola kutularını ve çerez satırlarını bellekte atar. */
11
+ export declare function girisBilgisiAyikla(metin: string): {
12
+ metin: string;
13
+ atildi: boolean;
14
+ };
15
+ export declare const GIRIS_BILGISI_ILETISI = "G\u00F6nderdi\u011Finiz veride parola, do\u011Frulama kodu ya da oturum \u00E7erezi olabilecek bir ifade var. Bu bilgiler yapay zek\u00E2ya ve Akredit'e hi\u00E7 verilmez; 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.";
@@ -1,7 +1,18 @@
1
- // Gelen veride kişisel veri denetimi. Açık sunucu kişisel veri işlemez (CLAUDE.md ilke 7): desen görülürse
2
- // istek reddedilir, kullanıcıdan maskeleyip yeniden göndermesi istenir. Bulunan değer hiçbir yere yazılmaz,
3
- // yalnız türü söylenir. Desenler @akredit/yz KVKK maskeleyicisinden gelir (TCKN, telefon, e-posta, IBAN,
4
- // öğrenci numarası, unvanlı ya da yaygın adla başlayan kişi adı, adres, imza).
1
+ // Gelen veride kişisel veri ve giriş bilgisi denetimi. Bulunan değer hiçbir yere yazılmaz, yalnız türü söylenir.
2
+ // Desenler @akredit/yz KVKK maskeleyicisinden gelir.
3
+ //
4
+ // Kapsam (İP-49): hocanın kendi sayfasında yayımlanmış görev bilgisi (Dersi Verenler, Koordinatör, iletişim)
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 (Bologna araçları).
7
+ // - "ogrenci" (varsayılan): öğrenci ve kimlik verisi (T.C. kimlik no, öğrenci numarası, IBAN) reddedilir.
8
+ // - "tam": bütün desenler (not tablosu ve ÇK/PÇ: öğrenci adı ve numarası takma adla gelmelidir).
9
+ //
10
+ // 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, telefon, kimlik numarası gibi öbür desenler
12
+ // yine aranır.
13
+ //
14
+ // Giriş bilgisi (ilke 8): parola, doğrulama kodu ve oturum çerezi sunucuya hiç girmez. Böyle bir desen görülürse
15
+ // çağrı reddedilir; değer yanıtta tekrarlanmaz.
5
16
  import { maskele } from "../../../packages/yz/src/index.js";
6
17
  const TUR_ADI = {
7
18
  KİŞİ: "kişi adı",
@@ -13,28 +24,65 @@ const TUR_ADI = {
13
24
  ADRES: "adres",
14
25
  İMZA: "imza",
15
26
  };
16
- /** Bir girdideki bütün metinleri (anahtarlar dahil) gezer. */
17
- function metinler(deger, cikti = []) {
27
+ /** Bir girdideki bütün metinleri (anahtarlar dahil) gezer; künye alanlarındaki metinleri ayrı toplar. */
28
+ function metinler(deger, kunyeAnahtarlari, cikti = { genel: [], kunye: [] }, kunyede = false) {
18
29
  if (typeof deger === "string")
19
- cikti.push(deger);
30
+ (kunyede ? cikti.kunye : cikti.genel).push(deger);
20
31
  else if (Array.isArray(deger))
21
32
  for (const x of deger)
22
- metinler(x, cikti);
33
+ metinler(x, kunyeAnahtarlari, cikti, kunyede);
23
34
  else if (deger !== null && typeof deger === "object")
24
35
  for (const [k, v] of Object.entries(deger)) {
25
- cikti.push(k);
26
- metinler(v, cikti);
36
+ cikti.genel.push(k);
37
+ metinler(v, kunyeAnahtarlari, cikti, kunyede || kunyeAnahtarlari.has(k));
27
38
  }
28
39
  return cikti;
29
40
  }
30
- /** Girdide kişisel veri olabilecek türlerin adları (değerler değil). Boşsa temiz. */
31
- export function kisiselVeriTurleri(girdi) {
41
+ /** "ogrenci" kapsamında aranan türler; öbürleri (ad, e-posta, telefon, adres, imza) kapalıdır. */
42
+ const OGRENCI_TURLERI = ["TCKN", "ÖĞRENCİ-NO", "IBAN"];
43
+ const TUM_TURLER = Object.keys(TUR_ADI);
44
+ export function kisiselVeriTurleri(girdi, kunyeAnahtarlari = [], kapsam = "tam") {
45
+ if (kapsam === "yok")
46
+ return [];
47
+ const kapali = kapsam === "ogrenci" ? TUM_TURLER.filter((t) => !OGRENCI_TURLERI.includes(t)) : [];
32
48
  const turler = new Set();
33
- for (const m of metinler(girdi))
34
- for (const e of maskele(m).eslemeler)
49
+ const m = metinler(girdi, new Set(kunyeAnahtarlari));
50
+ for (const x of m.genel)
51
+ for (const e of maskele(x, { kapali }).eslemeler)
52
+ turler.add(e.tur);
53
+ for (const x of m.kunye)
54
+ for (const e of maskele(x, { kapali: [...new Set([...kapali, "KİŞİ"])] }).eslemeler)
35
55
  turler.add(e.tur);
36
56
  return [...turler].map((t) => TUR_ADI[t]);
37
57
  }
38
58
  export function kisiselVeriIletisi(turler) {
39
- return `Gönderdiğiniz veride kişisel veri olabilecek ifadeler var (${turler.join(", ")}). Bu sunucu kişisel veri işlemez. Adları, numaraları ve iletişim bilgilerini maskeleyip (ör. "Öğrenci 1", "[KİŞİ-1]") yeniden gönderin.`;
59
+ return `Gönderdiğiniz veride kişisel veri olabilecek ifadeler var (${turler.join(", ")}). Öğrenci ve kimlik bilgileri bu araca takma adla gelmelidir. Bu kısımları takma adla (ör. "o1", "Öğrenci 1") değiştirip yeniden gönderin.`;
60
+ }
61
+ const OTURUM_KIMLIGI = /\b(jsessionid|phpsessid|asp\.net_sessionid|aspsessionid\w*|sessionid|connect\.sid|laravel_session|\.aspxauth)\s*=\s*[\w%.-]{6,}/iu;
62
+ /** Giriş bilgisi desenleri: etiketli parola ya da doğrulama kodu, çerez başlığı, oturum kimliği, erişim belirteci. */
63
+ const GIRIS_DESENLERI = [
64
+ /(^|[^\p{L}])(parola|şifre|sifre|password|passwd|pwd)\s*[:=]\s*\S+/iu,
65
+ /(doğrulama kodu|dogrulama kodu|onay kodu|tek kullanımlık kod|sms kodu|verification code|otp|2fa kodu)\s*[:=]?\s*\d{4,8}(?!\d)/iu,
66
+ /\b(set-cookie|cookie)\s*:\s*\S+=/iu,
67
+ OTURUM_KIMLIGI,
68
+ /\bBearer\s+[\w.~+/-]{16,}=*/u,
69
+ ];
70
+ /** Girdide giriş bilgisi var mı? Yalnız evet ya da hayır döner, değer döndürmez. */
71
+ export function girisBilgisiVar(girdi) {
72
+ const m = metinler(girdi, new Set());
73
+ return [...m.genel, ...m.kunye].some((x) => GIRIS_DESENLERI.some((d) => d.test(x)));
74
+ }
75
+ /** Yapıştırılan sayfadan oturum kimliklerini, parola kutularını ve çerez satırlarını bellekte atar. */
76
+ export function girisBilgisiAyikla(metin) {
77
+ let atildi = false;
78
+ const at = () => {
79
+ atildi = true;
80
+ return "";
81
+ };
82
+ const yeni = metin
83
+ .replace(new RegExp(OTURUM_KIMLIGI.source, "giu"), at)
84
+ .replace(/<input\b[^>]*type\s*=\s*["']?password[^>]*>/giu, at)
85
+ .replace(/\b(set-cookie|cookie)\s*:[^\n]*/giu, at);
86
+ return { metin: yeni, atildi };
40
87
  }
88
+ export const GIRIS_BILGISI_ILETISI = "Gönderdiğiniz veride parola, doğrulama kodu ya da oturum çerezi olabilecek bir ifade var. Bu bilgiler yapay zekâya ve Akredit'e hiç verilmez; 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.";