File size: 18,477 Bytes
eff511c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
from __future__ import annotations

import re
from typing import Any

from clause_retrieval import evidence_spans_matching
from sources import format_clause_sources
from utils import normalize_for_search


CASE_RULES = {
    "student_status_and_remedy": {
        "triggers": (
            "kaydinin sil", "kaydi sil", "ilisigi kes", "kayit sil", "itiraz",
            "uyaril", "teblig", "bildiril", "mazeret",
        ),
        "evidence_selectors": (
            {
                "document_id": "TR-KANUN-2547",
                "article_id": "Madde 44",
                "label": "b",
                "contains_all": ("azami süreler içinde", "kayıt yenilenmemesi", "ilişikleri kesilmez"),
            },
            {
                "document_id": "TR-KANUN-2547",
                "article_id": "Madde 44",
                "label": "b",
                "contains_all": ("dört yıl üst üste", "yetkili kurullarının kararı", "ilişikleri kesilebilir"),
            },
            {
                "document_id": "TR-KANUN-2547",
                "article_id": "Madde 54",
                "label": "ç",
                "contains_all": ("disiplin cezalarına karşı", "on beş gün", "itiraz edilebilir"),
            },
            {
                "document_id": "TR-KANUN-2547",
                "article_id": "Madde 54",
                "label": "e",
                "contains_all": ("itiraz hakkı kullanılmadan", "idari yargı"),
            },
            {
                "document_id": "TR-KANUN-2547",
                "article_id": "Madde 54",
                "label": "11",
                "contains_all": ("öğrenciye yapılacak tebligatta", "Tebligat Kanunu"),
            },
            {
                "document_id": "TR-KANUN-2547",
                "article_id": "Madde 14",
                "label": "6",
                "contains_all": ("enstitü", "kararlarına yapılacak itirazları", "karara bağlamak"),
            },
            {
                "document_id": "TR-KANUN-2547",
                "article_id": "Madde 15",
                "label": "4",
                "contains_all": ("enstitü", "yönetim kurullarının kararlarına", "kesin karara bağlamak"),
            },
        ),
    },
    "student_excuse_period": {
        "triggers": ("mazeret", "mazeret hakki", "mazeret izni"),
        "evidence_selectors": (
            {
                "document_id": "TR-KANUN-2547",
                "article_id": "Madde 46",
                "label": "g",
                "contains_all": ("mazeretleri", "yönetim kurulunca kabul edilmeyenler", "kayıt yaptıramaz"),
            },
        ),
    },
    "graduation_and_registration": {
        "triggers": (
            "mezun", "diploma", "ders sec", "ders kay", "kayit yaptir",
            "tez dersi", "uzmanlik dersi", "kredi", "yukumluluk",
        ),
        "evidence_selectors": (
            {
                "document_id": "TR-KANUN-2547",
                "article_id": "Madde 44",
                "label": "a",
                "contains_all": ("ders kredileri", "diğer yükümlülükleri", "tamamlamaları halinde"),
            },
            {
                "document_id": "TR-KANUN-2547",
                "article_id": "Madde 44",
                "label": "b",
                "contains_all": ("kayıt", "tez", "teorik ders"),
            },
        ),
    },
    "academic_guidance": {
        "triggers": ("danisman", "ogretim uyes", "rehber", "tez"),
        "evidence_selectors": (
            {
                "document_id": "TR-KANUN-2547",
                "article_id": "Madde 22",
                "label": "c",
                "contains_all": ("yardım etmek", "yol göstermek", "rehberlik etmek"),
            },
        ),
    },
    "institute_governance": {
        "triggers": ("enstitu", "mudurluk", "yonetim kurulu", "ogrenci isleri"),
        "evidence_selectors": (
            {
                "document_id": "TR-KANUN-2547",
                "article_id": "Madde 19",
                "label": "b",
                "contains_all": ("enstitü müdürü", "görevleri", "enstitü bakımından"),
            },
            {
                "document_id": "TR-KANUN-2547",
                "article_id": "Madde 19",
                "label": "e",
                "contains_all": ("enstitü kurulu", "yönetim kurulu", "görevleri"),
            },
        ),
    },
}


