moleg-api 0.2.0__tar.gz → 0.2.2__tar.gz
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.
- {moleg_api-0.2.0 → moleg_api-0.2.2}/PKG-INFO +1 -1
- {moleg_api-0.2.0 → moleg_api-0.2.2}/moleg_api/__init__.py +2 -0
- {moleg_api-0.2.0 → moleg_api-0.2.2}/moleg_api/cli.py +156 -29
- {moleg_api-0.2.0 → moleg_api-0.2.2}/moleg_api/errors.py +22 -0
- {moleg_api-0.2.0 → moleg_api-0.2.2}/moleg_api/laws.py +247 -8
- {moleg_api-0.2.0 → moleg_api-0.2.2}/moleg_api/models.py +3 -2
- {moleg_api-0.2.0 → moleg_api-0.2.2}/moleg_api/normalization.py +203 -45
- {moleg_api-0.2.0 → moleg_api-0.2.2}/moleg_api.egg-info/PKG-INFO +1 -1
- {moleg_api-0.2.0 → moleg_api-0.2.2}/moleg_api.egg-info/SOURCES.txt +2 -0
- {moleg_api-0.2.0 → moleg_api-0.2.2}/pyproject.toml +1 -1
- {moleg_api-0.2.0 → moleg_api-0.2.2}/tests/test_cli.py +2 -1
- {moleg_api-0.2.0 → moleg_api-0.2.2}/tests/test_laws.py +15 -6
- {moleg_api-0.2.0 → moleg_api-0.2.2}/tests/test_live_smoke.py +112 -0
- {moleg_api-0.2.0 → moleg_api-0.2.2}/tests/test_models.py +1 -0
- moleg_api-0.2.2/tests/test_sdk_fixes_0_2_1.py +375 -0
- moleg_api-0.2.2/tests/test_sdk_fixes_0_2_2.py +344 -0
- {moleg_api-0.2.0 → moleg_api-0.2.2}/LICENSE +0 -0
- {moleg_api-0.2.0 → moleg_api-0.2.2}/README.md +0 -0
- {moleg_api-0.2.0 → moleg_api-0.2.2}/moleg_api/__main__.py +0 -0
- {moleg_api-0.2.0 → moleg_api-0.2.2}/moleg_api/py.typed +0 -0
- {moleg_api-0.2.0 → moleg_api-0.2.2}/moleg_api/source.py +0 -0
- {moleg_api-0.2.0 → moleg_api-0.2.2}/moleg_api.egg-info/dependency_links.txt +0 -0
- {moleg_api-0.2.0 → moleg_api-0.2.2}/moleg_api.egg-info/entry_points.txt +0 -0
- {moleg_api-0.2.0 → moleg_api-0.2.2}/moleg_api.egg-info/requires.txt +0 -0
- {moleg_api-0.2.0 → moleg_api-0.2.2}/moleg_api.egg-info/top_level.txt +0 -0
- {moleg_api-0.2.0 → moleg_api-0.2.2}/setup.cfg +0 -0
- {moleg_api-0.2.0 → moleg_api-0.2.2}/tests/test_source.py +0 -0
- {moleg_api-0.2.0 → moleg_api-0.2.2}/tests/test_versions.py +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from .errors import (
|
|
4
4
|
AmbiguousLawError,
|
|
5
|
+
AsOfBeforeCoverageError,
|
|
5
6
|
MolegApiError,
|
|
6
7
|
NoResultError,
|
|
7
8
|
ParseFailureError,
|
|
@@ -124,6 +125,7 @@ __all__ = [
|
|
|
124
125
|
"MolegApiError",
|
|
125
126
|
"NoResultError",
|
|
126
127
|
"ParseFailureError",
|
|
128
|
+
"AsOfBeforeCoverageError",
|
|
127
129
|
"RateLimitError",
|
|
128
130
|
"RetryExhaustedError",
|
|
129
131
|
"SourceApiError",
|
|
@@ -28,10 +28,13 @@ from __future__ import annotations
|
|
|
28
28
|
import argparse
|
|
29
29
|
import json
|
|
30
30
|
import sys
|
|
31
|
+
from datetime import date, datetime
|
|
32
|
+
from importlib.metadata import PackageNotFoundError, version as _dist_version
|
|
31
33
|
from typing import Any
|
|
32
34
|
|
|
33
35
|
from .errors import (
|
|
34
36
|
AmbiguousLawError,
|
|
37
|
+
AsOfBeforeCoverageError,
|
|
35
38
|
MolegApiError,
|
|
36
39
|
NoResultError,
|
|
37
40
|
ParseFailureError,
|
|
@@ -42,6 +45,14 @@ from .errors import (
|
|
|
42
45
|
from .laws import MolegApi
|
|
43
46
|
from .models import DeferredLookup
|
|
44
47
|
|
|
48
|
+
|
|
49
|
+
def _pkg_version() -> str:
|
|
50
|
+
try:
|
|
51
|
+
return _dist_version("moleg-api")
|
|
52
|
+
except PackageNotFoundError:
|
|
53
|
+
return "unknown"
|
|
54
|
+
|
|
55
|
+
|
|
45
56
|
# Exit codes distinguish outcomes a shell/agent must branch on.
|
|
46
57
|
EXIT_OK = 0 # includes a zero-hit search (ok:true, count:0)
|
|
47
58
|
EXIT_AMBIGUOUS = 2 # multiple plausible identities — surface, don't pick
|
|
@@ -278,6 +289,34 @@ def _deferred_next(deferred: list[Any], *, cap: int = 3) -> tuple[list[dict[str,
|
|
|
278
289
|
return items, overflow
|
|
279
290
|
|
|
280
291
|
|
|
292
|
+
def _today_compact() -> str:
|
|
293
|
+
return date.today().strftime("%Y%m%d")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _compact_digits(value: Any) -> str:
|
|
297
|
+
return "".join(ch for ch in str(value) if ch.isdigit()) if value else ""
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def parse_as_of(raw: str) -> str:
|
|
301
|
+
"""Strictly parse an --as-of value to canonical YYYYMMDD.
|
|
302
|
+
|
|
303
|
+
Accepts YYYY-MM-DD or YYYYMMDD only, with real calendar validation (rejects
|
|
304
|
+
month 13, Feb 30, day 99, and non-dates). A malformed date is a usage error,
|
|
305
|
+
not a silent fallback to the current version.
|
|
306
|
+
"""
|
|
307
|
+
text = raw.strip()
|
|
308
|
+
for fmt in ("%Y-%m-%d", "%Y%m%d"):
|
|
309
|
+
try:
|
|
310
|
+
return datetime.strptime(text, fmt).strftime("%Y%m%d")
|
|
311
|
+
except ValueError:
|
|
312
|
+
continue
|
|
313
|
+
raise CliError(
|
|
314
|
+
f"--as-of 값 {raw!r}을(를) 날짜로 해석할 수 없다 — YYYY-MM-DD 또는 YYYYMMDD 형식만 허용(월 1~12·일 유효 범위).",
|
|
315
|
+
kind="usage_error",
|
|
316
|
+
exit_code=EXIT_USAGE,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
281
320
|
def _law_time_flags(identity: Any, as_of: str | None) -> tuple[dict[str, Any], list[str]]:
|
|
282
321
|
flags: dict[str, Any] = {}
|
|
283
322
|
lines: list[str] = []
|
|
@@ -287,18 +326,28 @@ def _law_time_flags(identity: Any, as_of: str | None) -> tuple[dict[str, Any], l
|
|
|
287
326
|
eff = getattr(identity, "effective_date", None)
|
|
288
327
|
if eff:
|
|
289
328
|
flags["effective_date"] = eff
|
|
290
|
-
|
|
291
|
-
#
|
|
292
|
-
#
|
|
329
|
+
e = _compact_digits(eff)
|
|
330
|
+
# 공포됐어도 시행일이 미래면 아직 효력이 없다. 반환 버전의 시행일을 오늘과 직접
|
|
331
|
+
# 대조해 '미시행'을 신호한다 — as_of 유무와 무관(미래 버전은 어느 경로로도 올 수 있다).
|
|
332
|
+
if len(e) == 8 and e > _today_compact():
|
|
333
|
+
flags["not_effective_as_of"] = True
|
|
334
|
+
lines.append(
|
|
335
|
+
"이 버전은 시행일이 미래 — 공포됐으나 아직 미시행이다. 현행 시행 문구로 인용 금지; "
|
|
336
|
+
"현재 시행 중 본문은 --as-of 없이(현행) 로드하라."
|
|
337
|
+
)
|
|
338
|
+
# --as-of는 그 시점에 시행 중이던 버전을 로드한다(로더가 역사 버전을 해결). 반환
|
|
339
|
+
# effective_date를 확인하라 — 요청 시점과 다르면 인접 버전이 반환된 것이다.
|
|
293
340
|
if as_of:
|
|
294
341
|
flags["as_of"] = as_of
|
|
295
|
-
a =
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
342
|
+
a = _compact_digits(as_of)
|
|
343
|
+
if a and e and e != a:
|
|
344
|
+
flags["version_mismatch"] = {"requested": a, "loaded": e}
|
|
345
|
+
if e > a:
|
|
346
|
+
# 하위호환: SKILL.md·catalog가 참조하는 기존 플래그 유지.
|
|
347
|
+
flags["version_request_unfulfilled"] = True
|
|
299
348
|
lines.append(
|
|
300
|
-
"
|
|
301
|
-
"
|
|
349
|
+
"요청한 시점(as_of)과 반환된 버전의 시행일이 다르다 — 반환 effective_date 기준으로 해석하라. "
|
|
350
|
+
"그 시점의 정확한 시행 버전 이력은 trace-law-history로 확인."
|
|
302
351
|
)
|
|
303
352
|
return flags, lines
|
|
304
353
|
|
|
@@ -347,6 +396,17 @@ def signals_for(command: str, result: Any, args: argparse.Namespace) -> dict[str
|
|
|
347
396
|
if count == 0:
|
|
348
397
|
flags["searched"] = _searched_scope(command, args)
|
|
349
398
|
discipline.append("0건 — 이 검색어·범위로 못 찾음일 뿐, 부재의 증명 아님.")
|
|
399
|
+
query = getattr(args, "query", None)
|
|
400
|
+
# 헌재·판례 검색 기본은 제목(사건명) — doctrine·법리 키워드는 --search-body(본문 전체)라야 매칭된다.
|
|
401
|
+
if eff_command in ("search-constitutional-decisions", "search-cases") and not getattr(args, "search_body", False) and query:
|
|
402
|
+
next_cmds.append({"why": "제목이 아닌 본문(판시·주문·이유) 전체 검색으로 재시도", "cmd": f"moleg {eff_command} {json.dumps(query, ensure_ascii=False)} --search-body"})
|
|
403
|
+
discipline.append("헌재·판례 검색 기본은 제목 검색 — doctrine·본문 키워드로 0건이면 --search-body로 재시도하라.")
|
|
404
|
+
# 별표 검색은 스코프(title=별표 제목 / source=소관 법령명)에 따라 결과가 갈린다 — 반대 스코프로 재시도.
|
|
405
|
+
elif eff_command == "search-annex-forms" and query:
|
|
406
|
+
scope = getattr(args, "search_scope", "title") or "title"
|
|
407
|
+
other = "source" if scope == "title" else "title"
|
|
408
|
+
next_cmds.append({"why": f"별표 검색 스코프 전환({scope}→{other})으로 재시도", "cmd": f"moleg search-annex-forms {json.dumps(query, ensure_ascii=False)} --search-scope {other}"})
|
|
409
|
+
discipline.append("별표 검색 title=별표 제목 / source=소관 법령명 매칭(토큰 근접) — 0건이면 반대 스코프로 재시도하라.")
|
|
350
410
|
# mechanical alternate: drop a narrowing filter if one was set.
|
|
351
411
|
broaden = _broaden_next(command, args)
|
|
352
412
|
if broaden:
|
|
@@ -379,6 +439,8 @@ def signals_for(command: str, result: Any, args: argparse.Namespace) -> dict[str
|
|
|
379
439
|
if tname == "LawText":
|
|
380
440
|
tf, tl = _law_time_flags(result.identity, as_of)
|
|
381
441
|
flags.update(tf); discipline.extend(tl)
|
|
442
|
+
if flags.get("not_effective_as_of"):
|
|
443
|
+
source = "법제처 / 공포본(장래 시행 — 아직 미시행)"
|
|
382
444
|
if _has_supplementary(result):
|
|
383
445
|
flags["has_supplementary"] = True
|
|
384
446
|
discipline.append("시행일·적용례·경과조치는 supplementary_provisions에 별도 — 조문 본문·법령 effective_date만으로 전환 범위 답 금지.")
|
|
@@ -392,6 +454,8 @@ def signals_for(command: str, result: Any, args: argparse.Namespace) -> dict[str
|
|
|
392
454
|
flags.update(af); discipline.extend(al)
|
|
393
455
|
tf, tl = _law_time_flags(result.identity, as_of)
|
|
394
456
|
flags.update(tf); discipline.extend(tl)
|
|
457
|
+
if flags.get("not_effective_as_of"):
|
|
458
|
+
source = "법제처 / 공포본 조문(장래 시행 — 아직 미시행)"
|
|
395
459
|
discipline.append("정의·예외·적용대상·요건은 text의 항·호·목 중첩에 있다 — 조문제목·상위 조문내용만으로 요약 금지.")
|
|
396
460
|
moved = _real_moved_to(result)
|
|
397
461
|
if moved:
|
|
@@ -451,6 +515,11 @@ def signals_for(command: str, result: Any, args: argparse.Namespace) -> dict[str
|
|
|
451
515
|
if command == "get-constitutional-decision" or (st and "detc" in str(st)):
|
|
452
516
|
kind, source = "constitutional_text", "법제처 / 헌재결정 본문"
|
|
453
517
|
discipline.append("헌재 결정 — 판시사항·심판대상조문을 로드된 본문에서만 인용, doctrine 망라 단정 금지.")
|
|
518
|
+
disposition = getattr(result.identity, "decision_type", None)
|
|
519
|
+
if disposition:
|
|
520
|
+
flags["disposition"] = disposition
|
|
521
|
+
elif not (getattr(result, "holdings", None) or getattr(result, "summary", None) or getattr(result, "reviewed_statutes", None)):
|
|
522
|
+
discipline.append("주문·처분 결과가 구조화 필드에 없음 — full_text의 【주 문】을 직접 읽어 각하/기각/합헌/위헌을 판단하고, 각하·기각을 본안(merits) 판단으로 오인하지 마라.")
|
|
454
523
|
else:
|
|
455
524
|
kind, source = "case_text", "법제처 / 판례 본문"
|
|
456
525
|
|
|
@@ -464,9 +533,19 @@ def signals_for(command: str, result: Any, args: argparse.Namespace) -> dict[str
|
|
|
464
533
|
discipline.append("선택 조문의 전후 문구 델타만 — 개정이유·입법의도·전체 변경 조문 망라 아님. trace-law-history·국회 법안자료로 보강.")
|
|
465
534
|
|
|
466
535
|
elif tname == "DelegationGraph":
|
|
467
|
-
|
|
468
|
-
|
|
536
|
+
rules = getattr(result, "rules", None) or []
|
|
537
|
+
flags["count"] = len(rules)
|
|
538
|
+
if not rules:
|
|
469
539
|
discipline.append("이 조회로 위임규정 못 찾음 ≠ 위임 없음 — 법령 체계도·다른 조문 범위·행정규칙·별표 경로를 더 확인.")
|
|
540
|
+
else:
|
|
541
|
+
discipline.append("위임 목록은 하위법령·인용법령 위임만 — 별표(과태료·범칙금액·기준표 등) 자체는 여기 없다. 금액·기준표는 위임된 시행령·시행규칙의 별표를 search-annex-forms(또는 load-delegated-criteria)로 확인.")
|
|
542
|
+
ident = getattr(result, "identity", None)
|
|
543
|
+
name = (getattr(ident, "name", None) or getattr(ident, "law_id", None) or "") if ident else ""
|
|
544
|
+
if name:
|
|
545
|
+
next_cmds.append({
|
|
546
|
+
"why": "위임된 별표·금액·기준표 확인",
|
|
547
|
+
"cmd": f"moleg search-annex-forms {json.dumps(name, ensure_ascii=False)} --source law",
|
|
548
|
+
})
|
|
470
549
|
|
|
471
550
|
elif tname == "LawStructure":
|
|
472
551
|
discipline.append("체계도는 계층 컨텍스트일 뿐 — 시행령·규칙·행정규칙이 이 법 아래 있음은 보이나, 조문 단위 위임·하위규칙 본문·운영기준 증명 아님. find-delegated-rules로 확인.")
|
|
@@ -521,22 +600,44 @@ def _law_list_signals(hits: list[Any]) -> dict[str, Any]:
|
|
|
521
600
|
flags: dict[str, Any] = {}
|
|
522
601
|
discipline: list[str] = []
|
|
523
602
|
next_cmds: list[dict[str, str]] = []
|
|
603
|
+
today = _today_compact()
|
|
524
604
|
by_name: dict[str, list[Any]] = {}
|
|
525
605
|
for h in hits:
|
|
526
606
|
by_name.setdefault(getattr(h.identity, "name", ""), []).append(h)
|
|
527
607
|
ambiguous = any(len(v) > 1 for v in by_name.values())
|
|
608
|
+
# The current in-force version is the LATEST 시행일 that is ≤ today; every
|
|
609
|
+
# other candidate (older OR future) is not currently in force. bare
|
|
610
|
+
# `get-law --law` returns that current version, so only it gets the bare
|
|
611
|
+
# steer; the rest get an --as-of targeting their own effective_date.
|
|
612
|
+
effs_all = [_compact_digits(getattr(h.identity, "effective_date", None)) for h in hits]
|
|
613
|
+
in_force_effs = [e for e in effs_all if len(e) == 8 and e <= today]
|
|
614
|
+
current_eff = max(in_force_effs) if in_force_effs else None
|
|
615
|
+
first_eff = effs_all[0] if effs_all else ""
|
|
616
|
+
future_top = len(first_eff) == 8 and first_eff > today
|
|
617
|
+
if future_top:
|
|
618
|
+
flags["top_candidate_not_yet_effective"] = True
|
|
528
619
|
if ambiguous:
|
|
529
620
|
flags["ambiguous_versions"] = True
|
|
530
|
-
discipline.append(
|
|
531
|
-
|
|
621
|
+
discipline.append(
|
|
622
|
+
"동명 후보가 시행일 다름 — 현행본은 그냥 get-law --law(로더가 현행 시행본 해결), 특정 시점 버전은 --as-of <시행일>. "
|
|
623
|
+
"목록은 최신 공포순이라 상단이 미시행(장래 시행)일 수 있으니 effective_date를 오늘과 대조하라."
|
|
624
|
+
)
|
|
625
|
+
elif future_top:
|
|
626
|
+
discipline.append(
|
|
627
|
+
"최신 후보가 아직 미시행(장래 시행)이다 — get-law --law는 현행 시행본을 반환한다. effective_date를 오늘과 대조해 인용하라."
|
|
628
|
+
)
|
|
629
|
+
for h in hits[:3]:
|
|
532
630
|
ident = h.identity
|
|
533
631
|
law_id = getattr(ident, "law_id", "") or ""
|
|
534
632
|
eff = getattr(ident, "effective_date", None)
|
|
633
|
+
e = _compact_digits(eff)
|
|
634
|
+
is_current = bool(current_eff) and e == current_eff
|
|
635
|
+
status = "현행" if is_current else ("미시행" if (len(e) == 8 and e > today) else "과거")
|
|
535
636
|
cmd = f"moleg get-law --law {law_id}"
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
next_cmds.append({"why":
|
|
637
|
+
if not is_current and e:
|
|
638
|
+
cmd += f" --as-of {e}"
|
|
639
|
+
why = f"후보 로드: {getattr(ident, 'name', '')}" + (f" (시행 {eff}, {status})" if eff else "")
|
|
640
|
+
next_cmds.append({"why": why, "cmd": cmd})
|
|
540
641
|
return {"flags": flags, "discipline": discipline, "next": next_cmds}
|
|
541
642
|
|
|
542
643
|
|
|
@@ -570,13 +671,19 @@ def _id_next(hits: list[Any], command: str) -> list[dict[str, str]]:
|
|
|
570
671
|
for h in hits[:2]:
|
|
571
672
|
val = getattr(h.identity, id_field, None)
|
|
572
673
|
if val:
|
|
573
|
-
|
|
674
|
+
cmd = f"moleg {command} --id {val}"
|
|
675
|
+
# 부처 해석 본문 로드는 --id만으론 안 되고 --source ministry --ministry <기관>이 필수다.
|
|
676
|
+
if command == "get-interpretation" and getattr(h.identity, "source_type", None) == "ministry":
|
|
677
|
+
ministry = getattr(h.identity, "ministry", None)
|
|
678
|
+
if ministry:
|
|
679
|
+
cmd += f" --source ministry --ministry {ministry}"
|
|
680
|
+
out.append({"why": f"본문 로드: {getattr(h.identity, 'title', getattr(h.identity, 'name', val))}", "cmd": cmd})
|
|
574
681
|
return out
|
|
575
682
|
|
|
576
683
|
|
|
577
684
|
def _searched_scope(command: str, args: argparse.Namespace) -> dict[str, Any]:
|
|
578
685
|
scope: dict[str, Any] = {}
|
|
579
|
-
for key in ("query", "concept", "basis", "ministry", "law_type", "court", "source", "annex_type", "rule_type", "as_of"):
|
|
686
|
+
for key in ("query", "concept", "basis", "ministry", "law_type", "court", "source", "search_scope", "annex_type", "rule_type", "search_body", "as_of"):
|
|
580
687
|
val = getattr(args, key, None)
|
|
581
688
|
if val:
|
|
582
689
|
scope[key] = val
|
|
@@ -632,6 +739,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
632
739
|
description="법제처 law.go.kr 법령 자료 조회 CLI. 항상 JSON 엔벨로프 1개를 stdout으로 출력.",
|
|
633
740
|
)
|
|
634
741
|
parser.add_argument("--raw", action="store_true", help="원본 소스 payload(raw)까지 직렬화(디버그).")
|
|
742
|
+
parser.add_argument(
|
|
743
|
+
"--version",
|
|
744
|
+
action="version",
|
|
745
|
+
version=f"moleg-api {_pkg_version()}",
|
|
746
|
+
help="설치된 moleg-api 버전을 출력하고 종료.",
|
|
747
|
+
)
|
|
635
748
|
sub = parser.add_subparsers(dest="command", metavar="<command>")
|
|
636
749
|
|
|
637
750
|
sub.add_parser("catalog", help="27개 서브커맨드·규약·kind 목록을 한 번에.")
|
|
@@ -657,7 +770,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
657
770
|
p = sub.add_parser("search-annex-forms", help="별표·서식 후보 검색.")
|
|
658
771
|
p.add_argument("query")
|
|
659
772
|
p.add_argument("--source", choices=["law", "administrative_rule"], default="law")
|
|
660
|
-
p.add_argument("--search-scope", dest="search_scope", choices=["title", "source", "body"], default="
|
|
773
|
+
p.add_argument("--search-scope", dest="search_scope", choices=["title", "source", "body"], default="title",
|
|
774
|
+
help="title=별표 제목 매칭(기본) / source=소관 법령명 매칭 / body=본문 전체. 제목으로 0건이면 source로 재시도.")
|
|
661
775
|
p.add_argument("--annex-type", dest="annex_type", default=None)
|
|
662
776
|
p.add_argument("--ministry", default=None); p.add_argument("--display", type=int, default=20)
|
|
663
777
|
|
|
@@ -665,7 +779,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
665
779
|
p.add_argument("query")
|
|
666
780
|
p.add_argument("--source", choices=["moleg", "ministry", "all", "all_ministries"], default="moleg")
|
|
667
781
|
p.add_argument("--ministry", default=None)
|
|
668
|
-
p.add_argument("--search-body", dest="search_body", action="store_true")
|
|
782
|
+
p.add_argument("--search-body", dest="search_body", action="store_true", help="제목이 아니라 본문(판시사항·주문·이유·해석 전문) 전체를 검색. doctrine·본문 키워드로 0건이면 이 옵션으로 재시도.")
|
|
669
783
|
p.add_argument("--interpreted-on", dest="interpreted_on", default=None)
|
|
670
784
|
p.add_argument("--display", type=int, default=20)
|
|
671
785
|
|
|
@@ -673,14 +787,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
673
787
|
p.add_argument("query")
|
|
674
788
|
p.add_argument("--court", choices=["all", "supreme", "lower"], default="all")
|
|
675
789
|
p.add_argument("--court-name", dest="court_name", default=None)
|
|
676
|
-
p.add_argument("--search-body", dest="search_body", action="store_true")
|
|
790
|
+
p.add_argument("--search-body", dest="search_body", action="store_true", help="제목이 아니라 본문(판시사항·주문·이유·해석 전문) 전체를 검색. doctrine·본문 키워드로 0건이면 이 옵션으로 재시도.")
|
|
677
791
|
p.add_argument("--decided-on", dest="decided_on", default=None)
|
|
678
792
|
p.add_argument("--case-number", dest="case_number", default=None)
|
|
679
793
|
p.add_argument("--display", type=int, default=20)
|
|
680
794
|
|
|
681
795
|
p = sub.add_parser("search-constitutional-decisions", help="헌재 결정 검색.")
|
|
682
796
|
p.add_argument("query")
|
|
683
|
-
p.add_argument("--search-body", dest="search_body", action="store_true")
|
|
797
|
+
p.add_argument("--search-body", dest="search_body", action="store_true", help="제목이 아니라 본문(판시사항·주문·이유·해석 전문) 전체를 검색. doctrine·본문 키워드로 0건이면 이 옵션으로 재시도.")
|
|
684
798
|
p.add_argument("--decided-on", dest="decided_on", default=None)
|
|
685
799
|
p.add_argument("--case-number", dest="case_number", default=None)
|
|
686
800
|
p.add_argument("--display", type=int, default=20)
|
|
@@ -714,7 +828,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
714
828
|
p.add_argument("--no-follow-moved", dest="no_follow_moved", action="store_true")
|
|
715
829
|
|
|
716
830
|
p = sub.add_parser("get-annex-form-body", help="별표·서식 본문 로드.")
|
|
717
|
-
p.add_argument("--id", dest="identifier", required=True, help="annex_id(검색이 준 값).")
|
|
831
|
+
p.add_argument("--id", "--annex-id", dest="identifier", required=True, help="annex_id(검색이 준 값). --annex-id로도 받음.")
|
|
718
832
|
p.add_argument("--source", choices=["law", "administrative_rule"], default="law")
|
|
719
833
|
p.add_argument("--title", default=None)
|
|
720
834
|
p.add_argument("--no-metadata", dest="no_metadata", action="store_true")
|
|
@@ -740,9 +854,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
740
854
|
p.add_argument("--date-from", dest="date_from", default=None)
|
|
741
855
|
p.add_argument("--date-to", dest="date_to", default=None)
|
|
742
856
|
|
|
743
|
-
p = sub.add_parser("compare-law-versions", help="개정 전후 조문 문구
|
|
744
|
-
_add_law(p); p.add_argument("--
|
|
745
|
-
p.add_argument("--article", default=None)
|
|
857
|
+
p = sub.add_parser("compare-law-versions", help="개정 전후 조문 문구 비교(소스 제공 전후 쌍).")
|
|
858
|
+
_add_law(p); p.add_argument("--article", default=None)
|
|
746
859
|
|
|
747
860
|
p = sub.add_parser("find-delegated-rules", help="위임 규정·하위 법령.")
|
|
748
861
|
_add_law(p); p.add_argument("--article", default=None)
|
|
@@ -784,6 +897,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
784
897
|
|
|
785
898
|
def _call(api: MolegApi, args: argparse.Namespace) -> Any:
|
|
786
899
|
c = args.command
|
|
900
|
+
# Strictly validate --as-of once, for every command that carries it, so a
|
|
901
|
+
# malformed date is a usage error rather than a silent wrong-version load.
|
|
902
|
+
if getattr(args, "as_of", None):
|
|
903
|
+
args.as_of = parse_as_of(args.as_of)
|
|
787
904
|
if c == "search-laws":
|
|
788
905
|
return api.search_laws(args.query, as_of=args.as_of, basis=args.basis,
|
|
789
906
|
law_type=args.law_type, ministry=args.ministry, display=args.display)
|
|
@@ -834,7 +951,7 @@ def _call(api: MolegApi, args: argparse.Namespace) -> Any:
|
|
|
834
951
|
date_range = (args.date_from, args.date_to) if (args.date_from and args.date_to) else None
|
|
835
952
|
return api.trace_law_history(args.law, date_range=date_range, article=args.article)
|
|
836
953
|
if c == "compare-law-versions":
|
|
837
|
-
return api.compare_law_versions(args.law,
|
|
954
|
+
return api.compare_law_versions(args.law, article=args.article)
|
|
838
955
|
if c == "find-delegated-rules":
|
|
839
956
|
return api.find_delegated_rules(args.law, article=args.article)
|
|
840
957
|
if c == "get-law-structure":
|
|
@@ -898,7 +1015,9 @@ CATALOG = {
|
|
|
898
1015
|
"0건·호출 실패 ≠ 부재. 종료코드로 구분: 0 ok(0건 포함) · 2 모호 · 3 소스접근실패 · 4 no-result · 5 usage/순서위반.",
|
|
899
1016
|
"load 계열에 법령명을 주면 needs_search_first(exit 5) — 먼저 search-laws로 law_id를 얻어라.",
|
|
900
1017
|
"deferred는 data.deferred[i]를 load-followup --json -로 파이프(손타이핑 금지).",
|
|
901
|
-
"로더(get-law/get-article)는 기본이 현행 통합본, --as-of
|
|
1018
|
+
"로더(get-law/get-article)는 기본이 현행 통합본, --as-of <날짜>(YYYY-MM-DD/YYYYMMDD만)면 그 시점 시행 버전을 로드한다. 반환 effective_date를 오늘과 대조해 현행/미시행을 판정하라 — flags.not_effective_as_of=공포됐으나 미시행(미래 시행일, 현행 인용 금지), version_mismatch={requested,loaded}=요청 시점과 반환 버전 시행일 불일치, version_request_unfulfilled=요청보다 이후 버전이 반환됨. 요청 시점이 통합본 커버리지보다 이르면 kind:version_request_unfulfilled+earliest_available(→trace-law-history).",
|
|
1019
|
+
"검색 스코프 뉘앙스: 헌재·판례는 기본이 제목(사건명) 검색이라 doctrine·본문 키워드는 --search-body(본문 전체)라야 매칭(0건이면 next가 재시도 제시). 별표는 --search-scope title(별표 제목)/source(소관 법령명, 토큰 근접)이라 0건이면 반대 스코프로. 부처 해석은 --source all_ministries(법제처+부처 혼합)로만 나오고, 부처 본문 로드는 --source ministry --ministry <기관> 필수(검색 hit의 follow_up.filters를 그대로 로더 인자로).",
|
|
1020
|
+
"개정 전후 델타=compare-law-versions는 소스가 주는 최근 두 버전만 비교하고 changes[].article은 본문에서 파싱한 실제 조문번호(매핑 불가 시 null). 임의 두 시점 델타는 get-article --as-of를 전후 날짜로 두 번 로드해 대조.",
|
|
902
1021
|
],
|
|
903
1022
|
"routing_rules": [
|
|
904
1023
|
"본문 로드: 살아있는 조문=get-article / 이동·삭제 가능성 있는 조문=load-article-context(기본이 이동추적).",
|
|
@@ -906,6 +1025,7 @@ CATALOG = {
|
|
|
906
1025
|
"이 법 아래 무엇이 있나: 계층 조망=get-law-structure(위임 증명 아님) / 조문 단위 위임 규정=find-delegated-rules.",
|
|
907
1026
|
"넓은 탐색: 넓은 질의의 용어·관련법 조사계획=expand-legal-query / 유사 제도(비슷한 기제)를 가진 법 후보(설계용)=find-comparable-mechanisms.",
|
|
908
1027
|
"묶음 로더 — authority=특정 조문의 해석/판례/헌재 권위 / bundle=진입점 모를 때 단일법·넓은 질문(--mode) / institutional=명시된 다법령 집합(--statute 반복) / delegated=단일법의 하위규칙·별표 집행기준 본문.",
|
|
1028
|
+
"별표 금액·기준표: 위임된 시행령·시행규칙의 별표를 search-annex-forms --search-scope source <법령명> → get-annex-form-body --id(=--annex-id)로 로드. 표 파싱이 무너지면 structured_data.parsing_confidence=low — 금액은 text를 1차로 인용하라. bare id 로드는 소관법령ID·pdf_link 등 링크 메타를 복구하지 못하니 현행성은 모법 버전으로 확인.",
|
|
909
1029
|
],
|
|
910
1030
|
"commands": {
|
|
911
1031
|
"검색·계획(후보)": [
|
|
@@ -975,6 +1095,13 @@ def main(argv: list[str] | None = None, *, api: MolegApi | None = None) -> int:
|
|
|
975
1095
|
_emit({"ok": False, "command": args.command, "kind": "source_access_error", "error": str(exc),
|
|
976
1096
|
"discipline": ["일시적 소스 접근 실패이지 법령 부재 아님 — 잠시 후 재시도. 시행일 등은 폴백 규율로 채우되 위헌·판례류는 '1차 확인 필요'로 남겨라."]})
|
|
977
1097
|
return EXIT_SOURCE
|
|
1098
|
+
except AsOfBeforeCoverageError as exc:
|
|
1099
|
+
law_id = getattr(exc, "law_id", None) or getattr(args, "law", "")
|
|
1100
|
+
_emit({"ok": False, "command": args.command, "kind": "version_request_unfulfilled", "error": str(exc),
|
|
1101
|
+
"flags": {"as_of": getattr(args, "as_of", None), "earliest_available": getattr(exc, "earliest_available", None)},
|
|
1102
|
+
"discipline": ["요청 시점이 이 법령의 통합본 제공 최초 시행일보다 앞선다(일시적 실패 아님) — 그 이전 본문은 통합본으로 제공되지 않으니 연혁으로 확인하라."],
|
|
1103
|
+
"next": [{"why": "가용 최초 버전·개정 연혁 확인", "cmd": f"moleg trace-law-history --law {law_id}"}]})
|
|
1104
|
+
return EXIT_NO_RESULT
|
|
978
1105
|
except ParseFailureError as exc:
|
|
979
1106
|
_emit({"ok": False, "command": args.command, "kind": "parse_error", "error": str(exc),
|
|
980
1107
|
"discipline": ["소스 응답을 정규화하지 못함(소스접근 실패·법령 부재 아님) — 다른 경로/커맨드로 확인하라."]})
|
|
@@ -47,3 +47,25 @@ class RetryExhaustedError(SourceApiError):
|
|
|
47
47
|
|
|
48
48
|
class ParseFailureError(MolegApiError):
|
|
49
49
|
"""A source response could not be normalized into the public model."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AsOfBeforeCoverageError(MolegApiError):
|
|
53
|
+
"""The requested as_of date predates law.go.kr's consolidated-version coverage.
|
|
54
|
+
|
|
55
|
+
A valid identifier and a well-formed date, but no consolidated version text
|
|
56
|
+
exists at (or before) that date — a permanent coverage-floor condition, not a
|
|
57
|
+
transient source failure. Carries the earliest available version date so the
|
|
58
|
+
caller can steer to amendment history instead of retrying.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
message: str,
|
|
64
|
+
*,
|
|
65
|
+
law_id: str | None = None,
|
|
66
|
+
earliest_available: str | None = None,
|
|
67
|
+
) -> None:
|
|
68
|
+
super().__init__(message)
|
|
69
|
+
self.message = message
|
|
70
|
+
self.law_id = law_id
|
|
71
|
+
self.earliest_available = earliest_available
|