diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..989b973 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,150 @@ +# 변경 이력 + +이 문서는 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/) 형식을 따르며, 버전은 [유의적 버전](https://semver.org/lang/ko/)을 따릅니다. + +`0.x` 단계이므로 마이너 버전 올림에 호환성 깨짐이 포함될 수 있습니다. + +--- + +## [0.3.0] — 2026-07-19 + +에이전트 소비자를 위한 릴리스입니다. 응답이 자기 상태를 더 정확히 알리고, 컨텍스트를 덜 먹으며, 새 출처 계열이 하나 늘었습니다. + +### 추가 + +**행정기관의 판단 — 위원회 의결과 행정심판 재결.** 12개 위원회(개인정보보호위 `ppc`, 공정위 `ftc`, 금융위 `fsc`, 증선위 `sfc`, 방통위 `kcc`, 인권위 `nhrck`, 권익위 `acr`, 노동위 `nlrc`, 고용보험심사위 `eiac`, 산재재심사위 `iaciac`, 중앙토지수용위 `oclt`, 중앙환경분쟁조정위 `ecc`)와 4개 특별 심판기관을 포함한 행정심판(`decc`, `acr`, `adap`, `tt`, `kmst`)에 닿습니다. + +감독기관이 자기가 집행하는 법을 *적용한* 기록이며, **판례와 구분해서** 다룹니다 — 별도 타입(`AdjudicationHit`, `AdjudicationText`)과 별도 `kind`(`committee_decision_text`, `administrative_appeal_text`)를 가지고, 결과마다 이것이 판례가 아님을 밝히는 권위 문장이 붙습니다. + +```python +api.search_committee_decisions("개인정보 유출", committee="ppc") +api.get_administrative_appeal("12345", tribunal="tt") +``` + +**개정이유** — `get_revision_reason()`. law.go.kr이 이미 싣고 있으나 정규화 과정에서 버려지던 「개정이유 및 주요내용」을 노출합니다. `mst`로 판본을 고정하거나 `as_of`로 시점을 지정합니다. + +**컨텍스트 예산 3종.** + +- `get_law_toc()` / `get-law --toc` — 본문 없이 조문 지도만. 개인정보 보호법 기준 276KB → 19KB +- `--brief` — 결정문 5종에서 요지만 남기고 전문을 뺍니다. 어떤 결정문에서는 페이로드의 82%가 줄었습니다 +- `flags.large_payload` — 20,000자를 넘고 좁힐 옵션이 있었을 때 붙습니다. 명령별 조언이 `discipline`에 함께 옵니다 + +**모든 엔벨로프에 `version`.** 어느 버전이 답했는지 알 수 없으면, 빠진 필드가 "이 릴리스에서 미지원"인지 "호출이 잘못됨"인지 구분할 수 없습니다. 배포 메타데이터가 아니라 **실제로 실행된 코드**의 버전을 보고합니다 — 체크아웃이 `sys.path`에 있으면 site-packages를 가리기 때문입니다. + +### 변경 — 호환성 주의 + +**없는 식별자의 종료코드가 3 → 4로 바뀌었습니다.** + +law.go.kr은 모르는 식별자에 대해 오류가 아니라 빈 본문으로 답합니다. 이전에는 이걸 파싱 실패로 분류해 종료코드 3을 냈고, 그 규율은 "잠시 후 재시도"였습니다 — **영원히 성공할 수 없는 조회에 대한 조언**이었습니다. + +이제 `no_result`(종료코드 4)로 가서 `search-*`로 유도합니다. 종료코드 3에 의존해 재시도 루프를 돌리던 코드가 있다면 확인이 필요합니다. + +### 수정 + +`_normalization/unwrap.py`의 `ParseFailureError` NameError. + +### 테스트 + +**전 명령 스모크 테스트.** 모든 서브커맨드를 디스패치까지 통과시키고 catalog·parser·dispatch의 합치를 검증합니다. argv를 파서에서 도출하므로 새 명령이 자동으로 커버됩니다. + +0.2.3 파일 분할에서 명령 분기 안에서만 실행되는 임포트 3개가 빠졌는데 CI가 그대로 통과한 사고 뒤에 추가됐습니다. + +--- + +## [0.2.4] — 2026-07-04 + +### 수정 + +`load_institutional_system`의 CLI 디스패치 복구. 0.2.3 분할에서 누락됐습니다. + +--- + +## [0.2.3] — 2026-07-04 + +### 변경 + +**내부 모듈 분할.** 커진 파일들을 책임 단위로 쪼갰습니다 — `_models`, `_normalization`, `_laws`, `_cli`. + +**공개 API는 그대로입니다.** 루트의 `models.py`·`laws.py`·`normalization.py`·`cli.py`가 얇은 호환 파사드로 남아 기존 임포트가 계속 동작합니다. 분할 전에 호환성을 잠그는 테스트를 먼저 넣었습니다. + +이 릴리스부터 커밋 메시지에 Conventional Commits를 씁니다. + +### 수정 + +`_normalization`의 헌재 처분(disposition) 임포트 복구. + +--- + +## [0.2.2] — 2026-07-04 + +### 수정 + +페르소나 트랩 감사에서 나온 **Tier 1·2 결함 12건**. 회귀 잠금은 `tests/test_sdk_fixes_0_2_2.py`에 있으며, 각 결함의 사유가 테스트 docstring에 기록돼 있습니다. + +--- + +## [0.2.1] — 2026-07-04 + +### 수정 + +실사용 왕복에서 발견된 결함 5건. + +- 별표 도달 범위 +- 위임 하위규범 재현율 +- 별표 라벨 복구 +- 헌재 행 키의 대문자 처리 — law.go.kr이 판례는 `prec`(소문자), 헌재는 `Detc`(대문자)로 키를 짓습니다 +- CLI `--version` + +--- + +## [0.2.0] — 2026-07-04 + +### 추가 + +**셸 CLI.** 모든 과업 메서드가 `moleg` 서브커맨드가 되고, 호출마다 JSON 엔벨로프 하나를 찍습니다. + +**위키 문서.** + +### 수정 + +과거 판본 로딩. `법령ID + 시행일`로는 판본이 고정되지 않고 오류 없이 현행이 돌아온다는 문제를 MST 해석 경로로 해결했습니다. + +--- + +## [0.1.1] — 2026-06-23 + +### 추가 + +**공용 기본 OC 자격증명.** 등록 없이 바로 동작합니다. OC는 무료이며 비밀이 아닌 계정 식별자입니다. + +GitHub Release 발행 시 PyPI로 자동 배포. + +--- + +## [0.1.0] — 2026-06-18 + +첫 공개 릴리스. + +### 추가 + +- 법령 검색과 본문·조문 로딩, 공포 기준 신원 해석 +- 연혁 추적과 개정 전후 비교 +- 위임 하위규범과 법령 체계도 +- 행정규칙 검색·로딩과 맥락 해소 +- 별표·서식 검색과 본문 추출 +- 법제처·부처 법령해석, 판례, 헌재 결정 +- 질의 확장과 유사 제도 발견 +- 단계적 컨텍스트 번들과 **실행 가능한 후속 조회** — 응답이 준 `deferred` 객체를 그대로 다시 넣으면 다음 조회가 실행됩니다 +- 타입이 붙은 예외 계층 +- PyPI OIDC 신뢰 배포 + +--- + +[0.3.0]: https://github.com/tjdwls101010/MOLEG-API/releases/tag/v0.3.0 +[0.2.4]: https://github.com/tjdwls101010/MOLEG-API/releases/tag/v0.2.4 +[0.2.3]: https://github.com/tjdwls101010/MOLEG-API/releases/tag/v0.2.3 +[0.2.2]: https://github.com/tjdwls101010/MOLEG-API/releases/tag/v0.2.2 +[0.2.1]: https://github.com/tjdwls101010/MOLEG-API/releases/tag/v0.2.1 +[0.2.0]: https://github.com/tjdwls101010/MOLEG-API/releases/tag/v0.2.0 +[0.1.1]: https://github.com/tjdwls101010/MOLEG-API/releases/tag/v0.1.1 +[0.1.0]: https://github.com/tjdwls101010/MOLEG-API/releases/tag/v0.1.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..97168ac --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,59 @@ +# 기여자 행동강령 + +## 우리의 약속 + +우리는 이 프로젝트의 참여자로서, 나이·체격·장애·민족·성별 특성과 정체성·경험 수준·학력·사회경제적 지위·국적·외모·인종·종교·성적 지향에 관계없이 모두에게 괴롭힘 없는 참여 환경을 만들 것을 약속합니다. + +개방적이고, 환대하며, 다양하고, 포용적이며, 건강한 공동체에 기여하는 방식으로 행동할 것을 약속합니다. + +## 우리의 기준 + +**환경에 기여하는 행동:** + +- 다른 사람에게 공감과 친절을 보이는 것 +- 서로 다른 의견·관점·경험을 존중하는 것 +- 건설적인 피드백을 주고, 정중히 받아들이는 것 +- 실수에 책임을 지고, 영향을 받은 사람에게 사과하며, 그 경험에서 배우는 것 +- 개인이 아니라 공동체 전체에 무엇이 최선인지에 초점을 맞추는 것 + +**받아들일 수 없는 행동:** + +- 성적인 언어나 이미지의 사용, 성적 관심이나 접근 +- 트롤링, 모욕적이거나 경멸적인 언동, 개인적·정치적 공격 +- 공개적 또는 사적인 괴롭힘 +- 명시적 허가 없이 물리적 주소나 이메일 주소 등 타인의 사적 정보를 공개하는 것 +- 그 밖에 전문적인 환경에서 부적절하다고 볼 수 있는 행동 + +## 적용 범위 + +이 행동강령은 프로젝트의 모든 공간에 적용되며, 개인이 공개된 자리에서 프로젝트를 대표할 때에도 적용됩니다. 여기에는 이슈, Pull Request, 코드 리뷰, 커밋 메시지, 논의 게시판이 포함됩니다. + +## 신고 + +용납할 수 없는 행동을 목격하거나 겪었다면 메인테이너에게 알려 주세요. + +GitHub에서 [@tjdwls101010](https://github.com/tjdwls101010)에게 연락하시면 됩니다. + +모든 신고는 신속하고 공정하게 검토됩니다. 메인테이너는 신고자의 신원과 안전을 존중할 의무가 있습니다. + +> 보안 취약점은 여기가 아니라 [SECURITY.md](SECURITY.md)의 절차를 따라 주세요. + +## 집행 지침 + +메인테이너는 이 행동강령에 어긋난다고 판단되는 댓글·커밋·코드·이슈·기타 기여를 제거·수정·거부할 권한과 책임을 가지며, 그 이유를 적절한 때에 설명합니다. + +위반의 정도에 따라 다음과 같이 대응합니다. + +**1. 정정 요구** — 부적절하다고 판단되는 언동에 대해, 무엇이 왜 부적절했는지를 명확히 하는 비공개 서면 경고를 보냅니다. 공개 사과를 요청할 수 있습니다. + +**2. 경고** — 지속적인 행동에 대해 경고와 함께 일정 기간의 상호작용 제한을 둡니다. + +**3. 일시적 차단** — 심각하거나 반복되는 위반에 대해 일정 기간 프로젝트 공간에서의 모든 상호작용을 금지합니다. + +**4. 영구적 차단** — 지속적 위반, 개인에 대한 괴롭힘, 집단에 대한 공격이나 폄훼에 대해 프로젝트 공간에서 영구히 배제합니다. + +## 출처 + +이 행동강령은 [Contributor Covenant](https://www.contributor-covenant.org) 버전 2.1을 바탕으로 작성되었습니다. + +원문: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ba9ccfe --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,209 @@ +# 기여 안내 + +`moleg-api`에 기여해 주셔서 감사합니다. 이 문서는 개발 환경, 테스트, 브랜치·커밋 규칙, 릴리스 절차를 다룹니다. + +내부 구조를 먼저 파악하려면 [Architecture](docs/wiki/Architecture.md)와 [Maintainer Notes](docs/wiki/Maintainer-Notes.md)를 보세요. + +## 개발 환경 + +```bash +git clone https://github.com/tjdwls101010/MOLEG-API.git +cd MOLEG-API +python -m venv .venv && source .venv/bin/activate +pip install -e ".[test]" +``` + +Python **3.10 이상**. CI는 3.10 / 3.11 / 3.12에서 돕니다. + +`test` 추가 의존성은 `pytest`뿐입니다. 런타임 의존성은 **0개**이며, 이건 유지해야 할 성질입니다 — 이 패키지는 다른 도구의 의존성 트리 안에 얹히는 것을 전제로 만들어졌습니다. 새 의존성을 추가하는 PR은 그것이 왜 불가피한지 설명이 필요합니다. + +`pyproject.toml`에 `dev` 추가 의존성도 있지만 `psycopg`만 더 넣으며, 지금은 **쓰이지 않습니다**(사라진 congress-db 연동의 잔재). `requirements-dev.txt`도 같은 이유로 아무 데서도 참조되지 않습니다. 개발에는 `test`를 쓰세요. + +### 환경변수 + +```bash +cp .env.example .env.local +``` + +| 변수 | 용도 | +|---|---| +| `MOLEG_OC` | law.go.kr 자격증명. **라이브 테스트에만 필요** | +| `CONGRESS_DB_READONLY_URL` | `.env.example`에 있지만 **패키지 어디서도 읽지 않습니다**. 잔재입니다 | + +`.env`와 `.env.*`는 gitignore돼 있고 `.env.example`만 커밋됩니다. `tests/test_source.py`가 `.env.example`의 두 키가 **값 없이** 선언돼 있는지 검사합니다 — 비밀이 커밋되는 것을 막는 장치입니다. + +## 테스트 + +**529개 테스트** — 오프라인 511개, 라이브 18개. + +```bash +# 오프라인만 — CI가 도는 것 +python -m pytest -q -m "not live" + +# 라이브만 +MOLEG_OC= python -m pytest -q -m live + +# 전부 +python -m pytest -q +``` + +> ⚠️ **`pytest`를 맨몸으로 돌리면 라이브 테스트도 함께 돕니다.** `pyproject.toml`에 `addopts`로 `live`를 빼는 설정이 없어서, `MOLEG_OC`가 환경변수나 `.env`/`.env.local` 어디에든 잡히면 실행됩니다. 오프라인만 돌리려면 `-m "not live"`를 명시하세요. + +라이브 테스트는 `tests/test_live_smoke.py` 한 파일이고, 모듈 수준 `pytestmark`로 `MOLEG_OC`가 없으면 스킵됩니다. law.go.kr이 아무것도 안 돌려주면 실패가 아니라 스킵으로 떨어집니다 — 상류 불안정이 CI를 빨갛게 만들면 안 되기 때문입니다. + +### 테스트 작성법 + +**`conftest.py`가 없습니다.** 녹화된 카세트도, VCR도, `responses`도 없습니다. 이음매는 `MolegSource` 프로토콜이고, 테스트는 직접 만든 가짜를 주입합니다. + +```python +class FakeSource: + def __init__(self, *, search_payloads=None, service_payloads=None, ...): + self.calls = [] + + def search(self, target, params): + self.calls.append(("search", target, params)) + return self.search_payloads.pop(0) # FIFO 큐 + +api = MolegApi(source=FakeSource(search_payloads=[{...}])) +``` + +각 큐는 **FIFO `pop(0)`**입니다. 그래서 테스트가 호출 *순서*를 검증할 수 있고, `self.calls`로 어떤 target과 파라미터가 나갔는지 확인할 수 있습니다 — 예를 들어 검증 가드가 **출처 호출 전에** 입력을 거부했는지, MST가 ID보다 우선됐는지. + +페이로드는 각 테스트 안에 인라인으로, 검증 대상 필드만 남겨 씁니다. + +다른 가짜들: + +| 이름 | 용도 | +|---|---| +| `StubApi` | `__getattr__` 포괄 스텁. SDK를 건드리지 않고 CLI 디스패치만 구동 | +| `FakeResponse` | `urlopen` 대역. HTTP 계층 자체를 시험 | + +CLI 테스트는 `capsys`로 엔벨로프 JSON을 파싱합니다. + +**CLI 테스트는 결과 데이터클래스 타입을 키로 씁니다**(명령 이름이 아니라). 그래서 명령 이름이 바뀌어도 살아남습니다. + +### 테스트 파일 지도 + +| 파일 | 다루는 것 | +|---|---| +| `test_laws.py` | 대부분. `FakeSource`로 모든 과업 메서드 | +| `test_cli.py` | 엔벨로프와 신호 도출, 오류→종료코드 매핑 | +| `test_source.py` | 전송 계층 — 재시도, 호출 제한, SSL/CA, OC 해석 순서, `.env` 파싱 | +| `test_models.py` | 데이터클래스 불변식, 재귀 직렬화, `include_raw` | +| `test_versions.py` | 판본 로딩, MST 해석, 연혁 파서 | +| `test_live_smoke.py` | 유일한 라이브 파일 | +| `test_refactor_compat.py` | 호환 파사드 잠금 | +| `test_sdk_fixes_0_2_1.py`, `test_sdk_fixes_0_2_2.py` | 회귀 잠금 | +| `test_*_0_3_0.py` (6개) | 0.3.0 작업 항목별 회귀 잠금 | + +**결함별 사유가 테스트 파일 docstring에 기록돼 있습니다.** 회귀 테스트를 추가할 때 이 관행을 따라 주세요 — 무엇을 잠갔는지보다 **왜 잠갔는지**가 나중에 훨씬 유용합니다. + +날짜에 의존하는 테스트는 `date.today()` 취약성을 피하려고 아주 먼 미래(2999년)나 과거를 씁니다. + +## CI + +두 워크플로가 있고, **라이브 테스트는 CI에서 절대 돌지 않습니다** — 둘 다 `-m "not live"`를 쓰고 `MOLEG_OC` 시크릿을 주입하지 않습니다. + +### `ci.yml` — PR과 main 푸시 + +**`test` 잡** (ubuntu, Python 3.10/3.11/3.12 매트릭스) + +1. `python -m pip install -e ".[test]"` +2. `python -m compileall moleg_api tests` +3. `git diff --check` — 공백 위생 +4. `python -m pytest -q -m "not live"` + +**`package` 잡** — 휠을 빌드해 임시 디렉터리에 설치한 뒤, **체크아웃 밖에서** 다음을 단언합니다. + +- 임포트된 `moleg_api.__file__`이 설치 대상 안에 있는가 (체크아웃이 아니라) +- `py.typed`가 함께 배포되는가 +- `__all__`의 모든 이름이 실제로 해석되는가 +- `LawIdentity.to_dict()`가 `raw`를 빼는가 +- `DeferredLookup.to_dict()`가 집합을 결정적으로 정렬하고 키 충돌을 명확히 구분하는가 + +> **린터도, 포매터도, 타입 체커도, 커버리지도 없습니다.** 정적 게이트는 `compileall`과 `git diff --check`가 전부입니다. 이건 의도된 최소주의라기보다 현재 상태에 가깝습니다 — 도입 PR은 환영하지만 별도 논의가 필요합니다. + +### `workflow.yml` — PyPI 배포 + +`release: published`에 발동하거나 `workflow_dispatch`로 수동 실행합니다. OIDC 신뢰 배포를 쓰므로 API 토큰이 없습니다. `pypi`로의 수동 배포는 `main` 브랜치에서만 허용됩니다. + +## 브랜치와 커밋 + +### 브랜치 이름 + +저장소 이력에는 **세 가지 체계가 공존**합니다 — `1-slug`(초기), `codex/50-slug`(에이전트 작업), `wi-p1-slug`(0.3.0 작업 항목). 새 작업은 다음 형태를 써 주세요. + +``` +<이슈번호>-<짧은-슬러그> 예: 42-annex-table-confidence +docs/<슬러그> 문서만 바꿀 때 +fix/<슬러그> 이슈 없는 버그 수정 +``` + +머지된 브랜치는 정리해 주세요. 현재 원격에 58개가 남아 있는데, 좋은 상태가 아닙니다. + +### 커밋 메시지 + +**Conventional Commits**를 씁니다. 0.2.3부터 적용된 규칙이라 그 이전 이력은 따르지 않습니다. + +``` +feat(laws): reach agency adjudications — committee decisions and 행정심판 재결 (#101) +fix(cli): split permanent identifier failures out of exit 3 (#96) +refactor(models): split model internals behind public facade +test(refactor): lock sdk compatibility before splitting modules +chore(release): 0.3.0 (#102) +``` + +스코프: `laws`, `cli`, `models`, `normalization`, `source`, `docs`, `release`. + +제목은 **무엇이 달라졌는지를 사용자 관점에서** 쓰세요. `feat(cli): add --toc flag`보다 `feat(cli): add a map, a précis, and a size signal`이 낫습니다 — 후자는 왜 그것들이 한 묶음인지를 말합니다. + +## Pull Request + +1. `main`에서 브랜치를 땁니다 +2. 테스트를 함께 씁니다. 버그 수정이면 **먼저 실패하는 테스트**를 쓰고, docstring에 결함의 사유를 남깁니다 +3. 과업 메서드를 건드렸으면 **네 곳을 함께** 고칩니다 — `_laws/api_*.py`, `_cli/parser.py`, `_cli/dispatch.py`, `_cli/catalog.py` +4. 문서를 함께 갱신합니다 — [API Reference](docs/wiki/API-Reference.md), [CLI Reference](docs/wiki/CLI-Reference.md) +5. 로컬에서 `python -m pytest -q -m "not live"`를 통과시킵니다 +6. PR을 엽니다. CI가 매트릭스와 패키징 게이트를 돌립니다 + +라이브 동작에 영향이 있는 변경이라면 로컬에서 라이브 스모크도 돌려 보고, PR 본문에 결과를 적어 주세요. CI는 그걸 검증하지 못합니다. + +## 릴리스 절차 + +메인테이너용입니다. + +1. **버전을 올립니다** — `moleg_api/_version.py`의 리터럴 한 곳. `pyproject.toml`이 이 속성을 읽으므로 다른 데는 없습니다 +2. **CHANGELOG.md**에 항목을 추가합니다 +3. `chore(release): X.Y.Z` 커밋으로 PR을 열고 머지합니다 +4. **점검**: + ```bash + python -m compileall moleg_api tests -q + python -m pytest -q -m "not live" + MOLEG_OC= python -m pytest -q -m live + moleg catalog + python -m build && python -m twine check dist/* + ``` +5. **주석 태그**를 답니다 (경량 태그가 아니라): + ```bash + git tag -a v0.3.1 -m "moleg-api 0.3.1" + git push origin v0.3.1 + ``` +6. **GitHub Release를 발행**합니다. 이게 `workflow.yml`을 발동시켜 PyPI로 배포합니다 + +`release: published`가 배포 트리거이므로, **태그만 밀고 Release를 안 만들면 PyPI에 올라가지 않습니다.** + +## 문서 + +문서는 **한국어**로 씁니다(파일명은 영어 유지). 위키 페이지 하나를 고칠 때 지켜야 할 것: + +- `docs/wiki/_Sidebar.md`에 새 페이지를 등록합니다 +- README와 `Home.md`가 그 페이지를 가리키는지 확인합니다 +- **README와 위키는 내용을 중복하지 않습니다.** README는 현관이고 위키는 깊이입니다. 한쪽에 속하는 내용은 다른 쪽에서 링크입니다 + +## 행동강령 + +이 프로젝트는 [행동강령](CODE_OF_CONDUCT.md)을 따릅니다. + +## 보안 + +취약점은 이슈로 열지 마세요. [SECURITY.md](SECURITY.md)의 절차를 따라 주세요. diff --git a/README.md b/README.md index 4ce2140..fb8b8d2 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,116 @@ -# moleg-api +

+ MOLEG-API — Search. Choose. Load. +

