moleg-api 0.2.4__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. {moleg_api-0.2.4 → moleg_api-0.3.0}/PKG-INFO +14 -10
  2. {moleg_api-0.2.4 → moleg_api-0.3.0}/README.md +13 -9
  3. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/__init__.py +14 -0
  4. moleg_api-0.3.0/moleg_api/_cli/brief_mode.py +53 -0
  5. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_cli/catalog.py +12 -5
  6. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_cli/constants.py +7 -2
  7. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_cli/dispatch.py +25 -0
  8. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_cli/foundation.py +0 -7
  9. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_cli/main.py +28 -3
  10. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_cli/parser.py +45 -7
  11. moleg_api-0.3.0/moleg_api/_cli/payload_size.py +57 -0
  12. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_cli/signals.py +83 -0
  13. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_cli/signals_meta.py +22 -0
  14. moleg_api-0.3.0/moleg_api/_laws/adjudication_registry.py +76 -0
  15. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/api.py +6 -0
  16. moleg_api-0.3.0/moleg_api/_laws/api_adjudications.py +117 -0
  17. moleg_api-0.3.0/moleg_api/_laws/api_law_toc.py +66 -0
  18. moleg_api-0.3.0/moleg_api/_laws/api_revision_reason.py +120 -0
  19. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/foundation.py +10 -0
  20. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_models/__init__.py +2 -1
  21. moleg_api-0.3.0/moleg_api/_models/adjudications.py +75 -0
  22. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_models/laws.py +59 -0
  23. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_models/serialization.py +11 -1
  24. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_normalization/__init__.py +1 -0
  25. moleg_api-0.3.0/moleg_api/_normalization/adjudications.py +164 -0
  26. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_normalization/article_units.py +20 -4
  27. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_normalization/unwrap.py +29 -1
  28. moleg_api-0.3.0/moleg_api/_version.py +18 -0
  29. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/models.py +6 -0
  30. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api.egg-info/PKG-INFO +14 -10
  31. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api.egg-info/SOURCES.txt +15 -0
  32. {moleg_api-0.2.4 → moleg_api-0.3.0}/pyproject.toml +4 -1
  33. moleg_api-0.3.0/tests/test_adjudications_0_3_0.py +278 -0
  34. moleg_api-0.3.0/tests/test_command_smoke_0_3_0.py +195 -0
  35. moleg_api-0.3.0/tests/test_context_efficiency_0_3_0.py +220 -0
  36. moleg_api-0.3.0/tests/test_exit_classification_0_3_0.py +116 -0
  37. {moleg_api-0.2.4 → moleg_api-0.3.0}/tests/test_refactor_compat.py +4 -1
  38. moleg_api-0.3.0/tests/test_revision_reason_0_3_0.py +156 -0
  39. moleg_api-0.3.0/tests/test_version_signal_0_3_0.py +100 -0
  40. {moleg_api-0.2.4 → moleg_api-0.3.0}/LICENSE +0 -0
  41. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/__main__.py +0 -0
  42. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_cli/__init__.py +0 -0
  43. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_cli/data.py +0 -0
  44. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_cli/signal_helpers.py +0 -0
  45. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/__init__.py +0 -0
  46. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/admin_scope.py +0 -0
  47. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/annex_tables.py +0 -0
  48. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/api_admin_rules.py +0 -0
  49. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/api_annex.py +0 -0
  50. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/api_article_context.py +0 -0
  51. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/api_authority_context.py +0 -0
  52. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/api_bundle.py +0 -0
  53. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/api_comparable.py +0 -0
  54. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/api_compare.py +0 -0
  55. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/api_delegated_criteria.py +0 -0
  56. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/api_followups.py +0 -0
  57. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/api_history.py +0 -0
  58. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/api_institutional.py +0 -0
  59. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/api_interpretations.py +0 -0
  60. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/api_judicial.py +0 -0
  61. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/api_law_loaders.py +0 -0
  62. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/api_query_expansion.py +0 -0
  63. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/api_search.py +0 -0
  64. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/api_structure.py +0 -0
  65. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/api_versions.py +0 -0
  66. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/article_gaps.py +0 -0
  67. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/authority_article_gaps.py +0 -0
  68. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/authority_context_details.py +0 -0
  69. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/authority_context_pipeline.py +0 -0
  70. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/authority_sources.py +0 -0
  71. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/authority_temporal_filters.py +0 -0
  72. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/authority_temporal_gaps.py +0 -0
  73. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/bridge.py +0 -0
  74. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/bundle_candidates.py +0 -0
  75. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/bundle_eager.py +0 -0
  76. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/bundle_finalize.py +0 -0
  77. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/bundle_modes.py +0 -0
  78. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/bundle_primary.py +0 -0
  79. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/bundle_state.py +0 -0
  80. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/candidates.py +0 -0
  81. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/config.py +0 -0
  82. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/context_load_gaps.py +0 -0
  83. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/delegated_criteria_details.py +0 -0
  84. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/delegated_criteria_pipeline.py +0 -0
  85. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/delegated_scope.py +0 -0
  86. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/followup_basic.py +0 -0
  87. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/followup_hits.py +0 -0
  88. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/followup_identities.py +0 -0
  89. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/followup_routing.py +0 -0
  90. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/followup_routing_authority.py +0 -0
  91. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/followup_routing_bundle.py +0 -0
  92. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/followup_searches.py +0 -0
  93. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/history_identity.py +0 -0
  94. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/identity_params.py +0 -0
  95. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/institutional_candidates.py +0 -0
  96. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/institutional_pipeline.py +0 -0
  97. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/institutional_resolution.py +0 -0
  98. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/limits_intents.py +0 -0
  99. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/ranking.py +0 -0
  100. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/requested_load_gaps.py +0 -0
  101. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/source_matching.py +0 -0
  102. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/support.py +0 -0
  103. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/temporal_gaps.py +0 -0
  104. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_laws/validation.py +0 -0
  105. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_models/admin.py +0 -0
  106. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_models/annex.py +0 -0
  107. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_models/authority.py +0 -0
  108. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_models/bundles.py +0 -0
  109. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_models/followups.py +0 -0
  110. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_models/query.py +0 -0
  111. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_models/types.py +0 -0
  112. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_normalization/annex.py +0 -0
  113. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_normalization/articles.py +0 -0
  114. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_normalization/authority.py +0 -0
  115. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_normalization/candidates.py +0 -0
  116. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_normalization/delegation.py +0 -0
  117. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_normalization/history_events.py +0 -0
  118. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_normalization/history_html.py +0 -0
  119. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_normalization/identities.py +0 -0
  120. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_normalization/primitives.py +0 -0
  121. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_normalization/references.py +0 -0
  122. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_normalization/row_format.py +0 -0
  123. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_normalization/source_refs.py +0 -0
  124. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/_normalization/structure.py +0 -0
  125. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/cli.py +0 -0
  126. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/errors.py +0 -0
  127. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/laws.py +0 -0
  128. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/normalization.py +0 -0
  129. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/py.typed +0 -0
  130. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api/source.py +0 -0
  131. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api.egg-info/dependency_links.txt +0 -0
  132. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api.egg-info/entry_points.txt +0 -0
  133. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api.egg-info/requires.txt +0 -0
  134. {moleg_api-0.2.4 → moleg_api-0.3.0}/moleg_api.egg-info/top_level.txt +0 -0
  135. {moleg_api-0.2.4 → moleg_api-0.3.0}/setup.cfg +0 -0
  136. {moleg_api-0.2.4 → moleg_api-0.3.0}/tests/test_cli.py +0 -0
  137. {moleg_api-0.2.4 → moleg_api-0.3.0}/tests/test_laws.py +0 -0
  138. {moleg_api-0.2.4 → moleg_api-0.3.0}/tests/test_live_smoke.py +0 -0
  139. {moleg_api-0.2.4 → moleg_api-0.3.0}/tests/test_models.py +0 -0
  140. {moleg_api-0.2.4 → moleg_api-0.3.0}/tests/test_sdk_fixes_0_2_1.py +0 -0
  141. {moleg_api-0.2.4 → moleg_api-0.3.0}/tests/test_sdk_fixes_0_2_2.py +0 -0
  142. {moleg_api-0.2.4 → moleg_api-0.3.0}/tests/test_source.py +0 -0
  143. {moleg_api-0.2.4 → moleg_api-0.3.0}/tests/test_versions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moleg-api