def is_normative_case_question(question: str) -> bool:
    q = normalize_for_search(question)
    if len(q) < 150:
        return False
    assessment_signal = any(
        signal in q
        for signal in (
            "kim sorumlu", "kimler sorumlu", "sorumluluklarini yapmamis",
            "sorumlulugunu yerine getirmemis", "burada kim", "bu durumda kim",
            "hukuken degerlendir", "mevzuata gore degerlendir",
            "hangi kanun maddelerinden yararlanabilir", "itiraz etmek istiyor",
            "kaydinin silinecegi", "ilisiginin kesilecegi",
        )
    )
    narrative_signal_count = sum(
        signal in q
        for signal in (
            "ancak", "daha sonra", "son asama", "son donem", "boyunca",
            "halde", "diyor", "ise", "cunku",
        )
    )
    actor_count = sum(
        actor in q
        for actor in ("ogrenci", "danisman", "enstitu", "universite", "yonetim", "senato")
    )
    institutional_action = any(
        signal in q for signal in ("bildiriliyor", "bildirildi", "kaydinin sil", "ilisigi kes", "itiraz")
    )
    return assessment_signal and narrative_signal_count >= 1 and (
        actor_count >= 2 or (actor_count >= 1 and institutional_action)
    )


def analyze_normative_case(question: str) -> dict[str, Any]:
    if not is_normative_case_question(question):
        return {}

    normalized = normalize_for_search(question)
    matched_rules = [
        rule_name
        for rule_name, rule in CASE_RULES.items()
        if any(trigger in normalized for trigger in rule["triggers"])
    ]
    evidence_selectors: list[dict[str, Any]] = []
    for rule_name in matched_rules:
        for selector in CASE_RULES[rule_name]["evidence_selectors"]:
            if selector not in evidence_selectors:
                evidence_selectors.append(selector)

    evidence_spans = evidence_spans_matching(evidence_selectors)
    if not evidence_spans:
        return {}

    explicit_actors = _case_actors(normalized)
    inferred_actors = _inferred_case_actors(normalized, explicit_actors)
    actors = explicit_actors + [actor for actor in inferred_actors if actor not in explicit_actors]
    facts = _case_facts(question)
    answer = _render_case_assessment(question, matched_rules, actors, facts, evidence_spans)
    return {
        "answer": answer,
        "case_frame": {
            "actors": actors,
            "explicit_actors": explicit_actors,
            "inferred_actors": inferred_actors,
            "facts": facts,
            "matched_rules": matched_rules,
            "responsibility_tests": _responsibility_tests(actors),
            "missing_evidence": _missing_evidence(),
            "assessment_mode": "source_locked_multi_hop",
        },
        "evidence_spans": evidence_spans,
        "evidence_ids": [item.get("evidence_id", "") for item in evidence_spans],
        "clause_ids": _unique(item.get("parent_clause_id", "") for item in evidence_spans),
        "document_ids": _unique(item.get("document_id", "") for item in evidence_spans),
        "source_ids": _unique(item.get("article_id", "") for item in evidence_spans),
        "scores": [20.0 for _ in evidence_spans],
        "score_breakdowns": [{"case_rule_match": 1.0} for _ in evidence_spans],
        "source_route": {"route": "normative_case_analysis"},
    }


