sud-cli 0.1.1__py3-none-any.whl
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.
- sud_cli/__init__.py +3 -0
- sud_cli/__main__.py +4 -0
- sud_cli/_wm.py +118 -0
- sud_cli/agenda.py +90 -0
- sud_cli/autofill.py +192 -0
- sud_cli/autofill_markers.json +56 -0
- sud_cli/backup.py +256 -0
- sud_cli/calendar_ics.py +191 -0
- sud_cli/case_diff.py +165 -0
- sud_cli/case_search_bridge.py +1322 -0
- sud_cli/chain.py +226 -0
- sud_cli/cli.py +3721 -0
- sud_cli/completeness.py +105 -0
- sud_cli/config.py +20 -0
- sud_cli/conflict.py +210 -0
- sud_cli/conflicts.py +207 -0
- sud_cli/court_alerts.py +156 -0
- sud_cli/court_source.py +108 -0
- sud_cli/data/completeness_rules.json +18 -0
- sud_cli/data/key_rates.json +59 -0
- sud_cli/data/npa_historical_editions.json +19 -0
- sud_cli/db.py +2072 -0
- sud_cli/deadline_matrix.py +99 -0
- sud_cli/deadline_state.py +25 -0
- sud_cli/deadlines.py +8 -0
- sud_cli/deadlines_state.py +1 -0
- sud_cli/deferral.py +5 -0
- sud_cli/efrsb_manual.py +85 -0
- sud_cli/efrsb_public.py +210 -0
- sud_cli/egrul_nalog.py +150 -0
- sud_cli/explain.py +190 -0
- sud_cli/fees_extra.py +104 -0
- sud_cli/forms.py +204 -0
- sud_cli/fssp_manual.py +91 -0
- sud_cli/fssp_semi.py +285 -0
- sud_cli/fssp_stats.py +105 -0
- sud_cli/government_transfers.json +8 -0
- sud_cli/holidays_rf.json +62 -0
- sud_cli/idempotency.py +78 -0
- sud_cli/importing.py +575 -0
- sud_cli/interest.py +144 -0
- sud_cli/jurisdiction.py +177 -0
- sud_cli/jurisdiction_rules.json +265 -0
- sud_cli/legal_deadlines.py +278 -0
- sud_cli/legal_engine.py +558 -0
- sud_cli/legal_rules.json +1058 -0
- sud_cli/limitation.py +70 -0
- sud_cli/mcp_server.py +1594 -0
- sud_cli/metrics.py +142 -0
- sud_cli/npa_as_of.py +21 -0
- sud_cli/output.py +89 -0
- sud_cli/payment.py +24 -0
- sud_cli/pdn.py +259 -0
- sud_cli/plenum_corpus.py +134 -0
- sud_cli/plenums_seed.json +45 -0
- sud_cli/practice_status.py +136 -0
- sud_cli/pravo_publication.py +361 -0
- sud_cli/pretrial.py +151 -0
- sud_cli/production_calendar.py +233 -0
- sud_cli/providers.py +95 -0
- sud_cli/readiness.py +234 -0
- sud_cli/regional_holidays.py +37 -0
- sud_cli/reminders.py +92 -0
- sud_cli/reports.py +349 -0
- sud_cli/sud_netguard.py +943 -0
- sud_cli/timeline_audit.py +80 -0
- sud_cli/verify.py +201 -0
- sud_cli-0.1.1.dist-info/METADATA +119 -0
- sud_cli-0.1.1.dist-info/RECORD +74 -0
- sud_cli-0.1.1.dist-info/WHEEL +4 -0
- sud_cli-0.1.1.dist-info/entry_points.txt +2 -0
- sud_cli-0.1.1.dist-info/licenses/AUTHORS +5 -0
- sud_cli-0.1.1.dist-info/licenses/LICENSE.md +661 -0
- sud_cli-0.1.1.dist-info/licenses/NOTICE +7 -0
sud_cli/mcp_server.py
ADDED
|
@@ -0,0 +1,1594 @@
|
|
|
1
|
+
"""sud-cli MCP stdio server: JSON-RPC 2.0 tool surface.
|
|
2
|
+
|
|
3
|
+
v2.2.0 (боевая рецензия colleague-legal, A2A 001a16046ebe, 21.10):
|
|
4
|
+
- типизированные inputSchema у всех инструментов (properties/required/enum/
|
|
5
|
+
default) вместо единого {"type":"object"} — агент видит сигнатуры;
|
|
6
|
+
- additionalProperties=false: неизвестный параметр — явная ошибка, а не
|
|
7
|
+
молчаливое игнорирование (fail-closed);
|
|
8
|
+
- выведены недостающие тулзы: case_history, payment, deferral,
|
|
9
|
+
efrsb_companies/persons/publications/managers, sudrf_live, mos_live
|
|
10
|
+
(через case-search bridge; без SUD_CLI_BRIDGE_LIVE возвращают
|
|
11
|
+
status=blocked и не порождают дочерних процессов).
|
|
12
|
+
|
|
13
|
+
v2.3.0 (письмо A2A colleague-legal 001a1608, 21.10):
|
|
14
|
+
- sud_cli_case_diff (первичное по письму, вариант «в»): offline-дифф двух
|
|
15
|
+
сохранённых snapshots одного дела (sud_cli.case_diff.diff_case_snapshots,
|
|
16
|
+
тот же код, что в CLI 'case-search case-diff');
|
|
17
|
+
- sud_cli_npa_compare (аддитивно, вариант «а»): сравнение двух редакций
|
|
18
|
+
сохранённого НПА-фикстура (sud_cli.db.compare_npa_editions);
|
|
19
|
+
- доменные ошибки DiffError/SearchError конвертируются в ValueError внутри
|
|
20
|
+
_call: handle_request ловит только (KeyError, TypeError, ValueError,
|
|
21
|
+
OSError), поэтому без конверсии serve_stdio падал бы вместо того, чтобы
|
|
22
|
+
вернуть клиенту isError=True с текстом ошибки.
|
|
23
|
+
|
|
24
|
+
v2.3.1 (боевой аудит colleague-legal, ТЗ task-brief, письмо A2A 001a1609):
|
|
25
|
+
- sud_cli_fees: claim_amount объявлен number (integer терял копейки), база
|
|
26
|
+
расчёта доходит до движка как Decimal; имя вида пошлины принимается и как
|
|
27
|
+
fee_kind (имя движка), и как kind (CLI --kind) — required через anyOf;
|
|
28
|
+
- CLI-алиасы объявлены в схемах там, где вызов по CLI-имени падал: fees
|
|
29
|
+
applicant, limitation type/years/months, interest start_date/end_date,
|
|
30
|
+
case_history case_id, efrsb_persons name;
|
|
31
|
+
- два имени с разными значениями — явная ошибка, а не молчаливый выбор одной
|
|
32
|
+
стороны (_pick);
|
|
33
|
+
- enum'ы приведены к значениям, которые действительно принимает движок:
|
|
34
|
+
юрисдикции пошлин (arbitration|general_jurisdiction|kas, алиасы
|
|
35
|
+
arbitral/soyu распознаёт хендлер), секции sudact, applicant, виды пошлины;
|
|
36
|
+
- serverInfo.version больше не константа, а sud_cli.__version__ — раньше
|
|
37
|
+
версия сервера расходилась с пакетом молча.
|
|
38
|
+
|
|
39
|
+
v2.11.0 (task-briefa §1, S-80): MCP/CLI паритет + права + идемпотентность:
|
|
40
|
+
- каждый инструмент несёт поле permission: read|write (источник — WRITE_TOOLS,
|
|
41
|
+
единый для всех 45); write-тулы требуют явный confirm: true (без него
|
|
42
|
+
status=needs_confirmation, запись не производится) и принимают
|
|
43
|
+
optional idempotency_key (реестр state-dir/idempotency.json, окно 10 мин,
|
|
44
|
+
повтор = первый результат + idempotent_replay=true в теле);
|
|
45
|
+
- +10 офлайн-утилитных тулов паритета (anchor/confirm/waive/list/audit/
|
|
46
|
+
autofill, calendar agenda/ics, reminders due, case-search alerts);
|
|
47
|
+
ни один сетевой live-тул в этой волне не добавлен;
|
|
48
|
+
- mcp_exposed_commands()/TOOL_CLI_PATH — карта паритета против дерева CLI,
|
|
49
|
+
live-тулы помечены LIVE_GATED_TOOLS и не входят в обязательный список.
|
|
50
|
+
"""
|
|
51
|
+
from __future__ import annotations
|
|
52
|
+
|
|
53
|
+
import json
|
|
54
|
+
import os
|
|
55
|
+
import sys
|
|
56
|
+
from datetime import date, timedelta
|
|
57
|
+
from decimal import Decimal
|
|
58
|
+
from pathlib import Path
|
|
59
|
+
from typing import Any
|
|
60
|
+
|
|
61
|
+
from . import __version__
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
DATE = {"type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "description": "ISO date YYYY-MM-DD"}
|
|
65
|
+
|
|
66
|
+
#: Виды пошлины из legal_rules.json (engine-имена; CLI вызывает их как --kind).
|
|
67
|
+
FEE_KINDS = ["property_claim", "court_order", "non_property_claim", "appeal",
|
|
68
|
+
"cassation_first", "cassation_supreme", "bankruptcy_petition",
|
|
69
|
+
"bankruptcy_creditor_claim"]
|
|
70
|
+
|
|
71
|
+
#: Юрисдикции: реальные ключи legal_rules.json. CLI знает только их; схема до
|
|
72
|
+
#: v2.3.1 рекламировала arbitral/soyu/constitutional, которых в движке нет.
|
|
73
|
+
FEE_JURISDICTIONS = ["arbitration", "general_jurisdiction", "kas"]
|
|
74
|
+
|
|
75
|
+
#: Алиасы юрисдикции для вызова по MCP (обратная совместимость со старым enum).
|
|
76
|
+
JURISDICTION_ALIASES = {
|
|
77
|
+
"arbitral": "arbitration", "as": "arbitration",
|
|
78
|
+
"soyu": "general_jurisdiction", "general": "general_jurisdiction",
|
|
79
|
+
"kas": "kas",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#: Канонические секции sudact (normalize_section); 'general' движок не знает.
|
|
83
|
+
SUDACT_SECTIONS = ["arbitral", "regular", "magistrate", "vsrf"]
|
|
84
|
+
|
|
85
|
+
TOOLS: list[dict[str, Any]] = [
|
|
86
|
+
{"name": "sud_cli_limitation", "description": "Calculate a civil limitation period.",
|
|
87
|
+
"schema": {"type": "object", "properties": {
|
|
88
|
+
"start": {**DATE, "description": "Начало течения срока"},
|
|
89
|
+
"period_type": {"type": "string", "default": "general", "description": "Тип срока (general/special/custom_years и др.); CLI-алиас: type"},
|
|
90
|
+
"type": {"type": "string", "description": "CLI-алиас period_type (--type)"},
|
|
91
|
+
"claim_notice_days": {"type": "integer", "default": 0, "description": "Дни досудебного порядка (+30)"},
|
|
92
|
+
"check_date": {**DATE, "description": "Дата проверки (default сегодня)"},
|
|
93
|
+
"custom_years": {"type": "integer", "default": 0, "description": "Лет (period_type=custom_years); CLI-алиас: years"},
|
|
94
|
+
"custom_months": {"type": "integer", "default": 0, "description": "Месяцев; CLI-алиас: months"},
|
|
95
|
+
"years": {"type": "integer", "description": "CLI-алиас custom_years (--years)"},
|
|
96
|
+
"months": {"type": "integer", "description": "CLI-алиас custom_months (--months)"}},
|
|
97
|
+
"required": ["start"], "additionalProperties": False}},
|
|
98
|
+
{"name": "sud_cli_deadlines", "description": "Calculate a procedural deadline.",
|
|
99
|
+
"schema": {"type": "object", "properties": {
|
|
100
|
+
"deadline_key": {"type": "string", "description": "Ключ из утверждённой матрицы сроков"},
|
|
101
|
+
"event_date": {**DATE}, "region": {"type": "string"}, "tz": {"type": "string"},
|
|
102
|
+
"case_id": {"type": "string"},
|
|
103
|
+
"deadlines_command": {"type": "string"},
|
|
104
|
+
"confirm": {"type": "boolean"}, "by": {"type": "string"},
|
|
105
|
+
"evidence": {"type": "string"}, "norm": {"type": "string"},
|
|
106
|
+
"waive": {"type": "boolean"}},
|
|
107
|
+
"required": ["deadline_key"], "additionalProperties": False}},
|
|
108
|
+
{"name": "sud_cli_deadlines_matrix", "description": "Deadline matrix (control points) for a case.",
|
|
109
|
+
"schema": {"type": "object", "properties": {
|
|
110
|
+
"case_id": {"type": "string", "description": "Case id; CLI-алиас: id"},
|
|
111
|
+
"id": {"type": "string", "description": "CLI-алиас case_id"},
|
|
112
|
+
"instance": {"type": "string", "enum": ["apk", "gpk", "kas"], "description": "Override инстанции"},
|
|
113
|
+
"as_of": {"type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "description": "Дата as-of (не влияет на расчёт)"},
|
|
114
|
+
"missing": {"type": "boolean", "description": "Только точки needs_anchor"}},
|
|
115
|
+
"anyOf": [{"required": ["case_id"]}, {"required": ["id"]}],
|
|
116
|
+
"additionalProperties": False}},
|
|
117
|
+
|
|
118
|
+
{"name": "sud_cli_fees", "description": "Calculate a state fee.",
|
|
119
|
+
"schema": {"type": "object", "properties": {
|
|
120
|
+
"jurisdiction": {"type": "string", "enum": FEE_JURISDICTIONS,
|
|
121
|
+
"description": "Юрисдикция (CLI --jurisdiction); хендлер принимает также алиасы arbitral/as→arbitration, soyu/general→general_jurisdiction"},
|
|
122
|
+
"fee_kind": {"type": "string", "enum": FEE_KINDS, "description": "Вид пошлины из правил 259-ФЗ (имя движка); CLI-алиас: kind"},
|
|
123
|
+
"kind": {"type": "string", "enum": FEE_KINDS, "description": "CLI-алиас fee_kind (--kind)"},
|
|
124
|
+
"applicant_type": {"type": "string", "enum": ["individual", "entity"], "description": "Обязателен для фиксированных пошлин: физлицо/организация; CLI-алиас: applicant"},
|
|
125
|
+
"applicant": {"type": "string", "enum": ["individual", "entity"], "description": "CLI-алиас applicant_type (--applicant)"},
|
|
126
|
+
"claim_amount": {"type": "number", "description": "Цена иска в рублях; копейки сохраняются (Decimal), округление п. 6 ст. 52 НК РФ — только на итоге"},
|
|
127
|
+
"claim_class": {"type": "string"},
|
|
128
|
+
"as_of": {"type": "string", "format": "date",
|
|
129
|
+
"pattern": "^\\d{4}-\\d{2}-\\d{2}$",
|
|
130
|
+
"description": "Тариф на дату (ISO YYYY-MM-DD): 2015-01-01..2024-09-08 — редакция 221-ФЗ, с 2024-09-09 — действующая 259-ФЗ; раньше 2015-01-01 — действующий тариф с warning в meta. Без поля считается по действующему тарифу (прежний контракт)"},
|
|
131
|
+
},
|
|
132
|
+
"required": ["jurisdiction"],
|
|
133
|
+
"anyOf": [{"required": ["fee_kind"]}, {"required": ["kind"]}],
|
|
134
|
+
"additionalProperties": False}},
|
|
135
|
+
{"name": "sud_cli_interest", "description": "Calculate statutory interest.",
|
|
136
|
+
"schema": {"type": "object", "properties": {
|
|
137
|
+
"amount": {"type": "number", "description": "Остаток долга (проценты на просрочку)"},
|
|
138
|
+
"start": {**DATE, "description": "Начало периода; CLI-алиас: start_date"},
|
|
139
|
+
"end": {**DATE, "description": "Конец периода; CLI-алиас: end_date"},
|
|
140
|
+
"start_date": {**DATE, "description": "CLI-алиас start (--from/--start)"},
|
|
141
|
+
"end_date": {**DATE, "description": "CLI-алиас end (--to/--end)"},
|
|
142
|
+
"article": {"type": "string", "enum": ["395", "317.1"], "default": "395"},
|
|
143
|
+
"payments": {"type": "array", "description": "Частичные погашения [{date, amount}]"}},
|
|
144
|
+
"required": ["amount"],
|
|
145
|
+
"anyOf": [{"required": ["start", "end"]}, {"required": ["start_date", "end_date"]}],
|
|
146
|
+
"additionalProperties": False}},
|
|
147
|
+
{"name": "sud_cli_practice_search", "description": "Search judicial practice (sudact).",
|
|
148
|
+
"schema": {"type": "object", "properties": {
|
|
149
|
+
"query": {"type": "string"},
|
|
150
|
+
"section": {"type": "string", "enum": SUDACT_SECTIONS, "default": "arbitral"},
|
|
151
|
+
"page": {"type": "integer", "default": 1}, "limit": {"type": "integer", "default": 10}},
|
|
152
|
+
"required": ["query"], "additionalProperties": False}},
|
|
153
|
+
{"name": "sud_cli_kad_search", "description": "Search KAD by participant.",
|
|
154
|
+
"schema": {"type": "object", "properties": {
|
|
155
|
+
"inn": {"type": "string"}, "ogrn": {"type": "string"}, "name": {"type": "string"},
|
|
156
|
+
"page": {"type": "integer", "default": 1}},
|
|
157
|
+
"anyOf": [{"required": ["inn"]}, {"required": ["ogrn"]}, {"required": ["name"]}],
|
|
158
|
+
"additionalProperties": False}},
|
|
159
|
+
{"name": "sud_cli_timeline_audit", "description": "Detect procedural timeline anomalies.",
|
|
160
|
+
"schema": {"type": "object", "properties": {
|
|
161
|
+
"events": {"type": "array", "items": {"type": "object"},
|
|
162
|
+
"description": "Хронология: [{timestamp (ISO, обязателен), action|doc_type, id, ...}] — движок читает event['timestamp']"}},
|
|
163
|
+
"required": ["events"], "additionalProperties": False}},
|
|
164
|
+
{"name": "sud_cli_npa_as_of", "description": "Get a historical legal norm edition.",
|
|
165
|
+
"schema": {"type": "object", "properties": {
|
|
166
|
+
"act": {"type": "string", "description": "Акт: gk_rf | uk_rf | nk_rf (ключи data/npa_historical_editions.json)"},
|
|
167
|
+
"article": {"type": "string"}, "date": {**DATE}},
|
|
168
|
+
"required": ["act", "article", "date"], "additionalProperties": False}},
|
|
169
|
+
{"name": "sud_cli_verify_quote", "description": "Verify a quoted fragment against npa bundle / case events / text file (verdict exact|fuzzy|not_found|edition_unavailable; правило «тексты только из реестра» — без «ok на глаз»).",
|
|
170
|
+
"schema": {"type": "object", "properties": {
|
|
171
|
+
"text": {"type": "string", "description": "Цитируемый фрагмент (не пустой)"},
|
|
172
|
+
"source": {"type": "string", "enum": ["npa", "case", "file"]},
|
|
173
|
+
"act": {"type": "string", "description": "source=npa: ключ бандла (gk_rf|uk_rf|nk_rf)"},
|
|
174
|
+
"article": {"type": "string"}, "date": {**DATE},
|
|
175
|
+
"case_id": {"type": "string", "description": "source=case: id дела в локальном реестре"},
|
|
176
|
+
"path": {"type": "string", "description": "source=file: plain-text файл"},
|
|
177
|
+
"max_fuzzy": {"type": "number", "minimum": 0, "maximum": 1, "default": 0,
|
|
178
|
+
"description": "0 = fuzzy выключен; порог ratio >= 1-max_fuzzy"}},
|
|
179
|
+
"required": ["text", "source"], "additionalProperties": False}},
|
|
180
|
+
{"name": "sud_cli_case_history", "description": "Case fate: all instances of a case number via sudact.",
|
|
181
|
+
"schema": {"type": "object", "properties": {
|
|
182
|
+
"case_number": {"type": "string", "description": "Номер дела; CLI-алиас: case_id"},
|
|
183
|
+
"case_id": {"type": "string", "description": "CLI-алиас case_number (позиционный аргумент case-history)"},
|
|
184
|
+
"section": {"type": "string", "enum": SUDACT_SECTIONS, "default": "arbitral"},
|
|
185
|
+
"max_related": {"type": "integer", "default": 5}, "limit": {"type": "integer", "default": 10}},
|
|
186
|
+
"anyOf": [{"required": ["case_number"]}, {"required": ["case_id"]}],
|
|
187
|
+
"additionalProperties": False}},
|
|
188
|
+
{"name": "sud_cli_payment", "description": "Payment requisites and purpose for a state fee.",
|
|
189
|
+
"schema": {"type": "object", "properties": {
|
|
190
|
+
"jurisdiction": {"type": "string"}, "amount": {"type": "integer"},
|
|
191
|
+
"court": {"type": "string"}, "defendant": {"type": "string"}},
|
|
192
|
+
"required": ["jurisdiction", "amount", "court", "defendant"], "additionalProperties": False}},
|
|
193
|
+
{"name": "sud_cli_deferral", "description": "Draft a motion to defer the state fee.",
|
|
194
|
+
"schema": {"type": "object", "properties": {
|
|
195
|
+
"applicant": {"type": "string"}, "court": {"type": "string"},
|
|
196
|
+
"amount": {"type": "integer"}, "reason": {"type": "string"}},
|
|
197
|
+
"required": ["applicant", "court", "amount", "reason"], "additionalProperties": False}},
|
|
198
|
+
{"name": "sud_cli_fssp_check_plan", "description": "FSSP BDIP manual-check plan: URL, params, checklist (human solves captcha; no network here).",
|
|
199
|
+
"schema": {"type": "object", "properties": {
|
|
200
|
+
"kind": {"type": "string", "enum": ["fiz", "ur"]},
|
|
201
|
+
"name": {"type": "string"}, "inn": {"type": "string"}, "ogrn": {"type": "string"},
|
|
202
|
+
"birth_date": {"type": "string"}, "region": {"type": "string"}},
|
|
203
|
+
"required": ["kind"], "additionalProperties": False}},
|
|
204
|
+
{"name": "sud_cli_fssp_record", "description": "Record a manual FSSP check result into case evidence registry (provenance: who/when/result).",
|
|
205
|
+
"schema": {"type": "object", "properties": {
|
|
206
|
+
"case_id": {"type": "string"}, "subject": {"type": "string"},
|
|
207
|
+
"result": {"type": "string", "enum": ["none", "found", "inconclusive"]},
|
|
208
|
+
"checked_by": {"type": "string"}, "notes": {"type": "string"},
|
|
209
|
+
"file": {"type": "string", "description": "path to screenshot/PDF evidence"}},
|
|
210
|
+
"required": ["case_id", "subject", "result", "checked_by"], "additionalProperties": False}},
|
|
211
|
+
{"name": "sud_cli_fssp_stats", "description": "Official FSSP BDIP aggregate statistics (public CSV, reference data).",
|
|
212
|
+
"schema": {"type": "object", "properties": {"region": {"type": "string"}},
|
|
213
|
+
"additionalProperties": False}},
|
|
214
|
+
{"name": "sud_cli_efrsb_link", "description": "EFRSB manual-check plan for humans (act 25.10): valid search link + steps; KAD stays the automated primary path. No network.",
|
|
215
|
+
"schema": {"type": "object", "properties": {
|
|
216
|
+
"kind": {"type": "string", "enum": ["ul", "ip", "fl"]},
|
|
217
|
+
"name": {"type": "string"}, "inn": {"type": "string"}, "ogrn": {"type": "string"}},
|
|
218
|
+
"required": ["kind"], "additionalProperties": False}},
|
|
219
|
+
{"name": "sud_cli_plenum_search", "description": "Search the VS/VAS rulings corpus (plenum/obzor paragraphs with sha256-verified quotes).",
|
|
220
|
+
"schema": {"type": "object", "properties": {"text": {"type": "string"}},
|
|
221
|
+
"required": ["text"], "additionalProperties": False}},
|
|
222
|
+
{"name": "sud_cli_license_info", "description": "AGPL section 13 interface: license, build mode, source retrieval, build_flags (no network).",
|
|
223
|
+
"schema": {"type": "object", "properties": {
|
|
224
|
+
"with_build_flags": {"type": "boolean", "default": True,
|
|
225
|
+
"description": "включить HMAC-build_flag метки сборки (пусто без SUD_BUILD_SECRET)"}},
|
|
226
|
+
"additionalProperties": False}},
|
|
227
|
+
{"name": "sud_cli_egrul_search", "description": "EGRYUL/EGRIP open counterparty check via egrul.nalog.ru (FNS public registry).",
|
|
228
|
+
"schema": {"type": "object", "properties": {
|
|
229
|
+
"query": {"type": "string", "description": "ИНН, ОГРН или наименование"},
|
|
230
|
+
"name_eq": {"type": "boolean", "description": "точное соответствие наименования", "default": False},
|
|
231
|
+
"rows_hint": {"type": "integer", "default": 1}}, "additionalProperties": False}},
|
|
232
|
+
{"name": "sud_cli_efrsb_companies", "description": "EFRSB bankruptcy search for legal entities/IP.",
|
|
233
|
+
"schema": {"type": "object", "properties": {
|
|
234
|
+
"inn": {"type": "string"}, "ogrn": {"type": "string"}, "name": {"type": "string"},
|
|
235
|
+
"limit": {"type": "integer", "default": 5}}, "additionalProperties": False}},
|
|
236
|
+
{"name": "sud_cli_efrsb_persons", "description": "EFRSB bankruptcy search for individuals.",
|
|
237
|
+
"schema": {"type": "object", "properties": {
|
|
238
|
+
"full_name": {"type": "string", "description": "ФИО; CLI-алиас: name"},
|
|
239
|
+
"name": {"type": "string", "description": "CLI-алиас full_name (--name)"},
|
|
240
|
+
"birth_date": {**DATE}, "limit": {"type": "integer", "default": 5}},
|
|
241
|
+
"anyOf": [{"required": ["full_name"]}, {"required": ["name"]}],
|
|
242
|
+
"additionalProperties": False}},
|
|
243
|
+
{"name": "sud_cli_efrsb_publications", "description": "EFRSB publications for a bankruptcy case.",
|
|
244
|
+
"schema": {"type": "object", "properties": {
|
|
245
|
+
"case_id": {"type": "string"}, "limit": {"type": "integer", "default": 10}},
|
|
246
|
+
"required": ["case_id"], "additionalProperties": False}},
|
|
247
|
+
{"name": "sud_cli_efrsb_managers", "description": "EFRSB arbitration managers lookup.",
|
|
248
|
+
"schema": {"type": "object", "properties": {
|
|
249
|
+
"search": {"type": "string"}, "inn": {"type": "string"}, "sro": {"type": "string"},
|
|
250
|
+
"limit": {"type": "integer", "default": 5}}, "additionalProperties": False}},
|
|
251
|
+
{"name": "sud_cli_sudrf_live", "description": "Look up a GAS Pravosudie case (bridge; requires SUD_CLI_BRIDGE_LIVE).",
|
|
252
|
+
"schema": {"type": "object", "properties": {"case_id": {"type": "string"}},
|
|
253
|
+
"required": ["case_id"], "additionalProperties": False}},
|
|
254
|
+
{"name": "sud_cli_mos_live", "description": "Look up a Moscow court case (bridge; requires SUD_CLI_BRIDGE_LIVE).",
|
|
255
|
+
"schema": {"type": "object", "properties": {"case_id": {"type": "string"}, "court_slug": {"type": "string"}},
|
|
256
|
+
"required": ["case_id"], "additionalProperties": False}},
|
|
257
|
+
{"name": "sud_cli_case_diff", "description": "Diff two saved snapshots of the same case (offline, no court contact).",
|
|
258
|
+
"schema": {"type": "object", "properties": {
|
|
259
|
+
"old": {"type": "string", "description": "Path to earlier snapshot JSON"},
|
|
260
|
+
"new": {"type": "string", "description": "Path to later snapshot JSON"},
|
|
261
|
+
"old_snapshot": {"type": "string"}, "new_snapshot": {"type": "string"}},
|
|
262
|
+
"anyOf": [{"required": ["old", "new"]}, {"required": ["old_snapshot", "new_snapshot"]}],
|
|
263
|
+
"additionalProperties": False}},
|
|
264
|
+
{"name": "sud_cli_npa_compare", "description": "Compare two editions of a saved NPA fixture.",
|
|
265
|
+
"schema": {"type": "object", "properties": {
|
|
266
|
+
"doc_id": {"type": "string"}, "edition1": {"type": "string"}, "edition2": {"type": "string"}},
|
|
267
|
+
"required": ["doc_id", "edition1", "edition2"], "additionalProperties": False}},
|
|
268
|
+
{"name": "sud_cli_case_new", "description": "Create a case in the local registry.",
|
|
269
|
+
"schema": {"type": "object", "properties": {
|
|
270
|
+
"number": {"type": "string"}, "case_number": {"type": "string"},
|
|
271
|
+
"court": {"type": "string"},
|
|
272
|
+
"plaintiff": {"type": "string"}, "defendant": {"type": "string"},
|
|
273
|
+
"owner": {"type": "string"}, "status": {"type": "string"}},
|
|
274
|
+
"anyOf": [{"required": ["number"]}, {"required": ["case_number"]}],
|
|
275
|
+
"required": ["court", "plaintiff", "defendant"], "additionalProperties": False}},
|
|
276
|
+
{"name": "sud_cli_case_set", "description": "Update case fields.",
|
|
277
|
+
"schema": {"type": "object", "properties": {
|
|
278
|
+
"case_id": {"type": "string"}, "id": {"type": "string"},
|
|
279
|
+
"status": {"type": "string"}, "owner": {"type": "string"}, "closed_at": {"type": "string"}},
|
|
280
|
+
"anyOf": [{"required": ["case_id"]}, {"required": ["id"]}],
|
|
281
|
+
"additionalProperties": False}},
|
|
282
|
+
{"name": "sud_cli_case_list", "description": "List cases with filters.",
|
|
283
|
+
"schema": {"type": "object", "properties": {
|
|
284
|
+
"status": {"type": "string"}, "owner": {"type": "string"},
|
|
285
|
+
"court": {"type": "string"}, "party": {"type": "string"}},
|
|
286
|
+
"additionalProperties": False}},
|
|
287
|
+
{"name": "sud_cli_case_card", "description": "Aggregate case card.",
|
|
288
|
+
"schema": {"type": "object", "properties": {
|
|
289
|
+
"case_id": {"type": "string"}, "id": {"type": "string"}},
|
|
290
|
+
"anyOf": [{"required": ["case_id"]}, {"required": ["id"]}],
|
|
291
|
+
"additionalProperties": False}},
|
|
292
|
+
{"name": "sud_cli_case_amount_add", "description": "Add an amount to a case.",
|
|
293
|
+
"schema": {"type": "object", "properties": {
|
|
294
|
+
"case_id": {"type": "string"}, "id": {"type": "string"},
|
|
295
|
+
"kind": {"type": "string"},
|
|
296
|
+
"amount": {"type": ["number", "string"]}, "note": {"type": "string"}},
|
|
297
|
+
"anyOf": [{"required": ["case_id"]}, {"required": ["id"]}],
|
|
298
|
+
"required": ["kind", "amount"], "additionalProperties": False}},
|
|
299
|
+
{"name": "sud_cli_case_doc_add", "description": "Add a document reference to a case.",
|
|
300
|
+
"schema": {"type": "object", "properties": {
|
|
301
|
+
"case_id": {"type": "string"}, "id": {"type": "string"},
|
|
302
|
+
"path": {"type": "string"}, "title": {"type": "string"}},
|
|
303
|
+
"anyOf": [{"required": ["case_id"]}, {"required": ["id"]}],
|
|
304
|
+
"required": ["path"], "additionalProperties": False}},
|
|
305
|
+
{"name": "sud_cli_case_note_add", "description": "Add a note to a case.",
|
|
306
|
+
"schema": {"type": "object", "properties": {
|
|
307
|
+
"case_id": {"type": "string"}, "id": {"type": "string"},
|
|
308
|
+
"text": {"type": "string"}, "author": {"type": "string"}},
|
|
309
|
+
"anyOf": [{"required": ["case_id"]}, {"required": ["id"]}],
|
|
310
|
+
"required": ["text"], "additionalProperties": False}},
|
|
311
|
+
{"name": "sud_cli_conflict_check", "description": "Conflict-of-interest heuristic over the local case registry (name-exact).",
|
|
312
|
+
"schema": {"type": "object", "properties": {
|
|
313
|
+
"plaintiff": {"type": "string"}, "defendant": {"type": "string"},
|
|
314
|
+
"court": {"type": "string"}, "case_number": {"type": "string"},
|
|
315
|
+
"case_id": {"type": "string"}, "id": {"type": "string"},
|
|
316
|
+
"parties": {"type": "array", "items": {"type": "string"}}},
|
|
317
|
+
"additionalProperties": False}},
|
|
318
|
+
{"name": "sud_cli_jurisdiction_check", "description": "Jurisdiction venue helper over VERIFIED rules.",
|
|
319
|
+
"schema": {"type": "object", "properties": {
|
|
320
|
+
"code": {"type": "string", "enum": ["apk", "gpk", "kas"]},
|
|
321
|
+
"claim": {"type": "string"},
|
|
322
|
+
"party_org": {"type": "boolean"}, "party_phys": {"type": "boolean"},
|
|
323
|
+
"unknown_rf_location": {"type": "boolean"}, "immovable": {"type": "boolean"},
|
|
324
|
+
"corporate": {"type": "boolean"}, "bankruptcy": {"type": "boolean"},
|
|
325
|
+
"ip_rights": {"type": "boolean"}, "contract_forum": {"type": "boolean"},
|
|
326
|
+
"arbitration_clause": {"type": "boolean"}, "multiple_defendants": {"type": "boolean"},
|
|
327
|
+
"foreign_element": {"type": "boolean"}, "consumer": {"type": "boolean"},
|
|
328
|
+
"alimony": {"type": "boolean"}},
|
|
329
|
+
"required": ["code", "claim"], "additionalProperties": False}},
|
|
330
|
+
{"name": "sud_cli_fee_exemption", "description": "Fee exemption check over VERIFIED category sample (333.35-37).",
|
|
331
|
+
"schema": {"type": "object", "properties": {
|
|
332
|
+
"jurisdiction": {"type": "string", "enum": ["apk", "gpk", "kas", "arbitration", "general_jurisdiction"]},
|
|
333
|
+
"category": {"type": "string"}},
|
|
334
|
+
"required": ["jurisdiction", "category"], "additionalProperties": False}},
|
|
335
|
+
{"name": "sud_cli_fee_refund", "description": "Fee refund per 333.40 VERIFIED grounds with share/conditions.",
|
|
336
|
+
"schema": {"type": "object", "properties": {
|
|
337
|
+
"paid": {"type": "string"}, "ground": {"type": "string"},
|
|
338
|
+
"claim_admitted": {"type": "boolean"}, "plaintiff_default": {"type": "boolean"},
|
|
339
|
+
"transferred_to_arbitration": {"type": "boolean"}},
|
|
340
|
+
"required": ["paid", "ground"], "additionalProperties": False}},
|
|
341
|
+
{"name": "sud_cli_fee_indexation", "description": "Indexation frame (183/208/189.1) or compute with operator CPI.",
|
|
342
|
+
"schema": {"type": "object", "properties": {
|
|
343
|
+
"code": {"type": "string", "enum": ["apk", "gpk", "kas"]},
|
|
344
|
+
"amount": {"type": "string"}, "cpi_start": {"type": "string"}, "cpi_end": {"type": "string"}},
|
|
345
|
+
"required": ["code"], "additionalProperties": False}},
|
|
346
|
+
{"name": "sud_cli_pretrial_calc", "description": "Pretrial response deadline calc.",
|
|
347
|
+
"schema": {"type": "object", "properties": {
|
|
348
|
+
"code": {"type": "string", "enum": ["apk", "zozpp", "osago"]},
|
|
349
|
+
"sent": {"type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$"}},
|
|
350
|
+
"required": ["code", "sent"], "additionalProperties": False}},
|
|
351
|
+
{"name": "sud_cli_pretrial_consequences", "description": "Consequences of noncompliance with pretrial order.",
|
|
352
|
+
"schema": {"type": "object", "properties": {
|
|
353
|
+
"code": {"type": "string", "enum": ["apk", "gpk", "kas"]},
|
|
354
|
+
"stage": {"type": "string", "enum": ["filing", "trial", "movement"]}},
|
|
355
|
+
"required": ["code", "stage"], "additionalProperties": False}},
|
|
356
|
+
|
|
357
|
+
# ---------------- W6a §1/§3/§4/§5: паритет недостающих утилитных команд ----
|
|
358
|
+
# Live-сетевые команды в этой волне НЕ экспонированы: новые тулзы только
|
|
359
|
+
# локальные (state-dir + rules), сеть запрещена контрактом task-briefa.
|
|
360
|
+
{"name": "sud_cli_deadlines_anchor", "description": "Anchor deadline start date with evidence (write: state=computed).",
|
|
361
|
+
"schema": {"type": "object", "properties": {
|
|
362
|
+
"case_id": {"type": "string"},
|
|
363
|
+
"key": {"type": "string", "description": "Ключ из утверждённой матрицы сроков"},
|
|
364
|
+
"event_date": {**DATE},
|
|
365
|
+
"method": {"type": "string", "enum": ["вручение", "публикация", "почта", "иное"]},
|
|
366
|
+
"evidence": {"type": "string"},
|
|
367
|
+
"by": {"type": "string", "description": "Актор (по умолчанию 'mcp')"}},
|
|
368
|
+
"required": ["case_id", "key", "event_date", "method", "evidence"],
|
|
369
|
+
"additionalProperties": False}},
|
|
370
|
+
{"name": "sud_cli_deadlines_confirm", "description": "Confirm (or waive) a deadline state (write).",
|
|
371
|
+
"schema": {"type": "object", "properties": {
|
|
372
|
+
"case_id": {"type": "string"},
|
|
373
|
+
"key": {"type": "string"},
|
|
374
|
+
"by": {"type": "string"},
|
|
375
|
+
"evidence": {"type": "string"},
|
|
376
|
+
"waive": {"type": "boolean", "default": False},
|
|
377
|
+
"norm": {"type": "string", "description": "Норма-ссылка (обязательна при waive)"}},
|
|
378
|
+
"required": ["case_id", "key", "by"],
|
|
379
|
+
"additionalProperties": False}},
|
|
380
|
+
{"name": "sud_cli_deadlines_waive", "description": "Waive a deadline with norm reference (write).",
|
|
381
|
+
"schema": {"type": "object", "properties": {
|
|
382
|
+
"case_id": {"type": "string"},
|
|
383
|
+
"key": {"type": "string"},
|
|
384
|
+
"by": {"type": "string"},
|
|
385
|
+
"norm": {"type": "string"},
|
|
386
|
+
"evidence": {"type": "string"}},
|
|
387
|
+
"required": ["case_id", "key", "by", "norm"],
|
|
388
|
+
"additionalProperties": False}},
|
|
389
|
+
{"name": "sud_cli_deadlines_list", "description": "Deadline states history (optionally per case).",
|
|
390
|
+
"schema": {"type": "object", "properties": {
|
|
391
|
+
"case_id": {"type": "string", "description": "Идентификатор дела; без него — все состояния"}},
|
|
392
|
+
"additionalProperties": False}},
|
|
393
|
+
{"name": "sud_cli_deadlines_audit", "description": "Deadline edit audit log for a case.",
|
|
394
|
+
"schema": {"type": "object", "properties": {
|
|
395
|
+
"case_id": {"type": "string"}},
|
|
396
|
+
"required": ["case_id"], "additionalProperties": False}},
|
|
397
|
+
{"name": "sud_cli_deadlines_autofill", "description": "Autofill deadline points from act-text markers (write; computed+needs_confirm only, never confirmed).",
|
|
398
|
+
"schema": {"type": "object", "properties": {
|
|
399
|
+
"case_id": {"type": "string"},
|
|
400
|
+
"event": {"type": "string", "description": "Обработать только этот event_id"}},
|
|
401
|
+
"required": ["case_id"], "additionalProperties": False}},
|
|
402
|
+
{"name": "sud_cli_calendar_agenda", "description": "Unified agenda of deadlines and hearings.",
|
|
403
|
+
"schema": {"type": "object", "properties": {
|
|
404
|
+
"days": {"type": "integer", "default": 14, "minimum": 1, "maximum": 365}},
|
|
405
|
+
"additionalProperties": False}},
|
|
406
|
+
{"name": "sud_cli_calendar_ics", "description": "RFC 5545 VCALENDAR of computed/confirmed deadlines (text only; no file writes).",
|
|
407
|
+
"schema": {"type": "object", "properties": {
|
|
408
|
+
"days": {"type": "integer", "default": 60, "minimum": 1, "maximum": 3650}},
|
|
409
|
+
"additionalProperties": False}},
|
|
410
|
+
{"name": "sud_cli_reminders_due", "description": "Deadlines in window + ready bus-free a2a strings (sud never calls the bus).",
|
|
411
|
+
"schema": {"type": "object", "properties": {
|
|
412
|
+
"days": {"type": "integer", "default": 7, "minimum": 1, "maximum": 90}},
|
|
413
|
+
"additionalProperties": False}},
|
|
414
|
+
{"name": "sud_cli_case_search_alerts", "description": "Local case-search alerts -> bus-free a2a strings (offline artifacts only).",
|
|
415
|
+
"schema": {"type": "object", "properties": {
|
|
416
|
+
"days": {"type": "integer", "default": 7, "minimum": 1, "maximum": 90},
|
|
417
|
+
"alerts_dir": {"type": "string", "description": "Каталог alert-артефактов (по умолчанию — локальные пути)"}},
|
|
418
|
+
"additionalProperties": False}},
|
|
419
|
+
# the wave §4 (S-70): структурный скелет формы = чистое чтение реестра + строка;
|
|
420
|
+
# без сайд-эффектов => permission read (в WRITE_TOOLS не попадает).
|
|
421
|
+
{"name": "sud_cli_form_new", "description": "Структурный скелет процессуальной формы по делу (md|json, плейсхолдеры, без нормативных цитат; read).",
|
|
422
|
+
"schema": {"type": "object", "properties": {
|
|
423
|
+
"kind": {"type": "string", "enum": ["vosstanovlenie_sroka", "otsrochka", "appeal_structure"]},
|
|
424
|
+
"case_id": {"type": "string", "description": "Дело из локального реестра"},
|
|
425
|
+
"format": {"type": "string", "enum": ["md", "json"], "default": "md"}},
|
|
426
|
+
"required": ["kind", "case_id"], "additionalProperties": False}},
|
|
427
|
+
# ---------------- the wave §1..§5: портфель, данные, ПДн (12 тулов) ----------------
|
|
428
|
+
# Порядок read|write — по матрице ТЗ §5: только изменение состояния или
|
|
429
|
+
# запись на диск получает write+confirm; счёт/чтение остаётся read.
|
|
430
|
+
{"name": "sud_cli_deadlines_chain", "description": "Цепочка инстанций дела по ключам VERIFIED матрицы (read; без якоря — null+needs_anchor, дат не выдумываем).",
|
|
431
|
+
"schema": {"type": "object", "properties": {
|
|
432
|
+
"case_id": {"type": "string"},
|
|
433
|
+
"instance": {"type": "string", "enum": ["apk", "gpk", "kas"],
|
|
434
|
+
"description": "Override инстанции дела"}},
|
|
435
|
+
"required": ["case_id"], "additionalProperties": False}},
|
|
436
|
+
{"name": "sud_cli_case_rep_add", "description": "Добавить представителя дела (write; полномочия не проверяются — hint о ручной проверке в ответе).",
|
|
437
|
+
"schema": {"type": "object", "properties": {
|
|
438
|
+
"case_id": {"type": "string"},
|
|
439
|
+
"side": {"type": "string", "enum": ["plaintiff", "defendant"]},
|
|
440
|
+
"name": {"type": "string"},
|
|
441
|
+
"authority_note": {"type": "string"},
|
|
442
|
+
"power_docs": {"type": "string", "description": "Свободное описание доверенности"}},
|
|
443
|
+
"required": ["case_id", "side", "name"], "additionalProperties": False}},
|
|
444
|
+
{"name": "sud_cli_case_rep_list", "description": "Список представителей дела (read; пусто = []).",
|
|
445
|
+
"schema": {"type": "object", "properties": {"case_id": {"type": "string"}},
|
|
446
|
+
"required": ["case_id"], "additionalProperties": False}},
|
|
447
|
+
{"name": "sud_cli_calendar_conflicts", "description": "Пересечения слушаний (фикс. набор event_type) и сроков по разным делам (read; справочно).",
|
|
448
|
+
"schema": {"type": "object", "properties": {
|
|
449
|
+
"days": {"type": "integer", "default": 30, "minimum": 1, "maximum": 3650}},
|
|
450
|
+
"additionalProperties": False}},
|
|
451
|
+
{"name": "sud_cli_report_portfolio", "description": "Сводка портфеля: дела по статусам, near/overdue, суммы Decimal, представители (read; без выдуманных KPI).",
|
|
452
|
+
"schema": {"type": "object", "properties": {
|
|
453
|
+
"as_of": {"type": "string", "description": "Дата отсчёта YYYY-MM-DD"},
|
|
454
|
+
"days": {"type": "integer", "default": 30, "minimum": 1, "maximum": 3650}},
|
|
455
|
+
"additionalProperties": False}},
|
|
456
|
+
{"name": "sud_cli_report_case", "description": "Развёртка дела: секции card + сводка матрицы + evidence verify (read; offline-пересчёт локальных path).",
|
|
457
|
+
"schema": {"type": "object", "properties": {"case_id": {"type": "string"}},
|
|
458
|
+
"required": ["case_id"], "additionalProperties": False}},
|
|
459
|
+
{"name": "sud_cli_metrics", "description": "Метрики state-dir: счётчики из таблиц состояния, last backup из meta (read; тот же блок, что в doctor).",
|
|
460
|
+
"schema": {"type": "object", "properties": {
|
|
461
|
+
"near_days": {"type": "integer", "default": 30, "minimum": 1, "maximum": 3650,
|
|
462
|
+
"description": "Горизонт near для счётчика сроков"}},
|
|
463
|
+
"additionalProperties": False}},
|
|
464
|
+
{"name": "sud_cli_backup_create", "description": "Снять snapshot store.db (write: файл 0600 + sha256 sidecar на диске state-dir).",
|
|
465
|
+
"schema": {"type": "object", "properties": {
|
|
466
|
+
"out": {"type": "string", "description": "Путь снимка (по умолчанию state-dir/backups)"}},
|
|
467
|
+
"additionalProperties": False}},
|
|
468
|
+
{"name": "sud_cli_backup_verify", "description": "Проверить снимок: integrity_check + quick_check + таблицы схемы + sidecar (read).",
|
|
469
|
+
"schema": {"type": "object", "properties": {"file": {"type": "string"}},
|
|
470
|
+
"required": ["file"], "additionalProperties": False}},
|
|
471
|
+
{"name": "sud_cli_backup_restore", "description": "Перезаписать store.db из снимка (write+confirm: деструктив, без confirm — только превью).",
|
|
472
|
+
"schema": {"type": "object", "properties": {"file": {"type": "string"}},
|
|
473
|
+
"required": ["file"], "additionalProperties": False}},
|
|
474
|
+
{"name": "sud_cli_case_export", "description": "Экспорт дела для субъекта/юриста: полный дамп секций + pdn_policy дословно из правил (read; без сокращений).",
|
|
475
|
+
"schema": {"type": "object", "properties": {
|
|
476
|
+
"case_id": {"type": "string"},
|
|
477
|
+
"format": {"type": "string", "enum": ["json", "md"], "default": "json"}},
|
|
478
|
+
"required": ["case_id"], "additionalProperties": False}},
|
|
479
|
+
{"name": "sud_cli_case_purge", "description": "Уничтожить данные дела: каскад таблиц + файлы, в audit только факт без ПДн (write+confirm).",
|
|
480
|
+
"schema": {"type": "object", "properties": {
|
|
481
|
+
"case_id": {"type": "string"},
|
|
482
|
+
"remove_files": {"type": "boolean", "default": True}},
|
|
483
|
+
"required": ["case_id"], "additionalProperties": False}},
|
|
484
|
+
]
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
# ---------------- W6a §1 (S-80): permission + идемпотентность ----------------
|
|
488
|
+
#: state-changing-тулы. Всё остальное — read. Поле permission дописывается
|
|
489
|
+
#: КАЖДОЙ схеме тула (включая исторические 35): один источник правды — этот
|
|
490
|
+
#: набор, а не 45 ручных строк, которые расходятся при первом же новом туле.
|
|
491
|
+
#: write-тулы дополнительно получают в properties поля confirm/idempotency_key:
|
|
492
|
+
#: additionalProperties:false обязывает объявлять всё, что диспатч принимает.
|
|
493
|
+
WRITE_TOOLS = frozenset({
|
|
494
|
+
# v2.15.0 S-61: внесение результата ручной проверки меняет реестр дела.
|
|
495
|
+
"sud_cli_fssp_record",
|
|
496
|
+
"sud_cli_case_new", "sud_cli_case_set", "sud_cli_case_amount_add",
|
|
497
|
+
"sud_cli_case_doc_add", "sud_cli_case_note_add",
|
|
498
|
+
"sud_cli_deadlines_anchor", "sud_cli_deadlines_confirm",
|
|
499
|
+
"sud_cli_deadlines_waive", "sud_cli_deadlines_autofill",
|
|
500
|
+
# the wave: запись представителя, снимок на диск, перезапись БД, уничтожение
|
|
501
|
+
# данных дела — всё это меняет состояние, значит write + confirm.
|
|
502
|
+
"sud_cli_case_rep_add", "sud_cli_backup_create",
|
|
503
|
+
"sud_cli_backup_restore", "sud_cli_case_purge",
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
CONFIRM_PROPERTY = {
|
|
507
|
+
"type": "boolean",
|
|
508
|
+
"description": "write-инструмент: без confirm=true вернёт status=needs_confirmation, запись не производится (W6a §1)",
|
|
509
|
+
}
|
|
510
|
+
IDEMPOTENCY_PROPERTY = {
|
|
511
|
+
"type": "string",
|
|
512
|
+
"description": "повтор с тем же ключом в окне 10 минут возвращает первый результат с флагом idempotent_replay=true (реестр state-dir/idempotency.json, 0600)",
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _apply_permission_fields() -> None:
|
|
517
|
+
for t in TOOLS:
|
|
518
|
+
t["permission"] = "write" if t["name"] in WRITE_TOOLS else "read"
|
|
519
|
+
if t["permission"] == "write":
|
|
520
|
+
props = t["schema"].setdefault("properties", {})
|
|
521
|
+
props.setdefault("confirm", dict(CONFIRM_PROPERTY))
|
|
522
|
+
props.setdefault("idempotency_key", dict(IDEMPOTENCY_PROPERTY))
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
_apply_permission_fields()
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
#: Паритет MCP↔CLI (S-80): инструмент отвечает позиционной команде CLI.
|
|
529
|
+
#: Тест сверяет белый список офлайн-утилит против дерева argparse, а также
|
|
530
|
+
#: то, что каждый тул имеет CLI-аналог (обратная строна паритета).
|
|
531
|
+
TOOL_CLI_PATH: dict[str, tuple] = {
|
|
532
|
+
"sud_cli_limitation": ("limitation",),
|
|
533
|
+
"sud_cli_deadlines": ("deadlines", "calculate"),
|
|
534
|
+
"sud_cli_deadlines_matrix": ("deadlines", "matrix"),
|
|
535
|
+
"sud_cli_deadlines_anchor": ("deadlines", "anchor"),
|
|
536
|
+
"sud_cli_deadlines_confirm": ("deadlines", "confirm"),
|
|
537
|
+
"sud_cli_deadlines_waive": ("deadlines", "waive"),
|
|
538
|
+
"sud_cli_deadlines_list": ("deadlines", "list"),
|
|
539
|
+
"sud_cli_deadlines_audit": ("deadlines", "audit"),
|
|
540
|
+
"sud_cli_deadlines_autofill": ("deadlines", "autofill"),
|
|
541
|
+
"sud_cli_fees": ("fees",),
|
|
542
|
+
"sud_cli_interest": ("interest",),
|
|
543
|
+
"sud_cli_practice_search": ("practice",),
|
|
544
|
+
"sud_cli_kad_search": ("kad",),
|
|
545
|
+
"sud_cli_timeline_audit": ("timeline-audit",),
|
|
546
|
+
"sud_cli_npa_as_of": ("npa-as-of",),
|
|
547
|
+
"sud_cli_verify_quote": ("verify", "quote"),
|
|
548
|
+
"sud_cli_case_history": ("case-history",),
|
|
549
|
+
"sud_cli_payment": ("payment",),
|
|
550
|
+
"sud_cli_deferral": ("deferral",),
|
|
551
|
+
"sud_cli_egrul_search": ("egrul",),
|
|
552
|
+
"sud_cli_fssp_check_plan": ("fssp", "check"),
|
|
553
|
+
"sud_cli_fssp_record": ("fssp", "record"),
|
|
554
|
+
"sud_cli_fssp_stats": ("fssp", "stats"),
|
|
555
|
+
"sud_cli_license_info": ("license",),
|
|
556
|
+
"sud_cli_efrsb_link": ("efrsb", "link"),
|
|
557
|
+
"sud_cli_plenum_search": ("plenum", "search"),
|
|
558
|
+
"sud_cli_efrsb_companies": ("efrsb", "companies"),
|
|
559
|
+
"sud_cli_efrsb_persons": ("efrsb", "persons"),
|
|
560
|
+
"sud_cli_efrsb_publications": ("efrsb", "publications"),
|
|
561
|
+
"sud_cli_efrsb_managers": ("efrsb", "managers"),
|
|
562
|
+
"sud_cli_sudrf_live": ("sudrf-live",),
|
|
563
|
+
"sud_cli_mos_live": ("mos-live",),
|
|
564
|
+
"sud_cli_case_diff": ("case-search", "case-diff"),
|
|
565
|
+
"sud_cli_npa_compare": ("npa", "compare"),
|
|
566
|
+
"sud_cli_case_new": ("case", "new"),
|
|
567
|
+
"sud_cli_case_set": ("case", "set"),
|
|
568
|
+
"sud_cli_case_list": ("case", "list"),
|
|
569
|
+
"sud_cli_case_card": ("case", "card"),
|
|
570
|
+
"sud_cli_case_amount_add": ("case", "amount", "add"),
|
|
571
|
+
"sud_cli_case_doc_add": ("case", "doc", "add"),
|
|
572
|
+
"sud_cli_case_note_add": ("case", "note", "add"),
|
|
573
|
+
"sud_cli_conflict_check": ("conflict", "check"),
|
|
574
|
+
"sud_cli_jurisdiction_check": ("jurisdiction", "check"),
|
|
575
|
+
"sud_cli_fee_exemption": ("fee-exemption",),
|
|
576
|
+
"sud_cli_fee_refund": ("fee-refund",),
|
|
577
|
+
"sud_cli_fee_indexation": ("fee-indexation",),
|
|
578
|
+
"sud_cli_pretrial_calc": ("pretrial", "calc"),
|
|
579
|
+
"sud_cli_pretrial_consequences": ("pretrial", "consequences"),
|
|
580
|
+
"sud_cli_calendar_agenda": ("calendar", "agenda"),
|
|
581
|
+
"sud_cli_calendar_ics": ("calendar", "ics"),
|
|
582
|
+
"sud_cli_reminders_due": ("reminders", "due"),
|
|
583
|
+
"sud_cli_case_search_alerts": ("case-search", "alerts"),
|
|
584
|
+
"sud_cli_form_new": ("form", "new"),
|
|
585
|
+
# the wave: каждый новый тул обязан иметь живую CLI-команду — паритет S-80.
|
|
586
|
+
"sud_cli_deadlines_chain": ("deadlines", "chain"),
|
|
587
|
+
"sud_cli_case_rep_add": ("case", "rep", "add"),
|
|
588
|
+
"sud_cli_case_rep_list": ("case", "rep", "list"),
|
|
589
|
+
"sud_cli_calendar_conflicts": ("calendar", "conflicts"),
|
|
590
|
+
"sud_cli_report_portfolio": ("report", "portfolio"),
|
|
591
|
+
"sud_cli_report_case": ("report", "case"),
|
|
592
|
+
"sud_cli_metrics": ("metrics",),
|
|
593
|
+
"sud_cli_backup_create": ("backup", "create"),
|
|
594
|
+
"sud_cli_backup_verify": ("backup", "verify"),
|
|
595
|
+
"sud_cli_backup_restore": ("backup", "restore"),
|
|
596
|
+
"sud_cli_case_export": ("case", "export"),
|
|
597
|
+
"sud_cli_case_purge": ("case", "purge"),
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
#: Сетевые live-команды (под гейтом SUD_CLI_BRIDGE_LIVE или native-live).
|
|
601
|
+
#: В новой волне ни один тул сюда не добавлен; существующие не тронуты.
|
|
602
|
+
LIVE_GATED_TOOLS = frozenset({
|
|
603
|
+
"sud_cli_practice_search", "sud_cli_kad_search", "sud_cli_case_history",
|
|
604
|
+
"sud_cli_efrsb_companies", "sud_cli_efrsb_persons",
|
|
605
|
+
"sud_cli_efrsb_publications", "sud_cli_efrsb_managers",
|
|
606
|
+
"sud_cli_sudrf_live", "sud_cli_mos_live",
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def tool_permission(name: str) -> str | None:
|
|
611
|
+
for t in TOOLS:
|
|
612
|
+
if t["name"] == name:
|
|
613
|
+
return t["permission"]
|
|
614
|
+
return None
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def mcp_exposed_commands() -> set:
|
|
618
|
+
"""CLI-пути команд, закрытые MCP-инструментами (для паритетного теста)."""
|
|
619
|
+
return {TOOL_CLI_PATH[t["name"]] for t in TOOLS if t["name"] in TOOL_CLI_PATH}
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def tool_list():
|
|
623
|
+
return {"tools": [{"name": t["name"], "description": t["description"],
|
|
624
|
+
"inputSchema": t["schema"], "permission": t["permission"]}
|
|
625
|
+
for t in TOOLS]}
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _pick(values: dict, names, label: str):
|
|
629
|
+
"""Значение параметра под любым из допустимых имён (CLI-алиас ↔ имя движка).
|
|
630
|
+
|
|
631
|
+
Оба имени принимаются, но молча выбрать одно при конфликте нельзя: агент,
|
|
632
|
+
передавший `kind` и `fee_kind` с разными значениями, получил бы ответ по
|
|
633
|
+
другому основанию, чем просил. Конфликт — явная ошибка.
|
|
634
|
+
"""
|
|
635
|
+
found = {name: values.pop(name) for name in names if name in values}
|
|
636
|
+
if not found:
|
|
637
|
+
return None
|
|
638
|
+
distinct = {repr(value) for value in found.values()}
|
|
639
|
+
if len(distinct) > 1:
|
|
640
|
+
raise ValueError(
|
|
641
|
+
f"conflicting values for {label}: {sorted(found)} — передайте только одно имя"
|
|
642
|
+
)
|
|
643
|
+
return next(iter(found.values()))
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _claim_amount(value):
|
|
647
|
+
"""Цена иска из JSON в денежный тип.
|
|
648
|
+
|
|
649
|
+
JSON `number` прибыл float'ом — его надо перевести в Decimal через str,
|
|
650
|
+
иначе двоичная погрешность осядет в копейках. bool — не деньги.
|
|
651
|
+
"""
|
|
652
|
+
if value is None:
|
|
653
|
+
return None
|
|
654
|
+
if isinstance(value, bool):
|
|
655
|
+
raise ValueError("claim_amount must be a number of rubles, not a boolean")
|
|
656
|
+
if isinstance(value, Decimal):
|
|
657
|
+
amount = value
|
|
658
|
+
elif isinstance(value, int):
|
|
659
|
+
amount = value
|
|
660
|
+
elif isinstance(value, float):
|
|
661
|
+
amount = Decimal(str(value))
|
|
662
|
+
elif isinstance(value, str):
|
|
663
|
+
from decimal import InvalidOperation
|
|
664
|
+
try:
|
|
665
|
+
amount = Decimal(value.strip())
|
|
666
|
+
except InvalidOperation:
|
|
667
|
+
raise ValueError(f"claim_amount must be a number of rubles, got {value!r}") from None
|
|
668
|
+
else:
|
|
669
|
+
raise ValueError(f"claim_amount must be a number of rubles, got {type(value).__name__}")
|
|
670
|
+
if isinstance(amount, Decimal) and not amount.is_finite():
|
|
671
|
+
raise ValueError(f"claim_amount must be finite, got {amount}")
|
|
672
|
+
return amount
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def _fees_arguments(args: dict) -> dict:
|
|
676
|
+
"""Нормализация вызова sud_cli_fees: алиасы имён + денежный тип базы."""
|
|
677
|
+
from .legal_engine import parse_as_of
|
|
678
|
+
|
|
679
|
+
values = dict(args)
|
|
680
|
+
jurisdiction = _pick(values, ("jurisdiction",), "jurisdiction")
|
|
681
|
+
if jurisdiction is None:
|
|
682
|
+
raise ValueError("jurisdiction is required (arbitration|general_jurisdiction|kas)")
|
|
683
|
+
key = str(jurisdiction).strip().lower()
|
|
684
|
+
jurisdiction = JURISDICTION_ALIASES.get(key, key)
|
|
685
|
+
fee_kind = _pick(values, ("fee_kind", "kind"), "fee_kind/kind")
|
|
686
|
+
if fee_kind is None:
|
|
687
|
+
raise ValueError(
|
|
688
|
+
"fee_kind is required; accepted names: fee_kind (engine) or kind (CLI --kind)"
|
|
689
|
+
)
|
|
690
|
+
applicant_type = _pick(values, ("applicant_type", "applicant"), "applicant_type/applicant")
|
|
691
|
+
claim_amount = _claim_amount(values.pop("claim_amount", None))
|
|
692
|
+
claim_class = values.pop("claim_class", None)
|
|
693
|
+
# v2.10.0 (W4d): дата-привязка тарифа. Значение отсутствует → None → прежний
|
|
694
|
+
# контракт (действующая редакция, конверт без provenance). Неверная строка —
|
|
695
|
+
# явная ошибка, а не молчаливый пересчёт «на сегодня».
|
|
696
|
+
as_of = parse_as_of(values["as_of"]) if values.get("as_of") is not None else None
|
|
697
|
+
values.pop("as_of", None)
|
|
698
|
+
if values:
|
|
699
|
+
raise TypeError(f"unknown fees arguments: {sorted(values)}")
|
|
700
|
+
return {"jurisdiction": jurisdiction, "fee_kind": fee_kind,
|
|
701
|
+
"applicant_type": applicant_type, "claim_amount": claim_amount,
|
|
702
|
+
"claim_class": claim_class, "as_of": as_of}
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _mcp_state_dir():
|
|
706
|
+
import os
|
|
707
|
+
from pathlib import Path
|
|
708
|
+
return Path(os.environ.get("SUD_CLI_STATE_DIR") or (Path.home() / ".local" / "share" / "sud-cli"))
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def _call(name, args):
|
|
712
|
+
if name == "sud_cli_efrsb_link":
|
|
713
|
+
from . import efrsb_manual
|
|
714
|
+
return efrsb_manual.link_plan(args["kind"], name=args.get("name"),
|
|
715
|
+
inn=args.get("inn"), ogrn=args.get("ogrn"))
|
|
716
|
+
if name == "sud_cli_plenum_search":
|
|
717
|
+
from . import plenum_corpus as pc
|
|
718
|
+
from .config import get_state_dir
|
|
719
|
+
return pc.search(get_state_dir(), args["text"])
|
|
720
|
+
if name == "sud_cli_license_info":
|
|
721
|
+
from . import _wm
|
|
722
|
+
from . import __version__ as _v
|
|
723
|
+
payload = _wm.source_info(_v)
|
|
724
|
+
payload["build_flags"] = _wm.build_flags(_v) if args.get("with_build_flags", True) else []
|
|
725
|
+
return payload
|
|
726
|
+
if name == "sud_cli_limitation":
|
|
727
|
+
from .limitation import calculate_limitation
|
|
728
|
+
values = dict(args)
|
|
729
|
+
picked = _pick(values, ("period_type", "type"), "period_type/type")
|
|
730
|
+
values["period_type"] = picked if picked is not None else "general"
|
|
731
|
+
for engine_name, cli_name in (("custom_years", "years"), ("custom_months", "months")):
|
|
732
|
+
chosen = _pick(values, (engine_name, cli_name), f"{engine_name}/{cli_name}")
|
|
733
|
+
if chosen is not None:
|
|
734
|
+
values[engine_name] = chosen
|
|
735
|
+
values["start"] = date.fromisoformat(values["start"])
|
|
736
|
+
if values.get("check_date"):
|
|
737
|
+
values["check_date"] = date.fromisoformat(values["check_date"])
|
|
738
|
+
return calculate_limitation(**values).to_dict()
|
|
739
|
+
if name == "sud_cli_timeline_audit":
|
|
740
|
+
from .timeline_audit import audit_timeline
|
|
741
|
+
return audit_timeline(args.get("events", []))
|
|
742
|
+
if name == "sud_cli_fee_exemption":
|
|
743
|
+
from .fees_extra import fee_exemption_check
|
|
744
|
+
return fee_exemption_check(load_rules(), jurisdiction=args["jurisdiction"], category_code=args["category"])
|
|
745
|
+
if name == "sud_cli_fee_refund":
|
|
746
|
+
from decimal import Decimal
|
|
747
|
+
from .fees_extra import fee_refund
|
|
748
|
+
return fee_refund(load_rules(), paid=Decimal(args["paid"]), ground=args["ground"],
|
|
749
|
+
claim_admitted=bool(args.get("claim_admitted")),
|
|
750
|
+
plaintiff_default=bool(args.get("plaintiff_default")),
|
|
751
|
+
transferred_to_arbitration=bool(args.get("transferred_to_arbitration")))
|
|
752
|
+
if name == "sud_cli_fee_indexation":
|
|
753
|
+
from decimal import Decimal
|
|
754
|
+
from .fees_extra import indexation_compute, indexation_frame
|
|
755
|
+
r = load_rules()
|
|
756
|
+
if args.get("amount"):
|
|
757
|
+
return indexation_compute(r, code=args["code"], amount=Decimal(args["amount"]),
|
|
758
|
+
cpi_start=Decimal(args["cpi_start"]) if args.get("cpi_start") else None,
|
|
759
|
+
cpi_end=Decimal(args["cpi_end"]) if args.get("cpi_end") else None)
|
|
760
|
+
return indexation_frame(r, code=args["code"])
|
|
761
|
+
if name == "sud_cli_npa_as_of":
|
|
762
|
+
from .npa_as_of import lookup_as_of
|
|
763
|
+
return lookup_as_of(args["act"], args["article"], args["date"])
|
|
764
|
+
if name == "sud_cli_verify_quote":
|
|
765
|
+
from .verify import verify_quote
|
|
766
|
+
values = dict(args or {})
|
|
767
|
+
return verify_quote(
|
|
768
|
+
values["text"], values["source"],
|
|
769
|
+
act=values.get("act"), article=values.get("article"), date=values.get("date"),
|
|
770
|
+
case_id=values.get("case_id"), path=values.get("path"),
|
|
771
|
+
state_dir=_mcp_state_dir(), max_fuzzy=values.get("max_fuzzy", 0.0))
|
|
772
|
+
if name == "sud_cli_deadlines":
|
|
773
|
+
from .legal_deadlines import calculate_deadline
|
|
774
|
+
allowed = {"deadline_key", "event_date", "region", "tz"}
|
|
775
|
+
values = {k: v for k, v in (args or {}).items() if k in allowed}
|
|
776
|
+
if values.get("event_date"):
|
|
777
|
+
values["event_date"] = date.fromisoformat(values["event_date"])
|
|
778
|
+
return calculate_deadline(**values).to_dict()
|
|
779
|
+
if name == "sud_cli_deadlines_matrix":
|
|
780
|
+
from .deadline_matrix import build_matrix
|
|
781
|
+
from .legal_engine import load_rules
|
|
782
|
+
values = dict(args or {})
|
|
783
|
+
case_id = _pick(values, ("case_id", "id"), "case_id/id")
|
|
784
|
+
if case_id is None:
|
|
785
|
+
raise ValueError("case_id is required")
|
|
786
|
+
instance_override = values.pop("instance", None)
|
|
787
|
+
as_of = values.pop("as_of", None)
|
|
788
|
+
missing = bool(values.pop("missing", False))
|
|
789
|
+
if values:
|
|
790
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
791
|
+
state_dir = _mcp_state_dir()
|
|
792
|
+
instance, points = build_matrix(state_dir, case_id, instance_override=instance_override, as_of=as_of)
|
|
793
|
+
if missing:
|
|
794
|
+
points = [p for p in points if p.get("needs_anchor")]
|
|
795
|
+
rules = load_rules()
|
|
796
|
+
calc_date = date.today().isoformat()
|
|
797
|
+
disclaimer = rules["mandatory_disclaimer"].format(calc_date=calc_date)
|
|
798
|
+
payload = {
|
|
799
|
+
"case_id": case_id,
|
|
800
|
+
"instance": instance,
|
|
801
|
+
"as_of": as_of,
|
|
802
|
+
"points": points,
|
|
803
|
+
"matrix": points,
|
|
804
|
+
"items": points,
|
|
805
|
+
"deadlines": points,
|
|
806
|
+
"entries": points,
|
|
807
|
+
"disclaimer": disclaimer,
|
|
808
|
+
"clean_room": rules.get("clean_room"),
|
|
809
|
+
}
|
|
810
|
+
return payload
|
|
811
|
+
if name == "sud_cli_fees":
|
|
812
|
+
from .legal_engine import STATUS_OK, calculate_fee, load_rules, validate_claim_class
|
|
813
|
+
# v2.3.1: вызов проходит через нормализацию — CLI-алиасы (kind, applicant),
|
|
814
|
+
# алиасы юрисдикции и цена иска в денежном типе (Decimal с копейками).
|
|
815
|
+
rules = load_rules()
|
|
816
|
+
fa = _fees_arguments(args)
|
|
817
|
+
cc = fa.get("claim_class")
|
|
818
|
+
if cc is not None:
|
|
819
|
+
# v2.3.3 (review R2): тот же guard класса, что CLI; без него
|
|
820
|
+
# claim_class="criminal" молча проходил в расчёт через MCP.
|
|
821
|
+
cr = validate_claim_class(cc, rules, fa.get("fee_kind"))
|
|
822
|
+
if cr.status != STATUS_OK:
|
|
823
|
+
return cr.to_dict()
|
|
824
|
+
return calculate_fee(rules=rules, **fa).to_dict()
|
|
825
|
+
if name == "sud_cli_interest":
|
|
826
|
+
from .interest import calculate_interest
|
|
827
|
+
values = dict(args)
|
|
828
|
+
# start/start_date и end/end_date — одно и то же разными именами (CLI
|
|
829
|
+
# десты start_date/end_date). Конфликт значений молча проигрывает одной
|
|
830
|
+
# стороне, поэтому тоже явная ошибка.
|
|
831
|
+
start_value = _pick(values, ("start_date", "start"), "start/start_date")
|
|
832
|
+
end_value = _pick(values, ("end_date", "end"), "end/end_date")
|
|
833
|
+
if start_value is None or end_value is None:
|
|
834
|
+
raise ValueError("start and end are required (или CLI-алиасы start_date/end_date)")
|
|
835
|
+
values["start"] = date.fromisoformat(start_value)
|
|
836
|
+
values["end"] = date.fromisoformat(end_value)
|
|
837
|
+
return calculate_interest(**values).to_dict()
|
|
838
|
+
if name in {"sud_cli_practice_search", "sud_cli_kad_search", "sud_cli_case_history"}:
|
|
839
|
+
# v2.3.2 (sa-1 F-05): живые поиски под тем же гейтом SUD_CLI_BRIDGE_LIVE,
|
|
840
|
+
# что и sudrf_live/mos_live — без него никаких исходящих запросов в суды.
|
|
841
|
+
# v2.3.3 (review R1): строго {1,true,yes,on} как в bridge — "0"/"off" раньше
|
|
842
|
+
# ОТКРЫВАЛИ live-доступ (truthy-строка). Единый контракт обоих входов.
|
|
843
|
+
if os.environ.get("SUD_CLI_BRIDGE_LIVE", "").strip().lower() not in {"1", "true", "yes", "on"}:
|
|
844
|
+
return {"status": "blocked", "reason": "live_access_disabled",
|
|
845
|
+
"hint": "Исходящие запросы к sudact включаются переменной SUD_CLI_BRIDGE_LIVE."}
|
|
846
|
+
if name == "sud_cli_practice_search":
|
|
847
|
+
from .case_search.public_api import search_practice
|
|
848
|
+
return search_practice(**args)
|
|
849
|
+
if name == "sud_cli_kad_search":
|
|
850
|
+
from .case_search.public_api import kad_search
|
|
851
|
+
return kad_search(**args)
|
|
852
|
+
if name == "sud_cli_case_history":
|
|
853
|
+
from .case_search.public_api import case_history
|
|
854
|
+
values = dict(args)
|
|
855
|
+
# CLI отдаёт номер дела позиционным аргументом (dest case_id); MCP-схема
|
|
856
|
+
# исторически звала его case_number. Оба имени принимаются, конфликт — ошибка.
|
|
857
|
+
chosen = _pick(values, ("case_number", "case_id"), "case_number/case_id")
|
|
858
|
+
if chosen is None:
|
|
859
|
+
raise ValueError("case_number is required; accepted names: case_number or case_id")
|
|
860
|
+
values["case_number"] = chosen
|
|
861
|
+
return case_history(**values)
|
|
862
|
+
if name == "sud_cli_payment":
|
|
863
|
+
from .payment import payment_details
|
|
864
|
+
return payment_details(**args)
|
|
865
|
+
if name == "sud_cli_deferral":
|
|
866
|
+
from .deferral import generate_deferral
|
|
867
|
+
return generate_deferral(**args)
|
|
868
|
+
if name == "sud_cli_fssp_check_plan":
|
|
869
|
+
from .fssp_manual import check_plan
|
|
870
|
+
values = dict(args or {})
|
|
871
|
+
kind = values.pop("kind", None)
|
|
872
|
+
if kind is None:
|
|
873
|
+
raise ValueError("kind is required")
|
|
874
|
+
kwargs = {}
|
|
875
|
+
for k in ("name", "inn", "ogrn", "birth_date", "region"):
|
|
876
|
+
if k in values:
|
|
877
|
+
kwargs[k] = values.pop(k)
|
|
878
|
+
if values:
|
|
879
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
880
|
+
return check_plan(kind, **kwargs)
|
|
881
|
+
if name == "sud_cli_fssp_record":
|
|
882
|
+
from .db import add_case_evidence, get_connection, init_db
|
|
883
|
+
from .fssp_manual import record_payload
|
|
884
|
+
values = dict(args or {})
|
|
885
|
+
case_id = values.pop("case_id", None)
|
|
886
|
+
subject = values.pop("subject", None)
|
|
887
|
+
result_v = values.pop("result", None)
|
|
888
|
+
checked_by = values.pop("checked_by", None)
|
|
889
|
+
if not (case_id and subject and result_v and checked_by):
|
|
890
|
+
raise ValueError("case_id, subject, result, checked_by are required")
|
|
891
|
+
payload = record_payload(subject=subject, result=result_v,
|
|
892
|
+
checked_by=checked_by,
|
|
893
|
+
notes=values.pop("notes", None),
|
|
894
|
+
evidence_file=values.pop("file", None))
|
|
895
|
+
if values:
|
|
896
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
897
|
+
state_dir = _mcp_state_dir()
|
|
898
|
+
init_db(state_dir)
|
|
899
|
+
conn = get_connection(state_dir)
|
|
900
|
+
try:
|
|
901
|
+
return add_case_evidence(conn, case_id, title=payload["title"],
|
|
902
|
+
path=payload["evidence_file"], note=payload["note"])
|
|
903
|
+
finally:
|
|
904
|
+
conn.close()
|
|
905
|
+
if name == "sud_cli_fssp_stats":
|
|
906
|
+
from .fssp_stats import statistics
|
|
907
|
+
values = dict(args or {})
|
|
908
|
+
region = values.pop("region", None)
|
|
909
|
+
if values:
|
|
910
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
911
|
+
return statistics(region)
|
|
912
|
+
if name == "sud_cli_efrsb_companies":
|
|
913
|
+
from .efrsb_public import search_companies
|
|
914
|
+
return search_companies(**args)
|
|
915
|
+
if name == "sud_cli_efrsb_persons":
|
|
916
|
+
from .efrsb_public import search_persons
|
|
917
|
+
values = dict(args)
|
|
918
|
+
# CLI-dest --name против имени движка full_name.
|
|
919
|
+
chosen = _pick(values, ("full_name", "name"), "full_name/name")
|
|
920
|
+
if chosen is None:
|
|
921
|
+
raise ValueError("full_name is required; accepted names: full_name or name")
|
|
922
|
+
values["full_name"] = chosen
|
|
923
|
+
return search_persons(**values)
|
|
924
|
+
if name == "sud_cli_efrsb_publications":
|
|
925
|
+
from .efrsb_public import case_publications
|
|
926
|
+
return case_publications(**args)
|
|
927
|
+
if name == "sud_cli_efrsb_managers":
|
|
928
|
+
from .efrsb_public import search_managers
|
|
929
|
+
return search_managers(**args)
|
|
930
|
+
if name in {"sud_cli_sudrf_live", "sud_cli_mos_live"}:
|
|
931
|
+
from .case_search_bridge import run_bridge_command
|
|
932
|
+
state_dir = Path(os.environ.get("SUD_CLI_STATE_DIR") or Path.home() / ".local" / "share" / "sud-cli")
|
|
933
|
+
kwargs = {"case_id": args["case_id"]}
|
|
934
|
+
if name == "sud_cli_mos_live":
|
|
935
|
+
kwargs["court_slug"] = args.get("court_slug")
|
|
936
|
+
return run_bridge_command("mos-live" if name.endswith("mos_live") else "sudrf-live",
|
|
937
|
+
state_dir=state_dir, **kwargs)
|
|
938
|
+
if name == "sud_cli_case_diff":
|
|
939
|
+
from .case_diff import DiffError, diff_case_snapshots
|
|
940
|
+
old = args.get("old") or args.get("old_snapshot")
|
|
941
|
+
new = args.get("new") or args.get("new_snapshot")
|
|
942
|
+
if not old:
|
|
943
|
+
raise ValueError("old snapshot path required")
|
|
944
|
+
if not new:
|
|
945
|
+
raise ValueError("new snapshot path required")
|
|
946
|
+
try:
|
|
947
|
+
return diff_case_snapshots(Path(old), Path(new))
|
|
948
|
+
except DiffError as exc:
|
|
949
|
+
# DiffError outside the handle_request catch set: convert, or the
|
|
950
|
+
# serve_stdio loop dies instead of returning isError=True.
|
|
951
|
+
raise ValueError(str(exc)) from exc
|
|
952
|
+
if name == "sud_cli_npa_compare":
|
|
953
|
+
from .db import SearchError, compare_npa_editions, get_connection, init_db
|
|
954
|
+
state_dir = Path(os.environ.get("SUD_CLI_STATE_DIR") or Path.home() / ".local" / "share" / "sud-cli")
|
|
955
|
+
init_db(state_dir)
|
|
956
|
+
conn = get_connection(state_dir)
|
|
957
|
+
try:
|
|
958
|
+
return compare_npa_editions(conn, args["doc_id"], args["edition1"], args["edition2"])
|
|
959
|
+
except SearchError as exc:
|
|
960
|
+
raise ValueError(str(exc)) from exc
|
|
961
|
+
finally:
|
|
962
|
+
conn.close()
|
|
963
|
+
if name == "sud_cli_case_new":
|
|
964
|
+
from .db import LEGAL_CASE_STATUSES, create_case, get_connection, init_db
|
|
965
|
+
state_dir = _mcp_state_dir()
|
|
966
|
+
init_db(state_dir)
|
|
967
|
+
conn = get_connection(state_dir)
|
|
968
|
+
try:
|
|
969
|
+
values = dict(args)
|
|
970
|
+
number = _pick(values, ("number", "case_number"), "number/case_number")
|
|
971
|
+
if number is None:
|
|
972
|
+
raise ValueError("number is required")
|
|
973
|
+
status = values.get("status")
|
|
974
|
+
if status is not None and status not in LEGAL_CASE_STATUSES:
|
|
975
|
+
raise ValueError("invalid_status")
|
|
976
|
+
res = create_case(
|
|
977
|
+
conn,
|
|
978
|
+
number,
|
|
979
|
+
values.get("court"),
|
|
980
|
+
values.get("plaintiff"),
|
|
981
|
+
values.get("defendant"),
|
|
982
|
+
owner=values.get("owner"),
|
|
983
|
+
status=status,
|
|
984
|
+
)
|
|
985
|
+
if res.get("duplicate"):
|
|
986
|
+
return {"status": "duplicate_window", "case_id": res["case_id"]}
|
|
987
|
+
return {"case_id": res["case_id"], "case_number": res.get("case_number"),
|
|
988
|
+
"court": res.get("court"), "status": res.get("status"),
|
|
989
|
+
"owner": res.get("owner")}
|
|
990
|
+
finally:
|
|
991
|
+
conn.close()
|
|
992
|
+
if name == "sud_cli_case_set":
|
|
993
|
+
from .db import LEGAL_CASE_STATUSES, get_connection, init_db, set_case
|
|
994
|
+
values = dict(args)
|
|
995
|
+
case_id = _pick(values, ("case_id", "id"), "case_id/id")
|
|
996
|
+
if case_id is None:
|
|
997
|
+
raise ValueError("case_id is required")
|
|
998
|
+
fields = {}
|
|
999
|
+
for k in ("status", "owner", "closed_at"):
|
|
1000
|
+
if k in values and values.get(k) is not None:
|
|
1001
|
+
fields[k] = values.pop(k)
|
|
1002
|
+
if values:
|
|
1003
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
1004
|
+
if "status" in fields and fields["status"] not in LEGAL_CASE_STATUSES:
|
|
1005
|
+
raise ValueError("invalid_status")
|
|
1006
|
+
state_dir = _mcp_state_dir()
|
|
1007
|
+
init_db(state_dir)
|
|
1008
|
+
conn = get_connection(state_dir)
|
|
1009
|
+
try:
|
|
1010
|
+
updated = set_case(conn, case_id, **fields)
|
|
1011
|
+
finally:
|
|
1012
|
+
conn.close()
|
|
1013
|
+
if updated is None:
|
|
1014
|
+
raise ValueError(f"case not found: {case_id}")
|
|
1015
|
+
return updated
|
|
1016
|
+
if name == "sud_cli_case_list":
|
|
1017
|
+
from .db import get_connection, init_db, list_cases
|
|
1018
|
+
state_dir = _mcp_state_dir()
|
|
1019
|
+
init_db(state_dir)
|
|
1020
|
+
conn = get_connection(state_dir)
|
|
1021
|
+
try:
|
|
1022
|
+
return list_cases(
|
|
1023
|
+
conn,
|
|
1024
|
+
status=args.get("status"),
|
|
1025
|
+
owner=args.get("owner"),
|
|
1026
|
+
court=args.get("court"),
|
|
1027
|
+
party=args.get("party"),
|
|
1028
|
+
)
|
|
1029
|
+
finally:
|
|
1030
|
+
conn.close()
|
|
1031
|
+
if name == "sud_cli_case_card":
|
|
1032
|
+
from .db import get_case_card, get_connection, init_db
|
|
1033
|
+
values = dict(args)
|
|
1034
|
+
case_id = _pick(values, ("case_id", "id"), "case_id/id")
|
|
1035
|
+
if case_id is None:
|
|
1036
|
+
raise ValueError("case_id is required")
|
|
1037
|
+
state_dir = _mcp_state_dir()
|
|
1038
|
+
init_db(state_dir)
|
|
1039
|
+
conn = get_connection(state_dir)
|
|
1040
|
+
try:
|
|
1041
|
+
card = get_case_card(conn, case_id)
|
|
1042
|
+
finally:
|
|
1043
|
+
conn.close()
|
|
1044
|
+
if card is None:
|
|
1045
|
+
raise ValueError(f"case not found: {case_id}")
|
|
1046
|
+
return card
|
|
1047
|
+
if name == "sud_cli_case_amount_add":
|
|
1048
|
+
from .db import InvalidAmountError, add_case_amount, get_connection, init_db
|
|
1049
|
+
values = dict(args)
|
|
1050
|
+
case_id = _pick(values, ("case_id", "id"), "case_id/id")
|
|
1051
|
+
if case_id is None:
|
|
1052
|
+
raise ValueError("case_id is required")
|
|
1053
|
+
state_dir = _mcp_state_dir()
|
|
1054
|
+
init_db(state_dir)
|
|
1055
|
+
conn = get_connection(state_dir)
|
|
1056
|
+
try:
|
|
1057
|
+
try:
|
|
1058
|
+
res = add_case_amount(
|
|
1059
|
+
conn, case_id, values.get("kind"), values.get("amount"), note=values.get("note")
|
|
1060
|
+
)
|
|
1061
|
+
except InvalidAmountError as exc:
|
|
1062
|
+
raise ValueError("invalid_amount") from exc
|
|
1063
|
+
finally:
|
|
1064
|
+
conn.close()
|
|
1065
|
+
if res is None:
|
|
1066
|
+
raise ValueError(f"case not found: {case_id}")
|
|
1067
|
+
return res
|
|
1068
|
+
if name == "sud_cli_case_doc_add":
|
|
1069
|
+
from .db import DocNotFoundError, add_case_document, get_connection, init_db
|
|
1070
|
+
values = dict(args)
|
|
1071
|
+
case_id = _pick(values, ("case_id", "id"), "case_id/id")
|
|
1072
|
+
if case_id is None:
|
|
1073
|
+
raise ValueError("case_id is required")
|
|
1074
|
+
state_dir = _mcp_state_dir()
|
|
1075
|
+
init_db(state_dir)
|
|
1076
|
+
conn = get_connection(state_dir)
|
|
1077
|
+
try:
|
|
1078
|
+
try:
|
|
1079
|
+
res = add_case_document(conn, case_id, values.get("path"), title=values.get("title"))
|
|
1080
|
+
except DocNotFoundError as exc:
|
|
1081
|
+
raise ValueError("doc_not_found") from exc
|
|
1082
|
+
finally:
|
|
1083
|
+
conn.close()
|
|
1084
|
+
if res is None:
|
|
1085
|
+
raise ValueError(f"case not found: {case_id}")
|
|
1086
|
+
return res
|
|
1087
|
+
if name == "sud_cli_case_note_add":
|
|
1088
|
+
from .db import add_case_note, get_connection, init_db
|
|
1089
|
+
values = dict(args)
|
|
1090
|
+
case_id = _pick(values, ("case_id", "id"), "case_id/id")
|
|
1091
|
+
if case_id is None:
|
|
1092
|
+
raise ValueError("case_id is required")
|
|
1093
|
+
state_dir = _mcp_state_dir()
|
|
1094
|
+
init_db(state_dir)
|
|
1095
|
+
conn = get_connection(state_dir)
|
|
1096
|
+
try:
|
|
1097
|
+
res = add_case_note(conn, case_id, values.get("text"), author=values.get("author"))
|
|
1098
|
+
finally:
|
|
1099
|
+
conn.close()
|
|
1100
|
+
if res is None:
|
|
1101
|
+
raise ValueError(f"case not found: {case_id}")
|
|
1102
|
+
return res
|
|
1103
|
+
if name == "sud_cli_conflict_check":
|
|
1104
|
+
from .conflict import check as conflict_check
|
|
1105
|
+
from .db import get_case, get_connection, init_db, list_cases
|
|
1106
|
+
|
|
1107
|
+
values = dict(args)
|
|
1108
|
+
case_id = _pick(values, ("case_id", "id"), "case_id/id")
|
|
1109
|
+
state_dir = _mcp_state_dir()
|
|
1110
|
+
init_db(state_dir)
|
|
1111
|
+
conn = get_connection(state_dir)
|
|
1112
|
+
try:
|
|
1113
|
+
if case_id is not None:
|
|
1114
|
+
case = get_case(conn, case_id)
|
|
1115
|
+
if case is None:
|
|
1116
|
+
raise ValueError(f"case not found: {case_id}")
|
|
1117
|
+
portfolio = [c for c in list_cases(conn) if c.get("case_id") != case_id]
|
|
1118
|
+
candidate = {
|
|
1119
|
+
"parties": [],
|
|
1120
|
+
"case_id": case_id,
|
|
1121
|
+
"court": case.get("court"),
|
|
1122
|
+
"case_number": case.get("case_number"),
|
|
1123
|
+
}
|
|
1124
|
+
for key in ("plaintiff", "defendant"):
|
|
1125
|
+
val = case.get(key)
|
|
1126
|
+
if val:
|
|
1127
|
+
candidate["parties"].append(val)
|
|
1128
|
+
else:
|
|
1129
|
+
portfolio = list_cases(conn)
|
|
1130
|
+
candidate = {
|
|
1131
|
+
"parties": list(values.pop("parties", []) or []),
|
|
1132
|
+
"court": values.pop("court", None),
|
|
1133
|
+
"case_number": values.pop("case_number", None),
|
|
1134
|
+
}
|
|
1135
|
+
for key in ("plaintiff", "defendant"):
|
|
1136
|
+
if key in values and values.get(key) is not None:
|
|
1137
|
+
candidate["parties"].append(values.pop(key))
|
|
1138
|
+
if values:
|
|
1139
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
1140
|
+
hits = conflict_check(portfolio, candidate)
|
|
1141
|
+
finally:
|
|
1142
|
+
conn.close()
|
|
1143
|
+
return {
|
|
1144
|
+
"clean": len(hits) == 0,
|
|
1145
|
+
"hits": hits,
|
|
1146
|
+
"method": "name_exact",
|
|
1147
|
+
"coverage": "local registry names only",
|
|
1148
|
+
"disclaimer": (
|
|
1149
|
+
"Отсутствие совпадений не означает отсутствие конфликта: покрытие — "
|
|
1150
|
+
"только имена локального реестра; внешние стороны и аффилированные лица "
|
|
1151
|
+
"не охвачены. Решение остаётся за человеком (boundary IX-3)."
|
|
1152
|
+
),
|
|
1153
|
+
}
|
|
1154
|
+
if name == "sud_cli_jurisdiction_check":
|
|
1155
|
+
from .jurisdiction import check_jurisdiction
|
|
1156
|
+
return check_jurisdiction(
|
|
1157
|
+
code=args.get("code"),
|
|
1158
|
+
claim=args.get("claim"),
|
|
1159
|
+
party_org=bool(args.get("party_org", False)),
|
|
1160
|
+
party_phys=bool(args.get("party_phys", False)),
|
|
1161
|
+
unknown_rf_location=bool(args.get("unknown_rf_location", False)),
|
|
1162
|
+
immovable=bool(args.get("immovable", False)),
|
|
1163
|
+
corporate=bool(args.get("corporate", False)),
|
|
1164
|
+
bankruptcy=bool(args.get("bankruptcy", False)),
|
|
1165
|
+
ip_rights=bool(args.get("ip_rights", False)),
|
|
1166
|
+
contract_forum=bool(args.get("contract_forum", False)),
|
|
1167
|
+
arbitration_clause=bool(args.get("arbitration_clause", False)),
|
|
1168
|
+
multiple_defendants=bool(args.get("multiple_defendants", False)),
|
|
1169
|
+
foreign_element=bool(args.get("foreign_element", False)),
|
|
1170
|
+
consumer=bool(args.get("consumer", False)),
|
|
1171
|
+
alimony=bool(args.get("alimony", False)),
|
|
1172
|
+
)
|
|
1173
|
+
if name == "sud_cli_pretrial_calc":
|
|
1174
|
+
from .pretrial import calc_pretrial
|
|
1175
|
+
return calc_pretrial(code=args.get("code"), sent=args.get("sent"))
|
|
1176
|
+
if name == "sud_cli_pretrial_consequences":
|
|
1177
|
+
from .pretrial import consequences
|
|
1178
|
+
return consequences(code=args.get("code"), stage=args.get("stage"))
|
|
1179
|
+
# ---------------- W6a §1/§3/§4/§5: диспатч новых офлайн-утилит -------------
|
|
1180
|
+
if name in {"sud_cli_deadlines_anchor", "sud_cli_deadlines_confirm",
|
|
1181
|
+
"sud_cli_deadlines_waive", "sud_cli_deadlines_autofill"}:
|
|
1182
|
+
from .db import (
|
|
1183
|
+
anchor_deadline_state,
|
|
1184
|
+
confirm_deadline_state,
|
|
1185
|
+
get_connection,
|
|
1186
|
+
init_db,
|
|
1187
|
+
waive_deadline_state,
|
|
1188
|
+
)
|
|
1189
|
+
values = dict(args or {})
|
|
1190
|
+
state_dir = _mcp_state_dir()
|
|
1191
|
+
init_db(state_dir)
|
|
1192
|
+
conn = get_connection(state_dir)
|
|
1193
|
+
try:
|
|
1194
|
+
case_id = values.pop("case_id", None)
|
|
1195
|
+
if case_id is None:
|
|
1196
|
+
raise ValueError("case_id is required")
|
|
1197
|
+
if name == "sud_cli_deadlines_anchor":
|
|
1198
|
+
key = values.pop("key", None)
|
|
1199
|
+
event_date = values.pop("event_date", None)
|
|
1200
|
+
method = values.pop("method", None)
|
|
1201
|
+
evidence = values.pop("evidence", None)
|
|
1202
|
+
actor = values.pop("by", None) or "mcp"
|
|
1203
|
+
if values:
|
|
1204
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
1205
|
+
if not (key and event_date and method and evidence):
|
|
1206
|
+
raise ValueError("key, event_date, method, evidence are required")
|
|
1207
|
+
return anchor_deadline_state(conn, state_dir, case_id, key,
|
|
1208
|
+
event_date, method, evidence, actor=actor)
|
|
1209
|
+
if name == "sud_cli_deadlines_confirm":
|
|
1210
|
+
key = values.pop("key", None)
|
|
1211
|
+
by = values.pop("by", None)
|
|
1212
|
+
evidence = values.pop("evidence", None)
|
|
1213
|
+
waive = bool(values.pop("waive", False))
|
|
1214
|
+
norm = values.pop("norm", None)
|
|
1215
|
+
if values:
|
|
1216
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
1217
|
+
if not (key and by):
|
|
1218
|
+
raise ValueError("key and by are required")
|
|
1219
|
+
if waive and not norm:
|
|
1220
|
+
raise ValueError("norm required for waive")
|
|
1221
|
+
return confirm_deadline_state(conn, state_dir, case_id, key, by,
|
|
1222
|
+
evidence=evidence, waive=waive, norm=norm)
|
|
1223
|
+
if name == "sud_cli_deadlines_waive":
|
|
1224
|
+
key = values.pop("key", None)
|
|
1225
|
+
by = values.pop("by", None)
|
|
1226
|
+
norm = values.pop("norm", None)
|
|
1227
|
+
evidence = values.pop("evidence", None)
|
|
1228
|
+
if values:
|
|
1229
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
1230
|
+
if not (key and by and norm):
|
|
1231
|
+
raise ValueError("key, by and norm are required")
|
|
1232
|
+
return waive_deadline_state(conn, state_dir, case_id, key, by, norm,
|
|
1233
|
+
evidence=evidence)
|
|
1234
|
+
event = values.pop("event", None)
|
|
1235
|
+
if values:
|
|
1236
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
1237
|
+
from .autofill import autofill_case
|
|
1238
|
+
return autofill_case(conn, state_dir, case_id, event_id=event)
|
|
1239
|
+
finally:
|
|
1240
|
+
conn.close()
|
|
1241
|
+
if name == "sud_cli_deadlines_list":
|
|
1242
|
+
from .db import get_connection, init_db, list_all_deadline_states, list_deadline_states
|
|
1243
|
+
state_dir = _mcp_state_dir()
|
|
1244
|
+
init_db(state_dir)
|
|
1245
|
+
conn = get_connection(state_dir)
|
|
1246
|
+
try:
|
|
1247
|
+
case_id = (args or {}).get("case_id")
|
|
1248
|
+
if case_id is not None:
|
|
1249
|
+
return list_deadline_states(conn, case_id)
|
|
1250
|
+
return list_all_deadline_states(conn)
|
|
1251
|
+
finally:
|
|
1252
|
+
conn.close()
|
|
1253
|
+
if name == "sud_cli_deadlines_audit":
|
|
1254
|
+
from .db import init_db, read_deadline_audit
|
|
1255
|
+
state_dir = _mcp_state_dir()
|
|
1256
|
+
init_db(state_dir)
|
|
1257
|
+
return read_deadline_audit(state_dir, args["case_id"])
|
|
1258
|
+
if name == "sud_cli_calendar_agenda":
|
|
1259
|
+
from .agenda import agenda_payload
|
|
1260
|
+
days = int((args or {}).get("days", 14))
|
|
1261
|
+
if not 1 <= days <= 365:
|
|
1262
|
+
raise ValueError("days must be within 1..365")
|
|
1263
|
+
return agenda_payload(_mcp_state_dir(), days=days)
|
|
1264
|
+
if name == "sud_cli_calendar_ics":
|
|
1265
|
+
from .calendar_ics import ics_for_state_dir
|
|
1266
|
+
days = int((args or {}).get("days", 60))
|
|
1267
|
+
if not 1 <= days <= 3650:
|
|
1268
|
+
raise ValueError("days must be within 1..3650")
|
|
1269
|
+
text, events = ics_for_state_dir(_mcp_state_dir(), days=days)
|
|
1270
|
+
return {"days": days, "count": len(events), "events": events, "ics": text}
|
|
1271
|
+
if name == "sud_cli_reminders_due":
|
|
1272
|
+
from .db import get_connection, init_db, list_all_deadline_states
|
|
1273
|
+
from .reminders import BUS_FREE_NOTE, due_items, generate_mail_commands
|
|
1274
|
+
days = int((args or {}).get("days", 7))
|
|
1275
|
+
if not 1 <= days <= 90:
|
|
1276
|
+
raise ValueError("days must be within 1..90")
|
|
1277
|
+
state_dir = _mcp_state_dir()
|
|
1278
|
+
init_db(state_dir)
|
|
1279
|
+
conn = get_connection(state_dir)
|
|
1280
|
+
try:
|
|
1281
|
+
states = list_all_deadline_states(conn)
|
|
1282
|
+
finally:
|
|
1283
|
+
conn.close()
|
|
1284
|
+
today = date.today()
|
|
1285
|
+
items = due_items(states, today, days)
|
|
1286
|
+
return {
|
|
1287
|
+
"days": days,
|
|
1288
|
+
"today": today.isoformat(),
|
|
1289
|
+
"window_to": (today + timedelta(days=days)).isoformat(),
|
|
1290
|
+
"items": items,
|
|
1291
|
+
"commands": generate_mail_commands(items, days),
|
|
1292
|
+
"bus_free": BUS_FREE_NOTE,
|
|
1293
|
+
}
|
|
1294
|
+
if name == "sud_cli_case_search_alerts":
|
|
1295
|
+
from . import court_alerts
|
|
1296
|
+
values = dict(args or {})
|
|
1297
|
+
days = int(values.pop("days", 7))
|
|
1298
|
+
if not 1 <= days <= 90:
|
|
1299
|
+
raise ValueError("days must be within 1..90")
|
|
1300
|
+
alerts_dir = values.pop("alerts_dir", None)
|
|
1301
|
+
if values:
|
|
1302
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
1303
|
+
if alerts_dir is None:
|
|
1304
|
+
candidates = court_alerts.alerts_search_dirs(_mcp_state_dir())
|
|
1305
|
+
alerts_dir = next((d for d in candidates if d.is_dir()), candidates[0])
|
|
1306
|
+
return court_alerts.alerts_payload(alerts_dir, date.today(), days)
|
|
1307
|
+
if name == "sud_cli_form_new":
|
|
1308
|
+
# the wave §4: read-тул — только чтение реестра + детерминированный скелет.
|
|
1309
|
+
from .db import get_case, get_connection, init_db
|
|
1310
|
+
from .forms import render_form
|
|
1311
|
+
values = dict(args or {})
|
|
1312
|
+
case_id = _pick(values, ("case_id", "id"), "case_id/id")
|
|
1313
|
+
kind = values.pop("kind", None)
|
|
1314
|
+
fmt = values.pop("format", "md")
|
|
1315
|
+
values.pop("case_id", None)
|
|
1316
|
+
values.pop("id", None)
|
|
1317
|
+
if values:
|
|
1318
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
1319
|
+
if case_id is None:
|
|
1320
|
+
raise ValueError("case_id is required")
|
|
1321
|
+
state_dir = _mcp_state_dir()
|
|
1322
|
+
init_db(state_dir)
|
|
1323
|
+
conn = get_connection(state_dir)
|
|
1324
|
+
try:
|
|
1325
|
+
case = get_case(conn, case_id)
|
|
1326
|
+
finally:
|
|
1327
|
+
conn.close()
|
|
1328
|
+
if case is None:
|
|
1329
|
+
raise ValueError(f"case not found: {case_id}")
|
|
1330
|
+
return render_form(kind, case, fmt=fmt)
|
|
1331
|
+
# ---------------- the wave §1..§5 ----------------
|
|
1332
|
+
# Каждый тул вызывает ТОТ ЖЕ движок, что и CLI-команда из TOOL_CLI_PATH:
|
|
1333
|
+
# вторая копия логики в MCP расходится с оригиналом при первой же правке.
|
|
1334
|
+
if name == "sud_cli_deadlines_chain":
|
|
1335
|
+
from .chain import build_chain
|
|
1336
|
+
values = dict(args or {})
|
|
1337
|
+
case_id = _pick(values, ("case_id", "id"), "case_id/id")
|
|
1338
|
+
instance = values.pop("instance", None)
|
|
1339
|
+
values.pop("case_id", None)
|
|
1340
|
+
values.pop("id", None)
|
|
1341
|
+
if values:
|
|
1342
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
1343
|
+
if case_id is None:
|
|
1344
|
+
raise ValueError("case_id is required")
|
|
1345
|
+
return build_chain(_mcp_state_dir(), case_id, instance_override=instance)
|
|
1346
|
+
if name == "sud_cli_case_rep_add":
|
|
1347
|
+
from .db import add_case_representative, get_connection, init_db
|
|
1348
|
+
values = dict(args or {})
|
|
1349
|
+
case_id = values.pop("case_id", None)
|
|
1350
|
+
side = values.pop("side", None)
|
|
1351
|
+
rep_name = values.pop("name", None)
|
|
1352
|
+
authority_note = values.pop("authority_note", None)
|
|
1353
|
+
power_docs = values.pop("power_docs", None)
|
|
1354
|
+
if values:
|
|
1355
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
1356
|
+
if not (case_id and side and rep_name):
|
|
1357
|
+
raise ValueError("case_id, side and name are required")
|
|
1358
|
+
state_dir = _mcp_state_dir()
|
|
1359
|
+
init_db(state_dir)
|
|
1360
|
+
conn = get_connection(state_dir)
|
|
1361
|
+
try:
|
|
1362
|
+
row = add_case_representative(conn, case_id, side=side, name=rep_name,
|
|
1363
|
+
authority_note=authority_note,
|
|
1364
|
+
power_docs=power_docs)
|
|
1365
|
+
finally:
|
|
1366
|
+
conn.close()
|
|
1367
|
+
if row is None:
|
|
1368
|
+
raise ValueError(f"case not found: {case_id}")
|
|
1369
|
+
from .db import REPRESENTATIVE_HINT
|
|
1370
|
+
return {**row, "hint": REPRESENTATIVE_HINT}
|
|
1371
|
+
if name == "sud_cli_case_rep_list":
|
|
1372
|
+
from .db import REPRESENTATIVE_HINT, get_connection, init_db
|
|
1373
|
+
from .db import list_case_representatives
|
|
1374
|
+
values = dict(args or {})
|
|
1375
|
+
case_id = _pick(values, ("case_id", "id"), "case_id/id")
|
|
1376
|
+
values.pop("case_id", None)
|
|
1377
|
+
values.pop("id", None)
|
|
1378
|
+
if values:
|
|
1379
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
1380
|
+
if case_id is None:
|
|
1381
|
+
raise ValueError("case_id is required")
|
|
1382
|
+
state_dir = _mcp_state_dir()
|
|
1383
|
+
init_db(state_dir)
|
|
1384
|
+
conn = get_connection(state_dir)
|
|
1385
|
+
try:
|
|
1386
|
+
rows = list_case_representatives(conn, case_id)
|
|
1387
|
+
finally:
|
|
1388
|
+
conn.close()
|
|
1389
|
+
return {"case_id": case_id, "items": rows, "hint": REPRESENTATIVE_HINT}
|
|
1390
|
+
if name == "sud_cli_calendar_conflicts":
|
|
1391
|
+
from .conflicts import conflicts_payload
|
|
1392
|
+
values = dict(args or {})
|
|
1393
|
+
days = int(values.pop("days", 30))
|
|
1394
|
+
if values:
|
|
1395
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
1396
|
+
if not 1 <= days <= 3650:
|
|
1397
|
+
raise ValueError("days must be within 1..3650")
|
|
1398
|
+
return conflicts_payload(_mcp_state_dir(), days=days)
|
|
1399
|
+
if name == "sud_cli_report_portfolio":
|
|
1400
|
+
from .reports import portfolio_payload
|
|
1401
|
+
values = dict(args or {})
|
|
1402
|
+
as_of = values.pop("as_of", None)
|
|
1403
|
+
days = int(values.pop("days", 30))
|
|
1404
|
+
if values:
|
|
1405
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
1406
|
+
if not 1 <= days <= 3650:
|
|
1407
|
+
raise ValueError("days must be within 1..3650")
|
|
1408
|
+
return portfolio_payload(_mcp_state_dir(), as_of=as_of, days=days)
|
|
1409
|
+
if name == "sud_cli_report_case":
|
|
1410
|
+
from .reports import case_payload
|
|
1411
|
+
values = dict(args or {})
|
|
1412
|
+
case_id = _pick(values, ("case_id", "id"), "case_id/id")
|
|
1413
|
+
values.pop("case_id", None)
|
|
1414
|
+
values.pop("id", None)
|
|
1415
|
+
if values:
|
|
1416
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
1417
|
+
if case_id is None:
|
|
1418
|
+
raise ValueError("case_id is required")
|
|
1419
|
+
payload = case_payload(_mcp_state_dir(), case_id)
|
|
1420
|
+
if payload is None:
|
|
1421
|
+
raise ValueError(f"case not found: {case_id}")
|
|
1422
|
+
return payload
|
|
1423
|
+
if name == "sud_cli_metrics":
|
|
1424
|
+
from .metrics import metrics_payload
|
|
1425
|
+
values = dict(args or {})
|
|
1426
|
+
near_days = int(values.pop("near_days", 30))
|
|
1427
|
+
if values:
|
|
1428
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
1429
|
+
if not 1 <= near_days <= 3650:
|
|
1430
|
+
raise ValueError("near_days must be within 1..3650")
|
|
1431
|
+
return metrics_payload(_mcp_state_dir(), near_days=near_days)
|
|
1432
|
+
if name == "sud_cli_backup_create":
|
|
1433
|
+
from pathlib import Path as _Path
|
|
1434
|
+
|
|
1435
|
+
from .backup import create_backup
|
|
1436
|
+
values = dict(args or {})
|
|
1437
|
+
out = values.pop("out", None)
|
|
1438
|
+
if values:
|
|
1439
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
1440
|
+
payload = create_backup(_mcp_state_dir(), out=_Path(out) if out else None)
|
|
1441
|
+
payload["disclaimer"] = (
|
|
1442
|
+
"Снимок содержит ПДн реестра: файл и sidecar записаны правами 0600."
|
|
1443
|
+
)
|
|
1444
|
+
return payload
|
|
1445
|
+
if name == "sud_cli_backup_verify":
|
|
1446
|
+
from .backup import verify_backup
|
|
1447
|
+
values = dict(args or {})
|
|
1448
|
+
file = values.pop("file", None)
|
|
1449
|
+
if values:
|
|
1450
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
1451
|
+
if not file:
|
|
1452
|
+
raise ValueError("file is required")
|
|
1453
|
+
payload = verify_backup(file)
|
|
1454
|
+
payload["ok"] = bool(payload["integrity_ok"] and payload["quick_ok"])
|
|
1455
|
+
return payload
|
|
1456
|
+
if name == "sud_cli_backup_restore":
|
|
1457
|
+
from .backup import restore_backup
|
|
1458
|
+
values = dict(args or {})
|
|
1459
|
+
file = values.pop("file", None)
|
|
1460
|
+
if values:
|
|
1461
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
1462
|
+
if not file:
|
|
1463
|
+
raise ValueError("file is required")
|
|
1464
|
+
# confirm уже снят гейтом _dispatch_gated: сюда доходит только
|
|
1465
|
+
# подтверждённый вызов, поэтому confirmed=True обоснован.
|
|
1466
|
+
return restore_backup(_mcp_state_dir(), file, confirmed=True)
|
|
1467
|
+
if name == "sud_cli_case_export":
|
|
1468
|
+
from .pdn import export_case, render_export_md
|
|
1469
|
+
values = dict(args or {})
|
|
1470
|
+
case_id = _pick(values, ("case_id", "id"), "case_id/id")
|
|
1471
|
+
fmt = values.pop("format", "json")
|
|
1472
|
+
values.pop("case_id", None)
|
|
1473
|
+
values.pop("id", None)
|
|
1474
|
+
if values:
|
|
1475
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
1476
|
+
if case_id is None:
|
|
1477
|
+
raise ValueError("case_id is required")
|
|
1478
|
+
payload = export_case(_mcp_state_dir(), case_id, fmt=fmt)
|
|
1479
|
+
if payload is None:
|
|
1480
|
+
raise ValueError(f"case not found: {case_id}")
|
|
1481
|
+
if fmt == "md":
|
|
1482
|
+
payload["markdown"] = render_export_md(payload)
|
|
1483
|
+
return payload
|
|
1484
|
+
if name == "sud_cli_case_purge":
|
|
1485
|
+
from .pdn import purge_apply
|
|
1486
|
+
values = dict(args or {})
|
|
1487
|
+
case_id = _pick(values, ("case_id", "id"), "case_id/id")
|
|
1488
|
+
remove_files = bool(values.pop("remove_files", True))
|
|
1489
|
+
values.pop("case_id", None)
|
|
1490
|
+
values.pop("id", None)
|
|
1491
|
+
if values:
|
|
1492
|
+
raise ValueError(f"unknown fields: {sorted(values)}")
|
|
1493
|
+
if case_id is None:
|
|
1494
|
+
raise ValueError("case_id is required")
|
|
1495
|
+
return purge_apply(_mcp_state_dir(), case_id, confirmed=True,
|
|
1496
|
+
actor="mcp", remove_files=remove_files)
|
|
1497
|
+
|
|
1498
|
+
|
|
1499
|
+
def _tool_schema(name: str) -> dict | None:
|
|
1500
|
+
for t in TOOLS:
|
|
1501
|
+
if t["name"] == name:
|
|
1502
|
+
return t["schema"]
|
|
1503
|
+
return None
|
|
1504
|
+
|
|
1505
|
+
|
|
1506
|
+
def _dispatch_gated(name, args) -> Any:
|
|
1507
|
+
"""W6a §1: права + идемпотентность поверх диспатча write-инструментов.
|
|
1508
|
+
|
|
1509
|
+
needs_confirmation — это ДАННЫЕ (status), не ошибка транспорта: запись не
|
|
1510
|
+
произведена, клиент получает явный сигнал повторить с confirm: true.
|
|
1511
|
+
Идемпотентный повтор внутри окна (10 мин) возвращает ПЕРВЫЙ результат с
|
|
1512
|
+
флагом idempotent_replay=true в теле — молча не дублирует и не ругается.
|
|
1513
|
+
|
|
1514
|
+
Fail-closed на неизвестном поле сохранён и до подтверждения: контракт
|
|
1515
|
+
additionalProperties=false и пин v2.4.0 (case_set отклоняет foo) не
|
|
1516
|
+
смещаются гейтом confirm — иначе write-тул становился терпимее read.
|
|
1517
|
+
"""
|
|
1518
|
+
if tool_permission(name) != "write":
|
|
1519
|
+
return _call(name, args)
|
|
1520
|
+
values = dict(args or {})
|
|
1521
|
+
confirm = values.pop("confirm", None)
|
|
1522
|
+
idem_key = values.pop("idempotency_key", None)
|
|
1523
|
+
schema = _tool_schema(name) or {}
|
|
1524
|
+
allowed = set((schema.get("properties") or {}).keys())
|
|
1525
|
+
unknown = set(values) - allowed
|
|
1526
|
+
if unknown:
|
|
1527
|
+
raise ValueError(f"unknown fields: {sorted(unknown)}")
|
|
1528
|
+
if confirm is not True:
|
|
1529
|
+
return {
|
|
1530
|
+
"status": "needs_confirmation",
|
|
1531
|
+
"permission": "write",
|
|
1532
|
+
"tool": name,
|
|
1533
|
+
"detail": "write-инструмент требует явного confirm: true — запись не произведена",
|
|
1534
|
+
}
|
|
1535
|
+
from . import idempotency
|
|
1536
|
+
state_dir = _mcp_state_dir()
|
|
1537
|
+
if idem_key is not None and str(idem_key):
|
|
1538
|
+
hit = idempotency.lookup(state_dir, name, str(idem_key))
|
|
1539
|
+
if hit is not None:
|
|
1540
|
+
replay = dict(hit)
|
|
1541
|
+
replay["idempotent_replay"] = True
|
|
1542
|
+
return replay
|
|
1543
|
+
value = _call(name, values)
|
|
1544
|
+
if idem_key is not None and str(idem_key) and isinstance(value, dict):
|
|
1545
|
+
idempotency.record(state_dir, name, str(idem_key), value)
|
|
1546
|
+
return value
|
|
1547
|
+
|
|
1548
|
+
|
|
1549
|
+
def handle_request(request):
|
|
1550
|
+
method = request.get("method")
|
|
1551
|
+
request_id = request.get("id")
|
|
1552
|
+
if method == "initialize":
|
|
1553
|
+
result = {"protocolVersion": "2024-11-05", "capabilities": {"tools": {}},
|
|
1554
|
+
"serverInfo": {"name": "sud-cli", "version": __version__}}
|
|
1555
|
+
elif method == "notifications/initialized":
|
|
1556
|
+
return None
|
|
1557
|
+
elif method == "tools/list":
|
|
1558
|
+
result = tool_list()
|
|
1559
|
+
elif method == "tools/call":
|
|
1560
|
+
try:
|
|
1561
|
+
params = request.get("params", {})
|
|
1562
|
+
# W6a §1: права (needs_confirmation) и идемпотентность write-тулов
|
|
1563
|
+
# применяются ДО движка — диспатч ниже знает только предметную логику.
|
|
1564
|
+
value = _dispatch_gated(params["name"], params.get("arguments", {}))
|
|
1565
|
+
if hasattr(value, "to_dict"): # v2.3.2 (sa-1 F-05): ApiOutcome -> JSON, не repr
|
|
1566
|
+
value = value.to_dict()
|
|
1567
|
+
result = {"content": [{"type": "text", "text": json.dumps(value, ensure_ascii=False, default=str)}], "isError": False}
|
|
1568
|
+
except (KeyError, TypeError, ValueError, OSError) as exc:
|
|
1569
|
+
result = {"content": [{"type": "text", "text": str(exc)}], "isError": True}
|
|
1570
|
+
except Exception as exc: # v2.3.2 (sa-1 F-06): доменные ошибки -> isError, stdio не умирает
|
|
1571
|
+
result = {"content": [{"type": "text", "text": f"unexpected: {type(exc).__name__}: {exc}"}], "isError": True}
|
|
1572
|
+
else:
|
|
1573
|
+
return {"jsonrpc": "2.0", "id": request_id,
|
|
1574
|
+
"error": {"code": -32601, "message": "Method not found"}}
|
|
1575
|
+
return {"jsonrpc": "2.0", "id": request_id, "result": result}
|
|
1576
|
+
|
|
1577
|
+
|
|
1578
|
+
def serve_stdio(input_stream=None, output_stream=None):
|
|
1579
|
+
input_stream = input_stream or sys.stdin
|
|
1580
|
+
output_stream = output_stream or sys.stdout
|
|
1581
|
+
for line in input_stream:
|
|
1582
|
+
if not line.strip():
|
|
1583
|
+
continue
|
|
1584
|
+
# v2.3.3 (review R1): не-JSON строка убивала serve_stdio целиком.
|
|
1585
|
+
try:
|
|
1586
|
+
frame = json.loads(line)
|
|
1587
|
+
except json.JSONDecodeError as exc:
|
|
1588
|
+
response = {"jsonrpc": "2.0", "id": None,
|
|
1589
|
+
"error": {"code": -32700, "message": f"Parse error: {exc}"}}
|
|
1590
|
+
else:
|
|
1591
|
+
response = handle_request(frame)
|
|
1592
|
+
if response is not None:
|
|
1593
|
+
output_stream.write(json.dumps(response, ensure_ascii=False) + "\n")
|
|
1594
|
+
output_stream.flush()
|