3
- Version: 0.2.4
3
+ Version: 0.3.0
4
4
  Summary: Task-level Python interface for loading Korean legal sources from law.go.kr
5
5
  Author: MOLEG-API maintainers
6
6
  License-Expression: MIT
@@ -41,7 +41,7 @@ The PyPI package is `moleg-api`; import it as `moleg_api`.
41
41
  pip install moleg-api
42
42
  ```
43
43
 
44
- 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](wiki/Installation.md).
44
+ 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).
45
45
 
46
46
  ## Quickstart
47
47
 
@@ -65,7 +65,7 @@ python -m moleg_api get-article --law 001248 제3조 # load the current artic
65
65
  python -m moleg_api get-article --law 001248 --as-of 2021-01-01 제3조 # the version in force then
66
66
  ```
67
67
 
68
- The CLI exposes 27 task subcommands plus `catalog`; `catalog` is the source of truth for the command list, routing rules, and result `kind` values.
68
+ 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.
69
69
 
70
70
  Public dataclasses serialize recursively with `to_dict()` / `to_json_string()` (raw payloads omitted unless `include_raw=True`).
71
71
 
@@ -77,19 +77,23 @@ Public dataclasses serialize recursively with `to_dict()` / `to_json_string()` (
77
77
  - Delegated rules, legal hierarchy, and administrative rules (고시·훈령·예규)
78
78
  - Law and administrative-rule annex/form bodies (별표·서식)
79
79
  - MOLEG and ministry legal interpretations, Supreme Court cases, and Constitutional Court (헌재) decisions
80
+ - Amendment rationale — the 「개정이유 및 주요내용」 text for a specific statute version
81
+ - Committee decisions from twelve regulators (개인정보보호위·공정위·금융위·인권위·노동위 등) and 행정심판 재결례 including four special tribunals — the record of an agency applying the law it administers, kept distinct from precedent
80
82
  - Query expansion, comparable-mechanism discovery, and staged context bundles with executable follow-up lookups
81
83
 
82
- Out of scope: legal advice, National Assembly bill data (status, votes, minutes), and latest statistics/news.
84
+ 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.
85
+
86
+ Out of scope: legal advice, National Assembly bill data (status, votes, minutes), legislative pre-announcement (입법예고 — a separate 국민참여입법센터 source), and latest statistics/news.
83
87
 
84
88
  ## Documentation
85
89
 
86
- Full documentation is in the [`wiki/`](wiki/Home.md) folder:
90
+ Full documentation is in the [`docs/wiki/`](docs/wiki/Home.md) folder:
87
91
 
88
- - [Installation](wiki/Installation.md) · [Quickstart](wiki/Quickstart.md) · [Core Concepts](wiki/Core-Concepts.md)
89
- - [CLI Reference](wiki/CLI-Reference.md) · [API Reference](wiki/API-Reference.md)
90
- - [Historical Versions](wiki/Historical-Versions.md) · [Sources & Coverage](wiki/Sources-and-Coverage.md)
91
- - [Gotchas](wiki/Gotchas.md) · [Error Handling](wiki/Error-Handling.md)
92
- - [Maintainer Notes](wiki/Maintainer-Notes.md)
92
+ - [Installation](docs/wiki/Installation.md) · [Quickstart](docs/wiki/Quickstart.md) · [Core Concepts](docs/wiki/Core-Concepts.md)
93
+ - [CLI Reference](docs/wiki/CLI-Reference.md) · [API Reference](docs/wiki/API-Reference.md)
94
+ - [Historical Versions](docs/wiki/Historical-Versions.md) · [Sources & Coverage](docs/wiki/Sources-and-Coverage.md)
95
+ - [Gotchas](docs/wiki/Gotchas.md) · [Error Handling](docs/wiki/Error-Handling.md)
96
+ - [Maintainer Notes](docs/wiki/Maintainer-Notes.md)
93
97
 
94
98
  ## Status
95
99
 
@@ -12,7 +12,7 @@ The PyPI package is `moleg-api`; import it as `moleg_api`.
12
12
  pip install moleg-api
13
13
  ```
14
14
 
15
- 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](wiki/Installation.md).
15
+ 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).
16
16
 
17
17
  ## Quickstart
18
18
 
@@ -36,7 +36,7 @@ python -m moleg_api get-article --law 001248 제3조 # load the current artic
36
36
  python -m moleg_api get-article --law 001248 --as-of 2021-01-01 제3조 # the version in force then
37
37
  ```
38
38
 
39
- The CLI exposes 27 task subcommands plus `catalog`; `catalog` is the source of truth for the command list, routing rules, and result `kind` values.
39
+ 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.
40
40
 
41
41
  Public dataclasses serialize recursively with `to_dict()` / `to_json_string()` (raw payloads omitted unless `include_raw=True`).
42
42
 
@@ -48,19 +48,23 @@ Public dataclasses serialize recursively with `to_dict()` / `to_json_string()` (
48
48
  - Delegated rules, legal hierarchy, and administrative rules (고시·훈령·예규)
49
49
  - Law and administrative-rule annex/form bodies (별표·서식)
50
50
  - MOLEG and ministry legal interpretations, Supreme Court cases, and Constitutional Court (헌재) decisions
51
+ - Amendment rationale — the 「개정이유 및 주요내용」 text for a specific statute version
52
+ - Committee decisions from twelve regulators (개인정보보호위·공정위·금융위·인권위·노동위 등) and 행정심판 재결례 including four special tribunals — the record of an agency applying the law it administers, kept distinct from precedent
51
53
  - Query expansion, comparable-mechanism discovery, and staged context bundles with executable follow-up lookups
52
54
 
53
- Out of scope: legal advice, National Assembly bill data (status, votes, minutes), and latest statistics/news.
55
+ 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.
56
+
57
+ Out of scope: legal advice, National Assembly bill data (status, votes, minutes), legislative pre-announcement (입법예고 — a separate 국민참여입법센터 source), and latest statistics/news.
54
58
 
55
59
  ## Documentation
56
60
 
57
- Full documentation is in the [`wiki/`](wiki/Home.md) folder:
61
+ Full documentation is in the [`docs/wiki/`](docs/wiki/Home.md) folder:
58
62
 
59
- - [Installation](wiki/Installation.md) · [Quickstart](wiki/Quickstart.md) · [Core Concepts](wiki/Core-Concepts.md)
60
- - [CLI Reference](wiki/CLI-Reference.md) · [API Reference](wiki/API-Reference.md)
61
- - [Historical Versions](wiki/Historical-Versions.md) · [Sources & Coverage](wiki/Sources-and-Coverage.md)
62
- - [Gotchas](wiki/Gotchas.md) · [Error Handling](wiki/Error-Handling.md)
63
- - [Maintainer Notes](wiki/Maintainer-Notes.md)
63
+ - [Installation](docs/wiki/Installation.md) · [Quickstart](docs/wiki/Quickstart.md) · [Core Concepts](docs/wiki/Core-Concepts.md)
64
+ - [CLI Reference](docs/wiki/CLI-Reference.md) · [API Reference](docs/wiki/API-Reference.md)
65
+ - [Historical Versions](docs/wiki/Historical-Versions.md) · [Sources & Coverage](docs/wiki/Sources-and-Coverage.md)
66
+ - [Gotchas](docs/wiki/Gotchas.md) · [Error Handling](docs/wiki/Error-Handling.md)
67
+ - [Maintainer Notes](docs/wiki/Maintainer-Notes.md)
64
68
 
65
69
  ## Status
66
70
 
@@ -1,5 +1,6 @@
1
1
  """Public MOLEG-API interface."""
2
2
 
3
+ from ._version import __version__
3
4
  from .errors import (
4
5
  AmbiguousLawError,
5
6
  AsOfBeforeCoverageError,
@@ -13,6 +14,9 @@ from .errors import (
13
14
  )
14
15
  from .laws import MolegApi
15
16
  from .models import (
17
+ AdjudicationHit,
18
+ AdjudicationIdentity,
19
+ AdjudicationText,
16
20
  AdministrativeRuleArticleText,
17
21
  AdministrativeRuleContext,
18
22
  AdministrativeRuleHit,
@@ -63,6 +67,9 @@ from .models import (
63
67
  LawStructure,
64
68
  LawStructureNode,
65
69
  LawText,
70
+ LawToc,
71
+ LawTocEntry,
72
+ RevisionReason,
66
73
  SupplementaryProvision,
67
74
  StructuredTableData,
68
75
  )
@@ -120,7 +127,13 @@ __all__ = [
120
127
  "LawIdentity",
121
128
  "LawStructure",
122
129
  "LawStructureNode",
130
+ "AdjudicationHit",
131
+ "AdjudicationIdentity",
132
+ "AdjudicationText",
123
133
  "LawText",
134
+ "LawToc",
135
+ "LawTocEntry",
136
+ "RevisionReason",
124
137
  "MolegApi",
125
138
  "MolegApiError",
126
139
  "NoResultError",
@@ -132,4 +145,5 @@ __all__ = [
132
145
  "SupplementaryProvision",
133
146
  "StructuredTableData",
134
147
  "UnsupportedFormatError",
148
+ "__version__",
135
149
  ]
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import MISSING, fields, replace
4
+ from typing import Any
5
+
6
+ # A decision detail ships the same reasoning twice over: `text` and `full_text`
7
+ # are different sections of the document, not duplicates, but between them they
8
+ # are ~82% of the payload (29,321 of 35,866 characters on decision 193332). The
9
+ # structured extract — 판시사항, 결정요지, referenced authority — is what a reader
10
+ # needs to decide whether the decision is even on point. Loading 82,700 bytes to
11
+ # find out costs more than the answer is worth.
12
+ #
13
+ # Brief mode drops the full-body fields and keeps the extract. It does *not* drop
14
+ # `summary`: 요지 is the entire reason to ask for a brief, and cutting it would
15
+ # shrink the payload further while removing the only thing brief mode is for.
16
+ _BRIEF_DROPPED_FIELDS = ("text", "full_text", "reasoning")
17
+
18
+
19
+ def to_brief(result: Any) -> Any:
20
+ """Blank the full-body fields on a loaded authority document.
21
+
22
+ Returns the same type rather than a narrower one, so the model, the kind, and
23
+ every consumer's field access stay valid — the difference is signalled on the
24
+ envelope, not encoded in a second shape of the same thing.
25
+ """
26
+ # Reset each field to its declared default rather than to a blanket "" or
27
+ # None. `text: str = ""` and `reasoning: str | None = None` mean different
28
+ # things by absence, and blanking a nullable field to "" turns "not loaded"
29
+ # into "loaded and empty" — a distinction a caller reads as a fact about the
30
+ # document rather than about the request.
31
+ defaults = {f.name: f.default for f in fields(result)}
32
+ blanked = {
33
+ name: defaults[name]
34
+ for name in _BRIEF_DROPPED_FIELDS
35
+ if name in defaults and defaults[name] is not MISSING
36
+ }
37
+ return replace(result, **blanked) if blanked else result
38
+
39
+
40
+ def brief_dropped(result: Any) -> list[str]:
41
+ """Which full-body fields this document actually carried, for the flag.
42
+
43
+ Reported rather than assumed: a decision with no `full_text` on file must not
44
+ claim brief mode withheld one, or the caller will go looking for a section
45
+ that never existed.
46
+ """
47
+ return [
48
+ name
49
+ for name in _BRIEF_DROPPED_FIELDS
50
+ if getattr(result, name, None)
51
+ ]
52
+
53
+ __all__ = [name for name in globals() if not name.startswith("__")]
@@ -6,20 +6,23 @@ CATALOG = {
6
6
  "convention": [
7
7
  "kind 접미사가 후보≠본문을 구조로 표시: *_hit/*_candidate/*_planning = 검색 후보(인용 불가), *_text/*_context/*_identity = 로드된 본문.",
8
8
  "인용은 항상 로드된 본문에서만 — search/expand/find-comparable 결과는 '다음에 무엇을 로드할지의 메뉴'일 뿐.",
9
- "출처 권위 보존: 법제처 법령 > 법제처 해석 > 부처 해석 ≠ 판례 ≠ 헌재. flags.source_type/라벨을 답에 반영.",
10
- "0건·호출 실패 ≠ 부재. 종료코드로 구분: 0 ok(0건 포함) · 2 모호 · 3 소스접근실패 · 4 no-result · 5 usage/순서위반.",
9
+ "출처 권위 보존: 법제처 법령 > 법제처 해석 > 부처 해석 ≠ 판례 ≠ 헌재 ≠ 위원회 결정 ≠ 행정심판 재결. flags.source_type/source_authority를 답에 반영하고 평탄화 금지 — 위원회 결정·행정심판 재결은 행정기관의 판단이라 행정소송으로 뒤집힐 수 있고 판례가 아니다.",
10
+ "0건·호출 실패 ≠ 부재. 종료코드로 구분: 0 ok(0건 포함) · 2 모호 · 3 소스 접근·응답 문제 · 4 no-result · 5 usage/순서위반.",
11
+ "exit 3의 두 얼굴을 kind로 갈라라 — source_access_error=진짜 일시 장애(타임아웃·429·5xx)라 재시도가 맞고, parse_error=인식 불가 응답이라 재시도해도 그대로다(식별자 오류부터 의심). 불량 식별자로 본문이 없으면 exit 3이 아니라 no_result(exit 4)로 온다 — '일시 장애'로 읽지 마라.",
11
12
  "load 계열에 법령명을 주면 needs_search_first(exit 5) — 먼저 search-laws로 law_id를 얻어라.",
12
13
  "deferred는 data.deferred[i]를 load-followup --json -로 파이프(손타이핑 금지).",
14
+ "컨텍스트 예산: 큰 법 전체 로드(get-law) 전에 --toc로 조문 지도부터, 판결·헌재·해석은 --brief로 요지부터. flags.large_payload가 뜨면 범위를 좁힐 인자가 있다는 신호다(검색은 후보 반환이 설계라 예외).",
13
15
  "로더(get-law/get-article)는 기본이 현행 통합본, --as-of <날짜>(YYYY-MM-DD/YYYYMMDD만)면 그 시점 시행 버전을 로드한다. 반환 effective_date를 오늘과 대조해 현행/미시행을 판정하라 — flags.not_effective_as_of=공포됐으나 미시행(미래 시행일, 현행 인용 금지), version_mismatch={requested,loaded}=요청 시점과 반환 버전 시행일 불일치, version_request_unfulfilled=요청보다 이후 버전이 반환됨. 요청 시점이 통합본 커버리지보다 이르면 kind:version_request_unfulfilled+earliest_available(→trace-law-history).",
14
16
  "검색 스코프 뉘앙스: 헌재·판례는 기본이 제목(사건명) 검색이라 doctrine·본문 키워드는 --search-body(본문 전체)라야 매칭(0건이면 next가 재시도 제시). 별표는 --search-scope title(별표 제목)/source(소관 법령명, 토큰 근접)이라 0건이면 반대 스코프로. 부처 해석은 --source all_ministries(법제처+부처 혼합)로만 나오고, 부처 본문 로드는 --source ministry --ministry <기관> 필수(검색 hit의 follow_up.filters를 그대로 로더 인자로).",
15
17
  "개정 전후 델타=compare-law-versions는 소스가 주는 최근 두 버전만 비교하고 changes[].article은 본문에서 파싱한 실제 조문번호(매핑 불가 시 null). 임의 두 시점 델타는 get-article --as-of를 전후 날짜로 두 번 로드해 대조.",
16
18
  ],
17
19
  "routing_rules": [
18
20
  "본문 로드: 살아있는 조문=get-article / 이동·삭제 가능성 있는 조문=load-article-context(기본이 이동추적).",
19
- "개정: 무엇이 바뀌었나(전후 문구 델타)=compare-law-versions / 어떤 개정들이 있었나(연혁)=trace-law-history.",
21
+ "개정 3분해: 왜 바뀌었나(제안 취지 원문)=get-revision-reason / 무엇이 바뀌었나(전후 문구 델타)=compare-law-versions / 어떤 개정들이 있었나(연혁)=trace-law-history. 연혁 이벤트의 identity.mst가 곧 get-revision-reason --mst 입력이다.",
20
22
  "이 법 아래 무엇이 있나: 계층 조망=get-law-structure(위임 증명 아님) / 조문 단위 위임 규정=find-delegated-rules.",
21
23
  "넓은 탐색: 넓은 질의의 용어·관련법 조사계획=expand-legal-query / 유사 제도(비슷한 기제)를 가진 법 후보(설계용)=find-comparable-mechanisms.",
22
24
  "묶음 로더 — authority=특정 조문의 해석/판례/헌재 권위 / bundle=진입점 모를 때 단일법·넓은 질문(--mode) / institutional=명시된 다법령 집합(--statute 반복) / delegated=단일법의 하위규칙·별표 집행기준 본문.",
25
+ "감독기관이 실제로 판단했나(견제): 규제기관의 처분·의결서=search-committee-decisions --committee <ppc|ftc|fsc|sfc|kcc|nhrck|acr|nlrc|eiac|iaciac|oclt|ecc> / 그 처분에 불복한 재결=search-administrative-appeals --tribunal <decc|acr|adap|tt|kmst>. 둘 다 0건이어도 부재의 증명이 아니다(비공개·미접수 가능).",
23
26
  "별표 금액·기준표: 위임된 시행령·시행규칙의 별표를 search-annex-forms --search-scope source <법령명> → get-annex-form-body --id(=--annex-id)로 로드. 표 파싱이 무너지면 structured_data.parsing_confidence=low — 금액은 text를 1차로 인용하라. bare id 로드는 소관법령ID·pdf_link 등 링크 메타를 복구하지 못하니 현행성은 모법 버전으로 확인.",
24
27
  ],
25
28
  "commands": {
@@ -27,21 +30,25 @@ CATALOG = {
27
30
  "search-laws", "resolve-promulgated-law", "search-administrative-rules", "search-annex-forms",
28
31
  "search-interpretations", "search-cases", "search-constitutional-decisions",
29
32
  "expand-legal-query", "find-comparable-mechanisms",
33
+ "search-committee-decisions", "search-administrative-appeals",
30
34
  ],
31
35
  "본문 로드": [
32
36
  "get-law", "get-article", "load-article-context", "get-administrative-rule",
33
37
  "load-administrative-rule-context", "get-annex-form-body", "get-interpretation",
34
38
  "get-case", "get-constitutional-decision",
39
+ "get-committee-decision", "get-administrative-appeal",
35
40
  ],
36
- "연혁·체계·위임": ["trace-law-history", "compare-law-versions", "find-delegated-rules", "get-law-structure"],
41
+ "연혁·체계·위임": ["trace-law-history", "get-revision-reason", "compare-law-versions", "find-delegated-rules", "get-law-structure"],
37
42
  "권위·묶음": ["load-authority-context", "load-legal-context-bundle", "load-institutional-system", "load-delegated-criteria", "load-followup"],
38
43
  },
39
44
  "kinds": [
40
45
  "law_hit_list", "admin_rule_hit_list", "annex_form_hit_list", "interpretation_hit_list",
41
46
  "case_hit_list", "constitutional_hit_list", "comparable_planning_list", "query_expansion_planning",
47
+ "committee_decision_hit_list", "administrative_appeal_hit_list",
48
+ "committee_decision_text", "administrative_appeal_text",
42
49
  "law_text", "article_text", "article_context", "admin_rule_text", "admin_rule_context",
43
50
  "annex_form_text", "interpretation_text", "case_text", "constitutional_text", "law_identity",
44
- "law_history", "law_diff", "delegation_graph", "law_structure_hierarchy_only",
51
+ "law_history", "revision_reason_text", "law_toc_map", "law_diff", "delegation_graph", "law_structure_hierarchy_only",
45
52
  "legal_context_bundle", "authority_context",
46
53
  "ambiguous", "source_access_error", "parse_error", "no_result",
47
54
  "needs_search_first", "usage_error", "unsupported", "error",
@@ -4,8 +4,11 @@ from .foundation import *
4
4
 
5
5
  EXIT_OK = 0 # includes a zero-hit search (ok:true, count:0)
6
6
  EXIT_AMBIGUOUS = 2 # multiple plausible identities — surface, don't pick
7
- EXIT_SOURCE = 3 # transient source access failure (rate limit / retry / source)
8
- EXIT_NO_RESULT = 4 # a load found no source text for a valid identifier
7
+ EXIT_SOURCE = 3 # source could not be read OR could not be parsed — kind splits it:
8
+ # source_access_error = transient (rate limit / retry / 5xx), retry is right
9
+ # parse_error = unrecognized response shape, retry won't help
10
+ EXIT_NO_RESULT = 4 # a well-formed lookup found no source text — includes a bad identifier,
11
+ # which law.go.kr signals with an empty detail body (see unwrap_service_payload)
9
12
  EXIT_USAGE = 5 # bad arguments, or a loader was handed a law name (search first)
10
13
 
11
14
  # Public MolegApi methods reachable through load-followup rehydration. Guards the
@@ -54,6 +57,8 @@ SEARCH_COMMANDS = frozenset(
54
57
  "search-interpretations",
55
58
  "search-cases",
56
59
  "search-constitutional-decisions",
60
+ "search-committee-decisions",
61
+ "search-administrative-appeals",
57
62
  }
58
63
  )
59
64
 
@@ -3,9 +3,22 @@ from __future__ import annotations
3
3
  from .foundation import *
4
4
  from .constants import CliError, EXIT_USAGE, FOLLOWUP_HANDOFFS, FOLLOWUP_INTERFACES
5
5
  from .data import _statute_args
6
+ from .brief_mode import brief_dropped, to_brief
6
7
  from .signals_meta import parse_as_of
7
8
 
8
9
  def _call(api: MolegApi, args: argparse.Namespace) -> Any:
10
+ result = _dispatch(api, args)
11
+ if getattr(args, "brief", False):
12
+ # Record what was actually withheld *before* blanking it. Signals only
13
+ # sees the trimmed result, and "empty because brief" must stay
14
+ # distinguishable from "empty because the source had none" — otherwise a
15
+ # caller goes hunting for a section that never existed.
16
+ args.brief_dropped = brief_dropped(result)
17
+ return to_brief(result)
18
+ return result
19
+
20
+
21
+ def _dispatch(api: MolegApi, args: argparse.Namespace) -> Any:
9
22
  c = args.command
10
23
  # Strictly validate --as-of once, for every command that carries it, so a
11
24
  # malformed date is a usage error rather than a silent wrong-version load.
@@ -31,11 +44,21 @@ def _call(api: MolegApi, args: argparse.Namespace) -> Any:
31
44
  if c == "search-constitutional-decisions":
32
45
  return api.search_constitutional_decisions(args.query, search_body=args.search_body,
33
46
  decided_on=args.decided_on, case_number=args.case_number, display=args.display)
47
+ if c == "search-committee-decisions":
48
+ return api.search_committee_decisions(args.query, committee=args.committee, display=args.display)
49
+ if c == "get-committee-decision":
50
+ return api.get_committee_decision(args.identifier, committee=args.committee)
51
+ if c == "search-administrative-appeals":
52
+ return api.search_administrative_appeals(args.query, tribunal=args.tribunal, display=args.display)
53
+ if c == "get-administrative-appeal":
54
+ return api.get_administrative_appeal(args.identifier, tribunal=args.tribunal)
34
55
  if c == "expand-legal-query":
35
56
  return api.expand_legal_query(args.query, display=args.display, include_websearch_hint=not args.no_websearch_hint)
36
57
  if c == "find-comparable-mechanisms":
37
58
  return api.find_comparable_mechanisms(args.concept, display=args.display)
38
59
  if c == "get-law":
60
+ if args.toc:
61
+ return api.get_law_toc(args.law, as_of=args.as_of, basis=args.basis)
39
62
  return api.get_law(args.law, as_of=args.as_of, basis=args.basis,
40
63
  articles=args.article or None, include_metadata=not args.no_metadata)
41
64
  if c == "get-article":
@@ -60,6 +83,8 @@ def _call(api: MolegApi, args: argparse.Namespace) -> Any:
60
83
  if c == "trace-law-history":
61
84
  date_range = (args.date_from, args.date_to) if (args.date_from and args.date_to) else None
62
85
  return api.trace_law_history(args.law, date_range=date_range, article=args.article)
86
+ if c == "get-revision-reason":
87
+ return api.get_revision_reason(args.law, mst=args.mst, as_of=args.as_of)
63
88
  if c == "compare-law-versions":
64
89
  return api.compare_law_versions(args.law, article=args.article)
65
90
  if c == "find-delegated-rules":
@@ -4,7 +4,6 @@ import argparse
4
4
  import json
5
5
  import sys
6
6
  from datetime import date, datetime
7
- from importlib.metadata import PackageNotFoundError, version as _dist_version
8
7
  from typing import Any
9
8
 
10
9
  from ..errors import (
@@ -21,10 +20,4 @@ from ..laws import MolegApi
21
20
  from ..models import DeferredLookup
22
21
 
23
22
 
24
- def _pkg_version() -> str:
25
- try:
26
- return _dist_version("moleg-api")
27
- except PackageNotFoundError:
28
- return "unknown"
29
-
30
23
  __all__ = [name for name in globals() if not name.startswith("__")]
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from .foundation import *
4
+ from .._version import __version__
4
5
  from .catalog import CATALOG
5
6
  from .constants import (
6
7
  CliError,
@@ -14,10 +15,22 @@ from .constants import (
14
15
  from .data import _to_data
15
16
  from .dispatch import _call
16
17
  from .parser import build_parser
18
+ from .payload_size import large_payload_signals
17
19
  from .signals import signals_for
18
20
 
19
21
  def _emit(payload: dict[str, Any]) -> None:
20
- print(json.dumps(payload, ensure_ascii=False, indent=2))
22
+ # Stamped here rather than at each call site so no envelope — success, error,
23
+ # or catalog — can ship without it. A consumer that cannot tell which version
24
+ # answered cannot tell whether a missing field means "not supported yet" or
25
+ # "the call failed", which is how a stale install reads as a data gap.
26
+ stamped: dict[str, Any] = {}
27
+ for key, value in payload.items():
28
+ stamped[key] = value
29
+ if key == "command":
30
+ stamped["version"] = __version__
31
+ if "version" not in stamped:
32
+ stamped["version"] = __version__
33
+ print(json.dumps(stamped, ensure_ascii=False, indent=2))
21
34
 
22
35
 
23
36
  def main(argv: list[str] | None = None, *, api: MolegApi | None = None) -> int:
@@ -66,7 +79,8 @@ def main(argv: list[str] | None = None, *, api: MolegApi | None = None) -> int:
66
79
  return EXIT_NO_RESULT
67
80
  except ParseFailureError as exc:
68
81
  _emit({"ok": False, "command": args.command, "kind": "parse_error", "error": str(exc),
69
- "discipline": ["소스 응답을 정규화하지 못함(소스접근 실패·법령 부재 아님) 다른 경로/커맨드로 확인하라."]})
82
+ "discipline": ["소스가 인식 불가한 형태의 응답을 같은 호출을 재시도해도 대개 그대로다(일시 장애와 다름).",
83
+ "식별자 오류 가능성을 먼저 배제하라(search-*로 신원 재확인). 그래도 같으면 다른 경로/커맨드로 확인하고, 부재로 단정하지 마라."]})
70
84
  return EXIT_SOURCE
71
85
  except NoResultError as exc:
72
86
  msg = str(exc)
@@ -84,7 +98,8 @@ def main(argv: list[str] | None = None, *, api: MolegApi | None = None) -> int:
84
98
  return EXIT_USAGE
85
99
  else:
86
100
  _emit({"ok": False, "command": args.command, "kind": "no_result", "error": msg,
87
- "discipline": ["이 식별자·조회로 소스 본문 없음 — 검색어·범위를 밝히고 대체 경로를 시도하기 부재로 단정 금지."]})
101
+ "discipline": ["이 식별자·조회로 소스 본문 없음 — 일시 장애가 아니니 재시도는 무의미하다. 식별자가 틀렸을 있으니 search-* 계열로 신원을 재확인하라.",
102
+ "검색어·범위를 밝히고 대체 경로를 시도하기 전 부재로 단정 금지."]})
88
103
  return EXIT_NO_RESULT
89
104
  except UnsupportedFormatError as exc:
90
105
  _emit({"ok": False, "command": args.command, "kind": "unsupported", "error": str(exc),
@@ -105,6 +120,16 @@ def main(argv: list[str] | None = None, *, api: MolegApi | None = None) -> int:
105
120
  if "count" in sig:
106
121
  envelope["count"] = sig["count"]
107
122
  envelope["data"] = _to_data(result, include_raw=include_raw)
123
+ # Measured on the serialized data, after every narrowing option has had its
124
+ # effect — a guess from a row count would fire on 139 short articles and stay
125
+ # quiet on 20 long ones, and would need updating for every command added later.
126
+ size_flags, size_discipline = large_payload_signals(
127
+ args.command,
128
+ len(json.dumps(envelope["data"], ensure_ascii=False)),
129
+ {"articles": len(result.articles)} if isinstance(getattr(result, "articles", None), list) else None,
130
+ )
131
+ sig["flags"].update(size_flags)
132
+ sig["discipline"].extend(size_discipline)
108
133
  if sig["flags"]:
109
134
  envelope["flags"] = sig["flags"]
110
135
  if sig["discipline"]:
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from .foundation import *
4
+ from .._version import __version__
4
5
  from .signals_meta import parse_as_of
5
6
 
6
7
  def _add_law(p: argparse.ArgumentParser, required: bool = True) -> None:
@@ -19,6 +20,11 @@ def _add_articles(p: argparse.ArgumentParser, required: bool = False) -> None:
19
20
  p.add_argument("--article", dest="article", action="append", default=[], required=required, help="조문(예: 제3조). 반복 지정 가능.")
20
21
 
21
22
 
23
+ def _add_brief(p: argparse.ArgumentParser) -> None:
24
+ p.add_argument("--brief", action="store_true",
25
+ help="전문(text/full_text) 없이 요지·판시사항·참조 관계만. 인용은 전문 로드 후.")
26
+
27
+
22
28
  def _add_budget(p: argparse.ArgumentParser) -> None:
23
29
  p.add_argument("--budget", choices=["minimal", "standard", "broad"], default="standard")
24
30
 
@@ -32,12 +38,12 @@ def build_parser() -> argparse.ArgumentParser:
32
38
  parser.add_argument(
33
39
  "--version",
34
40
  action="version",
35
- version=f"moleg-api {_pkg_version()}",
36
- help="설치된 moleg-api 버전을 출력하고 종료.",
41
+ version=f"moleg-api {__version__}",
42
+ help="실행 중인 moleg-api 버전을 출력하고 종료(엔벨로프의 version과 같은 값).",
37
43
  )
38
44
  sub = parser.add_subparsers(dest="command", metavar="<command>")
39
45
 
40
- sub.add_parser("catalog", help="27개 서브커맨드·규약·kind 목록을 한 번에.")
46
+ sub.add_parser("catalog", help="전체 서브커맨드·규약·kind 목록을 한 번에.")
41
47
 
42
48
  # ---- searches / planning ------------------------------------------- #
43
49
  p = sub.add_parser("search-laws", help="현행/공포 법령 신원 후보 검색.")
@@ -89,6 +95,28 @@ def build_parser() -> argparse.ArgumentParser:
89
95
  p.add_argument("--case-number", dest="case_number", default=None)
90
96
  p.add_argument("--display", type=int, default=20)
91
97
 
98
+ p = sub.add_parser("search-committee-decisions", help="위원회 결정문 검색(개보위·공정위·금융위·인권위 등 12종).")
99
+ p.add_argument("query", nargs="?", default=None, help="안건명·사건명 키워드(생략 시 최신 목록).")
100
+ p.add_argument("--committee", required=True,
101
+ help="기관 코드: ppc(개인정보보호위) ftc(공정위) fsc(금융위) sfc(증선위) kcc(방통위) nhrck(인권위) acr(권익위) nlrc(노동위) eiac(고용보험심사위) iaciac(산재재심사위) oclt(중앙토지수용위) ecc(중앙환경분쟁조정위).")
102
+ p.add_argument("--display", type=int, default=20)
103
+
104
+ p = sub.add_parser("get-committee-decision", help="위원회 결정문 본문 로드(--brief면 요지만).")
105
+ p.add_argument("--id", dest="identifier", required=True, help="decision_id(검색이 준 값).")
106
+ p.add_argument("--committee", required=True, help="검색에 쓴 기관 코드와 동일해야 한다.")
107
+ _add_brief(p)
108
+
109
+ p = sub.add_parser("search-administrative-appeals", help="행정심판 재결례 검색(일반 + 특별심판 4종).")
110
+ p.add_argument("query", nargs="?", default=None, help="사건명 키워드(생략 시 최신 목록).")
111
+ p.add_argument("--tribunal", default="decc",
112
+ help="decc(일반 행정심판위) acr(권익위 특별) adap(소청심사위) tt(조세심판원) kmst(해양안전심판원).")
113
+ p.add_argument("--display", type=int, default=20)
114
+
115
+ p = sub.add_parser("get-administrative-appeal", help="행정심판 재결 본문 로드(--brief면 요지만).")
116
+ p.add_argument("--id", dest="identifier", required=True)
117
+ p.add_argument("--tribunal", default="decc", help="검색에 쓴 심판기관 코드와 동일해야 한다.")
118
+ _add_brief(p)
119
+
92
120
  p = sub.add_parser("expand-legal-query", help="질의 확장·관련법/용어/조문 조사 계획.")
93
121
  p.add_argument("query"); p.add_argument("--display", type=int, default=5)
94
122
  p.add_argument("--no-websearch-hint", dest="no_websearch_hint", action="store_true")
@@ -97,9 +125,11 @@ def build_parser() -> argparse.ArgumentParser:
97
125
  p.add_argument("concept"); p.add_argument("--display", type=int, default=5)
98
126
 
99
127
  # ---- loaders -------------------------------------------------------- #
100
- p = sub.add_parser("get-law", help="법령 본문 로드.")
128
+ p = sub.add_parser("get-law", help="법령 본문 로드(--toc면 본문 없이 조문 목차만).")
101
129
  _add_law(p); _add_as_of(p); _add_basis(p); _add_articles(p)
102
130
  p.add_argument("--no-metadata", dest="no_metadata", action="store_true")
131
+ p.add_argument("--toc", action="store_true",
132
+ help="본문 대신 조문 목차(장·절 표제 + 조번호·조제목 + 삭제/이동)만. 큰 법의 지도를 수 KB로.")
103
133
 
104
134
  p = sub.add_parser("get-article", help="조문 하나 로드.")
105
135
  _add_law(p); p.add_argument("article"); _add_as_of(p); _add_basis(p)
@@ -124,19 +154,22 @@ def build_parser() -> argparse.ArgumentParser:
124
154
  p.add_argument("--no-metadata", dest="no_metadata", action="store_true")
125
155
  p.add_argument("--no-structuring", dest="no_structuring", action="store_true")
126
156
 
127
- p = sub.add_parser("get-interpretation", help="법령해석례 본문 로드.")
157
+ p = sub.add_parser("get-interpretation", help="법령해석례 본문 로드(--brief면 질의·회답만).")
128
158
  p.add_argument("--id", dest="identifier", required=True)
129
159
  p.add_argument("--source", choices=["moleg", "ministry", "all", "all_ministries"], default=None)
130
160
  p.add_argument("--ministry", default=None)
131
161
  p.add_argument("--no-metadata", dest="no_metadata", action="store_true")
162
+ _add_brief(p)
132
163
 
133
- p = sub.add_parser("get-case", help="판례 본문 로드.")
164
+ p = sub.add_parser("get-case", help="판례 본문 로드(--brief면 요지만).")
134
165
  p.add_argument("--id", dest="identifier", required=True)
135
166
  p.add_argument("--no-metadata", dest="no_metadata", action="store_true")
167
+ _add_brief(p)
136
168
 
137
- p = sub.add_parser("get-constitutional-decision", help="헌재 결정 본문 로드.")
169
+ p = sub.add_parser("get-constitutional-decision", help="헌재 결정 본문 로드(--brief면 요지만).")
138
170
  p.add_argument("--id", dest="identifier", required=True)
139
171
  p.add_argument("--no-metadata", dest="no_metadata", action="store_true")
172
+ _add_brief(p)
140
173
 
141
174
  # ---- history / structure / delegation ------------------------------ #
142
175
  p = sub.add_parser("trace-law-history", help="개정 연혁 이벤트.")
@@ -144,6 +177,11 @@ def build_parser() -> argparse.ArgumentParser:
144
177
  p.add_argument("--date-from", dest="date_from", default=None)
145
178
  p.add_argument("--date-to", dest="date_to", default=None)
146
179
 
180
+ p = sub.add_parser("get-revision-reason", help="특정 버전의 「개정이유 및 주요내용」·공포문 원문.")
181
+ _add_law(p)
182
+ p.add_argument("--mst", default=None, help="버전 MST(trace-law-history 이벤트의 identity.mst). 미지정이면 최신 버전.")
183
+ p.add_argument("--as-of", dest="as_of", default=None, help="그 시점 시행 버전의 개정이유(YYYY-MM-DD/YYYYMMDD).")
184
+
147
185
  p = sub.add_parser("compare-law-versions", help="개정 전후 조문 문구 비교(소스 제공 전후 쌍).")
148
186
  _add_law(p); p.add_argument("--article", default=None)
149
187
 
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from .foundation import *
4
+
5
+ # Measured on 2026-07-19 against live law.go.kr (bytes of the emitted envelope):
6
+ #
7
+ # load-delegated-criteria --budget standard 513,570
8
+ # load-institutional-system --budget standard 396,609
9
+ # get-law (139 articles, no --article) 276,748
10
+ # get-case (one decision) 82,700
11
+ # load-legal-context-bundle --budget broad 37,822
12
+ # load-authority-context 6,889
13
+ # get-article 2,280
14
+ #
15
+ # The threshold is in *characters*, not bytes, because characters track token
16
+ # cost and Korean text is three bytes each — measuring bytes would make the same
17
+ # amount of reading look three times worse in Korean than in English. 20,000
18
+ # characters is roughly 10–13k tokens: enough that a caller who could have
19
+ # narrowed the request should be told they didn't.
20
+ LARGE_PAYLOAD_CHARS = 20_000
21
+
22
+ # Searches are exempt. They return candidate identities by design — that breadth
23
+ # is what resolves ambiguity, and 20 hits costs ~18,000 characters, close enough
24
+ # to the line that including them would make the signal fire on the one command
25
+ # where the size is the intended behaviour.
26
+ _EXEMPT_PREFIXES = ("search-", "resolve-", "expand-", "find-comparable")
27
+
28
+ _ADVICE = {
29
+ "get-law": "전체 로드 대신 --toc로 조문 지도를 먼저 받고, 필요한 조문만 --article로 표적 로드하라.",
30
+ "get-case": "--brief로 요지(판시사항·결정요지)만 먼저 받고, 전문은 인용이 필요할 때만 로드하라.",
31
+ "get-constitutional-decision": "--brief로 요지만 먼저 받고, 전문은 인용이 필요할 때만 로드하라.",
32
+ "get-interpretation": "--brief로 질의·회답만 먼저 받고, 전문은 인용이 필요할 때만 로드하라.",
33
+ "load-delegated-criteria": "--budget minimal로 좁히거나, 기준이 걸린 조문만 --article로 지정하라.",
34
+ "load-institutional-system": "--budget minimal로 좁히거나, 실제로 볼 법령만 --statute으로 지정하라.",
35
+ "load-legal-context-bundle": "--budget minimal로 좁히거나, 진입 조문을 --article로 지정하라.",
36
+ }
37
+
38
+ _DEFAULT_ADVICE = "이 응답은 컨텍스트를 크게 차지한다 — 범위를 좁힐 인자(--article·--budget·--brief 등)가 있는지 확인하라."
39
+
40
+
41
+ def large_payload_signals(command: str, data_chars: int, extra: dict[str, Any] | None = None) -> tuple[dict[str, Any], list[str]]:
42
+ """Flag a response big enough that the caller should have narrowed it.
43
+
44
+ Conditional like every other signal here: a small statute loads silently, so
45
+ the flag firing means something. Measured on the serialized data rather than
46
+ guessed from a row count, because the same number of articles can differ by
47
+ an order of magnitude in length — and because a size check written against
48
+ the actual output keeps working for commands added later.
49
+ """
50
+ if data_chars < LARGE_PAYLOAD_CHARS or any(command.startswith(p) for p in _EXEMPT_PREFIXES):
51
+ return {}, []
52
+ flags: dict[str, Any] = {"large_payload": {"chars": data_chars}}
53
+ if extra:
54
+ flags["large_payload"].update(extra)
55
+ return flags, [_ADVICE.get(command, _DEFAULT_ADVICE)]
56
+
57
+ __all__ = [name for name in globals() if not name.startswith("__")]