def _render_case_assessment(

    question: str,

    matched_rules: list[str],

    actors: list[str],

    facts: list[str],

    evidence_spans: list[dict[str, Any]],

) -> str:
    if "student_status_and_remedy" in matched_rules or "student_excuse_period" in matched_rules:
        return _render_student_status_remedy_assessment(question, facts, evidence_spans)

    has_graduation = "graduation_and_registration" in matched_rules
    has_guidance = "academic_guidance" in matched_rules
    has_institute = "institute_governance" in matched_rules

    sections = [
        "**Normatif vaka değerlendirmesi**",
        "",
        "Bu olayda tek bir kişiyi doğrudan sorumlu ilan etmek mevcut corpusla mümkün değil. "
        "2547 sayılı Kanun sorumluluk çerçevesini kuruyor; fakat tez dersi ve uzmanlık alan dersi "
        "kaydını kimin kontrol edeceği üniversitenin lisansüstü yönetmeliği, senato kararları ve "
        "enstitü iş akışında belirlenir.",
    ]

    if facts:
        sections.extend(["", "**Sistemin ayırdığı olay unsurları**"])
        sections.extend(f"- {fact}" for fact in facts[:5])

    sections.extend(["", "**Kanundan kesin olarak çıkarılabilenler**"])
    if has_graduation:
        sections.extend([
            "1. **Mezuniyet koşulu:** Madde 44/a uyarınca öğrenci, belirlenen ders kredileri ile "
            "diğer yükümlülükleri başarıyla tamamlamadan diploma hakkı kazanmaz.",
            "2. **Yerel düzenleme zorunluluğu:** Madde 44/b; kayıt, devam, tez ve teorik ders "
            "içerikleri ile diploma alınmasına ilişkin ayrıntıları, YÖK'ün temel ilkelerine uygun "
            "olarak üniversite senatosunun düzenlemesine bırakır. Bu nedenle yalnızca 2547'ye "
            "bakarak 'tez dersi seçilmemişse her durumda mezuniyet imkansızdır' denemez; ilgili "
            "üniversite düzenlemesi de görülmelidir.",
        ])
    if has_guidance:
        sections.append(
            "3. **Akademik rehberlik:** Madde 22/c öğretim üyesine öğrenciye gerekli konularda "
            "yardım etme, yol gösterme ve rehberlik etme görevi verir. Bu genel görev danışmanın "
            "süreçte tamamen ilgisiz sayılamayacağını gösterir; ancak ders kaydını kontrol veya "
            "onay yükümlülüğünü tek başına kanıtlamaz."
        )
    if has_institute:
        sections.append(
            "4. **Enstitü yönetimi:** Madde 19, enstitü müdürü ile enstitü kurulu ve yönetim "
            "kurulunun enstitü bakımından yönetsel görev taşıdığını gösterir. Buna karşılık dönemlik "
            "ders kaydını hangi personelin doğrulayacağını bu madde açıkça söylemez."
        )

    sections.extend([
        "",
        "**Aktör bazlı sorumluluk testi**",
        "- **Öğrenci - koşullu sorumluluk:** Üniversite düzenlemesi tez ve uzmanlık alan dersini "
        "seçmeyi açıkça öğrenciye yüklüyorsa, öğrencinin kayıt yükümlülüğünü yerine getirmediği "
        "söylenebilir. Tezin fiilen tamamlanmış olması, kayıt yükümlülüğünü kendiliğinden ortadan kaldırmaz.",
        "- **Danışman - olası rehberlik/süreç sorumluluğu:** Danışmanın ders seçimini onaylama veya "
        "izleme görevi yerel düzenlemede ya da bilgi sistemi iş akışında varsa sorumluluğu doğabilir. "
        "Mevcut corpus yalnızca genel rehberlik görevini kanıtlıyor; somut ihlal için yeterli değil.",
        "- **Enstitü/anabilim dalı/idare - olası kontrol sorumluluğu:** Öğrencinin bir dönem tez "
        "çalışmasını yürütmesine izin verildiği, danışmanlık ve ilerleme işlemlerinin kayda alındığı "
        "halde ders eksikliğinin yalnız mezuniyet aşamasında fark edilmesi bir süreç kontrol boşluğuna "
        "işaret eder. Bunun hukuki veya idari sorumluluğa dönüşmesi, yerel görev ve onay kurallarına bağlıdır.",
        "",
        "**Sonuç**",
        "Mevcut kanun corpusuna göre en güvenilir sonuç, öğrencinin olası kayıt eksikliği yanında "
        "danışman ve enstitü süreçlerinde de olası gözetim/kontrol eksikliği bulunduğudur. Ancak "
        "sorumluluğun kesin dağılımı yapılamaz. Kesin karar için üniversitenin Lisansüstü Eğitim ve "
        "Öğretim Yönetmeliği, enstitü usul ve esasları, senato kararları, danışman onay akışı ve "
        "öğrenci bilgi sistemi kayıtları corpusa eklenmelidir.",
    ])

    answer = "\n".join(sections)
    sources = format_clause_sources(evidence_spans, question=question, answer=answer)
    return f"{answer}\n\n---\n\n**Kaynaklar**\n{sources}"