-`moleg-api` is a task-level Python interface for loading Korean legal sources from [law.go.kr](https://www.law.go.kr/). It normalizes the Ministry of Government Legislation (법제처) OpenAPI so applications and agents can retrieve legal context without memorizing individual `target` values, identifier rules (`ID`, `MST`, `LID`, `JO`), or per-endpoint response quirks. +

moleg-api

-You pick an interface by the legal task you need — search a statute, load an article, trace an amendment, find a delegated rule, read a Constitutional Court decision — not by a raw endpoint name. It is not a complete wrapper around every law.go.kr endpoint; the public interface is intentionally small and deep. The recurring pattern is **search or plan first, then load** the selected source: search hits are candidates, not citable text. +

+ 법제처 law.go.kr OpenAPI를 법적 과업 단위로 감싼 Python SDK · CLI
+ 검색으로 후보를 고르고, 로더로 본문을 싣는다. +

-The PyPI package is `moleg-api`; import it as `moleg_api`. +

+ PyPI + Python versions + License: MIT +

-## Install +--- + +## 이게 무엇인가 + +법제처 OpenAPI는 195개 안내 문서에 흩어진 엔드포인트 묶음이다. 같은 개념이 엔드포인트마다 다른 한글 키 이름으로 오고, 조문 번호는 여섯 자리 `JO` 코드로 인코딩되며, 법령 하나를 가리키는 식별자만 `ID`·`MST`·`LID` 세 가지다. "지금 시행 중인 조문"과 "공포된 조문"은 아예 다른 엔드포인트 계열이고, 연혁만 HTML로 돌아온다. + +`moleg-api`는 그 복잡성을 호출자에게 떠넘기지 않는다. 공개 표면은 **원시 엔드포인트가 아니라 법적 과업**으로 되어 있다 — 법령을 검색하고, 조문을 싣고, 개정 이력을 추적하고, 위임된 하위규범을 찾고, 헌재 결정을 읽는다. + +```python +api.get_article(identity, "제3조") # ← 이렇게 부른다 +# target=eflawjosub, JO=000300, MST=… ← 이런 건 몰라도 된다 +``` + +## 왜 이런 모양인가 + +이 패키지의 1차 소비자는 **LLM 에이전트**다. 사람이 브라우저로 법령을 찾을 때와 달리, 에이전트는 (a) 무엇을 인용해도 되는지 스스로 판단해야 하고 (b) 컨텍스트 예산이 유한하며 (c) 빈 결과를 "그런 것은 없다"로 오독하면 그대로 답에 실린다. + +그래서 세 가지가 라이브러리 설계에 박혀 있다. + +**하나 — 검색 결과는 인용할 수 없다.** 검색은 *후보*(`*Hit`)를 주고, 로더는 *본문*(`*Text`)을 준다. 이 경계는 타입으로 강제된다. 로더에 법령 *이름*을 넘기면 실행되지 않고 "먼저 `search_laws`를 부르라"는 오류가 난다. + +**둘 — 규율이 응답에 실려 나간다.** CLI 엔벨로프는 데이터만이 아니라 `discipline` 배열을 함께 싣는다. "0건은 이 검색어로 못 찾았다는 뜻일 뿐 부재의 증명이 아님", "법제처 해석 ≠ 부처 1차 해석 — 답에서 출처 유형 보존" 같은 문장이 기계가 읽을 수 있는 형태로 온다. + +**셋 — 크기를 신호로 준다.** `get-law`로 개인정보 보호법 전문을 부르면 276KB다. `--toc`는 같은 법의 조문 지도만 19KB로 준다. 좁힐 수 있었는데 안 좁혔으면 `flags.large_payload`가 붙는다. + +## 설치 ```bash pip install moleg-api ``` -Live calls use a law.go.kr OpenAPI credential (the "OC" — a free, non-secret account id). The package ships a shared default, so calls work out of the box with **no registration required**. To use your own OC (recommended for heavy use), register at law.go.kr and set `MOLEG_OC`, or pass `oc=` to `LawGoKrClient`. See [Installation](docs/wiki/Installation.md). +Python 3.10 이상. **런타임 의존성 0개** — 표준 라이브러리만 쓴다. + +라이브 호출에는 law.go.kr OpenAPI 자격증명(“OC”)이 필요하지만, **패키지에 공용 기본값이 들어 있어 등록 없이 바로 동작한다.** 본격적으로 쓸 거라면 law.go.kr에 직접 등록해 `MOLEG_OC`를 설정하는 편이 좋다 → [Installation](docs/wiki/Installation.md) -## Quickstart +## 첫 호출 -Python — search, then load the chosen candidate: +Python — 검색해서 고르고, 고른 것을 싣는다. ```python from moleg_api import MolegApi api = MolegApi() -hits = api.search_laws("주택임대차보호법") # candidate identities -article = api.get_article(hits[0].identity, "제3조") # loaded source text +hits = api.search_laws("주택임대차보호법") # 후보 (인용 불가) +article = api.get_article(hits[0].identity, "제3조") # 본문 (인용 가능) print(article.text) ``` -Shell — every method is also a `moleg` subcommand printing one JSON envelope: +셸 — 모든 메서드가 `moleg` 서브커맨드이고, 언제나 JSON 엔벨로프 하나를 찍는다. ```bash -python -m moleg_api catalog # self-documenting command list -python -m moleg_api search-laws "주택임대차보호법" # → candidates with law_id -python -m moleg_api get-article --law 001248 제3조 # load the current article -python -m moleg_api get-article --law 001248 --as-of 2021-01-01 제3조 # the version in force then +moleg catalog # 명령 목록·라우팅·kind 전체 명세 +moleg search-laws "주택임대차보호법" # → law_id를 가진 후보들 +moleg get-article --law 001248 제3조 # 현행 조문 +moleg get-article --law 001248 --as-of 2021-01-01 제3조 # 그날 시행 중이던 판본 ``` -The CLI exposes every interface as a subcommand plus `catalog`; `catalog` is the source of truth for the command list, routing rules, and result `kind` values. Each envelope carries the `version` that produced it. - -Public dataclasses serialize recursively with `to_dict()` / `to_json_string()` (raw payloads omitted unless `include_raw=True`). +`python -m moleg_api …`로도 같게 동작한다. -## What it covers +## 무엇을 다루는가 -- Current (`effective`) and promulgated (`promulgated`) statutes and articles, including moved/deleted status -- Supplementary provisions (부칙), law history, and before/after text comparison -- Historical versions — load the text in force on a past date with `as_of` -- Delegated rules, legal hierarchy, and administrative rules (고시·훈령·예규) -- Law and administrative-rule annex/form bodies (별표·서식) -- MOLEG and ministry legal interpretations, Supreme Court cases, and Constitutional Court (헌재) decisions -- Amendment rationale — the 「개정이유 및 주요내용」 text for a specific statute version -- Committee decisions from twelve regulators (개인정보보호위·공정위·금융위·인권위·노동위 등) and 행정심판 재결례 including four special tribunals — the record of an agency applying the law it administers, kept distinct from precedent -- Query expansion, comparable-mechanism discovery, and staged context bundles with executable follow-up lookups +- **법령 본문** — 현행(`effective`)·공포(`promulgated`) 기준 법령과 조문, 삭제·이동 상태 포함 +- **부칙·연혁·비교** — 부칙(시행일·적용례·경과조치), 개정 이력, 개정 전후 대비 +- **과거 판본** — `as_of`로 특정 시점에 시행 중이던 조문을 싣는다 +- **개정이유** — 특정 판본의 「개정이유 및 주요내용」 +- **위임 구조** — 위임 하위규범, 법령 체계도, 행정규칙(고시·훈령·예규) +- **별표·서식** — 법령·행정규칙의 별표/별지 본문. 과태료 기준·수수료표가 실제로 사는 곳 +- **해석과 판단** — 법제처 법령해석, 40개 부처 1차 해석, 대법원 판례, 헌재 결정 +- **행정기관의 판단** — 12개 위원회 의결(개인정보보호위·공정위·금융위·인권위·노동위 등)과 4개 특별행정심판을 포함한 행정심판 재결. **판례와 구분해서** 다룬다 +- **탐색 보조** — 질의 확장, 유사 제도 발견, 실행 가능한 후속 조회를 품은 단계적 컨텍스트 번들 -Context budgeting: `get-law --toc` returns a statute's article map without the text (276KB → 19KB on 개인정보 보호법), `--brief` returns a decision's 요지 without the full body, and `flags.large_payload` warns when a narrowing option was available. +**다루지 않는 것**: 법률 자문, 국회 의안 데이터(의안 진행·표결·회의록), 입법예고(국민참여입법센터는 별개 출처), 최신 통계·뉴스. -Out of scope: legal advice, National Assembly bill data (status, votes, minutes), legislative pre-announcement (입법예고 — a separate 국민참여입법센터 source), and latest statistics/news. +## 문서 -## Documentation +| | | +|---|---| +| [Home](docs/wiki/Home.md) | 위키 진입점 | +| [Installation](docs/wiki/Installation.md) · [Quickstart](docs/wiki/Quickstart.md) | 설치와 첫 성공까지 | +| [Core Concepts](docs/wiki/Core-Concepts.md) | 검색→선택→로드, 후보 vs 본문, 식별자, 권위 구분 | +| **[Agent Integration](docs/wiki/Agent-Integration.md)** | **에이전트에 물릴 때 읽을 것** — 엔벨로프 신호, catalog 계약, 종료코드, 컨텍스트 예산 | +| [CLI Reference](docs/wiki/CLI-Reference.md) · [API Reference](docs/wiki/API-Reference.md) | 32개 서브커맨드 / 33개 메서드 전수 | +| [Historical Versions](docs/wiki/Historical-Versions.md) | `as_of`와 "조용한 현행" 함정 | +| [Sources & Coverage](docs/wiki/Sources-and-Coverage.md) | 출처 계열별 커버리지와 범위 밖 | +| [Gotchas](docs/wiki/Gotchas.md) | 조용히 틀리는 지점들 | +| [Error Handling](docs/wiki/Error-Handling.md) | 예외 계층, 재시도가 옳은 경우와 아닌 경우 | +| [Architecture](docs/wiki/Architecture.md) · [Maintainer Notes](docs/wiki/Maintainer-Notes.md) | 내부 구조 | -Full documentation is in the [`docs/wiki/`](docs/wiki/Home.md) folder: +## 상태 -- [Installation](docs/wiki/Installation.md) · [Quickstart](docs/wiki/Quickstart.md) · [Core Concepts](docs/wiki/Core-Concepts.md) -- [CLI Reference](docs/wiki/CLI-Reference.md) · [API Reference](docs/wiki/API-Reference.md) -- [Historical Versions](docs/wiki/Historical-Versions.md) · [Sources & Coverage](docs/wiki/Sources-and-Coverage.md) -- [Gotchas](docs/wiki/Gotchas.md) · [Error Handling](docs/wiki/Error-Handling.md) -- [Maintainer Notes](docs/wiki/Maintainer-Notes.md) +알파 (`0.3.0`). 인터페이스는 쓸 만하지만 law.go.kr의 라이브 동작은 출처·자격증명·엔드포인트 가용성에 따라 달라진다. 검색 결과는 로더가 본문을 실어 오기 전까지 후보로 취급하라. -## Status +**이 패키지는 법적 출처 로더이지 법률 자문이 아니다.** -Alpha (`0.2.x`). The interface is ready for use, but law.go.kr live behavior can vary by source, credential, and endpoint availability. Treat search results as candidates until a detail loader has retrieved source text. This package is a legal-source loader, not legal advice. +기여는 [CONTRIBUTING.md](CONTRIBUTING.md), 취약점 신고는 [SECURITY.md](SECURITY.md), 변경 이력은 [CHANGELOG.md](CHANGELOG.md)를 보라. -## License +## 라이선스 -MIT — see [LICENSE](LICENSE). +MIT — [LICENSE](LICENSE) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..6fff734 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,87 @@ +# 보안 정책 + +## 취약점 신고 + +**보안 취약점을 공개 이슈로 열지 마세요.** + +GitHub의 비공개 취약점 신고 기능을 이용해 주세요. + +1. 저장소의 **Security** 탭으로 갑니다 +2. **Report a vulnerability**를 누릅니다 +3. 재현 절차와 영향 범위를 적어 제출합니다 + +메인테이너만 볼 수 있으며, 수정본이 나오기 전까지 내용이 공개되지 않습니다. + +신고에 포함하면 좋은 것: + +- 영향받는 버전 +- 재현 절차 또는 개념 증명 +- 예상되는 영향 +- 알고 있다면 완화 방법 + +접수 확인까지 보통 며칠이 걸립니다. 이 프로젝트는 상근 인력이 유지보수하지 않으므로 응답 시간을 보장하지 못합니다. + +## 지원 버전 + +| 버전 | 지원 | +|---|---| +| 0.3.x | ✅ | +| 0.2.x 이하 | ❌ | + +알파 단계이며 최신 마이너 버전만 수정합니다. 이전 버전으로의 백포트는 없습니다. + +## 이 패키지의 보안 특성 + +신고를 판단할 때 참고가 될 만한 사실들입니다. + +### 런타임 의존성이 없다 + +표준 라이브러리만 씁니다(`urllib`, `json`, `ssl`, `dataclasses`). **공급망 공격 표면이 사실상 없습니다** — 이 패키지가 끌어오는 서드파티 코드가 없습니다. + +### OC는 비밀이 아니다 + +law.go.kr OpenAPI 자격증명(“OC”)은 무료 계정 식별자이며 **모든 요청 URL에 평문 쿼리 파라미터로 실려 나갑니다.** 출처가 그렇게 설계돼 있습니다. + +패키지에는 공용 기본값이 들어 있어 등록 없이 동작합니다. 이건 유출이 아니라 의도된 편의입니다. + +그럼에도 OC는 새어 나가면 지저분하므로 모든 경로에서 마스킹됩니다. + +- 모든 오류 메시지·응답 스니펫에서 `***`로 치환 +- `HistoryEvent.article_link`에 실리는 law.go.kr 링크에서 `OC=` 파라미터를 정규식 제거 + +**OC가 마스킹을 우회해 노출되는 경로를 발견했다면 신고 대상입니다.** + +### TLS 검증은 끌 수 없다 + +인증서 검증을 비활성화하는 옵션을 제공하지 않습니다. CA 번들 경로를 여러 후보에서 찾아 주지만(macOS Homebrew Python 대응), 검증 자체를 건너뛰지는 않습니다. + +**검증을 우회하게 만드는 입력이나 설정 조합을 발견했다면 신고 대상입니다.** + +### 아웃바운드 전용 + +이 패키지는 `https://www.law.go.kr`로 나가는 요청만 만듭니다. 서버를 띄우지 않고, 포트를 열지 않으며, 파일 시스템에 쓰지 않습니다. + +읽는 파일은 현재 작업 디렉터리의 `.env.local`과 `.env`뿐이고, `MOLEG_OC` 값을 읽기 위해서입니다. 읽은 값은 `os.environ`에 심어지지 않습니다. + +### 신뢰 경계 + +law.go.kr에서 오는 응답은 **신뢰할 수 없는 입력으로 취급됩니다.** 정규화 계층이 모든 파싱을 담당하며, 알아보지 못한 모양은 조용히 넘기지 않고 `ParseFailureError`로 올립니다. + +`eval`·`exec`·`pickle`·동적 임포트를 쓰지 않습니다. + +**출처 응답으로 예기치 않은 코드 실행이나 자원 소진을 유발할 수 있다면 신고 대상입니다.** + +## 범위 밖 + +다음은 이 패키지의 보안 문제가 아닙니다. + +- **law.go.kr 자체의 취약점** — 법제처에 신고하세요 +- **법령 데이터의 정확성** — 출처의 문제이며 [Gotchas](docs/wiki/Gotchas.md)에서 다루는 해석 함정입니다 +- **공용 기본 OC의 호출 제한** — 자기 OC를 발급받으세요 ([Installation](docs/wiki/Installation.md)) +- **법적 결론의 오류** — 이 패키지는 출처 로더이지 법률 자문이 아닙니다 + +## PyPI 배포 + +[PyPI](https://pypi.org/project/moleg-api/) 배포는 GitHub Actions의 **OIDC 신뢰 배포**로만 이뤄집니다. 장기 API 토큰을 쓰지 않습니다. + +배포는 GitHub Release 발행 시 자동으로 돌며, 워크플로가 배포 전에 다음을 검증합니다 — 테스트 통과, `twine check`, 그리고 설치된 휠이 체크아웃이 아닌 설치 대상에서 임포트되는지. diff --git a/docs/wiki/API-Reference.md b/docs/wiki/API-Reference.md index 35342f2..3f36916 100644 --- a/docs/wiki/API-Reference.md +++ b/docs/wiki/API-Reference.md @@ -1,171 +1,361 @@ -# API Reference +# API 레퍼런스 -`MolegApi` is the single public class of `moleg_api`. It exposes **27 task-level methods** for loading Korean legal sources from [law.go.kr](https://www.law.go.kr/). You choose a method by the *legal work* you need — search for a statute, load an article, trace amendment history, find delegated rules — and the method keeps law.go.kr source targets, identifier quirks, and article-number formatting inside the package. +`MolegApi`가 `moleg_api`의 단일 공개 클래스다. **33개 과업 메서드**를 노출한다. ```python from moleg_api import MolegApi -api = MolegApi() # bundled default credential; no registration required +api = MolegApi() # 내장 기본 자격증명, 등록 불필요 hits = api.search_laws("주택임대차보호법", display=5) article = api.get_article(hits[0].identity, "제7조") print(article.text) ``` -Every method also has a CLI subcommand (the same name in `kebab-case`). See the [CLI Reference](CLI-Reference.md) for the JSON envelope contract. +**생성자** — `MolegApi(source=None)`. 기본값은 라이브 `LawGoKrClient`를 만든다. `source=`는 테스트용 주입이나 자격증명 세부 제어에만 쓴다. -- **Constructor:** `MolegApi(source=None)` — pass a custom `MolegSource` only for testing or advanced credential control; the default constructs a live `LawGoKrClient`. +모든 메서드는 같은 이름의 kebab-case CLI 서브커맨드를 갖는다(`get_law_toc`만 예외로 `get-law --toc`에 접힌다). → [CLI Reference](CLI-Reference.md) -## How to read this reference +## 전체에 적용되는 규약 -The methods split into four intent groups: +**식별자.** 로더는 `LawIdentity`, 검색에서 받은 `*Hit`, 또는 원시 ID 문자열을 받는다. 검색에서 받은 객체를 그대로 넘기는 것이 가장 안전하다 — 판본을 가리키는 `mst`가 함께 따라가기 때문이다. **법령 이름 문자열은 거부된다.** -1. **[Search & planning](#search--planning)** — find candidate identities or plan a query. Results are *candidates*, not legal substance. -2. **[Body loading](#body-loading)** — load the actual source text for a selected identity. -3. **[History, structure & delegation](#history-structure--delegation)** — amendment chronology, statute hierarchy, and delegated lower rules. -4. **[Authority & bundles](#authority--bundles)** — interpretations, cases, constitutional decisions, and staged multi-source bundles. +**`basis`와 `as_of`.** `basis="effective"`(기본)는 시행 중인 텍스트, `"promulgated"`는 공포본. `as_of="YYYYMMDD"`(또는 `"YYYY-MM-DD"`)는 그 시점에 시행 중이던 판본. -Across all groups, three conventions recur: +**조문 표기.** `"제10조의2"` 같은 사람 표기나 정수를 그대로 넘긴다. 여섯 자리 `JO` 값을 만들 일이 없다. -- **Identifiers.** Loader methods accept a `LawIdentity`, the matching `*Hit` from a search, **or** a bare source-ID string. Passing the object you got back from a search is the safest form; a bare string is treated as a source identifier. -- **`basis` and `as_of`.** `basis="effective"` (default) targets current-force text; `basis="promulgated"` targets the promulgated version. `as_of="YYYYMMDD"` asks for the text in force on a specific reference date. -- **Article notation.** Pass the human label such as `"제10조의2"` (or an `int` for a plain article number). You never format law.go.kr's six-digit `JO` value yourself. +**키워드 전용.** `*` 뒤의 인자는 전부 키워드 전용이다. -### Return types serialize to JSON - -Every method returns either a **dataclass** or a **list of dataclasses**. All of these dataclasses expose the same two serialization helpers: +**직렬화.** 모든 반환 타입이 아래 두 헬퍼를 갖는다. ```python -result.to_dict() # nested dict; omits raw source payloads -result.to_dict(include_raw=True) # includes preserved raw law.go.kr metadata -result.to_json_string() # JSON text; also accepts include_raw=True +result.to_dict() # 중첩 dict. 원본 페이로드 제외 +result.to_dict(include_raw=True) # law.go.kr 원본 포함 +result.to_json_string() # JSON 문자열. include_raw도 받는다 ``` -For methods returning a `list[...]`, serialize each element: `[hit.to_dict() for hit in hits]`. +리스트를 받았으면 원소별로: `[hit.to_dict() for hit in hits]` --- -## Search & planning +## 1. 검색·계획 — 후보를 준다 + +**결과는 후보 메타데이터다. 로더가 본문을 실어 오기 전에는 법적 실질을 인용할 수 없다.** 대부분의 검색 결과는 알맞은 상세 로더를 가리키는 `follow_up`을 함께 단다. + +```python +search_laws(query, *, as_of=None, basis="effective", + law_type=None, ministry=None, display=20) -> list[LawHit] +``` +법령 신원 후보 검색. `basis="promulgated"`면 시행예정 행이 가려지지 않도록 내부에서 `nw=1`을 붙인다. -Use these first, when the target identity is not yet fixed. **Search results are candidate metadata — do not cite them as legal substance until a body-loading method has retrieved the text.** Most searches return a `follow_up` field that points to the correct detail loader. +```python +resolve_promulgated_law(*, prom_law_nm=None, prom_no=None, + promulgation_dt=None) -> LawIdentity +``` +공포 사실(법령명·공포번호·공포일)에서 **정확히 하나의** 신원으로 잇는 엄격한 해석기. `search_laws`보다 엄격하다 — 여럿 남으면 `AmbiguousLawError`, 없으면 `NoResultError`. -| Method | Signature (kwargs abbreviated) | Returns | Use when | -|---|---|---|---| -| `search_laws` | `search_laws(query, *, as_of=None, basis="effective", law_type=None, ministry=None, display=20)` | `list[LawHit]` | You have a law name or keyword and need candidate current/promulgated statute identities. | -| `search_administrative_rules` | `search_administrative_rules(query, *, ministry=None, rule_type=None, issued_on=None, include_history=False, display=20)` | `list[AdministrativeRuleHit]` | Execution criteria may live in ministry notices, directives, or established rules outside statute text. | -| `search_annex_forms` | `search_annex_forms(query, *, source="law", search_scope="source", annex_type=None, ministry=None, display=20)` | `list[AnnexFormHit]` | Operative content may be in 별표ㆍ서식 — tables, thresholds, amounts, or required forms. | -| `search_interpretations` | `search_interpretations(query, *, source="moleg", ministry=None, search_body=False, interpreted_on=None, display=20)` | `list[InterpretationHit]` | You need official MOLEG/ministry interpretation of how a statute is applied. Use `source="all"` for MOLEG plus one ministry; `source="all_ministries"` only for deep, registry-wide analysis. | -| `search_cases` | `search_cases(query, *, court="all", court_name=None, search_body=False, decided_on=None, case_number=None, display=20)` | `list[JudicialDecisionHit]` | You need Supreme Court or lower-court precedent, holdings, or judicial limits. | -| `search_constitutional_decisions` | `search_constitutional_decisions(query, *, search_body=False, decided_on=None, case_number=None, display=20)` | `list[JudicialDecisionHit]` | You need constitutional-risk context, reviewed statutes, or Constitutional Court reasoning. | -| `expand_legal_query` | `expand_legal_query(query, *, display=5, include_websearch_hint=True)` | `LegalQueryExpansion` | A broad or lay-worded query needs legal terms, related laws/articles, and search-planning hints before you load anything. Returns planning context, **not** legal authority. | -| `find_comparable_mechanisms` | `find_comparable_mechanisms(concept, *, display=5)` | `list[LawIdentity]` | You are doing legislative design and want laws using a similar mechanism (e.g. 과징금, 인허가, 신고제). Article anchors are preserved in each candidate's `raw_keys`. | +```python +search_administrative_rules(query, *, ministry=None, rule_type=None, + issued_on=None, include_history=False, + display=20) -> list[AdministrativeRuleHit] +``` +고시·훈령·예규 검색. **`issued_on`은 발령일자이지 시행일이 아니다.** -Notes: +```python +search_annex_forms(query, *, source="law", search_scope="title", + annex_type=None, ministry=None, display=20) -> list[AnnexFormHit] +``` +별표·서식 검색. 과태료 기준·수수료표·부과기준이 실제로 사는 곳이다. +`source ∈ {law, administrative_rule}`, `search_scope ∈ {title, source, body}`. +`annex_type`은 한글/영문 별칭을 받는다 — 별표, 서식, 별지, 별도, 부록 (별도·부록은 `source="law"`에서만). -- `search_*` methods return an **empty list** for no results (not an error). `expand_legal_query` and `find_comparable_mechanisms` raise `NoResultError` on a blank query/concept. -- Interpretations, ordinary cases, and constitutional decisions are **separate authority types**, not flags on one search. Keep them distinct downstream. +```python +search_interpretations(query, *, source="moleg", ministry=None, + search_body=False, interpreted_on=None, + display=20) -> list[InterpretationHit] +``` +법령해석 검색. `source ∈ {moleg, ministry, all, all_ministries}`. +`"all"`은 법제처 **+ 지정한 부처 하나**이고, `"all_ministries"`가 40개 부처 전체 팬아웃이다(비용이 크므로 깊은 분석에만). + +```python +search_cases(query, *, court="all", court_name=None, search_body=False, + decided_on=None, case_number=None, display=20) -> list[JudicialDecisionHit] + +search_constitutional_decisions(query, *, search_body=False, decided_on=None, + case_number=None, display=20) -> list[JudicialDecisionHit] +``` +판례 / 헌재 결정 검색. `court ∈ {all, supreme, lower}`. **기본은 제목 검색**이므로 본문까지 훑으려면 `search_body=True`. + +```python +search_committee_decisions(query=None, *, committee, display=20) -> list[AdjudicationHit] +search_administrative_appeals(query=None, *, tribunal="decc", display=20) -> list[AdjudicationHit] +``` +위원회 의결 / 행정심판 재결 검색. 코드 목록은 [CLI Reference](CLI-Reference.md) 또는 [Sources & Coverage](Sources-and-Coverage.md)에 있다. 모르는 코드는 `UnsupportedFormatError`를 낸다 — "기록 없음"으로 오해되지 않게 하기 위해서다. + +```python +expand_legal_query(query, *, display=5, + include_websearch_hint=True) -> LegalQueryExpansion +``` +검색 *계획* 도구. 법률 용어·일상 용어·관련 법령·관련 조문 후보와 실행 가능한 `follow_up_searches`를 준다. **권위가 아니다.** 빈 질의에만 `NoResultError`를 내고, 선택적 출처의 실패는 예외가 아니라 `gaps`로 기록된다. + +```python +find_comparable_mechanisms(concept, *, display=5) -> list[LawIdentity] +``` +유사 제도 발견 — 과징금·인허가·신고제 같은 개념을 쓰는 다른 법령들. 입법 설계 비교용이며 **법적 동등성의 판단이 아니다.** 조문 앵커가 각 후보의 `raw_keys`에 보존된다. + +> `search_*`는 결과가 없으면 **빈 리스트**를 준다(예외 아님). `expand_legal_query`와 `find_comparable_mechanisms`는 빈 질의에 `NoResultError`를 낸다. --- -## Body loading +## 2. 본문 로드 — 인용 가능 + +```python +get_law(identifier, *, as_of=None, basis="effective", + articles=None, include_metadata=True) -> LawText +``` +법령 본문. 조문 목록과 부칙을 함께 준다. `articles=["제3조", "제7조"]`로 좁힐 수 있다. + +```python +get_law_toc(law_identifier, *, as_of=None, basis="effective", + include_metadata=False) -> LawToc +``` +장 제목과 조문 스텁(번호·제목·삭제/이동 상태)만 문서 순서대로. **본문 없음.** 컨텍스트 예산 장치이며 CLI에서는 `get-law --toc`다. + +```python +get_article(law_identifier, article, *, as_of=None, + basis="effective") -> ArticleText +``` +조문 하나. 사람 표기로 부른다. + +```python +load_article_context(law_identifier, article, *, as_of=None, + basis="effective", follow_moved=True) -> ArticleContext +``` +조문을 싣되 **이동·삭제 표시를 해소한다.** 「삭제」나 「제12조로 이동」 스텁을 운용 조문으로 착각하지 않게 한다. 목적지 로드가 실패하면 예외가 아니라 `ContextGap`으로 떨어진다. -Load the actual source text for a selected identity. Pass the `*Hit` or `LawIdentity` you got from a search where possible. +```python +get_administrative_rule(identifier, *, articles=None, + include_metadata=True) -> AdministrativeRuleText + +load_administrative_rule_context(identifier, *, articles=None, + include_metadata=True, + follow_moved=True) -> AdministrativeRuleContext +``` +행정규칙 본문 / 이동 해소판. `identifier`는 신원 객체, 히트, 일련번호, 또는 정확한 이름을 받는다. + +```python +get_annex_form_body(identifier, *, source="law", title=None, + include_metadata=True, + attempt_structuring=True) -> AnnexFormText +``` +별표·서식 본문. `attempt_structuring=True`면 표를 `StructuredTableData`로 재구성해 붙인다 — **다만 `parsing_confidence`를 확인하라.** `low`거나 행이 비었으면 평문 `text`로 돌아가야 하며, 그걸 "기준 없음"으로 읽으면 안 된다. -| Method | Signature (kwargs abbreviated) | Returns | Use when | -|---|---|---|---| -| `get_law` | `get_law(identifier, *, as_of=None, basis="effective", articles=None, include_metadata=True)` | `LawText` | You have a statute identity and need its effective or promulgated text (optionally limited to selected `articles`). | -| `get_article` | `get_article(law_identifier, article, *, as_of=None, basis="effective")` | `ArticleText` | You need one precise provision by human label such as `제10조의2`. | -| `resolve_promulgated_law` | `resolve_promulgated_law(*, prom_law_nm=None, prom_no=None, promulgation_dt=None)` | `LawIdentity` | A National Assembly bill row has reached promulgation and supplies bridge fields (law name, promulgation number, or date). Stricter than `search_laws`; raises `AmbiguousLawError` if several identities remain. | -| `get_administrative_rule` | `get_administrative_rule(identifier, *, articles=None, include_metadata=True)` | `AdministrativeRuleText` | You selected a notice/directive/established rule and need its inspectable text (raw source rows). | -| `get_annex_form_body` | `get_annex_form_body(identifier, *, source="law", title=None, include_metadata=True, attempt_structuring=True)` | `AnnexFormText` | An annex/form candidate may carry operative criteria and you need its `text/plain` body (with optional structured table rows). | -| `get_interpretation` | `get_interpretation(identifier, *, source=None, ministry=None, include_metadata=True)` | `InterpretationText` | A selected interpretation needs its question, answer, reason, and related-law text. | -| `get_case` | `get_case(identifier, *, include_metadata=True)` | `JudicialDecisionText` (labeled `case`) | A selected ordinary court case needs holdings, summary, full text, or referenced statutes/cases. | -| `get_constitutional_decision` | `get_constitutional_decision(identifier, *, include_metadata=True)` | `JudicialDecisionText` (labeled `constitutional`) | A selected Constitutional Court decision needs holdings, reviewed statutes, or reasoning. | +```python +get_interpretation(identifier, *, source=None, ministry=None, + include_metadata=True) -> InterpretationText +``` +해석 본문(질의요지·회답·이유·관련법령). 부처 해석은 `source="ministry", ministry="…"`가 필요하다. 두 부처(국세청·재정경제부)는 검색은 되지만 본문 조회가 안 되며 `UnsupportedFormatError`를 낸다. -Notes: +```python +get_case(identifier, *, include_metadata=True) -> JudicialDecisionText +get_constitutional_decision(identifier, *, include_metadata=True) -> JudicialDecisionText +``` +판례 / 헌재 결정 본문. **로더가 태그를 교차 검증한다** — 헌재 신원을 `get_case`에 넘기면(또는 그 반대) `UnsupportedFormatError`가 난다. 권위 유형이 섞이는 것을 타입 수준에서 막는다. -- `get_case` raises `UnsupportedFormatError` if passed a constitutional identity, and `get_constitutional_decision` raises it for an ordinary case identity — use the matching loader for each. -- `include_metadata=False` (and, on the judicial loaders, dropping raw metadata) trims the payload when you are budgeting context. -- For articles that may have been **moved or deleted**, prefer `load_article_context` / `load_administrative_rule_context` (below) so you do not mistake a move marker for operative text. +```python +get_committee_decision(decision_id, *, committee) -> AdjudicationText +get_administrative_appeal(decision_id, *, tribunal="decc") -> AdjudicationText +``` +위원회 의결 / 행정심판 재결 본문. 결과는 이것이 판례가 **아님**을 밝히는 권위 문장을 항상 달고 나온다. --- -## History, structure & delegation +## 3. 연혁·개정·비교·체계 -Chronology, hierarchy, and the lower rules a statute delegates to. +```python +trace_law_history(law_identifier, *, date_range=None, article=None, + promulgation_bridge=None) -> LawHistory +``` +개정 연혁. `HistoryEvent`에 공포번호·시행일·개정 유형·사유가 담긴다. `date_range`는 `(from, to)` 튜플이다. 전체 법 연혁은 law.go.kr이 **HTML로만** 주는 경로를 파싱한다. + +```python +get_revision_reason(law_identifier, *, mst=None, as_of=None, + include_metadata=True) -> RevisionReason +``` +특정 **판본**의 「개정이유 및 주요내용」. 판본 선택 규칙: -| Method | Signature (kwargs abbreviated) | Returns | Use when | -|---|---|---|---| -| `trace_law_history` | `trace_law_history(law_identifier, *, date_range=None, article=None, promulgation_bridge=None)` | `LawHistory` | You need amendment chronology, amendment reasons, promulgation numbers, or effective dates — not the current text. Pass `article=` for article-level events. | -| `compare_law_versions` | `compare_law_versions(law_identifier, *, before=None, after=None, article=None)` | `LawDiff` | You need MOLEG's `oldAndNew` before/after text surface for a statute or article. Arbitrary two-date windows are **not** supported (raises `UnsupportedFormatError`). | -| `find_delegated_rules` | `find_delegated_rules(law_identifier, *, article=None)` | `DelegationGraph` | Statute text may delegate details to enforcement decrees/rules, notices, or administrative rules. Optionally filter by source `article`. | -| `get_law_structure` | `get_law_structure(law_identifier, *, depth=0)` | `LawStructure` | You need the broader 법률 → 시행령 / 시행규칙 / 행정규칙 hierarchy (`lsStmd`), not article-level delegation. Does **not** provide source-article links. | -| `load_article_context` | `load_article_context(law_identifier, article, *, as_of=None, basis="effective", follow_moved=True)` | `ArticleContext` | You need current/as-of article substance and must resolve moved/deleted article state before making a substance claim. | -| `load_administrative_rule_context` | `load_administrative_rule_context(identifier, *, articles=None, include_metadata=True, follow_moved=True)` | `AdministrativeRuleContext` | You need current operational criteria from an administrative rule and must resolve moved/deleted articles first. | -| `load_delegated_criteria` | `load_delegated_criteria(law_identifier, *, articles=None, query=None, budget="standard", as_of=None)` | `LegalContextBundle` | From a known statute/article, you need the *bodies* of subordinate administrative rules and annexes/forms for concrete operational criteria. | +- `mst`를 주면 그 판본으로 정확히 고정 (연혁 이벤트의 `identity.mst`를 쓴다) +- `as_of`를 주면 그날 시행 중이던 판본 +- **둘 다 없으면** 파일상 가장 최신 시행일 판본 — 이것이 **미래 시행 판본일 수 있다** -Notes: +오래된 판본은 이유도 공포문도 없는 경우가 흔하고, 그러면 `NoResultError`다. -- `compare_law_versions` raises `NoResultError` when the source exposes no comparable changes. -- `get_law_structure` is hierarchy context only. Use `find_delegated_rules` when you need actual article-level delegation rows. -- The `load_*_context` methods surface **gaps** and **deferred lookups** for moved-destination loads that failed, rather than raising — inspect those before treating loaded text as final. +> 개정이유는 **제안자의 자기 진술**이며 그 판본 하나에만 적용된다. 중립적 요약이 아니다. + +```python +compare_law_versions(law_identifier, *, before=None, after=None, + article=None) -> LawDiff +``` +개정 전후 대비. **임의의 두 날짜 구간은 지원하지 않는다** — 출처가 못 한다. `UnsupportedFormatError`가 난다. 임의 두 시점을 비교하려면 `get_article`을 `as_of` 달리해 두 번 부르라. + +```python +find_delegated_rules(law_identifier, *, article=None) -> DelegationGraph +get_law_structure(law_identifier, *, depth=0) -> LawStructure +``` +조문 단위 위임 관계 / 법령 체계도(법률 → 시행령 → 시행규칙 → 행정규칙). 둘은 다른 질문에 답한다 — 체계도는 계층 맥락일 뿐 조문 단위 위임의 증거가 아니다. + +**`find_delegated_rules`의 결과에 별표는 없다.** 과태료 기준표 같은 것은 `search_annex_forms`로 따로 찾아야 한다. --- -## Authority & bundles +## 4. 권위·번들 -Article-scoped authority (interpretations/cases/decisions) and staged multi-source bundles for broad questions. +```python +load_authority_context(law_identifier, *, articles, query=None, + budget="standard", as_of=None) -> AuthorityContext +``` +정밀 도구. **지정한 조문들에 스코프를 건** 해석·판례·헌재 결정을 모으고, 조문과 어긋나거나 날짜가 없거나 개정 이전인 것을 걸러낸다. `articles`는 **필수 키워드**다. -| Method | Signature (kwargs abbreviated) | Returns | Use when | -|---|---|---|---| -| `load_authority_context` | `load_authority_context(law_identifier, *, articles, query=None, budget="standard", as_of=None)` | `AuthorityContext` | You need interpretations, cases, and constitutional decisions scoped to **specific statute articles**, filtered to dated matches so mismatched/pre-amendment authority is not treated as current support. | -| `load_legal_context_bundle` | `load_legal_context_bundle(query=None, *, promulgation_bridge=None, law_identifier=None, articles=None, mode="question", budget="standard", as_of=None)` | `LegalContextBundle` | The question is broad, under-specified, or starts from a statute/bill anchor and you want one bounded first pass over likely sources. | -| `load_institutional_system` | `load_institutional_system(statute_identifiers, *, articles=None, budget="standard", as_of=None)` | `LegalContextBundle` (`mode="institutional_system"`) | You have **already selected** the statute set for a 제도 and want them composed into one staged bundle. | -| `load_followup` | `load_followup(lookup)` | *(varies)* | You received a `DeferredLookup` or `FollowUpSearch` from a bundle or query expansion and want it executed via the right public loader. Returns whatever the routed method returns; raises `UnsupportedFormatError` for WebSearch handoffs. | +인용은 결과의 **`current_authorities`**에서 하라. `loaded`에는 실린 것 전부가, `current_authorities`에는 실제로 대상 조문을 참조하고 시점이 맞는 것만 남는다. -A `LegalContextBundle` is a **source-loading packet, not a conclusion**. Inspect its fields before deciding what to load next: +```python +load_legal_context_bundle(query=None, *, promulgation_bridge=None, + law_identifier=None, articles=None, + mode="question", budget="standard", + as_of=None) -> LegalContextBundle +``` +넓은 질문에 대한 한 번의 제한된 1차 통과. -- `loaded` — successfully loaded law/article/delegation (and, for `load_delegated_criteria`, rule/annex) bodies. -- `candidates` — bounded discovery results not yet loaded in full. -- `deferred` — staged follow-up lookups you can run with `load_followup`. -- `ambiguities`, `gaps`, `source_notes` — unresolved choices, missing pieces, and non-fatal source failures. +| `mode` | 필요한 것 | 동작 | +|---|---|---| +| `"question"` | `query` | 질의 확장 후 후보 탐색. 법령 후보가 **여럿이면 자동 선택하지 않고** `Ambiguity`와 `gap`을 남긴다 | +| `"promulgated_bill"` | `promulgation_bridge` | 공포 사실에서 신원을 잇는다. law.go.kr 미반영 지연도 gap으로 기록 | +| `"statute_review"` | `law_identifier` | 탐색 없이 바로 그 법령으로 | -Executing a follow-up: +```python +load_institutional_system(statute_identifiers, *, articles=None, + budget="standard", as_of=None) -> LegalContextBundle +``` +**이미 고른** 법령 집합을 하나의 제도로 훑는다. 집합을 발견하거나 무엇이 주된 법인지 결정하지 않는다. 개별 법령의 실패는 예외가 아니라 기록된다. ```python -bundle = api.load_legal_context_bundle(query="자동차 방치 처리 기준", mode="question") +load_delegated_criteria(law_identifier, *, articles=None, query=None, + budget="standard", as_of=None) -> LegalContextBundle +``` +법령 하나에 닻을 내리고 행정규칙과 별표·서식의 **본문까지** 제한적으로 싣는다. 이름이 아니라 구체적 집행 기준이 필요할 때. -for lookup in bundle.deferred: - if lookup.interface == "load_administrative_rule_context": - rule_context = api.load_followup(lookup) - break +```python +load_followup(lookup) -> Any ``` +`DeferredLookup` 또는 `FollowUpSearch`를 실행한다. `interface` 문자열로 알맞은 공개 메서드에 라우팅한다. `websearch*`·`congress-db*`는 `UnsupportedFormatError` — 의도된 경계 표시다. -See [Follow-up Lookups](Core-Concepts.md) for the deferred-lookup workflow. +> **`budget`만으로 결정되지 않는다.** 번들의 상세 로드는 질의 내용이 함께 정한다. `"위헌"` 같은 키워드가 없으면 `--budget broad`여도 헌재 결정을 싣지 않는다. → [Agent Integration](Agent-Integration.md)의 의도 게이트 --- -## Errors +## 공개 데이터클래스 + +전부 `@dataclass(frozen=True)`이며 `moleg_api`에서 직접 임포트할 수 있다. + +### 법령 + +| 타입 | 핵심 필드 | +|---|---| +| `LawIdentity` | `law_id` `name` `basis` `mst` `lid` `promulgation_date` `effective_date` `promulgation_number` `law_type` `ministry` `raw_keys` | +| `LawHit` | `identity` `raw` `follow_up` | +| `LawText` | `identity` `articles: list[ArticleText]` `supplementary_provisions` | +| `ArticleText` | `identity` `article` `text` `title` `effective_date` `article_kind` `revision_type` `moved_from` `moved_to` `has_changes` `is_deleted` | +| `ArticleContext` | `requested_article` `current_article` `loaded_articles` `deferred` `gaps` `source_notes` | +| `LawToc` / `LawTocEntry` | `identity` `entries` `article_count` / `article` `title` `heading` `entry_kind` `is_deleted` `moved_to` | +| `SupplementaryProvision` | `source_type` `text` `promulgation_date` `promulgation_number` `title` | +| `LawHistory` / `HistoryEvent` | `identity` `events` `source_failures` / `changed_date` `effective_date` `promulgation_number` `bill_id` `revision_type` `article` `reason` `article_link` | +| `RevisionReason` | `identity` `mst` `reason` `promulgation_text` | +| `LawDiff` / `LawDiffChange` | `identity` `before_identity` `after_identity` `changes` / `article` `before_text` `after_text` `title` | +| `DelegationGraph` / `DelegatedRule` | `identity` `rules` / `source_article` `delegated_type` `delegated_name` `delegated_law_id` `delegated_mst` `delegated_article` `text` | +| `LawStructure` / `LawStructureNode` | `identity` `instruments` / `name` `source_type` `instrument_type` `law_id` `mst` `children`(재귀) | + +### 행정규칙 · 별표 + +| 타입 | 핵심 필드 | +|---|---| +| `AdministrativeRuleIdentity` | `serial_id` `name` `rule_id` `rule_type` `issuing_date` `effective_date` `ministry` `current_status` `source_law_id` `source_law_name` `source_article` | +| `AdministrativeRuleHit` / `Text` / `Context` / `ArticleText` | 법령 쪽과 같은 구조. `Text`는 평문 `text`와 `articles`를 **둘 다** 갖는다 | +| `AnnexFormIdentity` | `annex_id` `title` `source_type` `related_name` `annex_number` `annex_type` `file_link` `pdf_link` `detail_link` | +| `AnnexFormText` | `identity` `text` `file_type` `extraction_method` `extraction_confidence` `structured_data` | +| `StructuredTableData` | `title` `headers` `rows` `units` `parsing_confidence` `notes` | + +### 권위 + +| 타입 | 핵심 필드 | +|---|---| +| `InterpretationIdentity` / `Hit` / `Text` | `interpretation_id` `title` `source_type` `case_number` `reply_agency` / `question` `answer` `reason` `related_laws` `referenced_articles` | +| `JudicialDecisionIdentity` / `Hit` / `Text` | `decision_id` `court` `case_number` `decision_date` / `holdings`(판시사항) `summary`(요지) `full_text`(전문) `referenced_statutes` `reviewed_statutes` `referenced_articles` `reviewed_articles` | +| `AdjudicationIdentity` / `Hit` / `Text` | `decision_id` `body` `body_name` `source_type` `source_authority` `respondent_agency` `review_agency` `decided_on` / `disposition`(주문) `summary`(요지) `reasoning`(이유) `claim` `applicant` `respondent` | +| `ArticleReference` | `law_name` `article` `law_id` — 자유 텍스트에서 파싱한 조문 인용 | -All methods raise subclasses of `MolegApiError`: +### 번들·후속 -| Exception | Meaning | +| 타입 | 핵심 필드 | |---|---| -| `NoResultError` | The source returned no usable result for the requested legal task. | -| `AmbiguousLawError` | The request matched multiple plausible law identities (carries `.candidates`). | -| `UnsupportedFormatError` | The requested format, handoff, or source path is outside this interface. | -| `SourceApiError` | The law.go.kr source API failed or returned an invalid response. | -| `RateLimitError` | Subclass of `SourceApiError` — the source rate-limited the request. | -| `RetryExhaustedError` | Subclass of `SourceApiError` — retryable failures continued through all attempts. | -| `ParseFailureError` | A source response could not be normalized into the public model. | +| `LegalContextBundle` | `request` `loaded` `candidates` `deferred` `ambiguities` `gaps` `source_notes` | +| `AuthorityContext` | `request` `target_articles` `loaded` **`current_authorities`** `candidates` `deferred` `gaps` | +| `LoadedContext` | `laws` `articles` `delegations` `law_structures` `administrative_rules` `annex_forms` `interpretations` `cases` `constitutional_decisions` | +| `CandidateContext` | `query_expansion` `laws` `administrative_rules` `annex_forms` `interpretations` `cases` `constitutional_decisions` | +| `BundleRequest` | `query` `mode` `budget` `articles` `statute_ids` `promulgation_bridge` `as_of` | +| `DeferredLookup` / `FollowUpSearch` | `interface` `query` `reason` `source_type` `filters` | +| `Ambiguity` | `kind` `message` `candidates` | +| `ContextGap` | `kind` `reason` `query` `recommended_interface` | +| `LegalQueryExpansion` | `original_query` `law_candidates` `term_candidates` `related_terms` `related_articles` `related_laws` `follow_up_searches` `empty_sources` `source_failures` | + +### 리터럴 타입 별칭 + +| 별칭 | 값 | +|---|---| +| `Basis` | `effective`, `promulgated` | +| `AnnexFormSource` | `law`, `administrative_rule` | +| `AnnexSearchScope` | `title`, `source`, `body` | +| `AnnexType` | `annex`/`별표`, `form`/`서식`, `attached_form`/`별지`, `separate`/`별도`, `appendix`/`부록` | +| `InterpretationSearchSource` | `moleg`, `ministry`, `all`, `all_ministries` | +| `CaseCourt` | `all`, `supreme`, `lower` | +| `BundleMode` | `question`, `promulgated_bill`, `statute_review` | +| `BundleBudget` | `minimal`, `standard`, `broad` | + +--- + +## 직렬화 계약 + +`to_dict(include_raw=False)` / `to_json_string(include_raw=False)`. + +- **재귀적** — 중첩 데이터클래스, 리스트, 튜플, 집합, 딕셔너리를 모두 따라 내려간다. +- **`raw` 필드는 기본적으로 빠진다.** 컨텍스트 예산 때문이다. `raw_keys`는 감사용이라 항상 남는다. +- `to_json_string`은 `ensure_ascii=False`(한글 그대로)와 `sort_keys=True`(diff 안정)로 찍는다. +- 집합은 정렬해서 리스트로 나간다 — 출력이 결정적이어야 하기 때문이다. +- 비문자열 딕셔너리 키는 충돌 없이 처리된다. `{1: "a", "1": "b"}`가 손실 없이 직렬화된다. +- `LawToc`의 항목은 빈 필드를 생략한다. 보통은 "값 없음"도 정보지만 139줄짜리 목차에서는 자리표시자가 페이로드의 대부분이 된다. + +--- + +## 예외 + +전부 `MolegApiError`의 하위 클래스다. 상세는 [Error Handling](Error-Handling.md). + +| 예외 | 의미 | 재시도? | +|---|---|---| +| `NoResultError` | 쓸 만한 결과 없음 | 무의미 | +| `AmbiguousLawError` | 복수 신원. `.kind` `.candidates` 보유 | 해당 없음 | +| `UnsupportedFormatError` | 이 인터페이스 밖의 형식·경로·핸드오프 | 무의미 | +| `ParseFailureError` | 응답을 모델로 정규화하지 못함 | 무의미 | +| `AsOfBeforeCoverageError` | `as_of`가 통합본 커버리지 이전. `.law_id` `.earliest_available` 보유 | 무의미(영구) | +| `SourceApiError` | law.go.kr 실패 또는 잘못된 응답 | 경우에 따라 | +| `RateLimitError` | HTTP 429. `SourceApiError`의 하위 | **가능** | +| `RetryExhaustedError` | 재시도 가능한 실패가 전부 소진. `SourceApiError`의 하위 | **가능** | -**A source-access failure is not a legal no-result.** When you catch a `SourceApiError` (or its subclasses), retry or surface the source gap — do not conclude that the law, authority, or rule does not exist. +**출처 접근 실패는 법적 부재가 아니다.** `SourceApiError` 계열을 잡았을 때 재시도하거나 출처 공백으로 드러내라. 그 법·권위·규칙이 존재하지 않는다고 결론짓지 마라. --- -## See also +## 관련 문서 -- [API Guide](API-Reference.md) — narrative walkthrough of choosing a method by legal task. -- [CLI Reference](CLI-Reference.md) — the `moleg` subcommands and JSON envelope contract. -- [Quickstart](Quickstart.md) — end-to-end first-use example. -- [Follow-up Lookups](Core-Concepts.md) — running deferred lookups from bundles and expansions. -- [Source Coverage & Limits](Sources-and-Coverage.md) — which law.go.kr sources are covered and their known limits. +- [Core Concepts](Core-Concepts.md) — 왜 이렇게 나뉘어 있는지 +- [Agent Integration](Agent-Integration.md) — 엔벨로프 신호와 컨텍스트 예산 +- [CLI Reference](CLI-Reference.md) — 같은 메서드의 셸 표면 +- [Sources & Coverage](Sources-and-Coverage.md) — 출처 계열별 커버리지와 한계 diff --git a/docs/wiki/Agent-Integration.md b/docs/wiki/Agent-Integration.md new file mode 100644 index 0000000..6430caf --- /dev/null +++ b/docs/wiki/Agent-Integration.md @@ -0,0 +1,233 @@ +# 에이전트 통합 + +이 패키지의 1차 소비자는 LLM 에이전트다. 사람이 브라우저로 법령을 찾을 때와 달리 에이전트는 세 가지 제약을 동시에 받는다. + +1. **무엇을 인용해도 되는지 스스로 판단해야 한다.** 검색 결과 목록을 본문으로 착각하면 그대로 답에 실린다. +2. **컨텍스트 예산이 유한하다.** 조문 139개짜리 법 전문을 부르면 276KB가 들어온다. +3. **빈 결과의 의미를 오독하기 쉽다.** "0건"을 "그런 것은 없다"로 바꿔 말하면 그건 거짓이다. + +CLI 엔벨로프는 데이터만 나르는 통이 아니라 이 세 제약에 대응하는 장치다. 이 페이지는 그 장치를 전부 설명한다. + +--- + +## 1. 계약을 먼저 읽는다 — `catalog` + +```bash +moleg catalog +``` + +`catalog`는 **네트워크도 자격증명도 쓰지 않는다.** 명령을 구성하기 전에 이걸 한 번 읽으면 유효한 명령 전체, 언제 무엇을 쓰는지, 볼 수 있는 모든 `kind`를 알 수 있다 — argparse 도움말이나 소스를 읽을 필요 없이. + +돌려주는 것은 네 부분이다. + +| 키 | 내용 | +|---|---| +| `convention` | 불변 규칙 8개 — `kind` 접미사 규약, 인용 가능 조건, 출처 권위 서열, 종료코드 범례, 시간 의미론, 검색 스코프 주의점 | +| `routing_rules` | 갈림길 6개 — 조문 로딩 경로 선택, 개정 관련 3분기, 체계도 vs 위임, 번들 넷 중 무엇을 쓸지, 감독기관 확인 경로, 별표 경로 | +| `commands` | 32개 과업 명령을 4개 버킷으로 (`catalog` 자신은 목록에 없음) | +| `kinds` | 나올 수 있는 `kind` 값 목록 | + +`routing_rules` 중 특히 유용한 것 하나 — **개정 3분기**다. + +- `get-revision-reason` → **왜** 고쳤나 (「개정이유 및 주요내용」) +- `compare-law-versions` → **무엇이** 바뀌었나 (조문 전후 대비) +- `trace-law-history` → **어떤 개정들이** 있었나 (연혁 목록) + +`trace-law-history`가 준 이벤트의 `identity.mst`를 `get-revision-reason --mst`에 넣으면 그 개정의 이유로 정확히 내려간다. + +> **알려진 차이 하나.** `catalog`의 `kinds` 목록에는 코드가 실제로 배출하는 `catalog`와 `version_request_unfulfilled` 두 값이 빠져 있다. `kinds`를 완결된 열거로 신뢰하지 말고, 모르는 `kind`를 만나면 접미사 규약으로 판단하라. + +--- + +## 2. 엔벨로프 세 신호 + +성공 응답은 항상 이 모양이다. + +```json +{ + "ok": true, + "command": "get-law", + "version": "0.3.0", + "kind": "law_text", + "source": "법제처 / 현행 법령 본문", + "data": { }, + "flags": { }, + "discipline": [ ], + "next": [ ] +} +``` + +`flags`·`discipline`·`next`는 **비어 있으면 아예 나오지 않는다.** 있을 때만 의미가 있다. + +### `flags` — 기계가 읽는 상태 + +지금 손에 든 데이터의 상태를 구조화해 알린다. 자주 보게 될 것들: + +| 플래그 | 뜻과 대응 | +|---|---| +| `not_effective_as_of` | 실린 판본의 시행일이 오늘보다 미래다. 공포는 됐으나 **아직 효력 없음.** 현행으로 인용하면 안 된다 | +| `version_mismatch` | `{requested, loaded}` — 요청한 날짜와 실제로 실린 판본의 시행일이 다르다 | +| `ambiguous_versions` | 같은 이름의 법이 여러 판본으로 잡혔다 | +| `top_candidate_not_yet_effective` | 첫 후보가 미시행 판본이다. 무심코 `[0]`을 집으면 미래의 법을 인용하게 된다 | +| `articles_deleted_or_moved` | 실린 법 안에 삭제·이동된 조문이 몇 개 있는지 | +| `is_deleted` / `moved_to` | 조문 단위. 삭제 표시는 본문이 아니고, 이동 표시는 목적지가 따로 있다 | +| `source_authority` | 이 출처가 판례가 아님을 알리는 문장 (위원회 의결·행정심판 재결) | +| `large_payload` | `{chars, articles?}` — 좁힐 수 있는 옵션이 있었는데 안 썼다 | +| `brief` | `{withheld: [...]}` — `--brief`로 어떤 필드를 뺐는지 | +| `issued_on_is` | 행정규칙 필터가 **발령일자**이지 시행일이 아니라는 알림 | + +### `discipline` — 해석 규율 + +이 결과를 어떻게 다뤄야 하는지가 한국어 문장으로 실려 온다. 이 패키지에서 가장 독특한 부분이고, 에이전트라면 **답을 쓰기 전에 반드시 읽어야 하는 항목**이다. + +실제로 나오는 것들: + +- `"0건 — 이 검색어·범위로 못 찾음일 뿐, 부재의 증명 아님."` +- `"모호성이지 첫 후보를 고를 허가가 아님 — 후보를 사용자에게 제시."` +- 법제처 해석과 부처 1차 해석의 권위 층위를 답에서 보존하라는 지시 +- 헌법 원칙은 자유 텍스트 검색이라 "위헌 소지 없음"을 결론지을 수 없다는 경고 +- 행정기관에 기록이 없는 것이 그 기관이 아무 문제 없었다는 뜻이 아니라는 경고 +- 별표 검색 결과의 금액·기준은 본문을 실은 뒤에만 인용 가능하다는 제한 + +### `next` — 실행 가능한 다음 수 + +`{why, cmd}` 객체가 **최대 3개.** `cmd`는 그대로 셸에 붙여 실행할 수 있다. + +`search-laws`의 `next`는 특히 유용하다 — 후보를 시행일로 갈라 **현행 / 미시행 / 과거**로 라벨을 붙이고, 현행 판본에는 날짜 없는 명령을, 나머지에는 각자의 `--as-of`를 붙여 준다. + +--- + +## 3. 종료코드 5종 + +CLI는 **오류도 stdout에 엔벨로프로** 찍는다(`ok: false`). stderr로 흘리지 않으므로 파싱 경로가 하나다. 종료코드는 그 위에 얹히는 분류다. + +| 코드 | 이름 | 의미 | 에이전트의 대응 | +|---|---|---|---| +| **0** | `EXIT_OK` | 성공. **0건 검색도 여기 포함**(`ok:true, count:0`) | 진행. 단 0건을 부재로 바꿔 말하지 마라 | +| **2** | `EXIT_AMBIGUOUS` | 복수의 신원이 걸림 | `flags.candidates`를 **사람에게 제시**하라. 고르지 마라 | +| **3** | `EXIT_SOURCE` | 출처를 읽을 수 없거나 해석할 수 없음 | **`kind`로 갈린다** — 아래 참고 | +| **4** | `EXIT_NO_RESULT` | 형식은 맞는데 본문이 없음. **잘못된 식별자 포함** | 재시도 무의미. 식별자를 `search-*`로 다시 확인하라 | +| **5** | `EXIT_USAGE` | 인자 오류, 또는 로더에 법령 이름을 넘김 | 인자를 고쳐라. `needs_search_first`면 먼저 검색하라 | + +코드 1은 쓰지 않는다. + +**종료코드 3의 두 얼굴이 가장 중요하다.** + +- `kind: "source_access_error"` — 일시적(429, 5xx, 타임아웃). **재시도가 옳다.** +- `kind: "parse_error"` — 응답 모양을 알아보지 못함. **재시도해도 소용없다.** 식별자를 먼저 의심하고 `search-*`로 재확인하라. + +같은 종료코드로 정반대 대응이 필요하므로, 코드만 보고 재시도 루프를 돌리면 안 된다. + +그리고 **잘못된 식별자는 3이 아니라 4다.** law.go.kr은 없는 식별자에 빈 본문으로 답하는데, 이걸 파싱 실패로 분류하면 영원히 성공할 수 없는 조회를 계속 재시도하게 된다. 그래서 `no_result`(4)로 보내 검색으로 유도한다. + +--- + +## 4. 컨텍스트 예산 + +실측값이다(2026-07-19, 라이브 law.go.kr, 엔벨로프 바이트). + +| 명령 | 크기 | +|---|---| +| `load-delegated-criteria --budget standard` | 513,570 | +| `load-institutional-system --budget standard` | 396,609 | +| `get-law` (조문 139개, `--article` 없이) | 276,748 | +| `get-case` (결정 1건) | 82,700 | +| `load-legal-context-bundle --budget broad` | 37,822 | +| `load-authority-context` | 6,889 | +| `get-article` | 2,280 | + +세 자릿수 배 차이가 난다. 그래서 좁히는 장치가 셋 있다. + +### `--toc` — 지도 먼저 + +```bash +moleg get-law --law 001248 --toc +``` + +장 제목과 조문 목록(번호·제목·삭제/이동 상태)만 문서 순서대로 준다. **본문은 없다.** 개인정보 보호법 기준 276KB → 19KB. + +전형적 패턴: `--toc`로 지도를 보고 → 필요한 조문만 `get-article`로 집는다. + +`LawToc`는 빈 필드를 아예 생략한다(`_omit_when_empty`). 보통은 "값이 없음"도 정보지만, 139줄짜리 목차에서는 그 자리표시자가 페이로드의 대부분을 차지하기 때문이다. + +### `--brief` — 요지 먼저 + +`get-interpretation`, `get-case`, `get-constitutional-decision`, `get-committee-decision`, `get-administrative-appeal` 다섯 명령에서 쓴다. + +`text`·`full_text`·`reasoning`을 덜어내고 **`summary`(요지)는 남긴다.** 요지를 남기는 게 이 모드의 전부다. 어떤 결정문에서는 뺀 부분이 페이로드의 82%였다(35,866자 중 29,321자). 이 결정이 논점에 걸리는지 확인하는 데 82KB를 쓰는 것은 답보다 비싸다. + +두 가지 설계 디테일이 실용적으로 중요하다. + +**타입이 바뀌지 않는다.** 필드를 지울 때 데이터클래스에 선언된 **기본값**으로 되돌린다. `text: str = ""`와 `reasoning: str | None = None`은 부재의 의미가 다르므로, nullable을 `""`로 지우면 "안 실었음"이 "실었는데 비었음"으로 바뀐다. + +**무엇을 뺐는지 지우기 *전에* 기록한다.** `flags.brief.withheld`에 필드 이름이 남는다. 그래서 "brief라서 빈 것"과 "출처에 원래 없던 것"이 구분된다. 뺄 게 없었으면 그 사실도 명시된다. + +### `flags.large_payload` — 크기 신호 + +임계값 **20,000자**. 넘으면 붙고, 명령별로 어떻게 좁힐지 조언이 `discipline`에 함께 붙는다. + +바이트가 아니라 **문자**로 재는 것이 의도적이다. 문자 수가 토큰 비용을 근사하는데, 한글은 UTF-8에서 3바이트라 바이트로 재면 같은 분량의 읽을거리가 3배로 커 보인다. 20,000자는 대략 10~13k 토큰이다. + +그리고 **모든 좁히기 옵션이 적용된 뒤의 실제 출력**을 잰다. 행 개수로 추정하지 않는다 — "짧은 조문 139개"와 "긴 조문 20개"는 자릿수가 다르고, 실제 출력을 재는 검사는 나중에 명령이 추가돼도 계속 동작한다. + +검색 계열(`search-`, `resolve-`, `expand-`, `find-comparable`)은 **면제**다. 후보를 넓게 주는 것이 그 명령의 목적이고, 20건이면 18,000자쯤이라 임계에 가까워서 켜 두면 의도대로 동작한 유일한 명령에서 경고가 울린다. + +--- + +## 5. 의도 게이트 — `budget`이 전부가 아니다 + +번들 로더에서 **질의 내용이 무엇을 실을지 결정한다.** 모든 상세 로드 한도는 0에서 시작하고, 질의에서 키워드가 잡혀야 올라간다. + +| 의도 | 트리거 키워드 | 열리는 것 | +|---|---|---| +| `legal_meaning` | 의미, 뜻, 해석, 법령해석, 어떻게 보아야 | 해석 + 판례 + 헌재 | +| `application` | 적용, 요건, 조건, 판례, 사례, 분쟁, 책임 | 해석 + 판례 | +| `constitutional` | 위헌, 헌법, 기본권, 평등, 과잉금지원칙, 비례, 표현의 자유, 적법, 정당성 | 헌재 | + +그래서 `"제3조가 뭔가요"`는 판례를 싣지 않고, `"위헌 소지가 있나"`는 헌재 결정을 싣는다. **`--budget broad`를 줘도 질의에 해당 의도가 없으면 안 실린다.** + +이걸 모르면 "broad로 불렀는데 판례가 없다"를 커버리지 부족으로 오해하기 쉽다. 헌재 결정이 필요하면 질의에 그 의도를 담거나, `search_constitutional_decisions`를 직접 불러라. + +적용 대상 모드는 `question`과 `statute_review`다. + +--- + +## 6. 후속 조회 파이프 + +응답에 실려 온 `deferred` 항목은 그대로 다시 넣으면 실행된다. + +```bash +moleg load-legal-context-bundle --query "개인정보 유출 통지 의무" \ + | jq '.data.deferred[0]' \ + | moleg load-followup --json - +``` + +Python: + +```python +result = api.load_followup(bundle.deferred[0]) +``` + +**손으로 쓴 JSON은 거부된다.** `interface` 값이 알려진 26개 인터페이스 목록에 없으면 사용 오류(종료코드 5)가 난다. 리스트를 통째로 넘겨도 거부된다 — 하나씩 넣어야 한다. + +`interface`가 `websearch`나 `congress-db`인 항목은 `UnsupportedFormatError`를 낸다. 이건 버그가 아니라 **경계 표시**다. 그 사실은 다른 시스템 소관이라는 뜻이고, 에이전트라면 여기서 다른 도구로 넘어가야 한다. + +`load-followup`의 `kind`는 명령이 아니라 **결과 타입**에서 나온다. 그래서 후속 조회로 도착한 결과도 올바른 `kind`를 단다. + +--- + +## 7. 답을 쓰기 전 점검표 + +1. **인용하려는 것이 `_text`/`_context`에서 왔는가?** `_hit`/`_planning`이면 아직 본문을 안 실었다. +2. **`discipline`을 읽었는가?** 거기 적힌 제한을 답에 반영했는가. +3. **0건을 부재로 바꿔 말하지 않았는가?** 무엇을 어떤 범위로 검색했는지 함께 밝혔는가. +4. **권위 유형이 살아 있는가?** 부처 해석을 법제처 해석처럼, 위원회 의결을 판례처럼 쓰지 않았는가. +5. **시점이 맞는가?** `not_effective_as_of`나 `version_mismatch`가 붙지 않았는가. 실린 `effective_date`를 확인했는가. +6. **모호성을 대신 해결하지 않았는가?** 종료코드 2에서 `[0]`을 집지 않았는가. + +## 다음 + +- [CLI Reference](CLI-Reference.md) — 명령과 옵션 전수 +- [Error Handling](Error-Handling.md) — 예외 계층과 재시도 정책 +- [Gotchas](Gotchas.md) — 조용히 틀리는 지점 모음 +- [Core Concepts](Core-Concepts.md) — 이 신호들이 왜 이런 모양인지 diff --git a/docs/wiki/Architecture.md b/docs/wiki/Architecture.md new file mode 100644 index 0000000..fc1d8d8 --- /dev/null +++ b/docs/wiki/Architecture.md @@ -0,0 +1,153 @@ +# 내부 구조 + +코드를 고치려는 사람을 위한 페이지다. 쓰기만 할 거라면 [Core Concepts](Core-Concepts.md)로 충분하다. + +## 전체 지도 + +``` +moleg_api/ +├── __init__.py 공개 표면 — 71개 이름 (MolegApi, LawGoKrClient, 예외 9종, 모델 59종) +├── _version.py 버전 리터럴 (단일 진실 공급원) +├── laws.py 파사드 → _laws +├── models.py 파사드 → _models + 직렬화 메서드 설치 +├── normalization.py 파사드 → _normalization +├── cli.py 파사드 → _cli +├── errors.py 예외 계층 +├── source.py HTTP 전송 + MolegSource 프로토콜 +│ +├── _laws/ 9,297줄 · 66파일 과업 로직 +├── _normalization/ 2,240줄 · 16파일 law.go.kr 응답 정규화 +├── _cli/ 1,491줄 · 13파일 파서·디스패처·신호·엔벨로프 +└── _models/ 998줄 · 11파일 공개 데이터클래스 + 직렬화 +``` + +루트의 `laws.py`·`models.py`·`normalization.py`·`cli.py`는 **의도적으로 얇은 호환 계층**이다. 0.2.3에서 큰 파일들을 쪼갤 때 기존 임포트가 깨지지 않도록 남겼다. + +## `_laws/` — 4계층 + +과업 로직이 사는 곳이고 가장 큰 패키지다. 네 층으로 나뉜다. + +| 층 | 파일 | 역할 | +|---|---|---| +| **1. foundation** | `foundation.py` | 순수 재수출 허브. `..errors`·`..models`·`..normalization`·`..source`를 한 네임스페이스로 모아, 다른 모든 모듈이 `from .foundation import *` 한 줄로 전체 어휘를 본다. 로직 없음 | +| **2. config** | `config.py` | 상수와 정책 테이블. `foundation`만 임포트한다 | +| **3. support** | 약 40개 모듈 | 자유 함수와 상태 데이터클래스. `support.py`가 이들을 의존 순서대로 모으는 단일 집결점 | +| **4. api_\*** | 22개 믹스인 | 공개 메서드. `from .support import *` + 믹스인 클래스 하나 | + +`api.py`는 50줄이고 본문이 `pass`다 — **22개 믹스인을 합성하기만 한다.** + +```python +class MolegApi(FollowupMixin, LawSearchMixin, LawLoadersMixin, ...): + pass +``` + +생성자는 MRO 첫 믹스인인 `FollowupMixin`에 있다. + +```python +def __init__(self, source: MolegSource | None = None) -> None: + self.source = source or LawGoKrClient() +``` + +모든 믹스인은 `self.source.search / search_html / service / post_text`로 전선에 닿는다. + +> **주의 — 후행 별표 임포트.** 일부 support 모듈은 파일 **맨 아래에서** 형제 모듈을 별표 임포트한다. 남은 찌꺼기가 아니라 **의도된 장치**다 — 임포트 순환이 있어도 네임스페이스를 평평하게 유지한다. 모듈을 추가하면 `support.py`에 등록하고, 그 모듈을 필요로 하는 후행 블록에도 넣어야 한다. + +### support 모듈 군집 + +| 군집 | 모듈 | 책임 | +|---|---|---| +| 기반·검증 | `foundation`, `config`, `support`, `validation` | 네임스페이스, 엔드포인트·예산 테이블, 열거값 검증 | +| 신원·파라미터 | `identity_params`, `history_identity`, `authority_sources` | 문자열/히트/신원 → `LawIdentity`, 신원 → DRF 파라미터 | +| gap (연성 실패) | `article_gaps`, `requested_load_gaps`, `context_load_gaps`, `temporal_gaps`, `authority_article_gaps`, `authority_temporal_gaps`, `authority_temporal_filters` | 복구 가능한 실패를 예외 대신 `ContextGap` + `DeferredLookup`으로 | +| 스코프 매칭 | `delegated_scope`, `admin_scope`, `source_matching` | 하위규범·별표가 실제로 대상 조문을 인용하는지 판정 | +| 별표 파싱 | `annex_tables` | 헤더 재도출, 파이프 표·공백 정렬 표 파서 → `StructuredTableData` | +| 후보·랭킹 | `candidates`, `ranking`, `limits_intents` | 행 → 후보 모델, 중복 제거·점수·정렬, 예산→한도, 질의 의도 탐지 | +| 번들 파이프라인 | `bundle_state`, `bundle_modes`, `bundle_primary`, `bundle_candidates`, `bundle_eager`, `bundle_finalize` | 5단계 + 가변 `BundleState` | +| 권위 파이프라인 | `authority_context_pipeline`, `authority_context_details` | 대상 조문 로드 → 후보 검색 → 상세 로드 → 현행 권위 필터 | +| 제도 파이프라인 | `institutional_pipeline`, `institutional_candidates`, `institutional_resolution` | 명시된 법령 집합 순회 | +| 위임기준 파이프라인 | `delegated_criteria_pipeline`, `delegated_criteria_details` | 번들에서 후보 갱신 후 본문 로드 | +| 후속 조회 | `followup_basic`, `followup_identities`, `followup_hits`, `followup_searches`, `followup_routing`, `followup_routing_authority`, `followup_routing_bundle` | 후속 조회 생성·강제 변환·라우팅 | +| 연성 호출 | `bridge` | `safe_list(...)` — "부르고, 실패하면 gap과 deferred를 붙인다"의 범용 래퍼 | +| 레지스트리 | `adjudication_registry` | 위원회·심판기관 코드 표와 권위 문장 | + +### 설계 패턴 하나 — 연성 실패 + +`bridge.safe_list(...)`가 이 패키지의 성격을 가장 잘 보여준다. 번들 로더에서 개별 출처 호출이 실패해도 **예외를 올리지 않는다.** `gaps`에 무엇이 안 메워졌는지, `deferred`에 다시 시도할 방법을 남기고 계속 간다. + +이유: 열 개 출처를 훑는 1차 통과에서 하나가 실패했다고 아홉 개를 버리는 것은 손해이고, 무엇이 빠졌는지 호출자가 알 수만 있으면 나머지는 여전히 쓸모 있기 때문이다. + +## `_normalization/` — 무엇을 푸는가 + +law.go.kr OpenAPI는 서로 거의 아무것도 합의하지 않은 엔드포인트 가족이다. 정규화 계층은 그 전부를 frozen 데이터클래스로 평탄화하는 **단일 지점**이다. + +실제로 처리하는 난맥상들: + +**같은 개념, 다른 이름.** `first_value(row, *keys)`가 존재하는 키 중 첫 번째를 고른다. 별칭 집합이 크다 — 행정규칙의 위임 법령 ID 하나에 **20가지 철자**(위임법령ID / 근거법령ID / 수권법령ID / 상위법령ID / 모법령ID / 법적근거법령ID × 내부 공백 변형)가 있고, 위임 조문 가지 키는 24가지다. + +**봉투 대소문자.** law.go.kr은 행 요소 키를 **요청한 target이 아니라 자기 대소문자**로 짓는다 — 판례는 `prec`(소문자)인데 헌재는 `Detc`(대문자)다. 대소문자 무시로 매칭한다. + +**모양으로 매칭해야 하는 경우.** 4개 특별 심판기관은 `acrSpecialDecc`를 요청해도 `decc`로 키를 지은 행을 준다. 요청 target으로 키를 잡으면 **아무것도 안 나오고 "이 기관은 기록이 없다"로 읽힌다.** 그래서 이름이 아니라 모양으로 찾는다. + +**스칼라 vs 리스트.** 같은 필드가 어떤 때는 문자열, 어떤 때는 배열, 어떤 때는 단일 키 객체다. 장과 절 제목을 동시에 가진 행은 중첩 배열로 오는데, 여기 `str()`을 걸면 파이썬 repr(`[['제3장 …', '제1절 …']]`)이 나오고 그게 법령의 문언인 것처럼 실려 나간다. + +**조문 표기 4종.** 이미 포맷된 `제15조의2`, 6자리 `001502`, `15조의2`, 맨 정수 `15`를 전부 받는다. + +**날짜.** `compact_date`가 점 표기와 8자리를 처리하지만, 위원회는 `2020.6.8.`처럼 끝점이 붙고 0 패딩이 없는 형태를 내보내 공통 함수를 그냥 통과한다. 정규화 안 된 날짜는 정렬과 비교가 틀리고, **감독 질문에서는 날짜가 곧 발견**이다. 그래서 위원회 전용 날짜 처리가 따로 있다. + +**문자열 `"null"`.** 몇몇 출처가 네 글자 문자열 `"null"`을 보낸다. 그대로 두면 진짜 사건번호나 제목처럼 답에 실린다. + +**HTML만 주는 엔드포인트.** 연혁(`lsHistory`)은 HTML만 준다. 표준 `HTMLParser`로 파싱하며 **정확히 9열**을 요구한다. 1열 행은 "결과 없음" 안내 페이지로 취급하고, 그 외 폭은 `ParseFailureError`다. + +**순번을 조문번호로 착각하는 함정.** 구조문목록/신조문목록에서 `no`는 **순차 행 인덱스(1,2,3…)**다. 병렬 조인 키로는 유효하지만 조문 번호가 아니다. 이걸 `제N조`로 내보내면 **인용을 날조하게 된다.** 그래서 표시 라벨은 본문의 `제N조` 헤더에서 뽑고, 헤더가 없는 연속 행(항·호 조각)에는 앞의 것을 이어 붙인다. + +**헌재 처분 복구.** 헌재 상세에는 판결유형 키가 없어 `decision_type`이 항상 비어 있다. 【주문】 구간을 잘라 헌법불합치·한정위헌·한정합헌·합헌·위헌·각하·기각·인용을 **구체적인 것부터** 매칭한다. 이유 부분은 반대의견이 같은 표현을 반복하므로 **주문 구간만** 훑는다. + +**빈 페이로드의 분류.** 이 판단이 종료코드를 가른다. `is_empty_payload`는 의도적으로 보수적이다 — 공백류 스칼라와 빈 컨테이너만 빈 것으로 친다. 우리가 단지 *알아보지 못한* 페이로드는 "레코드 없음"으로 오분류되지 않고 `ParseFailureError`로 간다. + +## `_cli/` — 파서에서 엔벨로프까지 + +의존 순서대로 별표 임포트한다: `foundation → constants → data → signals_meta → signal_helpers → signals → parser → dispatch → catalog → main`. + +핵심은 **`signals_for(command, result, args)`**다. 결과를 보고 `kind`·`source`·`flags`·`discipline`·`next`를 도출한다. + +`kind`는 **명령 이름이 아니라 결과 데이터클래스 타입**에서 나온다. 그래서 `load-followup`으로 도착한 결과도 올바른 `kind`를 단다. `AdjudicationHit`은 `source_type`에 `appeal`이 있느냐로, `JudicialDecisionHit`은 `detc`가 있느냐로 갈린다. + +`catalog`는 `MolegApi()`를 만들기 **전에** 단락(short-circuit)한다. 그래서 자격증명도 네트워크도 필요 없다. + +## `MolegSource` 프로토콜 — 테스트 주입 지점 + +```python +class MolegSource(Protocol): + def search(self, target, params) -> dict: ... + def search_html(self, target, params) -> str: ... + def service(self, target, params) -> dict: ... + def post_text(self, path, params) -> str: ... +``` + +`MolegApi(source=...)`가 이 프로토콜을 구현하는 무엇이든 받는다. 테스트는 `FakeSource`(FIFO 큐로 페이로드를 돌려주고 호출을 기록)를 넘긴다. **`_laws/` 전체가 네트워크 없이 테스트된다.** + +`LawGoKrClient`는 표준 라이브러리 `urllib`만 쓴다. 세션도 커넥션 풀도 없이 시도마다 새 `urlopen`이다. 재시도 정책과 SSL 처리는 [Error Handling](Error-Handling.md)과 [Installation](Installation.md)에 있다. + +## `_models/` — frozen 데이터클래스와 직렬화 + +모든 공개 모델이 `@dataclass(frozen=True)`다. + +`models.py`가 `install_serialization_methods(globals(), public_module=__name__)`를 부른다. 이게 네임스페이스를 훑으며 모든 데이터클래스 **타입**에 대해 `__module__`을 `"moleg_api.models"`로 고쳐 쓰고 `to_dict`·`to_json_string`을 붙인다. + +직렬화 규칙과 그 근거는 [API Reference](API-Reference.md)의 직렬화 계약 절에 있다. 여기서는 덜 뻔한 것 하나만 — **비문자열 딕셔너리 키 처리**. `{1: "a", "1": "b"}`는 `str()`을 거치면 충돌한다. 충돌한 키를 `type:repr` 형태로 승격하고, 승격된 키가 다시 기존 문자열 키와 충돌하면 또 승격하는 루프를 돈다. 손실 없이, 결정적으로 직렬화된다. + +## 알려진 중복 + +문서화해 둘 가치가 있는 두 가지다. 둘 다 동작에는 영향이 없지만 코드를 고칠 때 헷갈린다. + +**`_models`의 follow-up 원시 타입.** `bundles.py`가 `.followups`에서 `Ambiguity`·`ContextGap`·`DeferredLookup`을 임포트한 **뒤 셋을 다시 정의한다.** `query.py`도 `FollowUpSearch`를 임포트한 뒤 재정의한다. 런타임에 서로 다른 클래스다. + +`_models/__init__.py`의 임포트 순서 덕에 **`followups` 쪽이 공개 네임스페이스를 이긴다.** 그림자 사본은 `to_dict`를 못 받으므로, 그걸 직접 만들면 구조는 맞는데 `isinstance`가 안 맞고 직렬화도 안 된다. 필드 목록이 같아서 부모의 `to_dict`는 여전히 동작한다(타입이 아니라 `is_dataclass`로 훑기 때문). + +**`_normalization`의 헬퍼.** `compact_whitespace`·`yes_no_or_none`·`is_deleted_article`·`article_text_marks_deleted`가 `primitives.py`와 `article_units.py`에 **바이트 단위로 동일하게** 두 번 정의돼 있다. 별표 임포트 순서상 `primitives` 쪽이 이긴다. + +## 관련 문서 + +- [Maintainer Notes](Maintainer-Notes.md) — 파일 크기 가드레일, 릴리스 전 점검 +- [CONTRIBUTING.md](../../CONTRIBUTING.md) — 개발 환경, 테스트, PR 절차 +- [API Reference](API-Reference.md) — 공개 표면 diff --git a/docs/wiki/CLI-Reference.md b/docs/wiki/CLI-Reference.md index 4711d0e..33e0b27 100644 --- a/docs/wiki/CLI-Reference.md +++ b/docs/wiki/CLI-Reference.md @@ -1,31 +1,117 @@ -# CLI Reference +# CLI 레퍼런스 -`moleg_api` ships a shell CLI so you can load [law.go.kr](https://www.law.go.kr/) legal sources without writing Python. Every task-level `MolegApi` method is exposed as a subcommand, and every call prints exactly **one JSON envelope** to stdout. The shell can branch on the outcome using the process **exit code** without parsing that JSON. +모든 공개 메서드가 `moleg` 서브커맨드로 노출된다. 모든 호출은 stdout에 **JSON 엔벨로프 하나**를 찍고, 셸은 그 JSON을 파싱하지 않고도 **종료코드**로 분기할 수 있다. -No configuration is required — the package ships a shared default OC credential, so calls work out of the box (see the [Installation](Installation.md) page for using your own OC). +엔벨로프 신호를 에이전트가 어떻게 읽어야 하는지는 [Agent Integration](Agent-Integration.md)에 있다. 이 페이지는 명령과 옵션의 전수 목록이다. -## Invocation - -Two entry points are equivalent: +## 실행 ```bash -python -m moleg_api [flags] # module form — always available after install -moleg [flags] # console-script alias (declared in pyproject.toml) +moleg [flags] # 콘솔 스크립트 +python -m moleg_api … # 모듈 형태 (체크아웃에서 편리) ``` -Every command accepts a global `--raw` flag (placed before the subcommand) that includes the raw law.go.kr payloads inside `data`, for debugging: +**전역 플래그** — 서브커맨드 *앞*에 온다. -```bash -moleg --raw get-article --law 001248 제3조 -``` +| 플래그 | 효과 | +|---|---| +| `--raw` | law.go.kr 원본 페이로드를 `data`에 포함. 디버깅용 | +| `--version` | `moleg-api 0.3.0`을 찍고 종료. 엔벨로프의 `version`과 같은 값 | +| `-h`, `--help` | 도움말 | + +명령별 옵션은 `moleg --help`로 볼 수 있다. + +--- + +## 명령 목록 + +32개 과업 명령 + `catalog`. 버킷 구분은 `catalog`의 분류와 같다. + +### 메타 + +| 명령 | 설명 | +|---|---| +| `catalog` | 명령 목록·규약·라우팅 규칙·`kind` 명세. **네트워크도 자격증명도 쓰지 않는다** | + +### 검색·계획 — 후보를 준다 (인용 불가) + +| 명령 | 인자 | 주요 옵션 | `kind` | +|---|---|---|---| +| `search-laws` | `query` | `--as-of` `--basis {effective,promulgated}` `--law-type` `--ministry` `--display`(20) | `law_hit_list` | +| `resolve-promulgated-law` | — | `--prom-law-nm` `--prom-no` `--promulgation-dt` | `law_identity` | +| `search-administrative-rules` | `query` | `--ministry` `--rule-type` `--issued-on` `--include-history` `--display`(20) | `admin_rule_hit_list` | +| `search-annex-forms` | `query` | `--source {law,administrative_rule}` `--search-scope {title,source,body}` `--annex-type` `--ministry` `--display`(20) | `annex_form_hit_list` | +| `search-interpretations` | `query` | `--source {moleg,ministry,all,all_ministries}` `--ministry` `--search-body` `--interpreted-on` `--display`(20) | `interpretation_hit_list` | +| `search-cases` | `query` | `--court {all,supreme,lower}` `--court-name` `--search-body` `--decided-on` `--case-number` `--display`(20) | `case_hit_list` | +| `search-constitutional-decisions` | `query` | `--search-body` `--decided-on` `--case-number` `--display`(20) | `constitutional_hit_list` | +| `search-committee-decisions` | `query`(선택) | `--committee` **(필수)** `--display`(20) | `committee_decision_hit_list` | +| `search-administrative-appeals` | `query`(선택) | `--tribunal`(기본 `decc`) `--display`(20) | `administrative_appeal_hit_list` | +| `expand-legal-query` | `query` | `--display`(5) `--no-websearch-hint` | `query_expansion_planning` | +| `find-comparable-mechanisms` | `concept` | `--display`(5) | `comparable_planning_list` | + +**`--committee` 코드 12종** +`ppc` 개인정보보호위 · `ftc` 공정위 · `fsc` 금융위 · `sfc` 증선위 · `kcc` 방통위 · `nhrck` 인권위 · `acr` 권익위 · `nlrc` 노동위 · `eiac` 고용보험심사위 · `iaciac` 산재재심사위 · `oclt` 중앙토지수용위 · `ecc` 중앙환경분쟁조정위 + +**`--tribunal` 코드 5종** +`decc` 일반 행정심판위(기본) · `acr` 권익위 특별 · `adap` 소청심사위 · `tt` 조세심판원 · `kmst` 해양안전심판원 -`moleg --version` prints the version of the code that is running and exits — the same value every envelope carries in its `version` field. `moleg --help` lists every task subcommand plus `catalog`. +> 소청·조세·해양안전 사안을 `decc`에서만 찾으면 조용히 0건이 나온다. 별도 심판기관 소관이기 때문이다. 0건일 때 `discipline`이 다른 `--tribunal`을 확인하라고 알려 준다. -Per-command flags are documented on `moleg --help`. The full command list, standing conventions, and routing rules live in `moleg catalog` (see [Start with `catalog`](#start-with-catalog)). +### 본문 로드 — 인용 가능 -## The envelope +| 명령 | 인자 | 주요 옵션 | `kind` | +|---|---|---|---| +| `get-law` | — | `--law`**(필수)** `--as-of` `--basis` `--article`(반복) `--no-metadata` `--toc` | `law_text` / `--toc`면 `law_toc_map` | +| `get-article` | `article` | `--law`**(필수)** `--as-of` `--basis` | `article_text` | +| `load-article-context` | `article` | `--law`**(필수)** `--as-of` `--basis` `--no-follow-moved` | `article_context` | +| `get-administrative-rule` | — | `--id`**(필수)** `--article`(반복) `--no-metadata` | `admin_rule_text` | +| `load-administrative-rule-context` | — | `--id`**(필수)** `--article` `--no-metadata` `--no-follow-moved` | `admin_rule_context` | +| `get-annex-form-body` | — | `--id`/`--annex-id`**(필수)** `--source` `--title` `--no-metadata` `--no-structuring` | `annex_form_text` | +| `get-interpretation` | — | `--id`**(필수)** `--source` `--ministry` `--no-metadata` `--brief` | `interpretation_text` | +| `get-case` | — | `--id`**(필수)** `--no-metadata` `--brief` | `case_text` | +| `get-constitutional-decision` | — | `--id`**(필수)** `--no-metadata` `--brief` | `constitutional_text` | +| `get-committee-decision` | — | `--id`**(필수)** `--committee`**(필수)** `--brief` | `committee_decision_text` | +| `get-administrative-appeal` | — | `--id`**(필수)** `--tribunal` `--brief` | `administrative_appeal_text` | -Every successful command prints a JSON object with this shape: +> `get-interpretation`의 `--source` 기본값은 **`None`**이다(검색 쪽 기본값 `moleg`와 다르다). 부처 해석 본문을 실으려면 `--source ministry --ministry <기관>`이 필요하다 — `--id`만으로는 안 실린다. + +### 연혁·체계·위임 + +| 명령 | 주요 옵션 | `kind` | +|---|---|---| +| `trace-law-history` | `--law`**(필수)** `--article` `--date-from` `--date-to` | `law_history` | +| `get-revision-reason` | `--law`**(필수)** `--mst` `--as-of` | `revision_reason_text` | +| `compare-law-versions` | `--law`**(필수)** `--article` | `law_diff` | +| `find-delegated-rules` | `--law`**(필수)** `--article` | `delegation_graph` | +| `get-law-structure` | `--law`**(필수)** `--depth`(0) | `law_structure_hierarchy_only` | + +**개정 관련 3분기** — 자주 헷갈리는 지점이다. + +- **왜** 고쳤나 → `get-revision-reason` (「개정이유 및 주요내용」) +- **무엇이** 바뀌었나 → `compare-law-versions` (조문 전후 대비) +- **어떤 개정들이** 있었나 → `trace-law-history` (연혁 목록) + +연혁 이벤트의 `identity.mst`를 `get-revision-reason --mst`에 넣으면 그 개정으로 정확히 내려간다. + +`--date-from`과 `--date-to`는 **둘 다** 줘야 범위 필터가 적용된다. + +### 권위·묶음 + +| 명령 | 주요 옵션 | `kind` | +|---|---|---| +| `load-authority-context` | `--law`**(필수)** `--article`**(필수, 반복)** `--query` `--budget` `--as-of` | `authority_context` | +| `load-legal-context-bundle` | `--query` `--law` `--article` `--mode {question,promulgated_bill,statute_review}` `--budget` `--as-of` `--prom-law-nm` `--prom-no` `--promulgation-dt` | `legal_context_bundle` | +| `load-institutional-system` | `--statute`**(반복, 최소 1개)** `--article` `--budget` `--as-of` | `legal_context_bundle` | +| `load-delegated-criteria` | `--law`**(필수)** `--article` `--query` `--budget` `--as-of` | `legal_context_bundle` | +| `load-followup` | `--json`**(필수)** — deferred 객체 또는 `-`(stdin) | 결과 타입에서 파생 | + +`--budget`은 `minimal | standard | broad`, 기본 `standard`. + +`load-institutional-system`은 법령을 반복 지정한다: `--statute 001248 --statute 001250` + +--- + +## 엔벨로프 ```json { @@ -35,117 +121,111 @@ Every successful command prints a JSON object with this shape: "kind": "article_text", "source": "법제처 / 법령 조문", "count": 3, - "data": { "...": "serialized result dataclass (raw omitted unless --raw)" }, - "flags": { "basis": "effective", "effective_date": "20260102" }, - "discipline": ["정의·예외·요건은 text의 항·호·목 중첩에 있다 — ..."], - "next": [{ "why": "...", "cmd": "moleg ..." }] + "data": { }, + "flags": { }, + "discipline": [ ], + "next": [ ] } ``` -| field | meaning | -|-------|---------| -| `ok` | `true` for a completed call (**including a zero-hit search**), `false` for any error envelope. | -| `command` | the subcommand that ran (e.g. `"get-article"`); `null` on an argument-parsing failure. | -| `version` | the moleg-api version that produced this envelope. Present on **every** envelope — success, error, and `catalog` — because a consumer that cannot tell which version answered cannot tell whether a missing field means "not supported in this release" or "the call went wrong". It reports the version of the *code that ran*, which is not necessarily the installed distribution: a checkout on `sys.path` shadows site-packages. | -| `kind` | the typed result category. Its suffix carries the **candidate vs. loaded** distinction (see [The `kind` suffix convention](#the-kind-suffix-convention)). On errors this is an error kind like `needs_search_first` or `source_access_error`. | -| `source` | a human-readable Korean source label, e.g. `"법제처 / 법령검색"`, `"법제처 / 헌재결정 본문"`. Present on success envelopes. | -| `count` | present only on list-returning searches — the number of hits. `0` on a zero-hit search. | -| `data` | the serialized result. A search returns a JSON array; a loader returns a single object. Nested dataclasses serialize recursively; raw source payloads are omitted unless `--raw` is passed. | -| `flags` | structural signals pulled from the result's own fields — e.g. `is_deleted`, `moved_to`, `not_effective_as_of`, `has_supplementary`, `authority_gaps`, `parsing_confidence`, `ambiguous_versions`, `count`, `searched`. Present only when the result carries such a signal. | -| `discipline` | source-integrity notes that appear **only when the relevant trap is live in this result** (a deleted article, an authority/article mismatch, a future effective date). The frequent path stays quiet; the rare dangerous states speak up. | -| `next` | the highest-leverage follow-up commands, ready to run and capped at three. The full follow-up set stays in `data`; any overflow is counted in `flags.more_followups`. | +| 키 | 의미 | +|---|---| +| `ok` | 완료면 `true`(**0건 검색 포함**), 오류면 `false` | +| `command` | 실행된 서브커맨드. 인자 파싱 실패 시 `null` | +| `version` | 이 응답을 만든 **코드**의 버전. 모든 엔벨로프에 실린다 | +| `kind` | 결과의 의미 유형. 접미사가 인용 가능 여부를 가른다 | +| `source` | 사람이 읽는 출처 표기. 예: `법제처 / 헌재결정 본문` | +| `count` | 목록 결과에만. 0건이면 `0` | +| `data` | 직렬화된 결과. `--raw` 없으면 원본 페이로드 제외 | +| `flags` | 기계가 읽는 상태값. **비어 있으면 키 자체가 없다** | +| `discipline` | 해석 규율 문장 목록. 해당 함정이 이 결과에서 실제로 살아 있을 때만 나온다 | +| `next` | 실행 가능한 다음 명령. **최대 3개**. 넘치면 `flags.more_followups`에 개수 | -On error, the envelope carries `ok: false`, an error `kind`, and an `error` message string (plus `discipline` and, where useful, `next`). It does **not** carry `data`. +오류 엔벨로프는 `ok: false`, 오류 `kind`, `error` 메시지를 싣고 `data`는 없다. **오류도 stdout으로 나간다** — stderr가 아니다. -### The `kind` suffix convention +`version`이 항상 실리는 이유: 설치된 배포판이 아니라 **실제로 돈 코드**의 버전이어야 하기 때문이다. 체크아웃이 `sys.path`에 있으면 site-packages를 가리므로, 배포 메타데이터를 읽으면 "누가 답했나"에 답할 수 없다. -`kind` encodes structurally whether a result is a **search candidate** (not citable on its own) or **loaded source text** (citable). The suffix tells you which: +### `kind` 접미사 규약 -- `*_hit` / `*_candidate` / `*_planning` — **search candidates.** These are a menu of what to load next, not source text. A `law_hit`, an `admin_rule_hit`, an `annex_form_hit`, a `comparable_planning` item, or a `query_expansion_planning` result must not be cited for wording, thresholds, holdings, or legal equivalence. -- `*_text` / `*_context` / `*_identity` — **loaded source text.** A `law_text`, `article_text`, `article_context`, `constitutional_text`, `law_identity`, etc. is the actual retrieved source. +- `*_hit` / `*_candidate` / `*_planning` → **검색 후보.** 다음에 무엇을 실을지의 메뉴다. 문언·금액·판시·법적 동등성을 여기서 인용하면 안 된다. +- `*_text` / `*_context` / `*_identity` → **실린 본문.** 인용 가능. -`kind` is derived from the **result dataclass type**, not from the subcommand name. So a list reached through [`load-followup`](#load-followup-and-the-pipe-idiom) is typed by the interface that actually executed, not by the command you typed. The closed list of all `kind` values is printed by `moleg catalog` under `kinds`. +`kind`는 서브커맨드가 아니라 **결과 데이터클래스 타입**에서 파생된다. 그래서 `load-followup`으로 도착한 결과도 실제 실행된 인터페이스에 맞는 `kind`를 단다. -The practical rule (also stated in `catalog`'s conventions): **cite only from loaded bodies.** `search-*`, `expand-legal-query`, and `find-comparable-mechanisms` results are always candidates — load the selected item before citing anything. +--- -## Exit codes +## 종료코드 -The exit code lets a script branch on the outcome without reading the JSON: +| 코드 | 이름 | 의미 | +|---|---|---| +| `0` | 성공 | **0건 검색 포함** (`ok:true`, `count:0`) | +| `2` | 모호 | 복수 신원이 걸림. 후보를 제시하라, 고르지 마라 | +| `3` | 출처 | 읽기 실패 또는 해석 실패. **`kind`로 갈린다** | +| `4` | 결과 없음 | 본문 없음. **없는 식별자 포함**. 재시도 무의미 | +| `5` | 사용 오류 | 인자 오류, 또는 로더에 법령 이름을 넘김 | -| code | name | meaning | -|------|------|---------| -| `0` | success | the call completed — **including a zero-hit search** (`ok:true`, `count:0`). | -| `2` | ambiguous | multiple plausible identities matched; the envelope lists candidates. Surface them, do not silently pick the first. | -| `3` | source access | the source side went wrong. **Check `kind` before retrying** — `source_access_error` is transient and worth a retry; `parse_error` is an unrecognized response shape that a retry will reproduce exactly. Neither is proof of absence. | -| `4` | no result | a loader found no source text — including an identifier law.go.kr has no record of, which it signals with an empty body. Re-confirm the identifier with `search-*`; retrying will not help. | -| `5` | usage | a bad argument, an unsupported format/path, **or** a loader handed a law *name* instead of a law ID (search first). | +코드 1은 쓰지 않는다. argparse 기본 종료코드 2는 `EXIT_AMBIGUOUS`와 충돌하지 않도록 5로 재매핑돼 있다. -Two outcomes are deliberately kept distinct: a **zero-hit search** is a scoped `ok:true` result with exit `0` and `count:0`, whereas a **source-access failure** is `ok:false` with `kind:"source_access_error"` and exit `3`. Neither is proof that a source does not exist. +**종료코드 3의 두 얼굴** — `source_access_error`는 일시적이라 재시도가 옳고, `parse_error`는 응답 모양을 못 알아본 것이라 재시도해도 똑같이 실패한다. 후자면 식별자를 먼저 의심하라. -**A bad identifier is exit 4, not exit 3.** Before 0.3.0, `get-article --law 999999 제3조` exited `3` with discipline telling the caller to retry shortly — advice for a lookup that could never succeed, since law.go.kr answers an unknown identifier with an empty body rather than an error. That case now maps to `no_result` (exit `4`) and steers back to `search-*`. +**없는 식별자는 4다.** law.go.kr은 없는 식별자에 빈 본문으로 답한다. 이걸 파싱 실패(3)로 분류하면 영원히 성공할 수 없는 조회를 재시도하게 되므로, `no_result`(4)로 보내 검색으로 유도한다. -Error kinds map to exit codes as follows: +`kind` → 종료코드 대응: -| exit | `kind` values | -|------|---------------| +| 코드 | `kind` | +|---|---| | `2` | `ambiguous` | | `3` | `source_access_error`, `parse_error`, `error` | -| `4` | `no_result` | +| `4` | `no_result`, `version_request_unfulfilled` | | `5` | `needs_search_first`, `usage_error`, `unsupported` | -## Start with `catalog` - -`moleg catalog` is the self-documenting entry point. It prints, in one envelope, every subcommand grouped by intent, the standing conventions, the routing rules for choosing between similar commands, and the closed list of `kind` values. There is no introspection ritual to run first — read the catalog, then read a command's `--help` for its flags. +--- -```bash -moleg catalog -``` - -The `data` object has four sections: - -- **`convention`** — the standing rules that apply to every call (the `kind` suffix meaning, "cite only from loaded bodies," source-authority ordering, the exit-code legend, the search→load contract, the deferred-pipe idiom, and how `--as-of` resolves historical versions). -- **`routing_rules`** — how to choose between adjacent commands, e.g. `get-article` vs. `load-article-context`, `compare-law-versions` vs. `trace-law-history`, `get-law-structure` vs. `find-delegated-rules`, and which bundle loader fits which situation. -- **`commands`** — every subcommand grouped into four buckets: `검색·계획(후보)` (search/planning candidates), `본문 로드` (body loaders), `연혁·체계·위임` (history/structure/delegation), and `권위·묶음` (authority/bundles). -- **`kinds`** — the closed list of every `kind` value the CLI can emit, success and error. +## 실전 예시 -## The search → load contract - -Loaders (`get-law`, `get-article`, `load-article-context`, `trace-law-history`, `compare-law-versions`, `find-delegated-rules`, `get-law-structure`, and the bundle loaders) take a numeric `law_id` string in `--law`, **not** a law name. Passing a name returns `kind:"needs_search_first"` at exit `5`, with the exact `search-laws` command to run: +### 로더에 법령 이름을 넘겼을 때 ```bash $ moleg get-article --law "주택임대차보호법" 제3조 ``` ```json { - "ok": false, - "command": "get-article", - "kind": "needs_search_first", - "error": "Identifier '주택임대차보호법' looks like a law name, not a law ID. ...", + "ok": false, "kind": "needs_search_first", + "error": "Identifier '주택임대차보호법' looks like a law name, not a law ID. …", "discipline": ["로더에 법령명이 들어옴 — search-laws로 law_id를 먼저 얻어 --law에 넘겨라."], "next": [{ "why": "먼저 신원 검색", "cmd": "moleg search-laws \"주택임대차보호법\"" }] } ``` +종료코드 5. -So the flow is always search → select a `law_id` → load: +### 0건 검색 ```bash -moleg search-laws "주택임대차보호법" # → candidates carrying law_id + effective_date -moleg get-article --law 001248 제3조 # load the current version -moleg get-article --law 001248 --as-of 2021-01-01 제3조 # load the version in force on that date +$ moleg search-cases "존재하지않는판례검색어" --display 3 ``` +```json +{ + "ok": true, "kind": "case_hit_list", "count": 0, "data": [], + "flags": { "count": 0, "searched": { "query": "존재하지않는판례검색어", "court": "all" } }, + "discipline": ["0건 — 이 검색어·범위로 못 찾음일 뿐, 부재의 증명 아님."] +} +``` +종료코드 0. `flags.searched`가 **무엇을 어떤 범위로 검색했는지** 되돌려 준다 — "이 검색어로 못 찾음"과 "존재하지 않음"을 구분하기 위해서다. + +0건일 때 나오는 `next` 제안들: +- 판례·헌재 검색은 기본이 제목 검색이므로 `--search-body` 재시도를 제안 +- 별표 검색은 `--search-scope`를 `title` ↔ `source`로 뒤집어 볼 것을 제안 +- `--ministry` `--law-type` `--court-name` `--rule-type` 같은 필터를 걸었다면, 그걸 뺀 재시도를 제안 -A `search-laws` whose name resolves to several effective dates sets `flags.ambiguous_versions` and returns disambiguating `next` commands — the current text loads with a bare `--law`, and a past version loads with `--as-of `: +### 동명 법령이 여러 판본일 때 ```bash $ moleg search-laws "주택임대차보호법" --display 3 ``` ```json { - "ok": true, "command": "search-laws", "kind": "law_hit_list", - "source": "법제처 / 법령검색", "count": 3, - "data": [ { "identity": { "law_id": "001248", "effective_date": "20260102", "...": "..." } }, "..." ], + "ok": true, "kind": "law_hit_list", "count": 3, "flags": { "count": 3, "ambiguous_versions": true }, - "discipline": ["동명 후보가 시행일 다름 — 현행본은 그냥 get-law --law, 특정 시점 버전은 --as-of <시행일>로 로드."], "next": [ { "why": "후보 로드: 주택임대차보호법 (시행 20260102)", "cmd": "moleg get-law --law 001248" }, { "why": "후보 로드: 주택임대차보호법 (시행 20230719)", "cmd": "moleg get-law --law 001248 --as-of 20230719" } @@ -153,42 +233,40 @@ $ moleg search-laws "주택임대차보호법" --display 3 } ``` -The complementary error case — a zero-hit search — is `ok:true` at exit `0` with `count:0`, and records what was searched so you can distinguish "not found by this query" from "does not exist": +`next`가 후보를 **현행 / 미시행 / 과거**로 갈라 라벨을 붙여 준다. 현행 판본에는 날짜가 없고, 나머지에는 각자의 시행일이 `--as-of`로 붙는다. -```bash -$ moleg search-cases "존재하지않는판례검색어" --display 3 -``` -```json -{ - "ok": true, "command": "search-cases", "kind": "case_hit_list", - "source": "법제처 / 판례 검색", "count": 0, "data": [], - "flags": { "count": 0, "searched": { "query": "존재하지않는판례검색어", "court": "all" } }, - "discipline": ["0건 — 이 검색어·범위로 못 찾음일 뿐, 부재의 증명 아님."] -} -``` +--- -## Budgeting context +## 컨텍스트 예산 -Three commands exist because the default load can be far larger than the question needs. Measured live on 2026-07-19: +기본 로드가 질문이 필요로 하는 것보다 훨씬 클 수 있다. 실측(2026-07-19): -| call | bytes | +| 호출 | 바이트 | |---|---| -| `get-law --law 011357` (139 articles) | 276,748 | +| `get-law --law 011357` (조문 139개) | 276,748 | | `get-law --law 011357 --toc` | 18,831 | | `get-case --id 193332` | 82,700 | | `get-case --id 193332 --brief` | 14,046 | -- **`get-law --toc`** returns the article map — chapter headings, article numbers and titles, deleted/moved status — with no article text. Its `kind` is `law_toc_map`, not `*_text`: **a table of contents is not the statute**, and an article title is not grounds to state what the article requires. Load the ones you picked with `--article` or `get-article`. -- **`--brief`** on `get-case`, `get-constitutional-decision`, `get-interpretation`, `get-committee-decision`, and `get-administrative-appeal` returns the 요지 and the structured extract without the full body. Verbatim quotation needs the full load; `flags.brief.withheld` lists exactly what was held back. -- **`flags.large_payload`** appears when a response is big enough that a narrowing option was probably available, with the character count and command-specific advice. It is measured on the serialized output, so it also covers commands added later. Searches are exempt — returning a breadth of candidates is what they are for. +- **`--toc`** — 조문 지도만. `kind`가 `law_toc_map`이지 `*_text`가 아니다. **목차는 법령이 아니고**, 조문 제목은 그 조문이 무엇을 요구하는지 말할 근거가 못 된다. +- **`--brief`** — 결정문 5종에서 요지만. 축자 인용에는 전체 로드가 필요하며, `flags.brief.withheld`가 무엇을 뺐는지 정확히 알려 준다. +- **`flags.large_payload`** — 20,000자를 넘고 좁힐 옵션이 있었을 때. 검색 계열은 면제다. -## Historical versions with `--as-of` +상세는 [Agent Integration](Agent-Integration.md)의 컨텍스트 예산 절. -`--as-of ` loads the statute or article text **in force on that date**, not today's. The loaders resolve the version whose 시행일 is the latest on or before the requested date. Always read the returned `flags.effective_date`; a `flags.version_request_unfulfilled` means no version was yet in force on the requested date and a later one was returned instead. For what a specific amendment *changed* (before/after wording), use `compare-law-versions` rather than diffing two separate loads. +--- -## `load-followup` and the pipe idiom +## `--as-of` -`load-legal-context-bundle` and `expand-legal-query` return follow-up records (`data.deferred[...]` and `data.follow_up_searches[...]`) describing what to load next. Execute one by **piping it back** into `load-followup` — do not hand-type the JSON, because Korean text and nested quotes make that error-prone: +`--as-of <날짜>`는 그 날짜에 **시행 중이던** 판본을 싣는다(시행일이 요청일 이하인 것 중 최신). `YYYY-MM-DD`와 `YYYYMMDD` 둘 다 받는다. + +날짜 형식이 틀리면 **조용히 현행으로 떨어지지 않고** 사용 오류(종료코드 5)가 난다. 달력 검증까지 한다 — 13월, 2월 30일, 99일은 거부된다. + +반드시 돌아온 `flags.effective_date`를 확인하라. 요청일에 시행 중이던 판본이 없으면 이후 판본이 실려 오고 `flags.version_mismatch`가 붙는다. 자세한 것은 [Historical Versions](Historical-Versions.md). + +--- + +## `load-followup` 파이프 ```bash moleg load-legal-context-bundle --query "온라인 플랫폼 허위광고 규율" \ @@ -196,11 +274,17 @@ moleg load-legal-context-bundle --query "온라인 플랫폼 허위광고 규율 | moleg load-followup --json - ``` -`load-followup` accepts either `--json ''` inline or `--json -` to read one object from stdin. The `interface` field in the object is validated against the known method set, so a search candidate cannot be smuggled in as if it were loaded text. Follow-ups whose interface points outside law.go.kr — `websearch` or `congress-db` handoffs — are recognized but belong to those other sources, not to this CLI. (The `next` field of most envelopes also contains ready-to-run `moleg load-followup --json ...` commands with the object already embedded, so you can often copy one directly instead of piping.) +`--json '<객체>'`로 인라인 전달도 되고 `--json -`로 stdin에서 읽어도 된다. **손으로 타이핑하지 마라** — 한국어 텍스트와 중첩 따옴표 때문에 오류가 나기 쉽고, `interface` 값이 알려진 목록에 없으면 거부된다. 리스트를 통째로 넘기는 것도 거부된다. + +대부분의 엔벨로프는 `next`에 객체가 이미 박힌 `moleg load-followup --json …` 명령을 직접 담아 주므로, 파이프 대신 그걸 복사해도 된다. + +`interface`가 `websearch`나 `congress-db`인 항목은 의도된 경계 표시다 — 그 사실은 다른 시스템 소관이다. + +--- -## Related pages +## 관련 문서 -- [Getting Started](Quickstart.md) — install, credentials, first calls. -- [Envelope and Discipline](CLI-Reference.md) — the envelope fields, `flags`, and `discipline` in depth. -- [Python API](API-Reference.md) — the `MolegApi` methods behind each subcommand. -- [Follow-up Lookups](Core-Concepts.md) — `DeferredLookup`, `FollowUpSearch`, and `load_followup`. +- [Agent Integration](Agent-Integration.md) — 신호를 읽는 법, `catalog` 계약, 컨텍스트 예산 +- [API Reference](API-Reference.md) — 각 서브커맨드 뒤의 Python 메서드 +- [Error Handling](Error-Handling.md) — 예외 계층과 재시도 정책 +- [Gotchas](Gotchas.md) — 조용히 틀리는 지점 diff --git a/docs/wiki/Core-Concepts.md b/docs/wiki/Core-Concepts.md index e1023a1..632eb9c 100644 --- a/docs/wiki/Core-Concepts.md +++ b/docs/wiki/Core-Concepts.md @@ -1,158 +1,120 @@ -# Core Concepts +# 핵심 개념 -`moleg-api` is a task-level loader for Korean legal sources from [law.go.kr](https://www.law.go.kr/). Its public interface is small and deep: you search or plan first, then load the selected legal text, authority detail, or delegated-rule context. A handful of concepts make the difference between a citation you can trust and a citation that quietly misleads. This page covers the six that matter most. +이 패키지의 공개 표면은 작고 깊다. 그 모양을 결정한 개념이 여섯 개 있고, 이것들이 신뢰할 수 있는 인용과 조용히 오도하는 인용을 가른다. -If you are looking for method signatures, see the [API Guide](API-Reference.md). If you are calling from a shell, see the [CLI](CLI-Reference.md). +메서드 시그니처는 [API Reference](API-Reference.md), 셸에서 쓸 거라면 [CLI Reference](CLI-Reference.md)를 보라. -## 1. The search → select → load discipline +## 1. 검색 → 선택 → 로드 -Every source family in this SDK separates *discovery* from *loading*, and the two are not interchangeable. +모든 출처 계열에서 *발견*과 *로딩*이 분리돼 있고, 둘은 대체 불가다. -- **Search / plan** methods (`search_laws`, `search_administrative_rules`, `search_interpretations`, `search_cases`, `search_constitutional_decisions`, `expand_legal_query`, `find_comparable_mechanisms`) return **candidate identity metadata** — enough to identify a source and decide whether to load it, but *not the source text*. -- **Load** methods (`get_law`, `get_article`, `get_administrative_rule`, `get_interpretation`, `get_case`, `get_constitutional_decision`, `get_annex_form_body`, and the `load_*_context` loaders) retrieve the actual normalized text of a source you selected. +**검색·계획 메서드** — `search_laws`, `search_administrative_rules`, `search_annex_forms`, `search_interpretations`, `search_cases`, `search_constitutional_decisions`, `search_committee_decisions`, `search_administrative_appeals`, `expand_legal_query`, `find_comparable_mechanisms`, `resolve_promulgated_law` — 는 **후보 식별 정보**를 준다. 출처를 특정하고 실을지 말지 판단하기에 충분한 정보이되, **본문은 아니다.** -**A search hit is a candidate, not a citation.** After only `search_laws()`, you have a law's identity, name, and dates — you do not have its article wording, duties, sanctions, or procedures. You must load the selected law or article before citing any of that. The same rule holds for every family: an interpretation, case, or Constitutional Court *search hit* carries title and date metadata only; the holding, reasoning, and reviewed statutes come from the corresponding `get_*` loader. +**로드 메서드** — `get_law`, `get_article`, `get_law_toc`, `get_administrative_rule`, `get_annex_form_body`, `get_interpretation`, `get_case`, `get_constitutional_decision`, `get_committee_decision`, `get_administrative_appeal`, 그리고 `load_*_context` 계열 — 은 실제 본문을 실어 온다. -Search results carry a `follow_up` field that spells out the next load step and pre-fills the identifiers, so you do not re-type keys by hand: +**검색 결과는 인용할 수 없다.** `search_laws()`만 부른 상태에서 당신이 가진 것은 법의 이름과 날짜와 식별자다. 그 법이 무엇을 의무로 지우고 어떤 제재를 두는지는 **모른다**. 다른 계열도 같다 — 해석·판례·헌재 검색 결과는 제목과 날짜 메타데이터일 뿐, 판시사항과 이유는 대응하는 로더에서 나온다. -```python -from moleg_api import MolegApi - -api = MolegApi() - -hits = api.search_laws("주택임대차보호법", basis="effective", display=5) -selected = hits[0].identity # a LawIdentity candidate — not citable text +`expand_legal_query()`와 `find_comparable_mechanisms()`는 그보다도 앞단이다. 이건 *계획* 도구이고, 그 결과는 다음에 무엇을 검색·로드할지의 메뉴이지 권위가 아니다. -law = api.get_law(selected, basis="effective") # now you have article text -print(law.articles[0].text) -``` - -```bash -python -m moleg_api search-laws "주택임대차보호법" # → candidates -python -m moleg_api get-law --law 001248 # load a chosen candidate -``` +**0건은 범위 한정 결과이지 부재의 증명이 아니다.** "이 검색어로, 이 출처 계열에서, 이 필터 조합으로 행이 없었다"는 뜻이다. 그런 법·규칙·판례·별표가 존재하지 않는다는 증명이 아니다. 없다고 말하기 전에 검색어를 넓히거나, 다른 출처 계열을 보거나, 상세 경로를 실어 봐야 한다. -`expand_legal_query()` and `find_comparable_mechanisms()` sit even earlier in the chain: they are *planning* tools. Their term, related-law, related-article, and comparable-mechanism suggestions are a menu of what to search or load next — never final authority. Load selected source text before citing anything from them. +## 2. 후보 vs 실린 본문 — 타입으로 강제된다 -**Empty results are scoped, not absolute.** A zero-hit search means "this exact query, on this basis/scope, returned no rows" — it does not prove that no such law, rule, precedent, or attached material exists. Widen the terms, try an alternate source family, or load a detail path before making any absence claim. +위 구분이 데이터 모델에 그대로 박혀 있어, 손에 든 것이 후보인지 본문인지 타입만 봐도 안다. -## 2. Candidate identity vs. loaded text - -The distinction above is reflected directly in the data model, so you can tell at a glance whether you are holding a candidate or real text. - -| You have… | Type | What it contains | Citable as source text? | +| 손에 든 것 | 타입 | 내용 | 인용 가능? | |---|---|---|---| -| A search hit | `LawHit`, `AdministrativeRuleHit`, `InterpretationHit`, `JudicialDecisionHit`, `AnnexFormHit` | A normalized `*Identity` plus the raw source row and a `follow_up` | No | -| A loaded body | `LawText`, `ArticleText`, `AdministrativeRuleText`, `InterpretationText`, `JudicialDecisionText`, `AnnexFormText` | The normalized text, articles, and structured fields | Yes | +| 검색 결과 | `LawHit`, `AdministrativeRuleHit`, `AnnexFormHit`, `InterpretationHit`, `JudicialDecisionHit`, `AdjudicationHit` | 정규화된 `*Identity` + 원본 행 + `follow_up` | **아니오** | +| 실린 본문 | `LawText`, `ArticleText`, `LawToc`, `AdministrativeRuleText`, `AnnexFormText`, `InterpretationText`, `JudicialDecisionText`, `AdjudicationText` | 정규화된 본문·조문·구조화 필드 | 예 | -A `LawHit` wraps a `LawIdentity` (the identity) plus the raw row; a `LawText` wraps that identity *and* the extracted articles and supplementary provisions. Reach for identity fields (name, dates, ministry, `law_id`, `mst`) from a hit; reach for wording, duties, and requirements only from a loaded body. +CLI에서는 `kind` 접미사가 같은 일을 한다 — `_hit_list`·`_candidate`·`_planning`이면 후보, `_text`·`_context`·`_identity`면 본문. -One caveat on loaded article text: definitions, exceptions, application targets, and requirements often live in nested 항 / 호 / 목 inside `ArticleText.text`, not in the article title (`조문제목`) or the top-level `조문내용`. Summarize from the full `text`, not from the title alone. +경계는 실행 시점에도 강제된다. 로더에 법령 **이름**을 넘기면 호출되지 않고 "먼저 `search_laws('…')`를 부르라"는 오류가 난다. 이름은 법을 유일하게 가리키지 못하고, 그 자리에서 아무거나 고르는 것이 이 패키지가 막으려는 실패이기 때문이다. -## 3. Effective (시행) vs. promulgated (공포) basis +**한 가지 주의**: 실린 조문에서도 정의·예외·적용 대상·요건은 조문 제목이나 최상위 `조문내용`이 아니라 **중첩된 항·호·목** 안에 사는 경우가 많다. `ArticleText.text`는 그 중첩을 펼쳐 담고 있으니, 제목만 보고 요약하지 마라. -Most load and search methods take a `basis` argument, typed as `Literal["effective", "promulgated"]`, defaulting to `"effective"`. It selects which legal reality you are asking about, and confusing the two produces correct-sounding but wrong answers. +## 3. 시행(effective) vs 공포(promulgated) -- **`basis="effective"` (시행일 기준)** — the text that is or was *in force*. This is the right basis for "what does current law say?", "is this in force now?", and "현재 시행" questions. It is the default. -- **`basis="promulgated"` (공포일 기준)** — lookup keyed by promulgation date and number. Use it for two situations: resolving a promulgation bridge from enacted-bill facts, and reconstructing historical promulgation context. +대부분의 메서드가 `basis` 인자를 받는다. `Literal["effective", "promulgated"]`, 기본값 `"effective"`. -**A promulgated law is not necessarily in force.** A statute can be promulgated (공포) with an effective date (시행일) still in the future. Resolving a promulgation bridge or loading promulgated text proves *identity and wording*, not *current force*. For current-force questions, pass an explicit reference date via `as_of=` and check whether the loaded effective date is later than your reference date before calling the law current. +- **`basis="effective"` (시행일 기준)** — 지금 또는 그때 **효력이 있던** 텍스트. "현행법이 뭐라고 하나"에 답하는 기준이며 기본값이다. +- **`basis="promulgated"` (공포일 기준)** — 공포일·공포번호로 키를 잡는다. 국회 의안의 공포 사실에서 법령 신원을 잇거나, 과거 공포 맥락을 재구성할 때 쓴다. -State the basis in your answer whenever the distinction matters — say whether a text was retrieved by effective-date or promulgation-date basis. +**공포된 법이 시행 중인 법은 아니다.** 공포는 됐는데 시행일이 미래인 상태가 흔하다. 공포 기준으로 텍스트를 실었다는 것은 *신원과 문언*을 확인한 것이지 *현재 효력*을 확인한 것이 아니다. -```python -# Current-force text (default basis) -current = api.get_law(selected, basis="effective") +이건 워낙 자주 틀리는 지점이라 신호로도 나온다 — 실린 판본의 시행일이 오늘보다 미래면 `flags.not_effective_as_of`가 붙고 `source` 표기 자체가 「공포본(장래 시행 — 아직 미시행)」으로 바뀐다. 이 플래그는 `--as-of`를 줬는지와 무관하게 뜬다. 미래 판본은 어느 경로로든 도착할 수 있기 때문이다. -# The version in force on a specific date -as_filed = api.get_article(selected, "제3조", basis="effective", as_of="2023-08-01") -``` +## 4. 식별자 — `ID`, `MST`, `LID`, `JO` -For history and repealed-law (구법 / 폐지법) questions, treat the task as loading a *historical* source: carry an as-of date and preserve the history/repeal status in your answer. A current-basis search returning nothing is not proof that the historical source never existed. +법령 하나는 시행일마다 하나씩, **판본의 계열**로 존재한다. 모든 판본이 같은 `law_id`를 공유하고, 각 판본은 자기만의 `mst`를 갖는다. -## 4. Versions and MST (same law, different effective dates) +| 필드 | 원래 이름 | 가리키는 것 | +|---|---|---| +| `law_id` | `ID` (법령ID) | **법 자체**. "이 법의 현행판" | +| `mst` | 법령일련번호 | **그 법의 특정 판본**. 시간 축의 손잡이 | +| `lid` | `LID` | 함께 보존되지만 파라미터 선택에는 쓰지 않는다 | -A single statute exists as a series of **versions**, one per effective date. All versions share the same `law_id` (법령ID), but each version has its own **`mst`** (법령일련번호, the master sequence number) that pins that specific version. +`법령ID + 시행일`로 상세를 조회하면 law.go.kr은 **오류 없이 현행 텍스트를 준다.** 판본을 고정하는 유일한 키가 `mst`다. 이게 `as_of`가 왜 존재하는지의 이유이며, [Historical Versions](Historical-Versions.md)에서 따로 다룬다. 다만 `mst`를 손으로 관리할 일은 없다 — 후보를 로더에 그대로 넘기면 판본이 함께 따라간다. -This is the key that makes historical and as-of loading work. law.go.kr's `ID + efYd` detail lookups do **not** select a past version — they silently return the current text even when you pass an old effective date. The `mst` is the only key that pins a version. The SDK handles this for you: when you pass an `as_of` that resolves to a non-current version, it lists the statute's version rows, finds the version in force at that date (latest 시행일 ≤ `as_of`), and reloads the correct version by `mst`. You do not manage `mst` by hand — but knowing it exists explains why `as_of` reliably returns historical text. +**`JO`**는 법제처의 여섯 자리 조문 코드다(`제15조의2` → `001502`, `제3조` → `000300`). **호출자는 이걸 볼 일이 없다.** 공개 메서드는 `"제15조의2"`, `"15조의2"`, 정수 `15`를 받아 내부에서 변환한다. -You can see the version fan-out directly in a search. `주택임대차보호법` returns several rows sharing `law_id` `001248`, each with a distinct `mst` and effective date: +원본 키들은 감사 목적으로 `identity.raw_keys`에 보존되지만, 코드가 딛고 설 것은 정규화된 필드다. -``` -law_id mst promulgation_date effective_date promulgation_number -001248 276291 20251001 20260102 21065 -001248 249999 20230418 20230719 19356 -``` - -Both `LawIdentity.law_id` and `LawIdentity.mst` are exposed on every identity. When you hand a `LawHit` or `LawIdentity` straight into a loader, its `mst` carries the exact version through — so a candidate you picked is the version you load. - -Do not treat `law_id`, `mst`, `lid`, and other raw keys as interchangeable. `law_id` names the *law*; `mst` names a *version of that law*. The raw source keys are preserved under `identity.raw_keys` for audit, but the normalized `law_id` / `mst` fields are what you build on. +## 5. 권위 유형은 평탄화되지 않는다 -## 5. Authority types are distinct (do not flatten them) +법제처는 여섯 종류의 서로 다른 권위를 노출한다. 이들은 무게가 다르고 서로를 대체하지 못한다. -MOLEG exposes four different kinds of legal authority, and they do not carry the same weight. Keep the source label attached to every citation. - -| Authority | Model source label | law.go.kr family | Notes | +| 권위 | 출처 계열 | 메서드 | 성격 | |---|---|---|---| -| MOLEG official interpretation (법령해석례) | `source_type` on the interpretation | `expc` | The 법제처 official interpretation. | -| Ministry first-instance interpretation | `source_type` / `source_target` | `*CgmExpc` | A central ministry's own reading — a different authority level from a MOLEG interpretation. | -| Supreme Court case (판례) | `JudicialDecisionIdentity.source_type` | `prec` | Ordinary court precedent. | -| Constitutional Court decision (헌재결정) | `JudicialDecisionIdentity.source_type` | `detc` | Constitutional review — not ordinary precedent. | +| 법제처 법령해석 (법령해석례) | `expc` | `search_interpretations(source="moleg")` / `get_interpretation` | 법제처의 공식 해석 | +| 부처 1차 해석 | `*CgmExpc` (40개 부처) | `search_interpretations(source="ministry", ministry=…)` | 개별 부처의 자체 판단. 법제처 해석과 **다른 층위** | +| 대법원 판례 | `prec` | `search_cases` / `get_case` | 일반 법원의 선례 | +| 헌재 결정 (헌재결정례) | `detc` | `search_constitutional_decisions` / `get_constitutional_decision` | 위헌심사. 일반 판례가 **아니다** | +| 위원회 의결 | 12개 위원회 | `search_committee_decisions` / `get_committee_decision` | 행정기관의 처분·의결. 판례가 **아니다** | +| 행정심판 재결 | 5개 심판기관 | `search_administrative_appeals` / `get_administrative_appeal` | 행정심판의 재결. 법원 판결이 **아니다** | -These map to distinct search and load methods (`search_interpretations` / `get_interpretation`, `search_cases` / `get_case`, `search_constitutional_decisions` / `get_constitutional_decision`), and each loaded result preserves its `source_type` / `source_target` so the authority level survives into your answer. A MOLEG interpretation, a ministry interpretation, a Supreme Court case, and a Constitutional Court decision are four different things — never merge them under a generic "the law says." +각 결과는 `source_type` / `source_authority`를 끝까지 달고 다닌다. 답을 쓸 때 이걸 "법에 따르면"으로 뭉개면 그 보존이 무의미해진다. -Two practical notes: +실무적으로 걸리는 세 지점. -- On `search_interpretations()`, `source="all"` means MOLEG **plus one specified ministry**; `source="all_ministries"` is the intentional, higher-cost fan-out across the ministry registry. They are not the same scope. -- Constitutional doctrines such as 과잉금지원칙 or 평등원칙 are **free-text search terms**, not indexed categories. The `detc` source has no doctrine field, so a keyword search can surface candidate decisions but cannot prove exhaustive doctrine coverage or "no constitutional risk." Load selected detail with `get_constitutional_decision()` before citing 판시사항, 결정요지, or reviewed statutes. +**`source="all"`은 전부가 아니다.** `search_interpretations()`에서 `"all"`은 법제처 **+ 지정한 부처 하나**를 뜻한다. 부처 전체를 훑는 것은 `"all_ministries"`이며, 의도적으로 비용이 큰 경로다. -When you need authority scoped to specific statute articles, use `load_authority_context()` and cite from its `current_authorities`, using the `referenced_articles` / `reviewed_articles` on each loaded result to confirm the authority actually addresses your target article. +**헌법 원칙은 색인이 아니다.** 과잉금지원칙·평등원칙 같은 도그마틱은 `detc`에 필드로 존재하지 않는다. 자유 텍스트 검색일 뿐이라, 후보를 찾을 수는 있어도 "위헌 소지 없음"이나 도그마틱 망라성을 증명하지 못한다. -## 6. Staged context bundles and deferred follow-ups +**행정기관 기록의 부재는 무사고의 증명이 아니다.** 위원회 의결과 행정심판 재결은 "그 기관이 실제로 판단했다"는 기록이다. 0건은 그 기관이 판단한 적이 없다는 뜻이지, 문제가 없었다는 뜻이 아니다. 그리고 소청·조세·해양안전 사안을 일반 행정심판위(`decc`)에서만 찾으면 조용히 0건이 나온다 — 그건 별도 심판기관 소관이다. -For a broad or under-specified question — or one that begins from a statute or bill anchor — loading each source by hand is tedious. `load_legal_context_bundle()` runs one bounded first pass over the likely sources and returns a `LegalContextBundle` that separates what was actually loaded from what is only a lead. +조문 단위로 권위를 붙여야 한다면 `load_authority_context()`를 쓰고, 그 결과의 **`current_authorities`**에서 인용하라. 여기엔 실제로 그 조문을 참조하고 날짜가 확인된 것만 남는다. -A bundle has three tiers: +## 6. 단계적 번들과 후속 조회 -- **`loaded`** (`LoadedContext`) — source text already retrieved and citable: laws, articles, delegations, and any eagerly loaded interpretation/case/ Constitutional Court detail. -- **`candidates`** (`CandidateContext`) — sources discovered but *not* loaded. These are still just candidates (see concept 2): identity metadata only. -- **`deferred`** (a list of `DeferredLookup`) — bounded next lookups the bundle chose not to run, plus `ambiguities`, `gaps`, and `source_notes`. +넓은 질문에 대해 출처를 하나씩 손으로 싣는 것은 지루하다. 번들 로더는 한 번의 제한된 1차 통과를 돌고, 결과를 **세 층으로 분리해서** 돌려준다. -**A bundle is source loading, not a conclusion.** Treat its candidates and deferred lookups as the next menu, never as proof that every relevant source body has been inspected. Do not claim exhaustive interpretation, case, Constitutional Court, administrative-rule, or annex/form coverage from a bundle alone. +- **`loaded`** (`LoadedContext`) — 이미 실린 본문. 인용 가능. +- **`candidates`** (`CandidateContext`) — 발견됐지만 안 실린 것. 개념 2에 따라 여전히 후보다. +- **`deferred`** (`list[DeferredLookup]`) — 돌리지 않기로 한 다음 조회들. `ambiguities`, `gaps`, `source_notes`가 함께 온다. -A **deferred follow-up** is a bounded, executable next step. Each `DeferredLookup` names an `interface`, a `query`, filters, and a `reason`. You run it through `load_followup()`, which routes it to the right task-level loader without you touching source target names, `ID`/`mst` rules, or article formatting: +**번들은 출처 로딩이지 결론이 아니다.** 번들 하나로 해석·판례·헌재·행정규칙·별표를 망라했다고 말할 수 없다. -```python -bundle = api.load_legal_context_bundle( - query="자동차 방치 처리 기준", - mode="question", - budget="standard", -) +`DeferredLookup`은 `interface`, `query`, `filters`, `reason`을 가진 실행 가능한 다음 수다. `load_followup()`에 넣으면 알맞은 로더로 라우팅된다 — 출처 target 이름도, `ID`/`MST` 규칙도, 조문 포맷도 만질 일이 없다. +```python for lookup in bundle.deferred: if lookup.interface == "load_administrative_rule_context": - rule_context = api.load_followup(lookup) # executes the routed loader + rule = api.load_followup(lookup) break ``` -`load_followup()` executes MOLEG-API follow-ups only. A follow-up whose `interface` is `websearch` or `congress-db` raises `UnsupportedFormatError` — a deliberate handoff signal: latest social facts belong to WebSearch, and National Assembly bill and vote facts belong to congress-db, not to this SDK. - -Two related staged loaders share the bundle shape: - -- `load_institutional_system()` — composes an explicit set of statutes you already selected (via repeated `statute_ids`) into one bundle. It composes the set; it does **not** discover it or decide which statute is primary. -- `load_delegated_criteria()` — anchors on one statute and additionally loads bounded administrative-rule and annex/form operational criteria. +`interface`가 `websearch`나 `congress-db`인 항목은 `UnsupportedFormatError`를 낸다. 버그가 아니라 **의도된 경계 표시**다 — 최신 사회적 사실은 웹 검색, 국회 의안·표결 사실은 별도 시스템 소관이고, 이 패키지는 그걸 아는 척하지 않는다. -Every public dataclass — including the whole bundle — serializes recursively, so you can inspect or persist it: +같은 모양을 공유하는 번들 로더가 셋 더 있다. -```python -payload = bundle.to_dict() # omits raw source payloads -debug = bundle.to_dict(include_raw=True) # keeps raw law.go.kr rows -text = bundle.to_json_string() -``` +- **`load_institutional_system()`** — 이미 고른 법령 **집합**을 하나의 제도로 훑는다. 집합을 구성해 줄 뿐, 어느 법이 주된 것인지 발견하거나 결정하지 않는다. +- **`load_delegated_criteria()`** — 법령 하나에 닻을 내리고, 행정규칙과 별표·서식의 **본문까지** 제한적으로 실어 온다. 이름만이 아니라 구체적 집행 기준이 필요할 때 쓴다. +- **`load_authority_context()`** — 지정한 조문들에 스코프를 건 정밀 도구. 조문과 어긋나거나 날짜가 없거나 개정 이전인 권위를 `current_authorities`에서 걸러낸다. -## Where to go next +## 다음 -- [API Guide](API-Reference.md) — every task-level method and its arguments. -- [CLI](CLI-Reference.md) — the same methods as shell subcommands and the JSON envelope. -- [Follow-up Lookups](Core-Concepts.md) — running deferred lookups end to end. -- [Source Coverage and Limits](Sources-and-Coverage.md) — what each source family does and does not expose. +- [Agent Integration](Agent-Integration.md) — 이 개념들이 엔벨로프 신호로 어떻게 표현되는지 +- [Gotchas](Gotchas.md) — 조용히 틀리는 지점 모음 +- [Historical Versions](Historical-Versions.md) — 판본과 `as_of` +- [Sources & Coverage](Sources-and-Coverage.md) — 출처 계열별 커버리지 diff --git a/docs/wiki/Error-Handling.md b/docs/wiki/Error-Handling.md index 84073eb..3430cbe 100644 --- a/docs/wiki/Error-Handling.md +++ b/docs/wiki/Error-Handling.md @@ -1,202 +1,192 @@ -# Error Handling +# 오류 처리 -moleg-api raises a small hierarchy of typed exceptions, all rooted at `MolegApiError`. Every exception distinguishes one thing above all: **a failure to reach the source is not the same as a legal source being absent.** A rate limit, a timeout, or a parse failure means "we could not get an answer right now" — never "no such law exists." Treat the two differently, and the errors below tell you which one you are looking at. +이 패키지의 예외 체계는 하나를 무엇보다 우선해서 구분한다. -All exception classes are importable from the package root: +> **출처에 닿지 못한 것과 법적 출처가 없는 것은 다르다.** + +호출 제한, 타임아웃, 파싱 실패는 "지금 답을 얻지 못했다"는 뜻이지 "그런 법은 없다"가 아니다. 이 둘을 섞으면 거짓 진술이 만들어진다. ```python from moleg_api import ( - MolegApiError, - NoResultError, - AmbiguousLawError, - UnsupportedFormatError, - SourceApiError, - RateLimitError, - RetryExhaustedError, - ParseFailureError, + MolegApiError, NoResultError, AmbiguousLawError, + UnsupportedFormatError, ParseFailureError, AsOfBeforeCoverageError, + SourceApiError, RateLimitError, RetryExhaustedError, ) ``` -## Exception hierarchy +## 계층 ``` Exception -└── MolegApiError base for all public-interface failures - ├── NoResultError no usable source text for the request - ├── AmbiguousLawError request matched multiple law identities - ├── UnsupportedFormatError source cannot provide a supported format - ├── ParseFailureError a response could not be normalized - └── SourceApiError the law.go.kr call itself failed - ├── RateLimitError law.go.kr rate limited the request (HTTP 429) - └── RetryExhaustedError retryable failures continued through all attempts +└── MolegApiError 공개 인터페이스의 모든 실패 + ├── NoResultError 쓸 만한 결과 없음 + ├── AmbiguousLawError 복수 신원이 걸림 + ├── UnsupportedFormatError 지원하지 않는 형식·경로 + ├── ParseFailureError 응답을 모델로 정규화하지 못함 + ├── AsOfBeforeCoverageError as_of가 통합본 커버리지 이전 + └── SourceApiError law.go.kr 호출 자체가 실패 + ├── RateLimitError HTTP 429 + └── RetryExhaustedError 재시도 가능한 실패가 전부 소진 ``` -Catch `MolegApiError` to handle any package-raised failure at once; catch a subclass to react to a specific condition. Because `RateLimitError` and `RetryExhaustedError` are subclasses of `SourceApiError`, catching `SourceApiError` also catches both. - -## The exceptions +`MolegApiError`를 잡으면 전부, 하위 클래스를 잡으면 특정 조건만 다룬다. `SourceApiError`를 잡으면 `RateLimitError`와 `RetryExhaustedError`도 함께 잡힌다. -### `MolegApiError` +--- -Base class for every failure the public interface raises. It carries no extra fields of its own. Use it as the broadest catch: - -```python -from moleg_api import MolegApi, MolegApiError - -api = MolegApi() -try: - article = api.get_article(identity, "제26조") -except MolegApiError as exc: - log.warning("moleg lookup failed: %s", exc) -``` +## 각 예외 ### `NoResultError` -The source API returned no usable result for a valid, well-formed request — for example, a load that found no article text for an identifier, or a history lookup that returned no events. This is a *scoped* absence: it means nothing was found for **this identifier, article, or query**, not that the material does not exist anywhere. Widen the search terms or scope before concluding a source is absent. +형식이 올바른 요청에 대해 출처가 쓸 만한 결과를 돌려주지 않았다. **범위 한정 부재**다 — 이 식별자·이 조문·이 질의로 못 찾았다는 뜻이지, 그런 자료가 어디에도 없다는 뜻이 아니다. -Three callers of `NoResultError` deserve special note: +세 가지 경우를 구분해 알아둘 만하다. -- **A loader handed a law name instead of a law ID.** `get_law`, `get_article`, and the other loaders expect a `LawIdentity`, a `LawHit`, or a numeric source identifier — not a free-text statute name. Passing a name raises `NoResultError` whose message tells you to search first. The CLI surfaces this distinctly as `needs_search_first` (exit code **5**); see below. -- **A nonexistent identifier is a `no_result`, not a source failure.** law.go.kr answers a detail lookup for an identifier it does not have with an *empty body* (`{}`), not with the `일치하는 …` sentence it returns for a law-name miss. Before 0.3.0 that empty body raised `ParseFailureError` and exited **3**, whose discipline told the caller to retry — advice for a lookup that can never succeed. It is now a `NoResultError` (exit **4**) that steers back to `search-*`. -- **A zero-hit search is not an error.** The search methods themselves may raise internally when nothing matches, but at the CLI a search that finds nothing is reported as a normal, scoped success (`ok:true`, `count:0`, exit **0**). Only *loads* raise a surfaced `no_result`. +**로더에 법령 이름을 넘겼을 때.** 로더는 `LawIdentity`, `LawHit`, 또는 숫자 식별자를 받는다. 자유 텍스트 법령명을 주면 "먼저 검색하라"는 메시지와 함께 이 예외가 난다. CLI에서는 `needs_search_first`(종료코드 5)로 따로 표시된다. -```python -from moleg_api import MolegApi, NoResultError +**없는 식별자도 여기다.** law.go.kr은 모르는 식별자에 대해 「일치하는 …」 문장이 아니라 **빈 본문**(`{}`)으로 답한다. 0.3.0 이전에는 이걸 파싱 실패로 분류해 종료코드 3을 냈고, 그 규율은 "잠시 후 재시도"였다 — 영원히 성공할 수 없는 조회에 대한 조언이었다. 지금은 `no_result`(종료코드 4)로 가서 `search-*`로 유도한다. -api = MolegApi() -try: - article = api.get_article(identity, "제999조") -except NoResultError: - # Nothing at this article for this identity — not proof the law lacks it. - ... -``` +**0건 검색은 오류가 아니다.** CLI에서 검색이 아무것도 못 찾으면 정상 성공이다 (`ok:true`, `count:0`, 종료코드 0). 예외로 올라오는 것은 *로드*뿐이다. ### `AmbiguousLawError` -The request matched **more than one** plausible law identity, and the package refuses to silently pick one for you. This is raised, for example, when a promulgation bridge (`resolve_promulgated_law`) matches several distinct statutes. The exception exposes the choices so you can present them: - -- `.candidates` — a list of the matched `LawIdentity` objects (name, `law_id`, `mst`, promulgation/effective dates, ministry, and so on). -- `.kind` — a short label for the ambiguity source (e.g. `"promulgation_bridge"`). -- `.message` — the human-readable summary. +복수의 그럴듯한 신원이 걸렸고, 패키지가 대신 고르기를 **거부한** 것이다. -Ambiguity is a signal to disambiguate, **not** a license to take the first candidate. Surface the candidates and let the caller (or user) choose, then re-issue the load with the selected identity. +| 속성 | 내용 | +|---|---| +| `.candidates` | 걸린 `LawIdentity` 목록 | +| `.kind` | 모호성의 종류를 나타내는 짧은 라벨 (예: `"promulgation_bridge"`) | +| `.message` | 사람이 읽는 요약 | ```python -from moleg_api import MolegApi, AmbiguousLawError - -api = MolegApi() try: - identity = api.resolve_promulgated_law(prom_law_nm="...", prom_no="...") + identity = api.resolve_promulgated_law(prom_law_nm="…", prom_no="…") except AmbiguousLawError as exc: for cand in exc.candidates: print(cand.name, cand.law_id, cand.promulgation_date) - # Pick one deliberately, then call get_law(chosen)… + # 의도적으로 하나를 고른 뒤 다시 로드한다 ``` +**모호성은 첫 후보를 고를 허가가 아니다.** `.candidates[0]`을 집으면 이 예외가 존재하는 이유를 무효로 만드는 것이다. 후보를 드러내고, 호출자나 사용자가 고르게 하라. + ### `UnsupportedFormatError` -The source endpoint could not provide a response in a format the package can consume — for example, law.go.kr returned HTML where JSON was requested, or a text-export endpoint returned a non-text content type. This is a source/path limitation, not a legal-absence result. The material may still be reachable by a different command or a different source (WebSearch, National Assembly bill data, and so on). +출처가 지원 가능한 형식으로 답하지 못했거나, 요청한 경로가 이 인터페이스 밖이다. 실제로 나오는 경우들: + +- 잘못된 열거값 (`basis`, `court`, `source`, 위원회·심판기관 코드 등) +- `compare_law_versions`에 임의의 두 날짜 구간을 준 경우 +- 헌재 신원을 `get_case`에 넘긴 경우(또는 그 반대) +- 본문 조회를 지원하지 않는 두 부처(국세청·재정경제부)의 해석 본문을 요구한 경우 +- `load_followup`에 `websearch`나 `congress-db` 인터페이스를 넘긴 경우 — **의도된 경계 표시다** + +**법적 부재가 아니다.** 다른 명령이나 다른 출처로는 닿을 수 있다. + +### `ParseFailureError` + +응답은 받았는데 공개 모델로 정규화하지 못했다. 신원 페이로드에 법령명이 없거나, 체계도 페이로드의 모양이 예상과 다르거나, HTML 표의 열 개수가 다를 때. + +**재시도해도 소용없다.** 종료코드 3을 일시적 오류들과 공유하지만, 그건 "출처 쪽에서 뭔가 잘못됐다"는 점만 같기 때문이다. 알아보지 못한 응답 모양은 다음 호출에서도 똑같이 알아보지 못한다. **식별자 착오를 먼저 배제하고**(`search-*`로 재확인), 그 다음 다른 명령이나 경로를 시도하라. + +### `AsOfBeforeCoverageError` + +`as_of`가 통합본 커버리지 시작 이전이다. + +| 속성 | 내용 | +|---|---| +| `.law_id` | 대상 법령 | +| `.earliest_available` | 조회 가능한 가장 이른 시점 | + +**영구 조건이지 일시적 실패가 아니다.** 재시도가 아니라 개정 연혁(`trace_law_history`)으로 가야 한다. CLI는 `kind: "version_request_unfulfilled"`로 내고 그 명령을 `next`에 담아 준다. ### `SourceApiError` -The law.go.kr call itself failed or returned an invalid response — a non-retryable HTTP error, or a body that was not valid JSON. This is the general "the source did not answer usefully" error and the parent of the two transient errors below. It signals a **source-access failure**, not that the requested law is absent. +law.go.kr 호출 자체가 실패했거나 유효하지 않은 응답을 줬다 — 재시도 불가 HTTP 오류, 또는 JSON이 아닌 본문. 아래 두 일시적 오류의 부모다. **출처 접근 실패이지 법령의 부재가 아니다.** ### `RateLimitError` -law.go.kr rate limited the request (HTTP **429**) and the package exhausted its retries. Because the package ships with a shared default OC credential, heavy or concurrent use can hit this. **This is a temporary access failure, never evidence that a law does not exist.** Back off and retry later; if you hit it often, register your own OC (see [Installation](Installation.md)) to get your own rate budget. - -### `RetryExhaustedError` +HTTP **429**를 받고 재시도가 소진됐다. 패키지가 공용 기본 OC를 싣고 있으므로, 무거운 사용이나 동시 호출에서 걸릴 수 있다. -A retryable source failure — a timeout, a connection error, or a retryable HTTP status (408, 500, 502, 503, 504) — persisted through every allowed attempt. Like `RateLimitError`, this is a **transient access failure, not an absence.** Retry later. (When retries are disabled by configuring `max_retries=0`, a retryable timeout or connection error surfaces as a plain `SourceApiError` instead.) +**일시적 접근 실패이며 법령이 없다는 증거가 절대 아니다.** 물러났다 재시도하라. 자주 걸린다면 자기 OC를 발급받아라 → [Installation](Installation.md) -### `ParseFailureError` +### `RetryExhaustedError` -A source response was retrieved but could not be normalized into the public data model — for example, an identity payload missing its law name, or a law-structure payload with an unexpected shape. This is neither a source-access failure nor a legal absence: the data arrived but did not fit the expected structure. +재시도 가능한 실패(타임아웃, 연결 오류, HTTP 408·500·502·503·504)가 허용된 모든 시도에서 계속됐다. 역시 **일시적이지 부재가 아니다.** -**Retrying will not help.** It shares exit code 3 with the transient errors, but only because both mean "the source side went wrong"; a response shape the package does not recognize will be just as unrecognizable on the next call. Rule out an identifier mistake first (`search-*` to re-confirm the identity), then try a different command or path before drawing any conclusion. +`max_retries=0`으로 재시도를 끈 경우, 타임아웃과 연결 오류는 이 예외 대신 평범한 `SourceApiError`로 나온다 — 재시도를 안 한 클라이언트가 "재시도 소진"을 보고하는 것은 말이 안 되기 때문이다. -## Transient failure versus absence +--- -The single most important distinction when handling these errors: +## 일시적 실패 vs 부재 — 대응표 -| Meaning | Exceptions | How to react | -| --- | --- | --- | -| **Temporary access failure** — could not reach or read the source right now | `RateLimitError`, `RetryExhaustedError`, `SourceApiError` | Retry later. Do **not** treat as "no such law." Fill time-sensitive facts (e.g. effective dates) from a fallback only with an "unverified" label; leave constitutionality/case-law claims as "needs primary confirmation." | -| **Response could not be normalized** | `ParseFailureError` | Not an absence, and **not transient** — a retry returns the same shape. Rule out an identifier mistake, then try another command or path. | -| **Scoped absence** — a valid request found nothing for this exact scope, *including an identifier the source has no record of* | `NoResultError` (surfaced as `no_result`) | Widen the query/scope, or re-confirm the identifier with `search-*`. Never conclude the material does not exist without stating the exact query, source family, and filters used. | -| **Wrong input / ordering** — a name where an ID was needed, bad arguments | `NoResultError` (surfaced as `needs_search_first`), argument errors | Search first to get the identity, or fix the arguments. | -| **Multiple matches** | `AmbiguousLawError` | Present the `candidates`; choose one deliberately, then re-load. | +| 의미 | 예외 | 대응 | +|---|---|---| +| **일시적 접근 실패** | `RateLimitError`, `RetryExhaustedError`, `SourceApiError` | 나중에 재시도. **"그런 법 없음"으로 다루지 마라.** 시급한 사실은 미확인 표지를 달아 대체 출처로만 채우고, 위헌성·판례 주장은 "1차 확인 필요"로 남겨라 | +| **정규화 실패** | `ParseFailureError` | 부재도 아니고 **일시적도 아니다.** 식별자 착오를 먼저 배제하고 다른 경로를 시도 | +| **범위 한정 부재** | `NoResultError` (`no_result`) | 질의·범위를 넓히거나 식별자를 재확인. **정확히 무엇을 어떤 범위·필터로 검색했는지 밝히지 않고 부재를 주장하지 마라** | +| **입력·순서 오류** | `NoResultError` (`needs_search_first`), 인자 오류 | 먼저 검색해 신원을 얻거나 인자를 고쳐라 | +| **커버리지 하한** | `AsOfBeforeCoverageError` | 영구. 연혁으로 이동 | +| **복수 일치** | `AmbiguousLawError` | 후보를 제시하고 의도적으로 선택 | -## Built-in retries +--- -`LawGoKrClient` retries transient source failures automatically before raising. The retry policy is configurable on the client: +## 내장 재시도 -- **Retryable conditions** — HTTP status **408, 429, 500, 502, 503, 504**, plus timeouts and connection (`URLError`) failures. -- **`max_retries`** — additional attempts after the first (default **2**, so up to **3** total attempts). -- **`retry_delay_seconds`** — fixed delay between attempts (default **0.5**). -- **`timeout_seconds`** — per-request timeout (default **30**). +`LawGoKrClient`가 예외를 올리기 전에 일시적 실패를 자동 재시도한다. -When retries are exhausted, an HTTP 429 raises `RateLimitError`; any other retryable condition raises `RetryExhaustedError`. Non-retryable HTTP errors and invalid-JSON bodies raise `SourceApiError` immediately, without retrying. +| 설정 | 기본값 | 의미 | +|---|---|---| +| 재시도 조건 | — | HTTP 408, 429, 500, 502, 503, 504 + 타임아웃 + 연결 오류 | +| `max_retries` | **2** | 첫 시도 이후 추가 시도 → 총 3회 | +| `retry_delay_seconds` | **0.5** | 시도 간 **고정** 대기. 지수 백오프도 지터도 없다 | +| `timeout_seconds` | **30** | 요청당 타임아웃 | -To tune the policy, construct the client explicitly: +재시도가 소진되면 429는 `RateLimitError`, 나머지는 `RetryExhaustedError`. **재시도 불가 상태 코드와 JSON이 아닌 본문은 재시도 없이 즉시** `SourceApiError`다. ```python -from moleg_api import MolegApi, LawGoKrClient - api = MolegApi(source=LawGoKrClient(max_retries=4, retry_delay_seconds=1.0)) ``` -## CLI exit codes +클라이언트 쪽 호출량 조절(토큰 버킷, 요청 간 지연)은 **없다.** 호출 제한 대응은 반응적이다 — 429를 받고 재시도한다. 동시 호출을 많이 낼 계획이라면 조절은 호출자 몫이다. -The `moleg` command prints one JSON envelope per invocation and maps each error class to a distinct exit code, so a shell or agent can branch on the outcome without parsing the message. The envelope's `kind` field names the condition; the exit code encodes it. +--- -| Exit | `kind` | Meaning | Underlying exception | -| --- | --- | --- | --- | -| **0** | (varies) | Success — **including a zero-hit search** (`ok:true`, `count:0`) | — | -| **2** | `ambiguous` | Multiple plausible identities; candidates in `flags.candidates` | `AmbiguousLawError` | -| **3** | `source_access_error` | Transient source access failure — **retry later** | `RateLimitError`, `RetryExhaustedError` | -| **3** | `parse_error` | Unrecognized response shape — **a retry will not help** | `ParseFailureError` | -| **3** | `error` | Other source-side failure | `SourceApiError` and other `MolegApiError` | -| **4** | `no_result` | A load found no source text — including an identifier the source has no record of | `NoResultError` | -| **5** | `needs_search_first` | A loader was handed a law name — search for the ID first | `NoResultError` (name-not-ID) | -| **5** | `unsupported` | The source cannot provide this format/path | `UnsupportedFormatError` | -| **5** | `usage_error` | Bad arguments, missing subcommand, or unknown command | argument parsing | +## CLI 종료코드 -There is no exit code 1: the errors that scripts must branch on each get a dedicated code (2–5), and everything else that succeeds is 0. +| 종료 | `kind` | 의미 | 대응 예외 | +|---|---|---|---| +| **0** | (다양) | 성공 — **0건 검색 포함** | — | +| **2** | `ambiguous` | 복수 신원. `flags.candidates`에 후보 | `AmbiguousLawError` | +| **3** | `source_access_error` | 일시적 접근 실패 — **재시도하라** | `RateLimitError`, `RetryExhaustedError` | +| **3** | `parse_error` | 알아보지 못한 응답 모양 — **재시도 무의미** | `ParseFailureError` | +| **3** | `error` | 그 밖의 출처 측 실패 | `SourceApiError`, 기타 `MolegApiError` | +| **4** | `no_result` | 본문 없음. **없는 식별자 포함** | `NoResultError` | +| **4** | `version_request_unfulfilled` | 요청 시점이 커버리지 이전 | `AsOfBeforeCoverageError` | +| **5** | `needs_search_first` | 로더에 법령 이름을 넘김 | `NoResultError` (이름 오입력) | +| **5** | `unsupported` | 지원하지 않는 형식·경로 | `UnsupportedFormatError` | +| **5** | `usage_error` | 인자 오류, 서브커맨드 누락, 알 수 없는 명령 | 인자 파싱 | -**Exit 3 carries two kinds, and only one of them is worth retrying.** `source_access_error` means the source could not be reached — back off and try again. `parse_error` means it answered with something the package cannot read, which the next identical call will do too. Branch on `kind`, not on the exit code alone, or a retry loop will spin on a permanent condition. +종료코드 1은 없다. 스크립트가 분기해야 하는 오류마다 전용 코드(2~5)를 줬고, 나머지 성공은 전부 0이다. -`no_result` (exit 4) is deliberately kept apart from `needs_search_first` (exit 5): the former is a genuine scoped absence, the latter a fixable input-ordering mistake. +**종료코드 3만 보고 재시도 루프를 돌리면 안 된다.** 두 `kind` 중 하나만 재시도할 가치가 있다. `kind`로 분기하지 않으면 영구 조건에서 루프가 돈다. -### CLI error envelope +`no_result`(4)와 `needs_search_first`(5)를 일부러 갈라 놓은 이유도 같다 — 전자는 진짜 범위 한정 부재고, 후자는 고칠 수 있는 입력 순서 실수다. -A failed CLI call still prints a structured envelope. For example, handing a loader a law name instead of an ID: - -```bash -python3 -m moleg_api get-article --law "주택임대차보호법" 제1조 -``` +**오류도 stdout으로 나간다.** stderr가 아니다. 파싱 경로가 하나로 유지된다. ```json { "ok": false, "command": "get-article", + "version": "0.3.0", "kind": "needs_search_first", - "error": "Identifier '주택임대차보호법' looks like a law name, not a law ID. Call `search_laws('주택임대차보호법')` to find the law ID, then pass the result or its `law_id` to this method.", - "discipline": [ "로더에 법령명이 들어옴 — search-laws로 law_id를 먼저 얻어라." ], - "next": [ { "why": "먼저 신원 검색", "cmd": "moleg search-laws \"주택임대차보호법\"" } ] + "error": "Identifier '주택임대차보호법' looks like a law name, not a law ID. …", + "discipline": ["로더에 법령명이 들어옴 — search-laws로 law_id를 먼저 얻어라."], + "next": [{ "why": "먼저 신원 검색", "cmd": "moleg search-laws \"주택임대차보호법\"" }] } ``` -The exit code for this envelope is **5**. The correct fix is to search first, then load with the resulting `law_id`: - -```bash -python3 -m moleg_api search-laws "주택임대차보호법" # → law_id -python3 -m moleg_api get-article --law 제1조 # load the chosen identity -``` - -When the failure is ambiguity, the envelope carries the candidates under `flags.candidates` and exits **2**, leaving the choice to you rather than picking one. A transient `source_access_error` (exit **3**) means retry later — it is not proof the law is absent. - -## See also +## 관련 문서 -- [Command-Line Interface](CLI-Reference.md) — the full JSON envelope contract and the search→load discipline. -- [Installation & Setup](Installation.md) — configuring your own OC credential to avoid the shared rate budget. -- [Data Models](API-Reference.md) — the `LawIdentity` fields returned in `AmbiguousLawError.candidates`. +- [Agent Integration](Agent-Integration.md) — 종료코드를 에이전트가 어떻게 다뤄야 하는지 +- [CLI Reference](CLI-Reference.md) — 엔벨로프 계약 전체 +- [Installation](Installation.md) — 자기 OC 설정으로 공용 호출 예산 벗어나기 +- [Gotchas](Gotchas.md) — 조용히 틀리는 지점 diff --git a/docs/wiki/Gotchas.md b/docs/wiki/Gotchas.md index 30f4ec1..30e60c0 100644 --- a/docs/wiki/Gotchas.md +++ b/docs/wiki/Gotchas.md @@ -1,221 +1,193 @@ -# Gotchas +# 함정 -`moleg-api` normalizes law.go.kr sources, but it does not decide what a source *means*. Several distinctions are easy to collapse and will quietly produce a wrong legal claim if you do. This page lists the traps that matter most to a first-time integrator, the model fields that expose each one, and the discipline each field enforces. +이 패키지는 law.go.kr 출처를 정규화하지만, 그 출처가 무엇을 *의미하는지* 결정하지는 않는다. 아래는 뭉개기 쉽고, 뭉개면 조용히 틀린 법적 주장이 만들어지는 지점들이다. -Two ideas run through every gotcha: +모든 항목을 관통하는 두 문장: -- **Search hits are identity candidates, not source text.** A `*_hit` / `*_candidate` / `*_planning` result tells you *what to load next*, never *what the law says*. Only `*_text` / `*_context` bodies loaded through a detail method are citable. -- **Absence of a result is not proof of absence.** A zero-hit search, a `NoResultError`, or a source-access failure means "this query, this scope, this moment" — not "no such law, article, precedent, or criterion exists." +> **검색 결과는 신원 후보이지 본문이 아니다.** +> **결과가 없는 것은 부재의 증명이 아니다.** -When a trap is *live* in a given result, the CLI surfaces a `discipline` line and often a `flags` entry for it (these fire only when the condition is actually present, so the common path stays quiet). The Python API exposes the same conditions as model fields. Both are shown below. +함정이 특정 결과에서 *실제로 살아 있을 때* CLI는 `discipline` 문장과 대개 `flags` 항목을 함께 낸다. 조건이 실제로 있을 때만 울리므로 평범한 경로는 조용하다. Python API에서는 같은 조건이 모델 필드로 드러난다. -## Search hits are candidates, not loadable text +--- -`search_laws()`, `search_administrative_rules()`, `search_annex_forms()`, `search_interpretations()`, `search_cases()`, and `search_constitutional_decisions()` return `*Hit` objects wrapping an `identity`. They resolve *which* source you mean; they do not carry citable article wording, duties, sanctions, procedures, holdings, or criteria. +## 1. 검색 결과에서 인용하지 마라 -Do not quote from a hit. Load the selected identity first, then cite the loaded body: +`search_*`는 `identity`를 감싼 `*Hit`을 준다. *어느* 출처인지를 확정할 뿐, 인용 가능한 문언·의무·제재·절차·판시·기준을 담고 있지 않다. ```python -hits = api.search_laws("주택임대차보호법", basis="effective", display=5) -selected = hits[0].identity # identity candidate — not citable -law = api.get_law(selected, basis="effective") -article = api.get_article(selected, "제3조", basis="effective") -print(article.text) # loaded source text — citable +hits = api.search_laws("주택임대차보호법", display=5) +selected = hits[0].identity # 후보 — 인용 불가 +article = api.get_article(selected, "제3조") +print(article.text) # 실린 본문 — 인용 가능 ``` -```bash -python -m moleg_api search-laws "주택임대차보호법" # kind: law_hit_list (candidates) -python -m moleg_api get-article --law 001248 제3조 # kind: article_text (loadable body) -``` +CLI는 순서를 구조적으로 강제한다 — 로더에 법령 **이름**을 넘기면 `kind: needs_search_first`(종료코드 5)로 거부한다. -The CLI enforces the ordering structurally: pass a **law name** to a loader (`get-law`, `get-article`, …) and it refuses with `kind: needs_search_first` (exit 5), because loaders take a `law_id`, not a name. +`expand_legal_query()`와 `find_comparable_mechanisms()`도 마찬가지다. 그 용어 후보·관련 법령·관련 조문·비교 대상은 *계획* 맥락이지 권위가 아니다. -```bash -$ python -m moleg_api get-article --law "주택임대차보호법" 제3조 -{ "ok": false, "kind": "needs_search_first", - "discipline": ["로더에 법령명이 들어옴 — search-laws로 law_id를 먼저 얻어 --law에 넘겨라."], - "next": [{"cmd": "moleg search-laws \"주택임대차보호법\""}] } -``` +## 2. 공포일과 시행일은 다르다 -The same rule applies to `expand_legal_query()` and `find_comparable_mechanisms()`: their `term_candidates`, `related_laws`, `related_articles`, and returned `LawIdentity` anchors are *planning* context. They are never legal authority until you load the selected law/article text. See [Follow-up lookups](Core-Concepts.md) for how a candidate's `follow_up` / `deferred` points to the exact next call. +법은 공포되고도 시행 전일 수 있다. `LawIdentity`가 둘을 별도 필드로 들고 있다 — `promulgation_date`, `effective_date`, 그리고 어느 기준으로 조회했는지의 `basis`. -## Effective date is not promulgation date +실제 사례: `search_laws("주택임대차보호법")`에서 `promulgation_date="20251001"`, `effective_date="20260102"`인 후보가 나온다. 2025년에 공포됐지만 2026년까지 시행되지 않는다. -A law can be promulgated (published) and still not be in force. `LawIdentity` carries both dates as separate fields: +CLI는 실린 판본의 시행일이 오늘보다 뒤면 `flags.not_effective_as_of`를 붙이고, `source` 표기 자체를 「공포본(장래 시행 — 아직 미시행)」으로 바꾼다. 이 플래그는 `--as-of`를 줬는지와 **무관하게** 뜬다 — 미래 판본은 어느 경로로든 도착할 수 있기 때문이다. -- `promulgation_date` — when it was published -- `effective_date` — when it takes (or took) effect -- `basis` — `"effective"` or `"promulgated"`, the basis the row was retrieved on +`resolve_promulgated_law()`로 공포 사실에서 신원을 확정한 것은 *신원*의 확정이지 *현재 효력*의 확인이 아니다. -For "is this currently in force?" questions, use `basis="effective"` and compare `effective_date` to your reference date. A live example from `search_laws("주택임대차보호법")` returns a candidate with `promulgation_date="20251001"` but `effective_date="20260102"` — promulgated in 2025, not effective until 2026. +## 3. 첫 후보를 집는 것이 가장 위험한 자리 -When you load through a bundle with a reference date, pass `as_of` and inspect the `not_effective_as_of` gap before calling a law current: +검색 결과의 `[0]`이 **현행 판본이라는 보장이 없다.** 미시행 판본이 위에 올 수 있고, 그때 `flags.top_candidate_not_yet_effective`가 붙는다. -```python -bundle = api.load_legal_context_bundle( - law_identifier="001248", articles=["제3조"], as_of="2025-11-01", -) -gap_kinds = {g.kind for g in bundle.gaps} -if "not_effective_as_of" in gap_kinds: - ... # promulgated / source-loadable, but NOT in force on 2025-11-01 -``` +`AmbiguousLawError`도 같다. `.candidates`는 사람에게 제시하라는 뜻이지 첫 항목을 쓰라는 뜻이 아니다. CLI는 `kind: ambiguous`(종료코드 2)로 내고, 규율 문장은 「모호성이지 첫 후보를 고를 허가가 아님 — 후보를 사용자에게 제시.」다. + +동명 법령이 시행일만 다른 경우가 가장 흔하다. 현행은 `get_law(law_id)`로, 특정 과거 판본은 `as_of=<시행일>`로 싣되 — **누가 고를지 정한 뒤에** 해야 한다. -The CLI raises `flags.not_effective_as_of` with the discipline line *"공포됐으나 기준일 미시행 — '현재 시행 중'으로 단정 금지"* when this gap is present. A resolved promulgation bridge (`resolve_promulgated_law()` → `LawIdentity`) proves *identity*, not *current force*: the `law_identity` result carries the discipline *"공포 bridge 신원 확정일 뿐 … not_effective 여부 확인"*. +## 4. `as_of`는 필터가 아니라 판본 해석이다 -## `as_of` loads the version in force *at that date* — verify what came back +`as_of`는 걸러내는 것이 아니라 그 시점에 시행 중이던 판본을 찾아온다. 그 시점에 시행 중인 판본이 없으면 **이후 판본**이 대신 온다. -Passing `--as-of YYYY-MM-DD` to a loader does not filter; it resolves the historical version that was in force on that date. If no version was in force yet, the loader returns a *later* version instead. Check the returned `effective_date` against your requested date. The CLI computes this for you and sets `flags.version_request_unfulfilled` with a discipline line when the returned effective date is after the requested one — meaning there was no version in force on your date, and you may be looking at a future amendment rather than the historical text you asked for. +돌아온 `effective_date`를 요청일과 반드시 비교하라. CLI는 이걸 계산해 `flags.version_mismatch`를 `{requested, loaded}`로 붙인다. -## A current-law bridge is not an amendment delta +`법령ID + 시행일`로 원시 조회하면 오류 없이 현행이 돌아온다는 것이 근본 원인이다 → [Historical Versions](Historical-Versions.md) -Resolving a `congress-db` promulgation bridge and loading current text proves *identity and current wording*. It does **not** prove *what an enacted bill changed*. Before describing a change, load the delta explicitly: +## 5. 현행 텍스트는 개정 델타가 아니다 -- `compare_law_versions()` → `LawDiff` gives before/after wording for the **selected rows only**. It does not carry amendment reason, legislative intent, full bill purpose, or exhaustive changed-provision coverage — the `law_diff` result says exactly this. -- `trace_law_history()` → `LawHistory` gives the amendment chronology. Full-law history keeps `HistoryEvent.article_text=None`; article-scoped history (`article=...`) may populate `article_text` with the post-change snapshot. +공포 사실로 신원을 잇고 현행 텍스트를 실은 것은 *신원과 현재 문언*을 증명한다. **어떤 법률안이 무엇을 바꿨는지는 증명하지 않는다.** -`HistoryEvent.bill_id` is populated **only** when you supply a `promulgation_bridge` map; the package does not query `congress-db` itself. The bridge keys for joining back are `promulgation_law_name`, `promulgation_number`, and `promulgation_date`. +- `compare_law_versions()` → 선택된 행의 전후 문언만. 개정 이유·입법 취지·법안 전체 목적·변경 조문 망라를 담지 않는다. +- `trace_law_history()` → 개정 연혁. 전체 법 연혁에서는 `HistoryEvent.article_text`가 `None`이고, 조문 스코프 연혁(`article=…`)에서만 채워질 수 있다. +- `get_revision_reason()` → 「개정이유 및 주요내용」. **제안자의 자기 진술**이며 중립 요약이 아니다. -## Deleted and moved articles are source *status*, not current text +`HistoryEvent.bill_id`는 `promulgation_bridge`를 직접 넘겼을 때**만** 채워진다. 이 패키지는 의안 데이터베이스를 조회하지 않는다. -A loaded `ArticleText` (and `AdministrativeRuleArticleText`) exposes status fields that must be checked before you treat the text as an operative rule: +## 6. 삭제·이동 표시는 본문이 아니다 -- `is_deleted: bool` and `revision_type == "삭제"` — the article is deleted. `제N조 삭제` is deleted source state, not a current duty, permission, sanction, or procedure. -- `moved_to` and `revision_type == "이동"` — the substance now lives at a different article. The marker is source state; the current text is at the destination. +실린 `ArticleText`가 상태 필드를 들고 있다. 운용 규범으로 다루기 전에 확인하라. + +- `is_deleted` 또는 `revision_type == "삭제"` — **「제N조 삭제」는 삭제 상태이지 현행 의무·허가·제재·절차가 아니다.** +- `moved_to` 또는 `revision_type == "이동"` — 실질은 다른 조문에 있다. 표시는 상태이고 본문은 목적지에 있다. ```python art = api.get_article(selected, "제5조") -if art.is_deleted or art.revision_type == "삭제": - ... # cite as deleted source state only if art.moved_to: - ctx = api.load_article_context(selected, "제5조") # follows the move - current = ctx.current_article # destination text + ctx = api.load_article_context(selected, "제5조") + current = ctx.current_article # 목적지 본문 ``` -`load_article_context()` follows a move to its destination by default and returns the destination in `current_article`; if `current_article is None` for a moved/deleted request, the substance was not loaded — do not cite current obligations. `load_administrative_rule_context()` does the same for administrative rules, exposing only loaded destination rows in `current_articles`. +`load_article_context()`는 기본적으로 이동을 따라간다. 이동·삭제 요청에서 `current_article`이 `None`이면 **실질이 실리지 않은 것**이다 — 현행 의무를 인용하지 마라. + +law.go.kr은 실제로 이동하지 않은 행에 `제0조`·`0`·빈 문자열을 센티널로 내보낸다. 패키지는 이걸 걸러내므로 파싱 잡음이 실제 목적지처럼 보이지 않는다. + +## 7. 실질은 제목이 아니라 중첩된 항·호·목에 있다 -The CLI raises `flags.is_deleted` / `flags.moved_to` with matching discipline lines when either condition is present on a loaded article. (Note: law.go.kr emits `제0조` and blank strings as a non-move sentinel; the CLI treats those as parse noise, not a real destination.) +`ArticleText.text`는 중첩 항·호·목 구조를 펼쳐 담는다. 정의·예외·적용 대상·요건이 그 안에 사는 경우가 많다. -## Article substance lives in the nested text, not the title +**조문 제목(`조문제목`)이나 최상위 `조문내용`만 보고 요약하지 마라.** 운용 조항을 놓친다. -`ArticleText.text` (and `AdministrativeRuleArticleText.text`) preserves the nested 항 / 호 / 목 structure. Definitions, exceptions, application targets, and requirements frequently live in those nested units. Do **not** summarize from the article title (`조문제목`) or a top-level `조문내용` alone — you will miss the operative clause. The `article_text` result carries the discipline *"정의·예외·적용대상·요건은 text의 항·호·목 중첩에 있다 — 조문제목·상위 조문내용만으로 요약 금지."* +## 8. 부칙은 별도 본문이다 -## Supplementary provisions (부칙) are separate source text +시행일·적용례·경과조치는 부칙에 산다. `supplementary_provisions`라는 **별도 리스트**로 나오며 본 조문과 분리돼 있다. -시행일, 적용례, and 경과조치 live in 부칙, exposed as `supplementary_provisions` — a separate list of `SupplementaryProvision`, distinct from the main `articles`. Do not answer a transition-scope question from a main article, or from the law-level `LawIdentity.effective_date` metadata, alone: +경과 규정 질문을 본 조문이나 법령 수준의 `identity.effective_date` 메타데이터만으로 답하지 마라. ```python -law = api.get_law(selected, basis="effective") -for prov in law.supplementary_provisions: # 부칙 — 시행일/적용례/경과조치 +law = api.get_law(selected) +for prov in law.supplementary_provisions: # 시행일·적용례·경과조치 print(prov.text) ``` -`AdministrativeRuleText` carries its own `supplementary_provisions`. Cite them separately for rule 시행일 / 적용례 / 경과조치; the rule-level `identity.effective_date` is not the full transition analysis. When present, the CLI sets `flags.has_supplementary` with the corresponding discipline line. +행정규칙도 자기 부칙을 갖는다. -## Administrative-rule `issued_on` is 발령일자, not 시행일자 +## 9. 행정규칙의 `issued_on`은 발령일자다 -`search_administrative_rules(issued_on=...)` filters on **발령일자 (issue date)**, not 시행일자 (effective date). The search parameter is named `issued_on` (not `as_of`) precisely to prevent this confusion. A rule can be issued but not yet effective, or superseded. +`search_administrative_rules(issued_on=…)`는 **발령일자**로 거른다. **시행일자가 아니다.** 파라미터 이름이 `as_of`가 아니라 `issued_on`인 것이 바로 이 혼동을 막기 위해서다. -Before calling a loaded rule the *current* operational criteria, compare the loaded `AdministrativeRuleText.identity.effective_date` to your reference date. If it is later, it is future-effective source text, not current criteria. The CLI attaches `flags.issued_on_note` on loaded administrative-rule results as a standing reminder, and `flags.issued_on_is` on the search result. +규칙은 발령됐어도 아직 시행 전이거나 이미 대체됐을 수 있다. 현행 집행 기준이라고 부르기 전에 실린 `identity.effective_date`를 기준일과 비교하라. -```bash -python -m moleg_api search-administrative-rules "..." --issued-on 20240101 # 발령일자 filter -``` +CLI는 실린 행정규칙에 `flags.issued_on_note`를 상시 붙이고, 검색 결과에는 `flags.issued_on_is`를 붙인다. + +## 10. 별표의 금액·기준은 본문을 실어야 나온다 -## Annex/form thresholds require loading the body +`search_annex_forms()`는 메타데이터와 파일·상세 링크만 준다. 임계값·금액·기준·서식 내용은 첨부된 표에 있고, `get_annex_form_body()`로 실어야 한다. -`search_annex_forms()` returns metadata and file/detail links only. Thresholds, amounts, criteria, and form content live in the attached table (별표·서식), which is loaded through `get_annex_form_body()`. **Do not cite a threshold, amount, criterion, or extracted row from a search hit** — the `annex_form_hit_list` result carries the discipline *"임계값·금액·기준은 get-annex-form-body로 본문 로드 후에만 인용."* +본문을 실었어도 `structured_data`는 **최선의 시도**일 뿐이다. `rows`에 의존하기 전에 `parsing_confidence`를 확인하라. **비었거나 `"low"`인 것은 "기준 없음"이 아니다** — 평문 `text`로 돌아가야 한다. -When a body loads, `AnnexFormText.structured_data` (a `StructuredTableData`) is best-effort. Check `parsing_confidence` before relying on `rows`. If `rows` is empty or `parsing_confidence` is `"low"`, that is **not** "no criteria" — fall back to the plain `text`. The CLI raises this as a discipline line when the structured rows are empty or low-confidence. +빈 `search_annex_forms()` 결과 역시 "이 검색으로 못 찾음"의 증거지 첨부 기준이 없다는 증명이 아니다. -An **empty** `search_annex_forms()` result is scoped evidence for "this exact search found nothing," not proof that no attached criteria, annex, form, or threshold table exists. Check the source law text, administrative-rule annex/forms, and alternate terms before any absence claim. +## 11. 권위 층위를 평탄화하지 마라 -## Preserve source authority — interpretations, cases, decisions are different levels +법제처 해석, 부처 1차 해석, 판례, 헌재 결정, 위원회 의결, 행정심판 재결은 서로 다른 권위 층위다. 신원이 구분 메타데이터를 들고 다닌다. -MOLEG official interpretations, ministry first-instance interpretations, ordinary court cases, Constitutional Court decisions, committee decisions, and administrative appeal rulings are distinct authority levels and must not be flattened. Their identity carries the distinguishing metadata: +**두 종류의 행정 판단은 판례가 아니다.** -- `InterpretationIdentity.source_type` / `source_target` — MOLEG interpretation vs. ministry interpretation. In search, `source="all"` means MOLEG **plus one specified ministry**; `source="all_ministries"` is a registry-wide fan-out — use it only when that breadth is intentional. -- `JudicialDecisionIdentity.source_type` / `court` — ordinary court case (`prec`) vs. Constitutional Court decision (`detc`). -- `AdjudicationIdentity.source_type` / `source_authority` — a committee decision (`committee_decision`) vs. an administrative appeal ruling (`administrative_appeal` / `special_administrative_appeal`). +위원회 의결은 그 법을 집행하는 감독기관의 행정 처분이다. 그 기관이 법을 *어떻게 적용하는지*를 보여줄 뿐 법이 *무엇을 의미하는지*를 정하지 않으며, 행정소송에서 뒤집힐 수 있다. -**Neither kind of adjudication is precedent.** A 위원회 결정 is an administrative disposition by the regulator that administers the statute — it shows how that agency applies the law, not what the law means, and it can be overturned in 행정소송. An 행정심판 재결 reviews *another* agency's disposition from inside the executive branch, and a losing party can still take it to court. Citing either as 판례 overstates what it settles. They carry separate `kind` values (`committee_decision_text`, `administrative_appeal_text`) for exactly this reason. +행정심판 재결은 행정부 내부에서 *다른* 기관의 처분을 심사한 것이다. 패소한 쪽은 여전히 법원으로 갈 수 있다. -The CLI preserves these via `flags.source_type` / `flags.source_authority` and a discipline line on loaded interpretation and decision bodies. Carry the label into any answer that cites the source. +둘 중 하나라도 판례로 인용하면 그것이 확정한 바를 과장하는 것이다. 그래서 `kind` 값도 따로 있다(`committee_decision_text`, `administrative_appeal_text`). -Use `referenced_articles` (interpretations, cases) and `reviewed_articles` (decisions) to confirm a loaded authority actually concerns your target article before citing it for that article. In a bundle, `authority_article_mismatch` / `authority_article_unverified` / `authority_article_partial_match` gaps flag when eager-loaded authority points to different articles — cite only what `current_authorities` contains, not everything in `loaded`. +권위를 특정 조문에 붙이기 전에 `referenced_articles`(해석·판례)와 `reviewed_articles`(결정)로 실제로 그 조문을 다루는지 확인하라. 번들에서는 `authority_article_mismatch` / `authority_article_unverified` / `authority_article_partial_match` gap이 어긋남을 알린다 — **`loaded` 전체가 아니라 `current_authorities`에 있는 것만 인용하라.** -## An agency with no records is not an agency that did nothing +## 12. 기록이 없는 기관이 아무것도 안 한 기관은 아니다 -`search-committee-decisions` and `search-administrative-appeals` get used to ask whether a regulator acted — which makes a zero-hit result the most dangerous result in the package, because the wrong reading points in exactly the direction the question was aimed. Zero hits are returned by all of: the agency never receiving a complaint, receiving one and not opening a case, deciding it but not publishing the 의결서, and the matter belonging to a different body's docket. None of those is "nothing happened." +`search-committee-decisions`와 `search-administrative-appeals`는 "감독기관이 실제로 움직였나"를 묻는 데 쓰인다. 그래서 **0건이 이 패키지에서 가장 위험한 결과**다 — 오독의 방향이 질문이 겨눈 방향과 정확히 일치하기 때문이다. -Two concrete traps: +0건이 나오는 경우들: 기관이 신고를 받은 적이 없다 / 받았지만 사건을 열지 않았다 / 의결했지만 의결서를 공개하지 않았다 / 그 사안이 다른 기관 소관이다. **어느 것도 "아무 일 없었다"가 아니다.** -- **Wrong docket reads as absence.** 소청, 조세, and 해양안전 rulings are *not* in the general `decc` list; they live in the special tribunals (`--tribunal acr|adap|tt|kmst`). A search that only asked `decc` has not covered them. -- **Wrong agency reads as absence.** The same conduct can sit with different regulators depending on which statute frames it. Try the other plausible `--committee` code before concluding anything. +구체적 함정 둘: -When the public record runs out, that is the point to reach for an official document request rather than to record a negative finding. +**소관을 잘못 잡으면 부재로 읽힌다.** 소청·조세·해양안전 재결은 일반 `decc` 목록에 없다. 특별 심판기관(`--tribunal acr|adap|tt|kmst`)에 있다. -## Constitutional doctrines are free-text search terms, not an index +**기관을 잘못 잡아도 부재로 읽힌다.** 같은 행위가 어느 법의 렌즈를 대느냐에 따라 다른 규제기관 소관이 된다. 결론 내기 전에 다른 `--committee` 코드를 시도하라. -`search_constitutional_decisions(search_body=True)` searches the `detc` source as **free text**. Doctrines like `과잉금지원칙` or `평등원칙` are query strings, not structured filters — law.go.kr exposes no doctrine/category field. A keyword search can surface candidate decisions, but it cannot prove doctrine-indexed coverage, and **it cannot support a "no constitutional risk" or "doctrine exhaustively covered" claim.** +공개 기록이 소진되는 지점은 부정적 결론을 기록할 자리가 아니라, **공식 자료요구를 할 자리**다. -This discipline fires even when the search returns zero hits — which is exactly when it matters most: +## 13. 헌법 원칙은 색인이 아니라 검색어다 + +`search_constitutional_decisions(search_body=True)`는 `detc`를 **자유 텍스트**로 검색한다. 과잉금지원칙·평등원칙 같은 도그마틱은 질의 문자열이지 구조화된 필터가 아니다 — law.go.kr에 도그마틱 필드가 없다. + +키워드 검색으로 후보를 띄울 수는 있어도, **"위헌 소지 없음"이나 "도그마틱 망라"를 뒷받침하지 못한다.** + +이 규율은 0건일 때도 뜬다. 가장 중요한 순간이 바로 그때이기 때문이다. ```bash -$ python -m moleg_api search-constitutional-decisions "과잉금지원칙" -{ "ok": true, "kind": "constitutional_hit_list", "count": 0, +$ moleg search-constitutional-decisions "과잉금지원칙" +{ "ok": true, "count": 0, "discipline": [ - "doctrine(과잉금지원칙 등)는 색인 아닌 자유텍스트 검색어 — '위헌 소지 없음'·doctrine 망라 단정 금지. get-constitutional-decision으로 로드.", + "doctrine(과잉금지원칙 등)는 색인 아닌 자유텍스트 검색어 — '위헌 소지 없음'·doctrine 망라 단정 금지…", "0건 — 이 검색어·범위로 못 찾음일 뿐, 부재의 증명 아님." ] } ``` -Load selected detail with `get_constitutional_decision()` before citing 판시사항, 결정요지, 심판대상조문, reviewed statutes, or full text. +## 14. 목차는 법령이 아니고, 요지는 판단이 아니다 -## Zero results and source-access failures are not proof of absence +`--toc`와 `--brief`는 로드 비용이 질문의 가치를 넘지 않게 하는 장치인데, 둘 다 **문서 하나 안에서** 후보-본문 간극을 다시 연다. -Two distinct outcomes both mean "not found here, now" — neither means "does not exist": +**`--toc`**는 `kind: law_toc_map`이지 `*_text`가 아니다. 조문 번호와 제목은 어디를 볼지 알려줄 뿐 무엇을 요구하는지 말하지 않는다 — 「제15조(개인정보의 수집·이용)」은 어떤 요건이 붙고 예외가 무엇인지 아무것도 알려주지 않는다. -**Zero-hit searches.** An empty `search_*` result is scoped to the exact query, source, and filter. It supports "this search returned zero hits," never "no such law / precedent / interpretation / delegated rule / annex / constitutional decision exists." Before any absence claim, disclose the searched terms and try alternate terms, source families, or detail loaders. The CLI returns these as `ok: true, count: 0` (exit 0) with the discipline *"0건 — 이 검색어·범위로 못 찾음일 뿐, 부재의 증명 아님."* An empty `find_delegated_rules()` (or its `NoResultError`) is likewise scoped to the searched law/article, not proof of no delegation. +**`--brief`**는 법원·기관 자신의 요약을 주고 전문을 뺀다. 결정요지는 부연이다. **판시 문구의 축자 인용은 전체 로드에서 나와야 한다.** `flags.brief.withheld`가 무엇을 뺐는지 정확히 알려 주며, 문서에 실제로 있던 것만 보고한다 — 원래 없던 항목이 있는 것처럼 보이지 않는다. -**Source-access failures.** `RateLimitError` and `RetryExhaustedError` are *temporary source problems*, not zero-hit results. The CLI reports them as `kind: source_access_error` (exit 3), kept structurally distinct from a zero-hit search (exit 0) and from `no_result` (exit 4). A rate limit must never collapse into "no current law" or "no source exists." (`ParseFailureError` — a response that could not be normalized — is also distinct: it is not source absence either.) +## 15. 0건과 접근 실패는 다르고, 둘 다 부재가 아니다 -| Outcome | CLI `kind` | Exit | Means | +| 결과 | CLI `kind` | 종료 | 의미 | |---|---|---|---| -| Zero-hit search | `*_hit_list`, `count: 0` | 0 | Nothing matched this query/scope | -| Multiple identities | `ambiguous` | 2 | Surface candidates; do not pick | -| Rate limit / retry / parse | `source_access_error`, `parse_error` | 3 | Temporary — retry, not absence | -| Loader found no body | `no_result` | 4 | No source text for this identifier | -| Law name given to a loader | `needs_search_first` | 5 | Search for the `law_id` first | - -See [CLI reference](CLI-Reference.md) for the full exit-code contract. - -## A table of contents is not the statute, and a 요지 is not the ruling - -`get-law --toc` and `--brief` exist to keep a load from costing more than the question is worth, and both re-open the candidate-vs-body gap *inside* a single document. +| 0건 검색 | `*_hit_list`, `count: 0` | 0 | 이 질의·범위에 일치 없음 | +| 복수 신원 | `ambiguous` | 2 | 후보를 제시하라, 고르지 마라 | +| 접근 실패 | `source_access_error` | 3 | **일시적** — 재시도 | +| 정규화 실패 | `parse_error` | 3 | **영구** — 재시도 무의미, 식별자를 의심하라 | +| 본문 없음 | `no_result` | 4 | 이 식별자에 본문 없음 | +| 이름을 로더에 | `needs_search_first` | 5 | 먼저 `law_id`를 찾아라 | -- `--toc` returns `kind: law_toc_map`, not `*_text`. An article's number and title tell you where to look, never what it requires — 「제15조(개인정보의 수집·이용)」 does not tell you which conditions apply or what the exceptions are. Load the article before saying what it does. -- `--brief` returns the court's or agency's own précis with the full body withheld. A 결정요지 paraphrases; a verbatim quotation attributed to a ruling must come from the full text. `flags.brief.withheld` lists exactly which sections were held back, and reports only those the document actually had — so it never implies a section exists that the source never carried. - -## `AmbiguousLawError` is not permission to pick the first candidate - -When a request matches multiple plausible law identities, the package raises `AmbiguousLawError`, which carries a `candidates` list. This is an **ambiguity to surface**, not a signal to silently take `candidates[0]`. Multiple plausible hits from a law-name search are the same: an ambiguity, not a default. - -```python -from moleg_api.errors import AmbiguousLawError - -try: - ... -except AmbiguousLawError as exc: - for cand in exc.candidates: # surface these — do not auto-select - ... -``` +**호출 제한이 "현행법 없음"으로 붕괴되면 안 된다.** 부재를 주장하기 전에 검색어와 출처 계열과 필터를 정확히 밝혀라. -The CLI reports this as `kind: ambiguous` (exit 2) with the discipline *"모호성이지 첫 후보를 고를 허가가 아님 — 후보를 사용자에게 제시."* Same-name candidates that differ by effective date are the common case: load the current one with `get_law(law_id)` and a specific historical version with `as_of=`, but only after you (or the user) have chosen which one. +빈 `find_delegated_rules()` 결과 역시 그 법령·조문에 한정된 것이지 위임이 없다는 증명이 아니다. 그리고 **위임 목록에 별표는 안 들어온다** — 과태료 기준표 같은 것은 `search_annex_forms`로 따로 찾아야 한다. -## See also +## 관련 문서 -- [Quickstart](Quickstart.md) — install and first calls -- [API guide](API-Reference.md) — when to use each interface -- [Follow-up lookups](Core-Concepts.md) — turning a candidate into the next executable call -- [CLI reference](CLI-Reference.md) — envelope, exit codes, and the search→load discipline -- [Source coverage and limits](Sources-and-Coverage.md) — supported source families and what is out of scope +- [Core Concepts](Core-Concepts.md) — 이 함정들이 왜 생기는지 +- [Agent Integration](Agent-Integration.md) — 답을 쓰기 전 점검표 +- [Historical Versions](Historical-Versions.md) — 시점 관련 함정 +- [Error Handling](Error-Handling.md) — 종료코드 계약 +- [Sources & Coverage](Sources-and-Coverage.md) — 출처 계열별 한계 diff --git a/docs/wiki/Historical-Versions.md b/docs/wiki/Historical-Versions.md index 0433a02..9bb0659 100644 --- a/docs/wiki/Historical-Versions.md +++ b/docs/wiki/Historical-Versions.md @@ -1,103 +1,118 @@ -# Historical Versions +# 과거 판본 -By default, [`get_law`](API-Reference.md) and [`get_article`](API-Reference.md) load a statute's **current** consolidated text. To load the text of a statute or article **as it was in force on a past date**, pass `as_of=` (Python) or `--as-of ` (CLI). The loader resolves the version whose 시행일 (effective date) is the latest on or before your date and returns that version's text. +기본적으로 `get_law`와 `get_article`은 **현행** 통합본을 싣는다. 과거 시점에 시행 중이던 텍스트가 필요하면 `as_of`를 준다. ```python -from moleg_api import MolegApi +current = api.get_article("001248", "제3조") +past = api.get_article("001248", "제3조", as_of="2015-01-01") -api = MolegApi() - -current = api.get_article("001248", "제3조") # current version -past = api.get_article("001248", "제3조", as_of="2015-01-01") # version in force on 2015-01-01 - -print(current.effective_date) # e.g. 20260102 -print(past.effective_date) # 20140101 +print(current.effective_date) # 예: 20260102 +print(past.effective_date) # 20140101 ← 요청일이 아니라 실제로 실린 판본의 시행일 ``` ```bash -python -m moleg_api get-article --law 001248 제3조 # current version -python -m moleg_api get-article --law 001248 --as-of 2015-01-01 제3조 # historical version +moleg get-article --law 001248 제3조 +moleg get-article --law 001248 --as-of 2015-01-01 제3조 ``` -Both `get_law` (whole statute) and `get_article` (one article) accept `as_of`. +로더는 **시행일이 요청일 이하인 것 중 가장 최신** 판본을 고른다. 위 예에서 2015-01-01을 물으면 시행일 20140101 판본이 오는데, 그게 그날 효력을 갖던 판본이기 때문이다. + +## 왜 별도 페이지인가 — 조용한 현행 함정 + +이게 이 패키지에서 가장 조용히 틀리는 지점이다. + +**law.go.kr에 `법령ID + 시행일`로 상세를 조회하면, 오류 없이 현행 텍스트가 돌아온다.** 과거 날짜를 줘도 그렇다. 예외도, 경고도, 아무 표시도 없다. 그대로 믿으면 "2015년 당시 법은 이랬습니다"라고 하면서 2026년 조문을 인용하게 된다. + +판본을 고정하는 유일한 키는 **MST(법령일련번호)**다. `ID`는 법을 가리키고, `MST`는 그 법의 특정 판본을 가리킨다. + +그래서 `as_of`는 날짜를 그냥 통과시키는 것이 아니라 세 단계를 밟는다. + +1. 그 법령의 판본 목록을 조회한다 (각 행이 MST · 시행일 · 공포번호를 가진다) +2. 시행일이 `as_of` 이하인 것 중 최신 판본을 고른다 +3. **그 MST로 다시 싣는다** -## Always read the returned `effective_date` +이 보정 경로는 필요할 때만 탄다. 평범한 현행 조회는 여전히 호출 한 번이다. -`as_of` is a *request*, not a guarantee. Confirm which version you actually got by reading the `effective_date` on the returned identity or article — it is the 시행일 of the version that was loaded, not the date you asked for. In the example above, asking for `2015-01-01` returns the version whose 시행일 is `20140101`, because that was the version in force on your date. +**실무적 결론: 원시 엔드포인트에 `efYd`를 직접 붙여 과거 판본을 기대하지 마라.** `as_of`를 쓰고, 돌아온 `effective_date`를 확인하라. -`effective_date` is exposed on: +## 돌아온 `effective_date`를 반드시 확인하라 -- [`ArticleText.effective_date`](API-Reference.md) — the loaded article's version date. -- [`LawText`](API-Reference.md)`.identity.effective_date` — the loaded statute version's date. +`as_of`는 *요청*이지 보장이 아니다. 실제로 무엇이 실렸는지는 반환된 신원의 `effective_date`가 말해 준다. -In CLI output it also surfaces in the envelope's `flags.effective_date`, alongside the echoed `flags.as_of`. +| 어디에 | 필드 | +|---|---| +| Python | `ArticleText.effective_date`, `LawText.identity.effective_date` | +| CLI | `flags.effective_date` (요청값은 `flags.as_of`에 그대로 반향) | -## When no version was in force yet +요청한 날짜와 실린 판본이 다르면 `flags.version_mismatch`가 `{requested, loaded}` 형태로 붙는다. -If you request a date **before the statute was first enacted** (or before any tracked version's 시행일), no historical version can be resolved, and the loader falls back to the current text. This is detectable: +## 요청일에 시행 중인 판본이 없을 때 -- **Python** — the returned `effective_date` will be *later* than your `as_of` date. Compare them. -- **CLI** — the envelope sets `flags.version_request_unfulfilled: true` and adds a `discipline` note. When this flag is present, the text you received is **not** the version in force on your requested date. +법이 제정되기 전 날짜를 물으면 그 시점에 시행 중이던 판본이 없다. ```bash -python -m moleg_api get-article --law 001248 --as-of 1950-01-01 제1조 +$ moleg get-article --law 001248 --as-of 1950-01-01 제1조 ``` - ```json { "flags": { - "basis": "effective", "effective_date": "20260102", - "as_of": "1950-01-01", + "as_of": "19500101", + "version_mismatch": { "requested": "19500101", "loaded": "20260102" }, "version_request_unfulfilled": true } } ``` -Here the returned `effective_date` (`20260102`) is far later than the requested `1950-01-01`, so the loaded text is the current version, not a 1950 one — the statute simply had no version in force on that date. Always name the reference date you actually loaded when you cite historical text. - -## Why `as_of` exists — the silent-current trap +실린 `effective_date`(20260102)가 요청일보다 훨씬 뒤다 — **요청한 시점의 판본이 아니다.** -law.go.kr's plain detail lookups (`ID` + `efYd`) do **not** select a past version. Handed a numeric law id and a non-current effective date, the source silently returns *current* text without any error. A version is pinned only by its master sequence number (MST, 법령일련번호), which is version-specific. +통합본 커버리지 자체가 시작되기 전이면 Python에서는 `AsOfBeforeCoverageError`가 난다. 이 예외는 `.earliest_available`을 들고 오므로 어디까지 거슬러 갈 수 있는지 바로 알 수 있다. 이건 **영구 조건이지 일시적 실패가 아니다** — 재시도해도 소용없고, 개정 연혁으로 가야 한다. -So `as_of` does more than pass a date through. The loader: +## 과거 텍스트 vs 개정이 바꾼 것 -1. Lists the statute's effective-date version rows (each carrying an MST, 시행일, and 공포번호). -2. Resolves the version whose 시행일 is the latest on or before `as_of`. -3. Reloads that version by its MST — the only key that actually pins a version. +두 질문은 다르다. -This correction path is only taken when needed, so ordinary current-text loads keep their single source call. The practical takeaway: **do not construct your own `efYd`-based date lookups against the raw endpoints expecting a past version** — use `as_of`, which performs the MST resolution for you, and then verify `effective_date`. +**"그날 조문이 어떠했나"** → `as_of`. 그 시점의 전문을 준다. -## Historical text vs. what an amendment changed - -`as_of` gives you the *full text as it stood* on a date. It does not tell you what a specific amendment **changed** relative to the prior version. For a before/after delta of the amended articles, use [`compare_law_versions`](API-Reference.md): +**"이 개정이 무엇을 바꿨나"** → `compare_law_versions`. 전후 대비를 준다. ```python diff = api.compare_law_versions("001248") for change in diff.changes: - print(change.article) - print("before:", change.before_text) - print("after:", change.after_text) + print(change.article, change.before_text, change.after_text) ``` +`compare_law_versions`는 law.go.kr이 자체적으로 노출하는 전후 비교 표면을 쓴다. **임의의 두 날짜 구간은 받지 않는다** — `before`/`after`에 날짜를 주면 `UnsupportedFormatError`다. 출처가 그걸 못 한다. + +임의의 두 시점을 비교하려면 `as_of`를 달리해 두 번 싣고 직접 비교하라. 어떤 개정들이 있었는지 열거하려면 `trace_law_history`를 쓴다. + +**"왜 바꿨나"** → `get_revision_reason`. 「개정이유 및 주요내용」을 준다. 다만 이건 제안자의 자기 진술이며 중립 요약이 아니고, 지정한 판본 하나에만 적용된다. + +세 명령을 잇는 실용 경로: + ```bash -python -m moleg_api compare-law-versions --law 001248 +moleg trace-law-history --law 001248 # 어떤 개정들이 있었나 → identity.mst 확보 +moleg get-revision-reason --law 001248 --mst # 그 개정은 왜 +moleg compare-law-versions --law 001248 # 무엇이 바뀌었나 ``` -`compare_law_versions` loads the before/after comparison pair that law.go.kr itself exposes for the statute (the source's `oldAndNew` surface). It does not accept arbitrary two-date windows — passing `before`/`after` dates raises `UnsupportedFormatError`, because the source does not support that comparison. To reconstruct a delta across two arbitrary dates, load each version with `as_of` and compare the texts yourself; to enumerate which amendments exist and their dates, use [`trace_law_history`](API-Reference.md). +## 날짜 형식 -## Accepted date formats +`YYYY-MM-DD`와 `YYYYMMDD`를 받는다. 내부적으로 8자리로 정규화된다. -`as_of` accepts `YYYY-MM-DD` (the CLI's documented form), `YYYY.M.D`, or a bare `YYYYMMDD` — all are normalized internally to the 8-digit form the source uses. +CLI는 **달력 검증까지 한다** — 13월, 2월 30일, 99일은 거부된다. 그리고 형식이 틀리면 **조용히 현행으로 떨어지지 않고** 사용 오류(종료코드 5)를 낸다. 잘못된 날짜가 아무 표시 없이 현행 조회가 되는 것이 이 페이지가 막으려는 바로 그 실패이기 때문이다. -## Notes and caveats +## 주의할 점 -- `as_of` applies to the `effective` basis (current-force text). It has no effect when loading `basis="promulgated"` text, which is keyed differently. -- Version resolution depends on the statute's effective-date version list being available from the source. If that list cannot be retrieved and the direct lookup also fails, the load raises rather than silently returning current text. -- An empty or failed version list is never proof that no past version exists — it is scoped to what the source returned for that lookup. +- `as_of`는 `basis="effective"`에 적용된다. `basis="promulgated"`는 키가 다르므로 효과가 없다. +- 판본 해석은 출처가 판본 목록을 돌려주는 데 달려 있다. 목록도 못 받고 직접 조회도 실패하면 **조용히 현행을 주는 대신 예외를 낸다.** +- 판본 목록이 비었다고 과거 판본이 없다는 증명은 아니다. 그 조회로 그렇게 돌아왔을 뿐이다. +- **미래 판본에 주의하라.** 시행일이 오늘보다 뒤인 판본은 `flags.not_effective_as_of`가 붙고 `source` 표기가 「공포본(장래 시행 — 아직 미시행)」으로 바뀐다. 이건 `--as-of`를 줬는지와 무관하게 뜬다. +- `get_revision_reason`에서 `mst`도 `as_of`도 주지 않으면 **파일상 가장 최신 시행일 판본**을 쓰는데, 이게 미래 시행 판본일 수 있다. "최신"과 "현재 효력"은 다르다. -## See also +## 관련 문서 -- [API Reference](API-Reference.md) — full signatures for `get_law`, `get_article`, `compare_law_versions`, and `trace_law_history`. -- [Command-Line Interface](CLI-Reference.md) — the JSON envelope, `flags`, and `discipline` notes. -- [Data Models](API-Reference.md) — `ArticleText`, `LawText`, `LawDiff`, and the `effective_date` field. +- [Core Concepts](Core-Concepts.md) — 식별자 체계와 시행 vs 공포 +- [API Reference](API-Reference.md) — `get_law`, `get_article`, `compare_law_versions`, `trace_law_history`, `get_revision_reason` 시그니처 +- [Gotchas](Gotchas.md) — 시점 관련 함정 모음 +- [Error Handling](Error-Handling.md) — `AsOfBeforeCoverageError` diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md index f4ce32f..26b1ccd 100644 --- a/docs/wiki/Home.md +++ b/docs/wiki/Home.md @@ -1,68 +1,60 @@ -# moleg-api +# moleg-api 위키 -**moleg-api** is a task-level Python interface for loading Korean legal sources from [law.go.kr](https://www.law.go.kr/). It normalizes law.go.kr's MOLEG OpenAPI responses so that applications and agent systems can retrieve legal context without memorizing individual `target` values, identifier rules (`ID`, `MST`, `LID`, `JO`, …), or per-endpoint response-shape quirks. You pick an interface by the legal work you need to do — search a statute, load an article, trace an amendment, find a delegated rule, read a Constitutional Court decision — not by a raw endpoint name. +`moleg-api`는 법제처 [law.go.kr](https://www.law.go.kr/) OpenAPI를 **법적 과업 단위**로 감싼 Python SDK이자 CLI다. PyPI 패키지명은 `moleg-api`, 임포트는 `moleg_api`. -The PyPI package name is `moleg-api`; import it as `moleg_api`. Live calls use a law.go.kr OpenAPI credential (the "OC"), but the package ships with a shared default, so calls work out of the box with **no registration required**. This is not a complete wrapper around every law.go.kr endpoint: the public interface is intentionally small and deep. The recurring pattern is **search or plan first, then load** the selected source text, authority detail, delegated-rule context, or annex/form body — search hits are candidates, not citable source text. +핵심 원리 한 줄: **검색으로 후보를 고르고, 로더로 본문을 싣는다.** -## What it covers +이 위키는 그 원리에서 파생되는 모든 것을 다룬다 — 왜 검색 결과를 인용하면 안 되는지, 왜 응답에 `discipline` 문자열이 실려 오는지, 왜 조문 번호를 여섯 자리로 바꿀 일이 없는지. -- Current (`effective`) and promulgated (`promulgated`) statutes -- Statute articles, including moved/deleted article status -- Supplementary provisions (부칙) -- Law history and before/after text comparison -- Delegated rules and legal hierarchy (statute → enforcement instrument → administrative rule) -- Administrative rules — 고시 (notices), 훈령 (directives), 예규 (established rules) -- Law and administrative-rule annex/form bodies (별표·서식), with optional structured table rows -- MOLEG official interpretations and ministry first-instance interpretations -- Ordinary court cases and Constitutional Court (헌재) decisions -- Query expansion, comparable-mechanism discovery, and staged context bundles with executable follow-up lookups +## 어디서부터 읽을 것인가 -## What it does not cover +**처음이라면** → [Installation](Installation.md) → [Quickstart](Quickstart.md) → [Core Concepts](Core-Concepts.md) -- **Legal advice.** moleg-api loads legal sources; it does not interpret them for a decision. -- **National Assembly bill data.** Bill status, sponsors, votes, and minutes are out of scope — use a separate bill source. -- **Latest statistics, news, or policy announcements.** Use WebSearch or another current external source. -- Empty search results are scoped to the exact query, source family, and filters used — never proof that no relevant source exists. -- MOLEG interpretation, ministry interpretation, ordinary court case, and Constitutional Court decision have different source authority and stay separate — they are not interchangeable. +**LLM 에이전트에 물리려 한다면** → [Core Concepts](Core-Concepts.md) → **[Agent Integration](Agent-Integration.md)** → [Gotchas](Gotchas.md) +이 패키지의 1차 소비자가 에이전트다. 엔벨로프의 세 신호, `catalog` 자기기술 명세, 종료코드 5종, 컨텍스트 예산 장치는 전부 그쪽을 위해 만들어졌다. -## Quick start +**특정 자료를 찾는다면** → [Sources & Coverage](Sources-and-Coverage.md)에서 출처 계열을 확인하고 [API Reference](API-Reference.md) 또는 [CLI Reference](CLI-Reference.md)로. -Search returns candidates; load the selected detail before citing legal substance. +**과거 시점의 법을 물어야 한다면** → [Historical Versions](Historical-Versions.md). 여기엔 조용히 틀리는 함정이 하나 있어 별도 페이지를 뒀다. -**Python** +**코드를 고치려 한다면** → [Architecture](Architecture.md) → [Maintainer Notes](Maintainer-Notes.md) → [CONTRIBUTING.md](../../CONTRIBUTING.md) -```python -from moleg_api import MolegApi +## 전체 목차 -api = MolegApi() +| 페이지 | 내용 | +|---|---| +| [Installation](Installation.md) | 설치, OC 자격증명 설정, 자격증명 해석 순서, macOS SSL 사정 | +| [Quickstart](Quickstart.md) | 검색→로드 왕복, 엔벨로프 해부, 번들과 후속 조회 | +| [Core Concepts](Core-Concepts.md) | 여섯 개념 — 검색→선택→로드, 후보 vs 본문, 시행 vs 공포, 식별자 체계, 권위 유형, 단계적 번들 | +| [Agent Integration](Agent-Integration.md) | `flags`·`discipline`·`next` 세 신호, `catalog` 계약, 종료코드, 컨텍스트 예산, 의도 게이트 | +| [CLI Reference](CLI-Reference.md) | 32개 과업 서브커맨드 + `catalog` 전수, 옵션, 엔벨로프 키, 종료코드 | +| [API Reference](API-Reference.md) | 33개 공개 메서드 시그니처 전수, 공개 데이터클래스, 직렬화 계약 | +| [Historical Versions](Historical-Versions.md) | `as_of`, MST가 판본을 고정하는 이유, "조용한 현행" 함정, 커버리지 하한 | +| [Sources & Coverage](Sources-and-Coverage.md) | 출처 계열 → 메서드/명령 대응표, 위원회·심판기관 코드, 40개 부처, 범위 밖 | +| [Gotchas](Gotchas.md) | 조용히 틀리는 지점 모음 | +| [Error Handling](Error-Handling.md) | 예외 계층, 일시적 실패 vs 영구 부재, 재시도 정책 | +| [Architecture](Architecture.md) | 4계층 구조, 22개 믹스인 합성, 정규화 계층이 푸는 문제 | +| [Maintainer Notes](Maintainer-Notes.md) | 호환 파사드, 파일 크기 가드레일, 알려진 중복, 릴리스 절차 | -hits = api.search_laws("자동차관리법", basis="effective", display=5) -identity = hits[0].identity +## 무엇을 다루고 무엇을 안 다루는가 -article = api.get_article(identity, "제26조", basis="effective") -print(article.text) -``` +커버리지 상세는 [Sources & Coverage](Sources-and-Coverage.md)에 있고, 여기서는 경계만 밝힌다. -**CLI** — every method is also a `moleg` subcommand that prints one JSON envelope: +**다룬다** — 법령 본문과 조문, 부칙, 연혁과 개정 전후 비교, 과거 판본, 개정이유, 위임 하위규범과 법령 체계도, 행정규칙, 별표·서식 본문, 법제처·부처 법령해석, 대법원 판례, 헌재 결정, 12개 위원회 의결과 행정심판 재결, 질의 확장과 단계적 컨텍스트 번들. -```bash -python -m moleg_api catalog # self-documenting entry point -python -m moleg_api search-laws "자동차관리법" # → candidates with law_id -python -m moleg_api get-article --law 001760 제26조 # load a chosen candidate -``` +**안 다룬다** -## Navigation +- **법률 자문.** 이 패키지는 법적 출처를 싣는다. 그것을 해석해 결론을 내지 않는다. +- **국회 의안 데이터.** 의안 진행 상황·발의자·표결·회의록은 범위 밖이다. 별도 출처를 써라. +- **입법예고.** 국민참여입법센터는 별개 시스템이다. +- **최신 통계·뉴스·정책 발표.** 웹 검색이나 다른 현재 출처를 써라. -- [Installation & Setup](Installation.md) — install the package and configure your own OC credential. -- [Quickstart](Quickstart.md) — the search-then-load flow and staged bundles, end to end. -- [API Reference](API-Reference.md) — every public `MolegApi` method and when to use it. -- [Command-Line Interface](CLI-Reference.md) — the `moleg` subcommands, the JSON envelope, and exit codes. -- [Follow-up Lookups](Core-Concepts.md) — how candidate results carry the next executable call. -- [Source Coverage & Limits](Sources-and-Coverage.md) — supported source families and what stays out of scope. -- [Data Models](API-Reference.md) — the public dataclasses and their serialization. -- [Error Handling](Error-Handling.md) — the exception hierarchy and source-access versus legal-absence. -- [Maintainer Notes](Maintainer-Notes.md) — package layout, compatibility facades, and refactor guardrails. +명시적으로 못박아 둘 두 가지가 더 있다. -## Status +**검색 0건은 부재의 증명이 아니다.** 그 검색어와 그 출처 계열, 그 필터 조합으로 못 찾았다는 뜻일 뿐이다. -The package is alpha (`0.2.x`). The interface is ready for package-level experimentation, but law.go.kr live behavior can vary by source, credential, and endpoint availability. Treat search results as candidates until a selected detail loader has retrieved source text. moleg-api is a legal-source loader, not legal advice. +**권위 유형은 평탄화되지 않는다.** 법제처 해석, 부처 1차 해석, 대법원 판례, 헌재 결정, 위원회 의결, 행정심판 재결은 출처 권위가 서로 다르고 서로 대체하지 못한다. 이 패키지는 그 구분을 타입과 `source_type`/`source_authority` 필드로 끝까지 보존한다. 답을 쓸 때 뭉개면 그 보존이 무의미해진다. + +## 상태 + +알파 (`0.3.0`). 인터페이스는 쓸 만하지만 law.go.kr의 라이브 동작은 출처·자격증명·엔드포인트 가용성에 따라 달라진다. 검색 결과는 로더가 본문을 실어 오기 전까지 후보다. diff --git a/docs/wiki/Installation.md b/docs/wiki/Installation.md index 36f038e..c23c8d6 100644 --- a/docs/wiki/Installation.md +++ b/docs/wiki/Installation.md @@ -1,99 +1,127 @@ -# Installation +# 설치와 설정 -MOLEG-API is a task-level Python interface for loading Korean legal sources from [law.go.kr](https://www.law.go.kr/). This page covers installing the package, the OC credential it uses to call law.go.kr, and the editable install used for development. +## 요구 사항 -## Requirements +- **Python 3.10 이상** (3.10 / 3.11 / 3.12에서 CI 검증) +- **런타임 의존성 0개** — 표준 라이브러리만 쓴다. `urllib`, `json`, `ssl`, `dataclasses`가 전부다. -- Python **3.10 or newer**. -- No third-party runtime dependencies. The package uses only the Python standard library, so a plain `pip install` pulls in nothing else. +의존성이 없는 것은 설계 결정이다. 이 패키지는 다른 도구의 의존성 트리 안에 얹히는 것을 전제로 만들어졌고, 거기서 버전 충돌을 일으키지 않는 편이 낫다고 봤다. -## Install with pip +## 설치 ```bash pip install moleg-api ``` -The distribution name is `moleg-api`; the import name is `moleg_api`: +배포명은 하이픈(`moleg-api`), 임포트는 밑줄(`moleg_api`)이다. -```python -import moleg_api -from moleg_api import MolegApi +타입 힌트가 함께 배포된다(PEP 561, `py.typed` 포함). mypy·pyright가 별도 스텁 없이 바로 읽는다. -api = MolegApi() -``` +설치하면 `moleg` 콘솔 명령이 등록된다. 체크아웃에서 바로 쓰려면 `python -m moleg_api`도 같게 동작한다. -Installing the package also registers a `moleg` console command, so every method is available from a shell without writing Python: +### 소스에서 ```bash -moleg catalog # self-documenting entry point -moleg search-laws "주택임대차보호법" # → candidates with law_id, mst +git clone https://github.com/tjdwls101010/MOLEG-API.git +cd MOLEG-API +pip install -e ".[test]" ``` -You can also invoke the CLI as a module, which is convenient from a checkout: +개발 환경 상세는 [CONTRIBUTING.md](../../CONTRIBUTING.md)를 보라. + +## 동작 확인 ```bash -python -m moleg_api catalog +moleg --version # moleg-api 0.3.0 +moleg catalog # 네트워크 없이 명령 명세를 찍는다 ``` -## The OC credential +`catalog`는 자격증명도 네트워크도 쓰지 않는다. 이건 되는데 다른 명령이 안 되면, 문제는 설치가 아니라 네트워크나 자격증명 쪽이다. -Live law.go.kr calls use an OpenAPI credential called the **OC** — a law.go.kr account's email id. It is **free** and **not a secret** (it travels as a plain query parameter on every request). - -The package ships with a shared default OC, so calls work **out of the box with no registration and no configuration**: +라이브 호출까지 확인하려면: ```bash -moleg search-laws "자동차관리법" --display 2 +moleg search-laws "주택임대차보호법" --display 3 ``` -The shared default funnels all traffic through one credential and can hit law.go.kr rate limits. If you call the API heavily, register your own OC at law.go.kr and set it. +## OC 자격증명 -### Setting your own OC +law.go.kr OpenAPI는 “OC”라는 계정 식별자를 쿼리 파라미터로 요구한다. **무료이고 비밀이 아니다** — 모든 요청 URL에 평문으로 실려 나간다. -Export it as an environment variable: +패키지에는 공용 기본값(`chunghun1`)이 들어 있어 **등록 없이 바로 동작한다.** 실험하거나 가볍게 쓸 때는 그대로 두면 된다. -```bash -export MOLEG_OC="your-law-go-kr-oc" -``` +다만 OC는 계정 단위로 호출량이 관리된다. 공용 기본값은 모든 사용자의 트래픽을 한 자격증명으로 몰아넣으므로 law.go.kr의 호출 제한에 걸릴 수 있다. 실사용에서는 [law.go.kr 오픈API](https://open.law.go.kr/)에서 자기 것을 발급받아 쓰는 편이 낫다. -`MOLEG_OC` is also read from a local `.env.local` or `.env` file in the current working directory (a plain `MOLEG_OC=your-oc` line). This lets the CLI pick up your OC without exporting it in every shell. - -From Python, pass your OC explicitly by constructing a client and handing it to `MolegApi`: +### 설정 방법 — 우선순위 순 ```python +# 1. 생성자 인자 (가장 강함) from moleg_api import MolegApi, LawGoKrClient +api = MolegApi(source=LawGoKrClient(oc="내계정")) +``` + +```bash +# 2. 환경변수 +export MOLEG_OC=내계정 + +# 3. 현재 작업 디렉터리의 .env.local +echo 'MOLEG_OC=내계정' > .env.local -api = MolegApi(source=LawGoKrClient(oc="your-law-go-kr-oc")) +# 4. 현재 작업 디렉터리의 .env +echo 'MOLEG_OC=내계정' > .env ``` -The CLI has no `--oc` flag; it takes the OC from `MOLEG_OC` (environment or a local `.env`/`.env.local`), falling back to the bundled default. +넷 다 없으면 내장 기본값을 쓴다. CLI에는 `--oc` 플래그가 없다 — 2~4번 경로를 쓴다. -### Resolution order +**주의할 점 두 가지.** -When a client resolves the OC to use, it takes the **first** of these that is set: +`.env` 파일은 **패키지 위치나 저장소 루트가 아니라 현재 작업 디렉터리(`Path.cwd()`) 기준**으로 찾는다. 어디서 실행하느냐에 따라 결과가 달라진다. -1. The `oc=` argument to `LawGoKrClient` (Python only). -2. `MOLEG_OC`, read from the environment, then from `.env.local`, then from `.env` in the current working directory. -3. The bundled shared default. +읽은 값은 `os.environ`에 심어지지 않는다. 조회할 때마다 파일에서 다시 읽으며, 같은 프로세스의 다른 라이브러리는 이 값을 보지 못한다. -So an explicit `oc=` always wins over the environment, and any `MOLEG_OC` value wins over the default. +`python-dotenv` 의존성은 없다. 파싱은 직접 구현돼 있고 — 빈 줄과 `#` 주석을 건너뛰고, 첫 `=`에서 나누고, 값을 감싼 따옴표 한 쌍을 벗긴다. -## Development install +### 마스킹 -To work on the package from a checkout, install it in editable mode with the test extras: +OC는 비밀이 아니지만 그래도 새어 나가면 지저분하다. 모든 오류 메시지와 응답 스니펫에서 `***`로 치환되고, `HistoryEvent.article_link`에 실리는 law.go.kr 링크에서도 `OC=` 파라미터가 정규식으로 제거된다. -```bash -pip install -e ".[test]" +## 클라이언트 조정 + +기본값을 바꿔야 하면 `LawGoKrClient`를 직접 만들어 넘긴다. 인자는 전부 키워드 전용이다. + +```python +from moleg_api import MolegApi, LawGoKrClient + +api = MolegApi(source=LawGoKrClient( + oc="내계정", + timeout_seconds=30, # 기본 30 + max_retries=2, # 기본 2 → 총 3회 시도 + retry_delay_seconds=0.5, # 기본 0.5. 고정 대기이며 지수 백오프가 아니다 + ca_file=None, # 아래 참고 +)) ``` -The `test` extra adds `pytest`. Run the suite from the repository root: +재시도 대상은 HTTP `408 429 500 502 503 504`와 타임아웃·네트워크 오류다. 그 밖의 상태 코드는 재시도하지 않고 즉시 올린다. 자세한 것은 [Error Handling](Error-Handling.md). -```bash -pytest +## macOS에서 SSL 인증서 오류가 날 때 + +Homebrew로 설치한 Python은 CA 번들을 못 찾는 경우가 있다. 이 패키지는 다음 순서로 알아서 찾는다. + +1. `ca_file=`로 명시한 경로 +2. 시스템 기본 검증 경로 (`ssl.get_default_verify_paths()`) +3. `/etc/ssl/cert.pem` → `/opt/homebrew/etc/openssl@3/cert.pem` → `/usr/local/etc/openssl@3/cert.pem` 중 실제로 존재하는 첫 번째 +4. 기본 컨텍스트 + +그래도 `CERTIFICATE_VERIFY_FAILED`가 나면 경로를 직접 준다. + +```python +LawGoKrClient(ca_file="/opt/homebrew/etc/openssl@3/cert.pem") ``` -Tests that call live law.go.kr endpoints are marked `live` and are skipped by default; a `dev` extra is also available for the fuller development toolchain. +**인증서 검증을 끄는 경로는 제공하지 않는다.** 필요해 보인다면 그건 CA 번들 경로 문제이지 검증 문제가 아니다. -## Next steps +## 다음 -- [Quickstart](Quickstart.md) — first search-then-load calls in Python and the CLI. -- [CLI](CLI-Reference.md) — the JSON envelope contract, exit codes, and the search→load discipline. +- [Quickstart](Quickstart.md) — 첫 왕복 +- [Core Concepts](Core-Concepts.md) — 왜 검색과 로드가 나뉘어 있는지 +- [Agent Integration](Agent-Integration.md) — 에이전트에 물릴 때 diff --git a/docs/wiki/Maintainer-Notes.md b/docs/wiki/Maintainer-Notes.md index 354d16e..3f2e03f 100644 --- a/docs/wiki/Maintainer-Notes.md +++ b/docs/wiki/Maintainer-Notes.md @@ -1,10 +1,8 @@ -# Maintainer Notes +# 메인테이너 노트 -This page explains the package layout and the refactor guardrails used to keep `moleg-api` maintainable without changing the public API. +내부 구조 자체는 [Architecture](Architecture.md)에, 개발 환경과 PR 절차는 [CONTRIBUTING.md](../../CONTRIBUTING.md)에 있다. 이 페이지는 **유지보수 규칙**이다. -## Public compatibility surface - -Keep these imports stable: +## 깨뜨리면 안 되는 공개 표면 ```python from moleg_api import MolegApi @@ -15,58 +13,95 @@ import moleg_api.normalization import moleg_api.cli ``` -The public facade modules (`models.py`, `laws.py`, `normalization.py`, `cli.py`) are intentionally small compatibility layers. They re-export the implementation packages below so existing users do not need to change imports. +루트의 `models.py`·`laws.py`·`normalization.py`·`cli.py`는 **의도적으로 얇은 호환 계층**이다. 0.2.3에서 큰 파일들을 쪼갤 때 기존 임포트가 깨지지 않도록 남겼다. `tests/test_refactor_compat.py`가 이걸 잠그고 있다. -The CLI has 27 task subcommands plus `catalog`. `moleg catalog` is the source of truth for the command list, routing rules, and emitted `kind` values. When adding or removing a task method, update the parser, catalog, API reference, and CLI reference together. +`moleg_api/__init__.py`의 `__all__`이 큐레이션된 공개 목록(71개)이다. `moleg_api.models.__all__`은 동적으로 계산되며 `Any`·`Literal`·`dataclass` 같은 비모델 이름이 새어 나오므로, **공개 계약의 기준은 `moleg_api/__init__.py`다.** -## Internal layout +## 과업 하나를 추가·제거할 때 -The implementation is grouped by responsibility: +과업 메서드를 하나 건드리면 **네 곳을 함께 고쳐야 한다.** -| Package/module area | Responsibility | -|---|---| -| `_models/` | Public dataclasses, serialization helpers, and model re-exports. | -| `_normalization/` | law.go.kr payload parsing and source-specific normalization. | -| `_laws/api_*.py` | Public `MolegApi` mixin methods. These files should read as orchestration shells. | -| `_laws/*_pipeline.py`, `_laws/*_details.py`, `_laws/*_candidates.py` | Private helpers for staged loaders, candidate discovery, eager detail loading, and gap/deferred construction. | -| `_cli/` | CLI parser, dispatcher, signal generation, catalog, and JSON envelope entry point. | +1. `_laws/api_*.py` — 메서드 자체 +2. `_cli/parser.py` — 서브커맨드와 옵션 +3. `_cli/dispatch.py` — 라우팅 +4. `_cli/catalog.py` — 명령 목록·라우팅 규칙·`kind` + +그리고 문서 두 곳: [API Reference](API-Reference.md)와 [CLI Reference](CLI-Reference.md). + +`tests/test_command_smoke_0_3_0.py`가 catalog·parser·dispatch의 합치를 검사한다. argv를 파서에서 도출하므로 **새 명령이 자동으로 커버된다.** 이 테스트는 0.2.3 파일 분할에서 명령 분기 안에서만 실행되는 임포트 3개가 빠졌는데 CI가 그대로 통과한 사고 뒤에 생겼다. + +> **현재 알려진 불일치.** `catalog`의 `kinds` 목록에 코드가 실제로 배출하는 `catalog`와 `version_request_unfulfilled`가 빠져 있다. `catalog`를 단일 진실 공급원이라고 문서화하는 이상 이건 실제 계약 위반이다. 고칠 때 `_cli/catalog.py`의 `kinds`에 두 값을 추가하고 [Agent Integration](Agent-Integration.md)의 경고 문단을 지운다. + +## 버전 -Prefer adding a helper next to the domain that owns it (`bundle_*`, `authority_context_*`, `delegated_criteria_*`, `institutional_*`) instead of creating generic utility modules. A maintainer should be able to find the helper by starting from the public method name. +`moleg_api/_version.py`의 리터럴이 **단일 진실 공급원**이다. -## File-size tradeoff +`importlib.metadata`를 쓰지 않는 것은 의도적이다. 메타데이터는 *설치된 배포판*을 설명하지만 실제로 도는 코드는 다른 데서 올 수 있다 — 체크아웃이 `sys.path`에 있으면 site-packages를 가리므로, `python -m moleg_api`가 체크아웃 코드를 실행하면서 메타데이터는 설치된 릴리스를 보고한다. 그러면 "어느 버전이 답했나"에 답할 수 없다. -The goal is small, meaningful files, not the maximum number of files. Use these guardrails: +리터럴은 코드와 함께 이동하므로 엔벨로프가 보고하는 버전은 언제나 그 응답을 만든 버전이다. `pyproject.toml`이 `[tool.setuptools.dynamic]`으로 이 속성을 읽으므로 빌드와 런타임이 어긋날 수 없다. -- Public facade files should stay thin and compatibility-focused. -- Public `api_*.py` mixin files should show validation, high-level orchestration, and final return shape. -- Private helpers should stay below roughly 250 lines of executable code. A file can be slightly longer in total lines when whitespace, imports, and public API docstrings make the code easier to scan. -- Split a file when it mixes different responsibilities, such as candidate discovery and detail loading. -- Do not split a cohesive algorithm just to satisfy a line-count target; prefer a clear domain name over a generic `utils.py`. +## 파일 크기 가드레일 -Current large-loader split: +목표는 **작고 의미 있는 파일**이지 파일 개수 최대화가 아니다. + +- 루트 파사드는 얇고 호환 목적만 +- 공개 `api_*.py` 믹스인은 검증·고수준 오케스트레이션·최종 반환 모양을 보여야 한다 +- 비공개 헬퍼는 **실행 코드 250줄 안팎** 아래로. 공백·임포트·공개 API docstring 때문에 총 줄 수는 더 길어도 된다 +- 서로 다른 책임이 섞이면 쪼갠다 (예: 후보 탐색과 상세 로드) +- **줄 수를 맞추려고 응집된 알고리즘을 쪼개지 마라.** 범용 `utils.py`보다 도메인 이름이 낫다 + +헬퍼는 그것을 소유한 도메인 옆에 둔다(`bundle_*`, `authority_context_*`, `delegated_criteria_*`, `institutional_*`). **공개 메서드 이름에서 출발해 헬퍼를 찾을 수 있어야 한다.** + +현재 큰 로더의 분할: + +| 공개 메서드 | 분할된 모듈 | +|---|---| +| `load_legal_context_bundle` | `bundle_modes`, `bundle_primary`, `bundle_candidates`, `bundle_eager`, `bundle_finalize` | +| `load_authority_context` | `authority_context_pipeline`, `authority_context_details` | +| `load_institutional_system` | `institutional_resolution`, `institutional_pipeline`, `institutional_candidates` | +| `load_delegated_criteria` | `delegated_criteria_pipeline`, `delegated_criteria_details` | +| `load_followup` | `followup_routing`, `followup_routing_authority`, `followup_routing_bundle` | -- `load_legal_context_bundle` uses `bundle_modes`, `bundle_primary`, `bundle_candidates`, `bundle_eager`, and `bundle_finalize`. -- `load_authority_context` uses `authority_context_pipeline` and `authority_context_details`. -- `load_institutional_system` uses `institutional_resolution`, `institutional_pipeline`, and `institutional_candidates`. -- `load_delegated_criteria` uses `delegated_criteria_pipeline` and `delegated_criteria_details`. -- `load_followup` uses `followup_routing`, `followup_routing_authority`, and `followup_routing_bundle`. +## 후행 별표 임포트는 건드리지 마라 -## Regression checks +`_laws/`의 일부 support 모듈은 **파일 맨 아래에서** 형제 모듈을 별표 임포트한다. 남은 찌꺼기처럼 보이지만 **의도된 장치**다 — 임포트 순환이 있어도 네임스페이스를 평평하게 유지한다. -Before release, run: +모듈을 추가하면 `support.py`에 의존 순서대로 등록하고, 그 모듈이 필요한 후행 블록에도 넣어야 한다. + +## 릴리스 전 점검 ```bash python -m compileall moleg_api tests -q python -m pytest -q -m "not live" MOLEG_OC=chunghun1 python -m pytest -q -m live -python -m moleg_api catalog +moleg catalog ``` -For packaging: +패키징: ```bash python -m build python -m twine check dist/* ``` -The bundled default OC is `chunghun1`. It is a free law.go.kr account id, not a secret. Users can still override it with `LawGoKrClient(oc=...)` or `MOLEG_OC`. +전체 릴리스 절차(버전 올리기, 태그, GitHub Release, PyPI 배포)는 [CONTRIBUTING.md](../../CONTRIBUTING.md)의 릴리스 절에 있다. + +## 알려진 중복 — 손대기 전에 읽을 것 + +동작에는 영향이 없지만 코드를 고칠 때 헷갈리는 두 지점이다. 상세는 [Architecture](Architecture.md). + +**`_models`의 follow-up 원시 타입** — `Ambiguity`·`ContextGap`·`DeferredLookup`이 `followups.py`와 `bundles.py`에, `FollowUpSearch`가 `followups.py`와 `query.py`에 중복 정의돼 있다. 임포트 순서상 `followups` 쪽이 공개 네임스페이스를 이기고, 그림자 사본은 직렬화 메서드를 못 받는다. + +**`_normalization`의 헬퍼 4종** — `compact_whitespace`·`yes_no_or_none`·`is_deleted_article`·`article_text_marks_deleted`가 `primitives.py`와 `article_units.py`에 바이트 단위로 동일하게 정의돼 있다. `primitives` 쪽이 이긴다. + +## 기본 OC + +`chunghun1`. 무료 law.go.kr 계정 식별자이며 비밀이 아니다. 사용자는 `LawGoKrClient(oc=…)`나 `MOLEG_OC`로 덮어쓸 수 있다. + +비밀이 아니지만 모든 오류 메시지·응답 스니펫에서 마스킹되고, `HistoryEvent.article_link`의 링크에서도 제거된다. 이 동작은 `tests/test_source.py`가 잠그고 있다. + +## 관련 문서 + +- [Architecture](Architecture.md) — 4계층 구조와 정규화 계층 +- [CONTRIBUTING.md](../../CONTRIBUTING.md) — 개발 환경, 테스트, 브랜치, 커밋, 릴리스 +- [CHANGELOG.md](../../CHANGELOG.md) — 릴리스 이력 diff --git a/docs/wiki/Quickstart.md b/docs/wiki/Quickstart.md index 14317ed..e0b6e5f 100644 --- a/docs/wiki/Quickstart.md +++ b/docs/wiki/Quickstart.md @@ -1,156 +1,193 @@ -# Quickstart +# 빠른 시작 -Load your first Korean legal source in five minutes. `moleg-api` is a task-level loader for [law.go.kr](https://www.law.go.kr/): you pick an interface by the legal work you need — search a statute, load an article — not by a raw endpoint name. The recurring pattern is **search first, then load** the selected source text. - -This page shows the same workflow in the **Python API** and the **CLI** side by side. For installation details see [Installation](Installation.md); for the concepts behind the workflow see [Core Concepts](Core-Concepts.md). - -## Install +이 페이지는 첫 왕복을 Python과 CLI로 나란히 보여준다. 개념적 배경은 [Core Concepts](Core-Concepts.md), 설치는 [Installation](Installation.md)에 있다. ```bash pip install moleg-api ``` -The PyPI package name is `moleg-api`; import it as `moleg_api`. Live calls use a law.go.kr OpenAPI credential (the "OC"), but the package ships with a shared default, so calls work out of the box with **no registration required**. If you call the API heavily, set your own OC to avoid the shared credential's rate limits: - -```bash -export MOLEG_OC="your-law-go-kr-oc" -``` - -See [Installation](Installation.md) for OC resolution order and the editable install used for development. +등록 없이 바로 동작한다. 이유는 [Installation](Installation.md)의 OC 절을 보라. -## Search, then load +## 1. 검색하고, 고르고, 싣는다 -A search returns *candidates* — normalized identities you choose from. You load selected source text before citing any legal substance. Multiple candidates are ambiguity, not permission to pick the first hit, and an empty result is a scoped no-result, not proof that no legal source exists. +이 세 박자가 패키지 전체의 리듬이다. 검색은 **후보**를 주고, 로더는 **본문**을 준다. 후보는 인용할 수 없다. -### Python API +### Python ```python from moleg_api import MolegApi api = MolegApi() -# 1. Search for law identity candidates. -hits = api.search_laws("주택임대차보호법", basis="effective", display=5) +# 검색 — 후보 목록 +hits = api.search_laws("주택임대차보호법", display=5) if not hits: - raise RuntimeError("No law candidates") + raise RuntimeError("후보 없음") # 0건은 예외가 아니라 빈 리스트다 -# 2. Choose one candidate's identity and load a precise article. +# 고른다 — 첫 후보를 무조건 집는 것은 위험하다. 아래 "모호성" 절 참고 identity = hits[0].identity -article = api.get_article(identity, "제7조", basis="effective") + +# 싣는다 — 이제 인용할 수 있는 본문 +article = api.get_article(identity, "제7조") print(article.article) # 제7조 print(article.title) # 차임 등의 증감청구권 -print(article.text) # full article body with 항·호·목 +print(article.text) # 항·호·목까지 펼쳐진 본문 ``` -`search_laws` returns a list of `LawHit` values, each carrying a normalized `LawIdentity` (with `law_id`, `name`, `basis`, `effective_date`, and more). Pass the `LawIdentity` — or the `LawHit` itself — straight into `get_article`, so you never format law.go.kr's identifier or six-digit `JO` article code yourself. An empty list means no source rows; it does not raise. - -`get_article` returns an `ArticleText` with the article label, title, body, effective date, and normalized source identity. See [API Reference](API-Reference.md) for the full method and field list. +`LawHit`이든 그 안의 `LawIdentity`든 로더에 그대로 넘기면 된다. 여섯 자리 `JO` 코드로 조문 번호를 바꾸는 일은 없다 — `"제7조"`, `"제15조의2"`, 또는 정수 `7`을 그대로 쓴다. ### CLI -Every `MolegApi` method is also a `moleg` subcommand, so you can load sources from a shell without writing Python. Each call prints exactly one JSON envelope to stdout. +모든 메서드가 서브커맨드이고, 언제나 JSON 엔벨로프 하나를 stdout에 찍는다. ```bash -# 1. Search — returns candidates with law_id. -python -m moleg_api search-laws "주택임대차보호법" --display 5 - -# 2. Load a chosen candidate by its law_id (from step 1). -python -m moleg_api get-article --law 001248 "제7조" +moleg search-laws "주택임대차보호법" --display 5 +moleg get-article --law 001248 "제7조" ``` -The search envelope carries the candidates under `data`, each with an `identity.law_id` you pass to a loader. It also suggests the next command: +## 2. 엔벨로프 읽기 + +검색 응답은 이런 모양이다. ```json { "ok": true, "command": "search-laws", + "version": "0.3.0", "kind": "law_hit_list", "source": "법제처 / 법령검색", - "count": 1, + "count": 2, "data": [ { "identity": { "law_id": "001248", "name": "주택임대차보호법", "basis": "effective", + "mst": "276291", "effective_date": "20260102", "law_type": "법률" } } ], + "flags": { "ambiguous_versions": true }, + "discipline": ["…"], "next": [ - { "why": "후보 로드: 주택임대차보호법 (시행 20260102)", + { "why": "현행 판본 로드", "cmd": "moleg get-law --law 001248" } ] } ``` -Passing a **law name** to a loader is rejected (`needs_search_first`, exit code 5) — always resolve a `law_id` with `search-laws` first. The envelope contract, exit codes, and `kind` suffix conventions are documented in [CLI Reference](CLI-Reference.md). To see the commands and routing rules at any time, run `python -m moleg_api catalog`. +핵심 키 넷. + +- **`kind`** — 결과의 의미 유형. `_hit_list`·`_candidate`·`_planning`으로 끝나면 후보라서 인용할 수 없고, `_text`·`_context`·`_identity`로 끝나면 실린 본문이다. 접미사만 봐도 인용 가능 여부가 갈린다. +- **`version`** — 이 응답을 만든 코드의 버전. 항상 실린다. +- **`flags`** — 기계가 읽을 상태값. 위 예의 `ambiguous_versions`는 같은 이름의 법이 여러 판본으로 잡혔다는 뜻이다. +- **`next`** — 바로 실행 가능한 다음 명령. 최대 3개. + +`discipline`은 이 결과를 해석할 때 지켜야 할 규율이 한국어 문장으로 실려 온다. 에이전트에 물릴 거라면 [Agent Integration](Agent-Integration.md)에서 세 신호를 전부 다룬다. + +## 3. 로더에 법령 이름을 넘기면 거부된다 + +```bash +moleg get-article --law "주택임대차보호법" "제7조" +``` + +```json +{ "ok": false, "kind": "needs_search_first", + "next": [{ "why": "먼저 신원 검색", "cmd": "moleg search-laws \"주택임대차보호법\"" }] } +``` + +종료코드 5. 의도된 동작이다 — 이름은 법을 유일하게 가리키지 못한다. 반드시 `search-laws`로 `law_id`를 먼저 얻어야 한다. + +## 4. 모호성은 첫 후보를 고를 허가가 아니다 + +같은 이름에 여러 판본이나 여러 법이 걸리면 `AmbiguousLawError`가 나거나(Python) `kind: "ambiguous"`, 종료코드 2가 나온다(CLI). + +```python +from moleg_api import AmbiguousLawError + +try: + identity = api.resolve_promulgated_law(prom_law_nm="…", prom_no="…") +except AmbiguousLawError as exc: + for candidate in exc.candidates: # 후보를 사람에게 제시하라 + print(candidate) +``` + +`exc.candidates`를 그냥 `[0]`으로 집으면, 이 예외가 존재하는 이유를 무효로 만드는 것이다. -## Broad or under-specified questions: use a staged bundle +## 5. 과거 시점의 법 묻기 -When you do not yet have a specific law name — the question is broad or under-specified — start with `load_legal_context_bundle()` instead of a single search. The bundle runs one bounded first pass over likely law.go.kr sources and returns candidates, any source text it could safely auto-load, plus a list of **deferred** follow-up lookups you can choose to run next. The bundle loads sources, not conclusions. +```python +past = api.get_article(identity, "제7조", as_of="2021-01-01") +print(past.identity.effective_date) # 실제로 실린 판본의 시행일 +``` + +```bash +moleg get-article --law 001248 --as-of 2021-01-01 "제7조" +``` -### Python API +**반드시 돌아온 `effective_date`를 확인하라.** 요청한 날짜에 시행 중이던 판본이 없으면 그 이후 판본이 실려 오고, `flags.version_mismatch`에 `{requested, loaded}`가 붙는다. 이 함정은 [Historical Versions](Historical-Versions.md)에서 따로 다룬다. + +## 6. 질문이 넓을 때 — 단계적 번들 + +법령 이름을 아직 모르거나 질문이 넓으면 개별 검색 대신 번들로 시작한다. 한 번의 제한된 1차 통과로 여러 출처를 훑고, 실은 것과 후보만 잡은 것을 **분리해서** 돌려준다. ```python bundle = api.load_legal_context_bundle( query="자동차 방치 처리 기준", - mode="question", - budget="standard", + mode="question", # question | promulgated_bill | statute_review + budget="standard", # minimal | standard | broad ) -# Candidate law identities discovered in the first pass. -print([law.name for law in bundle.candidates.laws]) +bundle.loaded.laws # 실제로 실린 본문 — 인용 가능 +bundle.candidates.laws # 발견만 된 후보 — 인용 불가 +bundle.gaps # 아직 안 메워진 것과 그걸 메울 출처 +bundle.deferred # 실행 가능한 다음 조회 +``` -# What remains unverified, and which source should fill it. -print([gap.kind for gap in bundle.gaps]) +`deferred` 항목은 손으로 풀어 쓸 필요 없이 그대로 다시 넣으면 된다. -# Executable follow-ups. Run one with load_followup(). +```python for lookup in bundle.deferred: if lookup.interface == "load_administrative_rule_context": - rule_context = api.load_followup(lookup) - print(rule_context.rule.identity.name) + rule = api.load_followup(lookup) break ``` -`bundle.candidates` is a `CandidateContext` (its `laws` list holds `LawIdentity` values), `bundle.loaded` holds any source text already retrieved, `bundle.gaps` holds `ContextGap` values naming what is still unverified, and `bundle.deferred` holds `DeferredLookup` values. Pass any `DeferredLookup` (or `FollowUpSearch`) to `load_followup()` and it routes to the right task-level loader for you. - -`mode` accepts `"question"`, `"promulgated_bill"`, or `"statute_review"`; `budget` accepts `"minimal"`, `"standard"`, or `"broad"` and bounds how much the first pass loads. +CLI에서는 파이프로 흘려보낸다. -Handoffs whose interface is `websearch.*` or `congress-db.*` are **outside** `moleg-api` — passing one to `load_followup()` raises `UnsupportedFormatError`, because those facts belong to WebSearch and National Assembly bill systems, not law.go.kr. See [Core Concepts](Core-Concepts.md) for how the bundle and deferred lookups fit together, and [Gotchas](Gotchas.md) for the distinctions the gaps guard against. +```bash +moleg load-legal-context-bundle --query "자동차 방치 처리 기준" \ + | jq '.data.deferred[0]' \ + | moleg load-followup --json - +``` -### CLI +**손으로 쓴 JSON은 거부된다.** 이전 응답이 준 객체만 받는다. -```bash -python -m moleg_api load-legal-context-bundle "자동차 방치 처리 기준" \ - --mode question --budget standard +번들에 대해 알아둘 것 하나: `mode="question"`에서 법령 후보가 **여럿**이면 번들은 아무 것도 자동으로 고르지 않는다. `Ambiguity`와 `ContextGap`을 남기고 `search_laws` 후속 조회를 제안할 뿐이다. 고르는 것은 호출자의 몫이다. -# Run a deferred follow-up the bundle returned, by piping its JSON object: -echo '' | python -m moleg_api load-followup --json - -``` +`budget`이 로드량을 정하지만, 그게 전부가 아니다 — 질의 내용이 무엇을 실을지도 정한다. 자세한 것은 [Agent Integration](Agent-Integration.md)의 의도 게이트 절. -## Serialize results +## 7. 직렬화 -Every public model dataclass supports `to_dict()` and `to_json_string()`, and they serialize recursively — a bundle serializes its nested laws, articles, candidates, and deferred lookups in one call. +모든 공개 데이터클래스가 재귀적으로 직렬화된다. 번들 하나를 부르면 그 안의 법령·조문·후보·후속 조회가 전부 따라 나온다. ```python -data = article.to_dict() # plain dict, ready for json.dumps -json_text = bundle.to_json_string() # JSON string +data = article.to_dict() # dict +text = bundle.to_json_string() # JSON 문자열 (ensure_ascii=False, sort_keys=True) -# Raw law.go.kr payloads are omitted by default; opt in for parser debugging. -debug_data = bundle.to_dict(include_raw=True) +debug = bundle.to_dict(include_raw=True) # law.go.kr 원본 페이로드까지 ``` -By default the `raw` source payload is dropped, which keeps ordinary application context clean. Use `include_raw=True` only when you need the original law.go.kr response for debugging. The CLI applies the same rule: it omits `raw` unless you pass the global `--raw` flag. +`raw`(원본 페이로드)는 **기본적으로 빠진다.** 컨텍스트 예산을 엔드포인트 모양 데이터로 낭비하지 않기 위해서다. 파서를 디버깅할 때만 켠다. CLI도 같은 규칙이고, 전역 `--raw` 플래그로 켠다. -## Where to go next +## 다음 -- [Core Concepts](Core-Concepts.md) — search-then-load discipline, bases, candidates vs. loaded text, bundles and deferred lookups. -- [API Reference](API-Reference.md) — every `MolegApi` method and model field. -- [CLI Reference](CLI-Reference.md) — the JSON envelope, exit codes, and subcommand list. -- [Gotchas](Gotchas.md) — distinctions (bill vs. current law, promulgation vs. effective date, empty result vs. absence) that quietly produce wrong claims. -- [Sources and Coverage](Sources-and-Coverage.md) — which law.go.kr source families are covered and their limits. +- [Core Concepts](Core-Concepts.md) — 여기서 본 규칙들의 근거 +- [Agent Integration](Agent-Integration.md) — 엔벨로프 신호 전체, 컨텍스트 예산, `catalog` 계약 +- [Gotchas](Gotchas.md) — 조용히 틀리는 지점들 +- [API Reference](API-Reference.md) · [CLI Reference](CLI-Reference.md) — 전수 목록 -This package is a legal-source loader, not legal advice. +이 패키지는 법적 출처 로더이지 법률 자문이 아니다. diff --git a/docs/wiki/Sources-and-Coverage.md b/docs/wiki/Sources-and-Coverage.md index b6deb6a..303b9ae 100644 --- a/docs/wiki/Sources-and-Coverage.md +++ b/docs/wiki/Sources-and-Coverage.md @@ -1,171 +1,198 @@ -# Sources and Coverage +# 출처와 커버리지 -MOLEG-API loads Korean legal sources from [law.go.kr](https://www.law.go.kr/). It covers the source families most useful for recurring legal source-loading tasks, and it deliberately does **not** expose every law.go.kr OpenAPI endpoint as a one-to-one SDK method. This page describes what is covered, what is out of scope, and the coverage limits you should keep in mind when citing what you load. +법제처 OpenAPI의 모든 엔드포인트를 1:1 메서드로 노출하지 않는다. **반복적으로 필요한 법적 출처 계열**을 골라 깊게 감쌌다. 이 페이지는 무엇이 덮여 있고, 무엇이 범위 밖이며, 인용할 때 무엇을 염두에 둬야 하는지를 다룬다. -Every method listed here has a matching `moleg` CLI subcommand. Run `python -m moleg_api catalog` for the full command list and routing rules. +여기 나오는 모든 메서드에 대응하는 `moleg` 서브커맨드가 있다. 전체 목록과 라우팅 규칙은 `moleg catalog`. -## What is covered +## 대응표 -| Source family | Python interfaces | CLI subcommands | +| 출처 계열 | Python | CLI | |---|---|---| -| Current and promulgated statutes | `search_laws`, `resolve_promulgated_law`, `get_law`, `get_article`, `load_article_context` | `search-laws`, `resolve-promulgated-law`, `get-law`, `get-article`, `load-article-context` | -| Supplementary provisions (부칙) | returned inside `get_law` | inside `get-law` | -| Law history and before/after text | `trace_law_history`, `compare_law_versions` | `trace-law-history`, `compare-law-versions` | -| Delegated rules and legal hierarchy | `find_delegated_rules`, `get_law_structure` | `find-delegated-rules`, `get-law-structure` | -| Administrative rules (고시·훈령·예규 등) | `search_administrative_rules`, `get_administrative_rule`, `load_administrative_rule_context` | `search-administrative-rules`, `get-administrative-rule`, `load-administrative-rule-context` | -| Annexes and forms (별표·서식) | `search_annex_forms`, `get_annex_form_body` | `search-annex-forms`, `get-annex-form-body` | -| MOLEG and ministry interpretations | `search_interpretations`, `get_interpretation` | `search-interpretations`, `get-interpretation` | -| Ordinary court cases | `search_cases`, `get_case` | `search-cases`, `get-case` | -| Constitutional Court decisions | `search_constitutional_decisions`, `get_constitutional_decision` | `search-constitutional-decisions`, `get-constitutional-decision` | -| Query planning and staged bundles | `expand_legal_query`, `find_comparable_mechanisms`, `load_legal_context_bundle`, `load_institutional_system`, `load_delegated_criteria` | `expand-legal-query`, `find-comparable-mechanisms`, `load-legal-context-bundle`, `load-institutional-system`, `load-delegated-criteria` | +| 법령 (현행·공포) | `search_laws`, `resolve_promulgated_law`, `get_law`, `get_law_toc`, `get_article`, `load_article_context` | `search-laws`, `resolve-promulgated-law`, `get-law`(`--toc`), `get-article`, `load-article-context` | +| 부칙 | `get_law` 결과에 포함 | `get-law` 안에 | +| 연혁·개정이유·비교 | `trace_law_history`, `get_revision_reason`, `compare_law_versions` | `trace-law-history`, `get-revision-reason`, `compare-law-versions` | +| 위임·체계 | `find_delegated_rules`, `get_law_structure` | `find-delegated-rules`, `get-law-structure` | +| 행정규칙 (고시·훈령·예규) | `search_administrative_rules`, `get_administrative_rule`, `load_administrative_rule_context` | 동명 | +| 별표·서식 | `search_annex_forms`, `get_annex_form_body` | 동명 | +| 법령해석 (법제처·부처) | `search_interpretations`, `get_interpretation` | 동명 | +| 판례 | `search_cases`, `get_case` | 동명 | +| 헌재 결정 | `search_constitutional_decisions`, `get_constitutional_decision` | 동명 | +| 위원회 의결 | `search_committee_decisions`, `get_committee_decision` | 동명 | +| 행정심판 재결 | `search_administrative_appeals`, `get_administrative_appeal` | 동명 | +| 계획·번들 | `expand_legal_query`, `find_comparable_mechanisms`, `load_legal_context_bundle`, `load_institutional_system`, `load_delegated_criteria`, `load_authority_context`, `load_followup` | 동명 | -### Current and promulgated statutes +--- -`search_laws` finds statute identity candidates by name or keyword; `get_law` and `get_article` load the text behind a chosen identity. Both accept a `basis` argument (`"effective"` or `"promulgated"`) — see [Effective date versus promulgation date](#effective-date-versus-promulgation-date). +## 계열별 상세 -```python -from moleg_api import MolegApi +### 법령 본문 -api = MolegApi() +`search_laws`가 이름·키워드로 신원 후보를 찾고, `get_law`·`get_article`이 고른 신원 뒤의 텍스트를 싣는다. 둘 다 `basis`(`"effective"` / `"promulgated"`)를 받는다. -hits = api.search_laws("주택임대차보호법", basis="effective", display=2) -law = api.get_law(hits[0].identity, basis="effective") -article = api.get_article(hits[0].identity, "제7조", basis="effective") -print(article.text) -``` +`get_law_toc`(CLI: `get-law --toc`)는 본문 없이 조문 지도만 준다. 컨텍스트 예산 장치다. -```bash -python -m moleg_api search-laws "주택임대차보호법" --display 2 -python -m moleg_api get-article --law 001248 제7조 -``` +`resolve_promulgated_law`는 더 엄격한 해석기다. 공포 메타데이터(법령명·공포번호·공포일)에서 자유 탐색 없이 **정확히 하나의** 공포 기준 신원으로 잇는다. -`resolve_promulgated_law` is a stricter bridge resolver: given the promulgation metadata of an enacted bill (law name, promulgation number, promulgation date) it resolves to one promulgated `LawIdentity`, rather than doing free-text discovery. +### 조문 — 이동·삭제 상태 포함 -### Statute articles, including moved and deleted status +`get_article`은 `제10조의2` 같은 사람 표기로 조문 하나를 싣는다. `ArticleText`가 `moved_from`/`moved_to` 쌍과 `is_deleted` 플래그를 들고 있어, 이동·삭제 표시가 운용 조문으로 오인되지 않는다. -`get_article` loads one provision by human notation such as `제10조의2`, without you formatting MOLEG's six-digit `JO` value. `ArticleText` carries a `moved_from` / `moved_to` pair and an `is_deleted` flag, so a moved or deleted article marker is not mistaken for operative text. +`load_article_context`는 여기에 얹혀서, 조문이 이동했으면 **현재 목적지를 해소한다.** 요청 조문, 목적지(안전하게 실린 경우), 실린 행 전체, 그리고 실질 주장 전에 해소해야 할 gap과 후속 조회를 함께 준다. -`load_article_context` builds on this: it loads the requested article and, when the article has moved, resolves the current destination article. It returns the requested article, the current destination (when one is safely loaded), all loaded rows, and any context gaps or deferred lookups you should resolve before making a substance claim. +### 부칙 -### Supplementary provisions (부칙) +`get_law`가 본 조문과 함께 추출해 `LawText.supplementary_provisions`에 담는다. 별도 로더는 없다. -`get_law` extracts supplementary provisions alongside the main articles and returns them in `LawText.supplementary_provisions` when the source law exposes them. There is no separate loader — 부칙 come back with the statute body. +### 연혁·개정이유·비교 -### Law history and before/after text comparison +세 명령이 서로 다른 질문에 답한다. -`trace_law_history` loads amendment-history events for a whole statute, a date range, or a single article (`article="제7조"`). Events carry chronology, amendment reasons, promulgation numbers, and effective dates. +| 질문 | 명령 | +|---|---| +| 어떤 개정들이 있었나 | `trace_law_history` | +| 그 개정은 **왜** 했나 | `get_revision_reason` | +| **무엇이** 바뀌었나 | `compare_law_versions` | -`compare_law_versions` loads MOLEG's `oldAndNew` before/after comparison surface for a statute or article. Note: the source does not support arbitrary two-date windows. Calling `compare_law_versions` with `before=`/`after=` raises `UnsupportedFormatError`; call it without those arguments to load the source-supplied before/after pair, and use `trace_law_history` to pick dates or amendment events. +`trace_law_history`는 전체 법령, 날짜 범위, 또는 조문 하나(`article="제7조"`)에 대해 개정 이력을 싣는다. 전체 법령 연혁은 law.go.kr이 **HTML로만** 주는 경로를 파싱해 온다. -### Delegated rules and legal hierarchy +`get_revision_reason`은 특정 **판본**의 「개정이유 및 주요내용」을 준다. `trace_law_history` 이벤트의 `identity.mst`를 `mst=`에 넣으면 그 개정으로 정확히 내려간다. 오래된 판본에는 없는 경우가 흔하다. -`find_delegated_rules` returns the delegated lower-rule context for a statute (enforcement decrees, enforcement rules, notices, administrative rules), optionally filtered by source article. `get_law_structure` loads the broader 법률 → 시행령 / 시행규칙 / 행정규칙 hierarchy from MOLEG's `lsStmd` structural view. The two differ: `find_delegated_rules` preserves article-level delegation links; `lsStmd` gives the hierarchy but does not provide source-article links. +`compare_law_versions`는 law.go.kr이 자체적으로 노출하는 전후 비교 표면을 쓴다. **임의의 두 날짜 구간은 지원하지 않으며** `before=`/`after=`에 날짜를 주면 `UnsupportedFormatError`가 난다. -### Administrative rules (행정규칙) +### 위임과 체계 -`search_administrative_rules` searches notices, directives, established rules, and other ministry-level administrative rules (고시·훈령·예규 등) — the practical execution criteria that often live outside statute text. `get_administrative_rule` loads a selected rule body, and `load_administrative_rule_context` stages the surrounding context for one rule. +`find_delegated_rules`는 법령(또는 조문)의 위임 하위규범 맥락을 준다 — 시행령·시행규칙·고시·행정규칙. 조문 단위 위임 링크를 보존한다. -### Annexes and forms (별표·서식) — text extraction only +`get_law_structure`는 법률 → 시행령 / 시행규칙 / 행정규칙의 계층도를 준다. **조문 단위 링크는 없다.** 계층 맥락일 뿐 조문 단위 위임의 증거가 아니다. -Operative content often lives in annex material: tables, thresholds, criteria, amounts, and required forms. `search_annex_forms` finds annex/form candidates attached to a statute (`source="law"`) or an administrative rule (`source="administrative_rule"`), filterable by `annex_type` (별표, 서식, 별지, 별도, 부록 and their English aliases). `get_annex_form_body` loads the body of a selected candidate as **plain text**. +> **위임 목록에 별표는 없다.** 과태료 기준표·수수료표 같은 것은 `search_annex_forms`로 따로 찾아야 한다. -Extraction preserves the `text/plain` body and, for table-like annexes, attempts best-effort structuring into `structured_data`. **This interface does not parse HWP or PDF files directly** — it uses law.go.kr's text export endpoints. When table structuring is low confidence, empty structured rows do not necessarily mean no criteria exist; the plain text is still preserved and authoritative. +### 행정규칙 -### MOLEG and ministry interpretations +고시·훈령·예규 등 — 법령 본문 밖에 사는 실무 집행 기준. `search_administrative_rules`의 `issued_on`은 **발령일자**이지 시행일이 아니다. -`search_interpretations` searches official legal interpretations, distinct from court decisions. The `source` argument selects the authority scope: `"moleg"` (MOLEG official interpretations, the default), `"ministry"` (a named ministry's first-instance interpretations), `"all"` (MOLEG plus one specified ministry), or `"all_ministries"` (registry-wide fan-out, for deep institutional analysis only). `get_interpretation` loads the question, answer, reason, and related-law text for a selected interpretation. MOLEG interpretation and ministry interpretation are separate authority types and are not interchangeable — see [Authority types stay separate](#authority-types-stay-separate). +### 별표·서식 — 텍스트 추출 -### Court cases and Constitutional Court decisions +운용적 내용이 첨부 자료에 사는 경우가 많다. 표, 임계값, 기준, 금액, 필수 서식. -`search_cases` / `get_case` cover ordinary court decisions (Supreme Court and lower courts). `search_constitutional_decisions` / `get_constitutional_decision` cover Constitutional Court decisions — reviewed statutes, holdings, and constitutional reasoning. These are distinct sources: ordinary court precedent and Constitutional Court decisions are not interchangeable, and neither is interchangeable with an interpretation. +`search_annex_forms`가 법령(`source="law"`)이나 행정규칙(`source="administrative_rule"`)에 붙은 후보를 찾는다. `annex_type`으로 별표·서식·별지·별도·부록(및 영문 별칭)을 거를 수 있다. `get_annex_form_body`가 고른 후보의 본문을 **평문**으로 싣는다. -Constitutional-doctrine discovery is keyword-based free-text search unless the loaded decision detail itself provides stronger structure. +**HWP나 PDF 파일을 직접 파싱하지 않는다** — law.go.kr의 텍스트 내보내기 엔드포인트를 쓴다. 표 구조화는 최선의 시도이며, 신뢰도가 낮아도 평문은 보존된다. 구조화 행이 비었다고 기준이 없는 것이 아니다. -### Committee decisions (위원회 결정문) +### 법령해석 -`search_committee_decisions` / `get_committee_decision` cover the decisions and dispositions of twelve regulators: 개인정보보호위원회 (`ppc`), 공정거래위원회 (`ftc`), 금융위원회 (`fsc`), 증권선물위원회 (`sfc`), 방송통신위원회 (`kcc`), 국가인권위원회 (`nhrck`), 국민권익위원회 (`acr`), 노동위원회 (`nlrc`), 고용보험심사위원회 (`eiac`), 산업재해보상보험재심사위원회 (`iaciac`), 중앙토지수용위원회 (`oclt`), 중앙환경분쟁조정위원회 (`ecc`). +법원 판단과 구분되는 공식 해석. `source`가 권위 범위를 정한다. -This is the record of an agency **applying** the statute it administers — a 과징금, a 시정명령, a 침해 판단. It answers a question the statute and the case law cannot: whether the supervising body actually acted, and when. It is **not precedent**; it shows one agency's enforcement practice and can be overturned in 행정소송. +| `source` | 범위 | +|---|---| +| `"moleg"` (기본) | 법제처 법령해석례 | +| `"ministry"` | 지정한 부처 하나의 1차 해석 (40개 부처 등록) | +| `"all"` | 법제처 **+ 지정한 부처 하나** | +| `"all_ministries"` | 40개 부처 전체 팬아웃. 비용이 크므로 깊은 분석에만 | -### Administrative appeals (행정심판 재결례) +부처 해석 **본문**을 실으려면 `--source ministry --ministry <기관>`이 필요하다. `--id`만으로는 안 실린다. -`search_administrative_appeals` / `get_administrative_appeal` cover the general 행정심판 docket (`decc`) and four special tribunals: 국민권익위원회 특별행정심판 (`acr`), 소청심사위원회 (`adap`), 조세심판원 (`tt`), 해양안전심판원 (`kmst`). +두 부처(국세청·재정경제부)는 **검색은 되지만 본문 조회가 안 된다.** `get_interpretation`이 `UnsupportedFormatError`를 낸다. -A 재결 reviews another agency's disposition from inside the executive branch. **The special tribunals' rulings are not in the general `decc` list** — a 소청·조세·해양안전 question searched only against `decc` will come back empty and read as absence. A 재결 is not a court judgment; the losing party can still bring 행정소송. +### 판례와 헌재 결정 -### Query planning and staged bundles +`search_cases` / `get_case`는 일반 법원(대법원·하급심), `search_constitutional_decisions` / `get_constitutional_decision`은 헌재를 다룬다. **서로 대체되지 않으며** 해석과도 대체되지 않는다. -These interfaces plan a search or assemble a staged bundle of the source families above. They are **planning aids, not legal authority** — use their outputs to drive the primary loaders before citing anything. +로더가 태그를 교차 검증한다 — 헌재 신원을 `get_case`에 넘기면 `UnsupportedFormatError`다. -- `expand_legal_query` — turns broad wording into candidate laws, legal and everyday terms, related articles/laws, and follow-up recommendations. -- `find_comparable_mechanisms` — finds source-backed law candidates that use a similar legal mechanism (예: 과징금, 인허가, 신고제), for comparative 제도 design. -- `load_legal_context_bundle` — a staged bundle for a broad question, with executable deferred follow-up lookups (see [Follow-up lookups](Core-Concepts.md)). -- `load_institutional_system` — one staged bundle across an explicitly selected set of statutes. -- `load_delegated_criteria` — subordinate administrative-rule and annex/form bodies from a known statute anchor. +헌재 결정 상세에는 판결유형 키가 없어서 `decision_type`이 항상 비어 있다. 패키지는 **주문(【주 문】)에서 처분을 복구해** 각하·기각이 본안 판단으로 오인되지 않게 한다. 이유 부분은 반대의견 때문에 같은 표현을 반복하므로 주문만 훑는다. -## What is not covered +헌법 도그마틱 탐색은 자유 텍스트 검색이다 → [Gotchas](Gotchas.md) 13번 -- **Legislative pre-announcement (입법예고).** Draft-stage notices live at 국민참여입법센터, a separate source law.go.kr's OpenAPI does not expose. Out of scope; use WebSearch. -- **National Assembly bill data.** MOLEG-API does not query bill databases. Bill status, sponsors, votes, committee minutes, and the promulgation bridge fields are a separate source's job. (`resolve_promulgated_law` *consumes* bridge metadata that a bill source provides, but does not itself retrieve it.) Use a National Assembly / 의안정보 source for anything on the legislative-process side. -- **Foreign or comparative law.** Only Korean law.go.kr sources are covered. Foreign statutes, treaties, and comparative-law material are out of scope — use WebSearch or another external source. -- **Latest statistics, news, policy announcements, and social context.** These are not legal sources; use WebSearch or another current source. -- **Legal advice.** MOLEG-API is a legal-source *loader*. It retrieves and normalizes source text; it does not interpret or advise. +### 위원회 의결 — 12개 기관 -## Coverage limits +| 코드 | 기관 | 코드 | 기관 | +|---|---|---|---| +| `ppc` | 개인정보보호위원회 | `acr` | 국민권익위원회 | +| `ftc` | 공정거래위원회 | `nlrc` | 노동위원회 | +| `fsc` | 금융위원회 | `eiac` | 고용보험심사위원회 | +| `sfc` | 증권선물위원회 | `iaciac` | 산업재해보상보험재심사위원회 | +| `kcc` | 방송통신위원회 | `oclt` | 중앙토지수용위원회 | +| `nhrck` | 국가인권위원회 | `ecc` | 중앙환경분쟁조정위원회 | -Keep these in mind when you cite what you load. +**감독기관이 자기가 집행하는 법을 *적용한* 기록**이다 — 과징금, 시정명령, 침해 판단. 법령과 판례가 답하지 못하는 질문에 답한다: 그 감독기관이 실제로 움직였는가, 언제. -### Empty results are scoped, not proof of absence +**판례가 아니다.** 한 기관의 집행 실무를 보여줄 뿐이며 행정소송에서 뒤집힐 수 있다. -An empty search result is scoped to the exact query, source family, and filters you used. It means "no source rows for this query," **not** "no relevant law, rule, case, interpretation, or annex exists." Broaden the query or try a related source family before concluding that something does not exist. +### 행정심판 재결 — 일반 + 특별 4종 -### Search hits are candidates, not citable text +| 코드 | 기관 | +|---|---| +| `decc` (기본) | 일반 행정심판위원회 | +| `acr` | 국민권익위원회 특별행정심판 | +| `adap` | 소청심사위원회 | +| `tt` | 조세심판원 | +| `kmst` | 해양안전심판원 | -Search hit metadata (titles, IDs, dates) is not citable source text. Treat search results as *candidates*: load the selected detail with the matching loader (`get_law`, `get_article`, `get_interpretation`, `get_case`, `get_annex_form_body`, and so on) before quoting or reasoning from source content. +재결은 행정부 내부에서 다른 기관의 처분을 심사한 것이다. -### Authority types stay separate +**특별 심판기관의 재결은 일반 `decc` 목록에 없다.** 소청·조세·해양안전 사안을 `decc`에서만 찾으면 0건이 나오고, 그건 부재로 읽힌다. 재결은 법원 판결이 아니며 패소한 쪽은 여전히 행정소송을 낼 수 있다. -MOLEG official interpretation, ministry first-instance interpretation, ordinary court case, and Constitutional Court decision are distinct authorities with different weight. The models preserve their authority labels; do not flatten them into one another. +### 계획과 번들 -### A regulator's empty docket is not an inactive regulator +위 계열들을 조합해 검색을 계획하거나 단계적 묶음을 만든다. **계획 보조이지 권위가 아니다.** -For the two adjudication families above, zero hits are returned by an agency that never received a complaint, one that received a complaint and opened no case, one that decided and did not publish, and a matter that belongs to a different body's docket. None of those is "nothing happened," and this is the family where that misreading does the most damage, since the question being asked is usually whether the agency failed to act. Re-check the other plausible `--committee` code and the special tribunals before recording a negative, and treat an exhausted public record as the point to make an official document request rather than a finding. +- `expand_legal_query` — 넓은 표현을 후보 법령·법률 용어·일상 용어·관련 조문·후속 조회 권고로 바꾼다 +- `find_comparable_mechanisms` — 유사한 법적 장치(과징금·인허가·신고제 등)를 쓰는 법령 후보. 입법 설계 비교용 +- `load_legal_context_bundle` — 넓은 질문에 대한 단계적 묶음 +- `load_institutional_system` — **이미 고른** 법령 집합을 하나로 +- `load_delegated_criteria` — 법령 하나에서 하위 행정규칙·별표 본문까지 +- `load_authority_context` — 지정 조문에 스코프를 건 권위 수집 +- `load_followup` — 위 결과가 준 후속 조회 실행 -### Effective date versus promulgation date +--- -A promulgated law can be loadable while **not yet effective** on your reference date. Use effective-date lookups (`basis="effective"`) for current-force questions, and promulgated-basis lookups (`basis="promulgated"`) to resolve enacted-law bridge metadata or historical promulgation context. +## 범위 밖 -When the reference date matters, pass `as_of="YYYY-MM-DD"` and inspect the returned `ContextGap` values such as `not_effective_as_of`. +- **법률 자문.** 이 패키지는 법적 출처를 *싣는다*. 해석하거나 조언하지 않는다. +- **입법예고.** 입법예고는 국민참여입법센터라는 별개 시스템에 있고 law.go.kr OpenAPI가 노출하지 않는다. +- **국회 의안 데이터.** 의안 진행·발의자·표결·회의록은 별개 출처 소관이다. `resolve_promulgated_law`는 다른 출처가 제공한 공포 메타데이터를 *소비*할 뿐 직접 가져오지 않는다. +- **외국법·비교법.** 한국 law.go.kr 출처만 다룬다. +- **최신 통계·뉴스·정책 발표·사회적 맥락.** 법적 출처가 아니다. -```python -api.get_law("001248", basis="effective", as_of="2026-01-01") -``` +`load_followup`에 `websearch`나 `congress-db` 인터페이스를 넘기면 `UnsupportedFormatError`가 난다 — 버그가 아니라 **경계 표시**다. -```bash -python -m moleg_api get-law --law 001248 --basis effective --as-of 2026-01-01 -``` +--- + +## 커버리지 한계 + +인용할 때 염두에 둘 것들. 상세는 [Gotchas](Gotchas.md). + +**0건은 범위 한정이다.** 그 질의·출처 계열·필터로 행이 없었다는 뜻이다. + +**검색 결과는 인용할 수 없다.** 제목·ID·날짜는 본문이 아니다. + +**권위 층위는 분리된다.** 법제처 해석 ≠ 부처 해석 ≠ 판례 ≠ 헌재 결정 ≠ 위원회 의결 ≠ 행정심판 재결. + +**빈 감독기관 기록은 무활동의 증명이 아니다.** 이 계열에서 오독의 손해가 가장 크다. 질문 자체가 대개 "그 기관이 안 움직였나"이기 때문이다. 다른 `--committee` 코드와 특별 심판기관을 확인하고, 공개 기록이 소진되면 그건 부정적 결론을 기록할 자리가 아니라 **공식 자료요구를 할 자리**다. -### Source failures are not legal absence +**공포 ≠ 시행.** 현행 효력 질문에는 `basis="effective"`를, 공포 사실 연결이나 과거 공포 맥락에는 `basis="promulgated"`를 쓴다. -`RateLimitError`, `RetryExhaustedError`, and other `SourceApiError` states mean source access failed. Treat them as temporary source-access problems or explicit gaps — never as evidence that a law or decision does not exist. See [Errors and Reliability](Error-Handling.md) for handling. +**출처 실패는 법적 부재가 아니다.** `RateLimitError`·`RetryExhaustedError`·기타 `SourceApiError`는 접근 실패다. -### Annex extraction preserves plain text +**별표 추출은 평문을 보존한다.** 구조화 신뢰도가 낮아도 평문은 남는다. -Annex/form body extraction keeps the plain text even when table structuring is low confidence. Empty `structured_data` rows do not mean the annex has no criteria — read the plain text. +--- -## Raw payloads +## 원본 페이로드 -Public return models put normalized fields first and omit raw law.go.kr payloads by default. When you need to debug a parser or inspect the underlying source shape, pass `include_raw=True`: +공개 반환 모델은 정규화된 필드를 앞세우고 law.go.kr 원본 페이로드는 기본적으로 뺀다. 파서를 디버깅하거나 원본 모양을 확인할 때만 켠다. ```python bundle.to_dict(include_raw=True) ``` -Normal application context should use the default `include_raw=False`. +CLI에서는 전역 `--raw` 플래그다. 평상시에는 기본값을 쓰라. -## See also +## 관련 문서 -- [Effective vs Promulgated](Core-Concepts.md) — choosing the right `basis` -- [Follow-up lookups](Core-Concepts.md) — executing deferred bundle lookups -- [Errors and Reliability](Error-Handling.md) — source-failure handling -- [CLI Reference](CLI-Reference.md) — the JSON envelope contract and subcommands +- [Core Concepts](Core-Concepts.md) — `basis` 선택과 권위 구분 +- [Agent Integration](Agent-Integration.md) — 엔벨로프 신호와 컨텍스트 예산 +- [Gotchas](Gotchas.md) — 각 한계가 실제로 어떻게 틀린 주장을 만드는지 +- [Error Handling](Error-Handling.md) — 출처 실패 처리 +- [CLI Reference](CLI-Reference.md) — 엔벨로프 계약과 서브커맨드 diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md index 152615c..a615572 100644 --- a/docs/wiki/_Sidebar.md +++ b/docs/wiki/_Sidebar.md @@ -4,10 +4,12 @@ - [Installation](Installation.md) - [Quickstart](Quickstart.md) - [Core Concepts](Core-Concepts.md) +- [Agent Integration](Agent-Integration.md) - [CLI Reference](CLI-Reference.md) - [API Reference](API-Reference.md) - [Historical Versions](Historical-Versions.md) - [Sources & Coverage](Sources-and-Coverage.md) - [Gotchas](Gotchas.md) - [Error Handling](Error-Handling.md) +- [Architecture](Architecture.md) - [Maintainer Notes](Maintainer-Notes.md) diff --git a/pyproject.toml b/pyproject.toml index 705591d..8c38a02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ moleg = "moleg_api.cli:main" [project.urls] Homepage = "https://github.com/tjdwls101010/MOLEG-API" -Documentation = "https://github.com/tjdwls101010/MOLEG-API/tree/main/wiki" +Documentation = "https://github.com/tjdwls101010/MOLEG-API/blob/main/docs/wiki/Home.md" Repository = "https://github.com/tjdwls101010/MOLEG-API" Issues = "https://github.com/tjdwls101010/MOLEG-API/issues"