def _render_student_status_remedy_assessment(

    question: str,

    facts: list[str],

    evidence_spans: list[dict[str, Any]],

) -> str:
    q = normalize_for_search(question)
    asks_excuse_period = "mazeret" in q and any(term in q for term in ("kac", "donem", "sure"))

    if asks_excuse_period:
        sections = [
            "İncelenen 2547, 2914 ve 2809 sayılı kanunlarda öğrencilere tanınmış "
            "genel ve sabit bir ‘kaç dönem mazeret hakkı’ bulunmuyor.",
            "",
            "2547 sayılı Kanunun 46/g maddesi yalnızca katkı payı veya öğrenim ücretini süresinde "
            "ödemeyen öğrencinin mazeretinin ilgili yükseköğretim kurumu yönetim kurulunca kabul "
            "edilebileceğini düzenler; mazeretin kaç dönem süreceğini belirlemez. Akademik kayıt, "
            "izin ve mazeret dönemlerinin sayısı üniversitenin eğitim-öğretim veya lisansüstü "
            "yönetmeliğinden kontrol edilmelidir.",
        ]
    else:
        sections = [
            "**Normatif vaka değerlendirmesi**",
            "",
            "Kayıt silme işleminin hukuki sebebi belirtilmeden tek bir itiraz maddesi seçmek doğru "
            "olmaz. Ancak mevcut corpus öğrencinin dayanabileceği hükümleri üç ihtimale ayırıyor:",
            "",
            "1. **Kayıt yenilememe veya ücret ödememe gerekçesi:** Madde 44/c'ye göre azami süre "
            "içinde yalnızca katkı payının/öğrenim ücretinin ödenmemesi veya kayıt yenilenmemesi "
            "öğrencinin ilişiğinin kesilmesine yetmez. Dört yıl üst üste bu durumun yaşanması halinde "
            "ise yetkili üniversite kurulu kararı ve YÖK onayıyla ilişik kesilebilir.",
            "2. **Disiplin cezası gerekçesi:** İşlem bir öğrenci disiplin cezasıysa Madde 54 uyarınca "
            "on beş gün içinde üniversite yönetim kuruluna itiraz edilebilir; ayrıca bu itiraz "
            "kullanılmadan idari yargıya da başvurulabilir. Öğrenciye yapılacak tebligatta kural olarak "
            "7201 sayılı Tebligat Kanunu uygulanır.",
            "3. **Enstitü veya fakülte kurulu kararı:** Madde 14/6 ve Madde 15/4, kurulun türüne göre "
            "senato veya üniversite yönetim kurulunun itirazı inceleme yetkisini düzenler.",
            "",
            "**Uyarılmama iddiası**",
            "2547 sayılı Kanun akademik nedenle kayıt silmeden önce her durumda ayrıca bir uyarı "
            "yapılmasını açık ve genel bir şart olarak koymuyor. Bu nedenle ‘uyarılmadım’ iddiası "
            "tek başına işlemi geçersiz kılmaz; kararın yazılı gerekçesi, dayandığı madde, tebliğ "
            "tarihi ve üniversitenin öğrenci/lisansüstü yönetmeliği birlikte incelenmelidir.",
            "",
            "**Pratik sonuç:** Öğrenci önce işlemin disiplin cezası mı, azami süre/kayıt yenilememe "
            "işlemi mi, yoksa enstitü yönetim kurulu kararı mı olduğunu yazılı olarak istemelidir. "
            "Doğru itiraz mercii ve süre bu ayrımdan sonra belirlenebilir.",
        ]

    answer = "\n".join(sections)
    source_evidence = evidence_spans
    if asks_excuse_period:
        source_evidence = [item for item in evidence_spans if item.get("article_id") == "Madde 46"]
    sources = format_clause_sources(source_evidence, question=question, answer=answer)
    return f"{answer}\n\n---\n\n**Kaynaklar**\n{sources}"


def _case_actors(normalized_question: str) -> list[str]:
    actor_terms = (
        ("ogrenci", "öğrenci"),
        ("danisman", "danışman"),
        ("enstitu", "enstitü"),
        ("anabilim dali", "anabilim dalı"),
        ("ogrenci isleri", "öğrenci işleri"),
        ("senato", "senato"),
        ("universite", "üniversite"),
    )
    return [label for token, label in actor_terms if token in normalized_question]


def _inferred_case_actors(normalized_question: str, explicit_actors: list[str]) -> list[str]:
    inferred = []
    if "tez" in normalized_question and "öğrenci" in explicit_actors and "danışman" not in explicit_actors:
        inferred.append("danışman")
    return inferred


def _case_facts(question: str) -> list[str]:
    cleaned = re.sub(r"\s+", " ", question).strip()
    parts = re.split(r"(?<=[.!?])\s+|\s+(?:ancak|fakat|ama)\s+", cleaned, flags=re.IGNORECASE)
    facts = []
    for part in parts:
        fact = part.strip(" .?!\"")
        if len(fact) < 20 or "kimler soruml" in normalize_for_search(fact):
            continue
        facts.append(fact)
    return facts


def _responsibility_tests(actors: list[str]) -> list[dict[str, str]]:
    tests = {
        "öğrenci": ("conditional", "Yerel düzenlemedeki ders kayıt yükümlülüğü doğrulanmalı."),
        "danışman": ("insufficient", "Ders seçimini onaylama veya izleme görevi doğrulanmalı."),
        "enstitü": ("insufficient", "Kayıt ve mezuniyet kontrol iş akışındaki görev doğrulanmalı."),
    }
    return [
        {"actor": actor, "status": tests[actor][0], "test": tests[actor][1]}
        for actor in actors
        if actor in tests
    ]


def _missing_evidence() -> list[str]:
    return [
        "Üniversite Lisansüstü Eğitim ve Öğretim Yönetmeliği",
        "Enstitü usul ve esasları",
        "Senato kararları",
        "Danışman ders onay iş akışı",
        "Öğrenci bilgi sistemi işlem kayıtları",
    ]


def _unique(values) -> list[str]:
    result = []
    for value in values:
        if value and value not in result:
            result.append(value)
    return result