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/cli.py
ADDED
|
@@ -0,0 +1,3721 @@
|
|
|
1
|
+
"""Command-line interface for sud-cli."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import contextlib
|
|
5
|
+
import io
|
|
6
|
+
import sys
|
|
7
|
+
from decimal import Decimal, InvalidOperation
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from sud_cli import __version__
|
|
11
|
+
from sud_cli import _wm
|
|
12
|
+
from sud_cli.config import get_state_dir
|
|
13
|
+
from sud_cli.case_search_bridge import run_bridge_command
|
|
14
|
+
from sud_cli.forms import FORM_KINDS
|
|
15
|
+
from sud_cli.output import blocked, error, result
|
|
16
|
+
from sud_cli.providers import list_providers
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def claim_amount_arg(raw: str) -> Decimal:
|
|
20
|
+
"""argparse type for ``--claim-amount``: рубли, копейки допустимы (v2.3.1).
|
|
21
|
+
|
|
22
|
+
Цена иска — денежная величина, а не счётный объект: п. 1 ст. 333.19,
|
|
23
|
+
ст. 333.21 НК РФ, ст. 91 ГПК РФ, ст. 103 АПК РФ определяют её точной
|
|
24
|
+
суммой с копейками, и округление по п. 6 ст. 52 НК РФ применяется только
|
|
25
|
+
к итоговой пошлине. ``int`` здесь терял копейки на входе, а ``float``
|
|
26
|
+
вносил двоичную погрешность в деньги — поэтому ``Decimal(str)``.
|
|
27
|
+
|
|
28
|
+
Zero and negative values are deliberately not rejected here: the rules
|
|
29
|
+
engine already answers them with a typed status, and two different refusal
|
|
30
|
+
paths for one parameter would only invite them to drift apart.
|
|
31
|
+
"""
|
|
32
|
+
text = raw.strip()
|
|
33
|
+
try:
|
|
34
|
+
value = Decimal(text)
|
|
35
|
+
except InvalidOperation:
|
|
36
|
+
raise argparse.ArgumentTypeError(
|
|
37
|
+
f"invalid claim amount {raw!r}: ожидаются рубли, опционально с копейками "
|
|
38
|
+
"(например 3697453 или 3697453.16)"
|
|
39
|
+
) from None
|
|
40
|
+
if not value.is_finite():
|
|
41
|
+
# Decimal('NaN')/Decimal('Infinity') are valid Decimals and would reach
|
|
42
|
+
# the tariff comparisons as nonsense.
|
|
43
|
+
raise argparse.ArgumentTypeError(
|
|
44
|
+
f"invalid claim amount {raw!r}: цена иска должна быть конечным числом"
|
|
45
|
+
)
|
|
46
|
+
return value
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
50
|
+
parser = argparse.ArgumentParser(
|
|
51
|
+
prog="sud-cli",
|
|
52
|
+
description="Command-line legal workbench with an integrated case-research engine.",
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--version", action="version", version=f"%(prog)s {__version__}"
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--state-dir",
|
|
59
|
+
dest="state_dir",
|
|
60
|
+
type=Path,
|
|
61
|
+
default=None,
|
|
62
|
+
help="Application state directory (default: ~/.local/share/sud-cli).",
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--json",
|
|
66
|
+
dest="json_mode",
|
|
67
|
+
action="store_true",
|
|
68
|
+
help="Emit machine-readable JSON output on stdout.",
|
|
69
|
+
)
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
"--explain",
|
|
72
|
+
dest="explain",
|
|
73
|
+
action="store_true",
|
|
74
|
+
help=("Добавить в конверт расчётных команд блок explain: нормы, данные, "
|
|
75
|
+
"формулы, ручные причины (fees, deadlines, limitation, interest, "
|
|
76
|
+
"pretrial, jurisdiction, verify)."),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
80
|
+
|
|
81
|
+
# providers
|
|
82
|
+
p_providers = subparsers.add_parser(
|
|
83
|
+
"providers", help="List available/offline/live source providers."
|
|
84
|
+
)
|
|
85
|
+
p_providers.set_defaults(func=cmd_providers)
|
|
86
|
+
|
|
87
|
+
# doctor
|
|
88
|
+
p_doctor = subparsers.add_parser(
|
|
89
|
+
"doctor", help="Check sud-cli's own local state and database."
|
|
90
|
+
)
|
|
91
|
+
p_doctor.set_defaults(func=cmd_doctor)
|
|
92
|
+
|
|
93
|
+
p_calendar = subparsers.add_parser("calendar", help="Проверка производственного календаря.")
|
|
94
|
+
calendar_sub = p_calendar.add_subparsers(dest="calendar_command", required=True)
|
|
95
|
+
p_calendar_check = calendar_sub.add_parser("check", help="Проверить манифест переносов.")
|
|
96
|
+
p_calendar_check.set_defaults(func=cmd_calendar_check)
|
|
97
|
+
p_calendar_agenda = calendar_sub.add_parser("agenda", help="Единый календарь сроков и слушаний.")
|
|
98
|
+
p_calendar_agenda.add_argument("--days", type=int, default=14)
|
|
99
|
+
p_calendar_agenda.set_defaults(func=cmd_calendar_agenda)
|
|
100
|
+
# W6a §3 (S-28): RFC 5545 экспорт computed/confirmed сроков.
|
|
101
|
+
p_calendar_ics = calendar_sub.add_parser("ics", help="Экспорт сроков в ICS (RFC 5545, VCALENDAR).")
|
|
102
|
+
p_calendar_ics.add_argument("--days", type=int, default=60, help="Горизонт окна, дней (1..3650).")
|
|
103
|
+
p_calendar_ics.add_argument("--out", type=Path, default=None, dest="out",
|
|
104
|
+
help="Записать календарь в файл (UTF-8, CRLF).")
|
|
105
|
+
p_calendar_ics.set_defaults(func=cmd_calendar_ics)
|
|
106
|
+
|
|
107
|
+
# the wave §3 (S-27): конфликты расписания (слушания фикс. набора × сроки).
|
|
108
|
+
p_calendar_conf = calendar_sub.add_parser(
|
|
109
|
+
"conflicts", help="Пересечения слушаний и сроков по разным делам (справочно).")
|
|
110
|
+
p_calendar_conf.add_argument("--days", type=int, default=30,
|
|
111
|
+
help="Окно вперёд от сегодня, дней (1..3650).")
|
|
112
|
+
p_calendar_conf.set_defaults(func=cmd_calendar_conflicts)
|
|
113
|
+
|
|
114
|
+
# the wave §4 (S-84): отчёты портфеля и дела — только счётчики из таблиц.
|
|
115
|
+
p_report = subparsers.add_parser("report", help="Отчёты портфеля и дела (the wave).")
|
|
116
|
+
report_sub = p_report.add_subparsers(dest="report_command", required=True)
|
|
117
|
+
p_report_portfolio = report_sub.add_parser("portfolio", help="Сводка по всем делам.")
|
|
118
|
+
p_report_portfolio.add_argument("--as-of", "--as-of-date", dest="as_of", default=None,
|
|
119
|
+
help="Дата отсчёта YYYY-MM-DD (по умолчанию — сегодня).")
|
|
120
|
+
p_report_portfolio.add_argument("--days", type=int, default=30,
|
|
121
|
+
help="Горизонт near, дней (1..3650).")
|
|
122
|
+
p_report_portfolio.set_defaults(func=cmd_report_portfolio)
|
|
123
|
+
p_report_case = report_sub.add_parser("case", help="Печатная развёртка дела.")
|
|
124
|
+
p_report_case.add_argument("case_id")
|
|
125
|
+
p_report_case.set_defaults(func=cmd_report_case)
|
|
126
|
+
|
|
127
|
+
# история §5 (S-85): backup/verify/restore snapshot'а store.db (sqlite stdlib).
|
|
128
|
+
p_backup = subparsers.add_parser("backup", help="Снимки базы (S-85).")
|
|
129
|
+
backup_sub = p_backup.add_subparsers(dest="backup_command", required=True)
|
|
130
|
+
p_backup_create = backup_sub.add_parser("create", help="Снять snapshot store.db.")
|
|
131
|
+
p_backup_create.add_argument("--out", type=Path, default=None,
|
|
132
|
+
help="Куда положить снимок (по умолчанию — state-dir/backups).")
|
|
133
|
+
p_backup_create.set_defaults(func=cmd_backup_create)
|
|
134
|
+
p_backup_verify = backup_sub.add_parser("verify", help="integrity_check + quick_check + таблицы.")
|
|
135
|
+
p_backup_verify.add_argument("file", type=Path)
|
|
136
|
+
p_backup_verify.set_defaults(func=cmd_backup_verify)
|
|
137
|
+
p_backup_restore = backup_sub.add_parser("restore", help="Перезаписать store.db из снимка.")
|
|
138
|
+
p_backup_restore.add_argument("file", type=Path)
|
|
139
|
+
p_backup_restore.add_argument("--yes", action="store_true", default=False,
|
|
140
|
+
help="Подтверждение деструктивного действия.")
|
|
141
|
+
p_backup_restore.set_defaults(func=cmd_backup_restore)
|
|
142
|
+
|
|
143
|
+
# история §5 (S-86): метрики state-dir (секция есть и в doctor).
|
|
144
|
+
p_metrics = subparsers.add_parser("metrics", help="Счётчики state-dir без выдуманных KPI.")
|
|
145
|
+
p_metrics.set_defaults(func=cmd_metrics)
|
|
146
|
+
|
|
147
|
+
# ready — agent-facing gate for everything that touches a court host
|
|
148
|
+
p_ready = subparsers.add_parser(
|
|
149
|
+
"ready",
|
|
150
|
+
help=(
|
|
151
|
+
"Report whether live court commands can run (env, binary, license, "
|
|
152
|
+
"Camofox runtime, rules version) and print the calling conventions. "
|
|
153
|
+
"Exits non-zero when something is missing. Contacts no court host."
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
p_ready.set_defaults(func=cmd_ready)
|
|
157
|
+
|
|
158
|
+
# import
|
|
159
|
+
p_import = subparsers.add_parser(
|
|
160
|
+
"import", help="Import a local JSON fixture into the offline store."
|
|
161
|
+
)
|
|
162
|
+
p_import.add_argument("kind", choices=["court", "npa"], help="Record kind to import.")
|
|
163
|
+
p_import.add_argument("file", type=Path, help="Path to a local JSON file.")
|
|
164
|
+
p_import.set_defaults(func=cmd_import)
|
|
165
|
+
|
|
166
|
+
# search
|
|
167
|
+
p_search = subparsers.add_parser(
|
|
168
|
+
"search", help="Search imported offline records."
|
|
169
|
+
)
|
|
170
|
+
p_search.add_argument("kind", choices=["court", "npa"], help="Record kind to search.")
|
|
171
|
+
p_search.add_argument("query", nargs="?", default=None, help="Free-text query.")
|
|
172
|
+
p_search.add_argument("--limit", type=int, default=20, help="Maximum results (default: 20).")
|
|
173
|
+
p_search.add_argument("--since", type=str, default=None, help="Start date (YYYY-MM-DD).")
|
|
174
|
+
p_search.add_argument("--until", type=str, default=None, help="End date (YYYY-MM-DD).")
|
|
175
|
+
p_search.set_defaults(func=cmd_search)
|
|
176
|
+
|
|
177
|
+
# npa
|
|
178
|
+
p_npa = subparsers.add_parser(
|
|
179
|
+
"npa", help="NPA fixture search and edition comparison."
|
|
180
|
+
)
|
|
181
|
+
npa_sub = p_npa.add_subparsers(dest="npa_command", required=True)
|
|
182
|
+
npa_search = npa_sub.add_parser("search", help="Search NPA fixtures.")
|
|
183
|
+
npa_search.add_argument("query", nargs="?", default=None)
|
|
184
|
+
npa_search.add_argument("--limit", type=int, default=20)
|
|
185
|
+
npa_search.set_defaults(func=cmd_npa_search)
|
|
186
|
+
npa_compare = npa_sub.add_parser("compare", help="Compare two NPA fixture editions.")
|
|
187
|
+
npa_compare.add_argument("--id", dest="doc_id", required=True)
|
|
188
|
+
npa_compare.add_argument("--edition1", required=True)
|
|
189
|
+
npa_compare.add_argument("--edition2", required=True)
|
|
190
|
+
npa_compare.set_defaults(func=cmd_npa_compare)
|
|
191
|
+
|
|
192
|
+
# law — official publication metadata search
|
|
193
|
+
p_law = subparsers.add_parser(
|
|
194
|
+
"law", help="Official legal publication metadata search."
|
|
195
|
+
)
|
|
196
|
+
law_sub = p_law.add_subparsers(dest="law_command", required=True)
|
|
197
|
+
p_law_search = law_sub.add_parser(
|
|
198
|
+
"search", help="Search official publication metadata (one HTTPS GET)."
|
|
199
|
+
)
|
|
200
|
+
p_law_search.add_argument(
|
|
201
|
+
"query", nargs="?", default=None, help="Free-text metadata query."
|
|
202
|
+
)
|
|
203
|
+
p_law_search.add_argument(
|
|
204
|
+
"--number", type=str, default=None, help="Exact document number."
|
|
205
|
+
)
|
|
206
|
+
p_law_search.set_defaults(func=cmd_law_search)
|
|
207
|
+
|
|
208
|
+
# court — native offline case/event timeline
|
|
209
|
+
p_court = subparsers.add_parser(
|
|
210
|
+
"court", help="Native offline court case import and timeline."
|
|
211
|
+
)
|
|
212
|
+
court_sub = p_court.add_subparsers(dest="court_command", required=True)
|
|
213
|
+
|
|
214
|
+
p_court_import = court_sub.add_parser(
|
|
215
|
+
"import", help="Import a local synthetic court case/event fixture."
|
|
216
|
+
)
|
|
217
|
+
p_court_import.add_argument("file", type=Path, help="Path to a local JSON file.")
|
|
218
|
+
p_court_import.set_defaults(func=cmd_court_import)
|
|
219
|
+
|
|
220
|
+
p_court_search = court_sub.add_parser(
|
|
221
|
+
"search", help="Search imported native court cases and events."
|
|
222
|
+
)
|
|
223
|
+
p_court_search.add_argument("query", nargs="?", default=None, help="Free-text query.")
|
|
224
|
+
p_court_search.add_argument(
|
|
225
|
+
"--limit", type=int, default=20, help="Maximum results (default: 20)."
|
|
226
|
+
)
|
|
227
|
+
p_court_search.add_argument("--since", type=str, default=None, help="Start date (YYYY-MM-DD).")
|
|
228
|
+
p_court_search.add_argument("--until", type=str, default=None, help="End date (YYYY-MM-DD).")
|
|
229
|
+
p_court_search.set_defaults(func=cmd_court_search)
|
|
230
|
+
|
|
231
|
+
p_court_case = court_sub.add_parser(
|
|
232
|
+
"case", help="Show a normalized case summary and event timeline."
|
|
233
|
+
)
|
|
234
|
+
p_court_case.add_argument("case_id", help="Synthetic case identifier.")
|
|
235
|
+
p_court_case.set_defaults(func=cmd_court_case)
|
|
236
|
+
|
|
237
|
+
# case-search — safe transitional compatibility bridge
|
|
238
|
+
p_cw = subparsers.add_parser(
|
|
239
|
+
"case-search",
|
|
240
|
+
help="Integrated case research engine: import, lookup, case history, live sources.",
|
|
241
|
+
)
|
|
242
|
+
cw_sub = p_cw.add_subparsers(dest="cw_command", required=True)
|
|
243
|
+
|
|
244
|
+
p_cw_providers = cw_sub.add_parser(
|
|
245
|
+
"providers", help="Bridge to case-search provider status."
|
|
246
|
+
)
|
|
247
|
+
p_cw_providers.set_defaults(func=cmd_case_search_providers)
|
|
248
|
+
|
|
249
|
+
for cw_cmd in ("import", "sudrf-import", "kad-import", "vsrf-import", "auto-import"):
|
|
250
|
+
p_cw_import = cw_sub.add_parser(
|
|
251
|
+
cw_cmd,
|
|
252
|
+
help=f"Bridge to case-search {cw_cmd} (saved file -> JSON).",
|
|
253
|
+
)
|
|
254
|
+
p_cw_import.add_argument("file", type=Path, help="Path to a saved file under the sud-cli state directory.")
|
|
255
|
+
p_cw_import.add_argument("--case", required=True, dest="case_id", help="Synthetic case identifier.")
|
|
256
|
+
p_cw_import.set_defaults(func=cmd_case_search_import)
|
|
257
|
+
|
|
258
|
+
p_cw_extract = cw_sub.add_parser(
|
|
259
|
+
"extract-text", help="Bridge to case-search extract-text (saved file -> text)."
|
|
260
|
+
)
|
|
261
|
+
p_cw_extract.add_argument("file", type=Path, help="Path to a saved file under the sud-cli state directory.")
|
|
262
|
+
p_cw_extract.set_defaults(func=cmd_case_search_extract)
|
|
263
|
+
|
|
264
|
+
p_cw_parity = cw_sub.add_parser(
|
|
265
|
+
"parity", help="Show sud-cli vs. case-search capability parity matrix."
|
|
266
|
+
)
|
|
267
|
+
p_cw_parity.set_defaults(func=cmd_case_search_parity)
|
|
268
|
+
|
|
269
|
+
p_cw_diff = cw_sub.add_parser(
|
|
270
|
+
"case-diff",
|
|
271
|
+
help="Compare two saved case snapshots (offline, no court contact).",
|
|
272
|
+
)
|
|
273
|
+
p_cw_diff.add_argument("old", type=Path, help="Earlier snapshot JSON.")
|
|
274
|
+
p_cw_diff.add_argument("new", type=Path, help="Later snapshot JSON.")
|
|
275
|
+
p_cw_diff.set_defaults(func=cmd_case_diff)
|
|
276
|
+
|
|
277
|
+
# W6a §4 (S-60): локальные алерты -> bus-free a2a-строки (шина не вызывается).
|
|
278
|
+
p_cw_alerts = cw_sub.add_parser(
|
|
279
|
+
"alerts",
|
|
280
|
+
help="Локальные алерты case-search: готовые a2a-строки (bus-free, сеть не трогается).",
|
|
281
|
+
)
|
|
282
|
+
p_cw_alerts.add_argument("--days", type=int, default=7, help="Окно по created_at, дней (1..90)")
|
|
283
|
+
p_cw_alerts.add_argument("--alerts-dir", dest="alerts_dir", type=Path, default=None,
|
|
284
|
+
help="Каталог alert-артефактов (по умолчанию — локальные пути).")
|
|
285
|
+
p_cw_alerts.set_defaults(func=cmd_case_search_alerts)
|
|
286
|
+
|
|
287
|
+
# Live flag-driven commands (Phase 8). These reach external court sources
|
|
288
|
+
# and stay gated behind SUD_CLI_BRIDGE_LIVE inside the bridge.
|
|
289
|
+
p_cw_search = cw_sub.add_parser(
|
|
290
|
+
"search-practice",
|
|
291
|
+
help="Live sudact practice search (needs SUD_CLI_BRIDGE_LIVE=1).",
|
|
292
|
+
)
|
|
293
|
+
p_cw_search.add_argument("query", help="Search phrase.")
|
|
294
|
+
p_cw_search.add_argument("--section", choices=["arbitral", "regular", "magistrate", "vsrf"], default=None)
|
|
295
|
+
p_cw_search.add_argument("--page", default=None, help="Result page (1..1000).")
|
|
296
|
+
p_cw_search.add_argument("--limit", default=None, help="Max results (1..30).")
|
|
297
|
+
p_cw_search.add_argument("--via-camofox", action="store_true", dest="via_camofox",
|
|
298
|
+
help="Use the external Camofox runtime instead of the embedded browser.")
|
|
299
|
+
p_cw_search.add_argument("--no-camofox", action="store_true", dest="no_camofox",
|
|
300
|
+
help="Force the embedded browser even when Camofox is running.")
|
|
301
|
+
p_cw_search.set_defaults(func=cmd_case_search_search_practice, via_camofox=True,
|
|
302
|
+
no_camofox=False)
|
|
303
|
+
|
|
304
|
+
p_cw_kad = cw_sub.add_parser(
|
|
305
|
+
"kad-search",
|
|
306
|
+
help="Live KAD participant search (needs SUD_CLI_BRIDGE_LIVE=1).",
|
|
307
|
+
)
|
|
308
|
+
p_cw_kad.add_argument("--inn", default=None, help="Participant INN (10 or 12 digits).")
|
|
309
|
+
p_cw_kad.add_argument("--ogrn", default=None, help="Participant OGRN/OGRNIP (13 or 15 digits).")
|
|
310
|
+
p_cw_kad.add_argument("--name", default=None, help="Participant name.")
|
|
311
|
+
p_cw_kad.add_argument("--page", default=None, help="Result page (1..1000).")
|
|
312
|
+
p_cw_kad.set_defaults(func=cmd_case_search_kad_search)
|
|
313
|
+
|
|
314
|
+
p_cw_gettext = cw_sub.add_parser(
|
|
315
|
+
"get-text",
|
|
316
|
+
help="Live sudact.ru document text (needs SUD_CLI_BRIDGE_LIVE=1).",
|
|
317
|
+
)
|
|
318
|
+
p_cw_gettext.add_argument("url", help="sudact.ru document URL.")
|
|
319
|
+
p_cw_gettext.add_argument("--no-cache", action="store_true", dest="no_cache",
|
|
320
|
+
help="Force refetch, ignore the upstream SQLite cache.")
|
|
321
|
+
p_cw_gettext.add_argument("--blocks", action="store_true",
|
|
322
|
+
help="Return УСТАНОВИЛ/РЕШИЛ blocks separately.")
|
|
323
|
+
p_cw_gettext.set_defaults(func=cmd_case_search_get_text)
|
|
324
|
+
|
|
325
|
+
# Positional live commands (Phase 11). These four were already vetted in the
|
|
326
|
+
# bridge allowlist but had no CLI entry point, so the bridge could reach
|
|
327
|
+
# them while the user could not.
|
|
328
|
+
p_cw_lookup = cw_sub.add_parser(
|
|
329
|
+
"lookup",
|
|
330
|
+
help="Live case lookup by case id (needs SUD_CLI_BRIDGE_LIVE=1).",
|
|
331
|
+
)
|
|
332
|
+
p_cw_lookup.add_argument("case_id", help="Case id, e.g. А40-12345/2026 or А40-12345/25.")
|
|
333
|
+
p_cw_lookup.add_argument(
|
|
334
|
+
"--source",
|
|
335
|
+
choices=["kad", "mos-gorsud", "mos-gorsud-dom", "sudrf"],
|
|
336
|
+
default=None,
|
|
337
|
+
help=(
|
|
338
|
+
"Upstream data source. Without it case-search defaults to its "
|
|
339
|
+
"'synthetic' provider, which only knows fixture ids, so a real "
|
|
340
|
+
"case reference always fails — pass a real source."
|
|
341
|
+
),
|
|
342
|
+
)
|
|
343
|
+
p_cw_lookup.set_defaults(func=cmd_case_search_lookup)
|
|
344
|
+
|
|
345
|
+
p_cw_hist = cw_sub.add_parser(
|
|
346
|
+
"case-history",
|
|
347
|
+
help="Live case hearing history (needs SUD_CLI_BRIDGE_LIVE=1).",
|
|
348
|
+
)
|
|
349
|
+
p_cw_hist.add_argument("case_id", help="Case id, e.g. А40-12345/2026.")
|
|
350
|
+
p_cw_hist.set_defaults(func=cmd_case_search_case_history)
|
|
351
|
+
|
|
352
|
+
p_cw_sudrf = cw_sub.add_parser(
|
|
353
|
+
"sudrf-live",
|
|
354
|
+
help="Live ГАС «Правосудие» lookup (needs SUD_CLI_BRIDGE_LIVE=1).",
|
|
355
|
+
)
|
|
356
|
+
p_cw_sudrf.add_argument("case_id", help="Case id, e.g. А40-12345/2026.")
|
|
357
|
+
p_cw_sudrf.set_defaults(func=cmd_case_search_sudrf_live)
|
|
358
|
+
|
|
359
|
+
p_cw_mos = cw_sub.add_parser(
|
|
360
|
+
"mos-live",
|
|
361
|
+
help="Live mos.ru case lookup (needs SUD_CLI_BRIDGE_LIVE=1).",
|
|
362
|
+
)
|
|
363
|
+
p_cw_mos.add_argument("court_slug", help="mos.ru court slug.")
|
|
364
|
+
p_cw_mos.add_argument("case_id", help="Case id, e.g. А40-12345/2026.")
|
|
365
|
+
p_cw_mos.set_defaults(func=cmd_case_search_mos_live)
|
|
366
|
+
|
|
367
|
+
# First-class superapp aliases. These call the embedded Python API directly;
|
|
368
|
+
# the case-search namespace above remains the compatibility bridge.
|
|
369
|
+
p_practice = subparsers.add_parser("practice", help="Search judicial practice.")
|
|
370
|
+
p_practice.add_argument("query")
|
|
371
|
+
p_practice.add_argument("--section", choices=["arbitral", "regular", "magistrate", "vsrf"], default="arbitral")
|
|
372
|
+
p_practice.add_argument("--page", type=int, default=1)
|
|
373
|
+
p_practice.add_argument("--limit", type=int, default=10)
|
|
374
|
+
p_practice.set_defaults(func=cmd_native_practice)
|
|
375
|
+
|
|
376
|
+
p_kad = subparsers.add_parser("kad", help="Search KAD by participant.")
|
|
377
|
+
p_kad.add_argument("--inn")
|
|
378
|
+
p_kad.add_argument("--ogrn")
|
|
379
|
+
p_kad.add_argument("--name")
|
|
380
|
+
p_kad.add_argument("--page", type=int, default=1)
|
|
381
|
+
p_kad.set_defaults(func=cmd_native_kad)
|
|
382
|
+
|
|
383
|
+
p_history = subparsers.add_parser("case-history", help="Show case history.")
|
|
384
|
+
p_history.add_argument("case_id")
|
|
385
|
+
p_history.add_argument("--section", choices=["arbitral", "regular", "magistrate", "vsrf"], default="arbitral")
|
|
386
|
+
p_history.add_argument("--max-related", type=int, default=5)
|
|
387
|
+
p_history.add_argument("--limit", type=int, default=10)
|
|
388
|
+
p_history.set_defaults(func=cmd_native_case_history)
|
|
389
|
+
|
|
390
|
+
p_sudrf = subparsers.add_parser("sudrf-live", help="Look up a ГАС Правосудие case.")
|
|
391
|
+
p_sudrf.add_argument("case_id")
|
|
392
|
+
p_sudrf.add_argument("--host", default="oblsud.mo.sudrf.ru")
|
|
393
|
+
p_sudrf.set_defaults(func=cmd_native_sudrf)
|
|
394
|
+
|
|
395
|
+
p_mos = subparsers.add_parser("mos-live", help="Look up a Moscow court case.")
|
|
396
|
+
p_mos.add_argument("court_slug")
|
|
397
|
+
p_mos.add_argument("case_id")
|
|
398
|
+
p_mos.set_defaults(func=cmd_native_mos)
|
|
399
|
+
|
|
400
|
+
# v0.1.0 ЕФРСБ (bankrot.fedresurs.ru) — банкротные проверки (мандат владельца 20.10)
|
|
401
|
+
p_efrsb = subparsers.add_parser(
|
|
402
|
+
"efrsb", help="ЕФРСБ: банкротные проверки ЮЛ/ИП/физлиц, публикации дел, управляющие.")
|
|
403
|
+
efrsb_sub = p_efrsb.add_subparsers(dest="efrsb_cmd", required=True)
|
|
404
|
+
e_companies = efrsb_sub.add_parser("companies", help="Поиск ЮЛ/ИП в банкротстве.")
|
|
405
|
+
e_companies.add_argument("--inn")
|
|
406
|
+
e_companies.add_argument("--ogrn")
|
|
407
|
+
e_companies.add_argument("--name")
|
|
408
|
+
e_persons = efrsb_sub.add_parser("persons", help="Поиск физлиц в банкротстве.")
|
|
409
|
+
e_persons.add_argument("--name", required=True)
|
|
410
|
+
e_persons.add_argument("--birth-date", default=None)
|
|
411
|
+
e_pubs = efrsb_sub.add_parser("publications", help="Публикации по делу.")
|
|
412
|
+
e_pubs.add_argument("--case-id", required=True)
|
|
413
|
+
e_link = efrsb_sub.add_parser("link", help="План ручной проверки ЕФРСБ (человек открывает ссылку; акт 25.10).")
|
|
414
|
+
e_link.add_argument("--kind", required=True, choices=["ul", "ip", "fl"])
|
|
415
|
+
e_link.add_argument("--name")
|
|
416
|
+
e_link.add_argument("--inn")
|
|
417
|
+
e_link.add_argument("--ogrn")
|
|
418
|
+
e_link.add_argument("--birth-date", dest="birth_date")
|
|
419
|
+
e_link.set_defaults(func=cmd_efrsb)
|
|
420
|
+
e_rec = efrsb_sub.add_parser("record", help="Фиксация результата ручной проверки в досье дела (provenance).")
|
|
421
|
+
e_rec.add_argument("--case", required=True)
|
|
422
|
+
e_rec.add_argument("--subject", required=True)
|
|
423
|
+
e_rec.add_argument("--result", required=True, choices=["found", "none", "inconclusive"])
|
|
424
|
+
e_rec.add_argument("--checked-by", dest="checked_by", required=True)
|
|
425
|
+
e_rec.add_argument("--source-ref", dest="source_ref")
|
|
426
|
+
e_rec.add_argument("--notes")
|
|
427
|
+
e_rec.set_defaults(func=cmd_efrsb)
|
|
428
|
+
p_pl = subparsers.add_parser("plenum", help="Корпус постановлений Пленума/обзоров ВС и ВАС РФ (S-55).")
|
|
429
|
+
pl_sub = p_pl.add_subparsers(dest="plenum_cmd", required=True)
|
|
430
|
+
pl_list = pl_sub.add_parser("list", help="Список карточек корпуса.")
|
|
431
|
+
pl_list.set_defaults(func=cmd_plenum)
|
|
432
|
+
pl_show = pl_sub.add_parser("show", help="Карточка полностью.")
|
|
433
|
+
pl_show.add_argument("doc_id")
|
|
434
|
+
pl_show.set_defaults(func=cmd_plenum)
|
|
435
|
+
pl_search = pl_sub.add_parser("search", help="Поиск по тексту параграфов корпуса.")
|
|
436
|
+
pl_search.add_argument("text")
|
|
437
|
+
pl_search.set_defaults(func=cmd_plenum)
|
|
438
|
+
pl_ver = pl_sub.add_parser("verify", help="Пересчёт и проверка sha256 цитат.")
|
|
439
|
+
pl_ver.set_defaults(func=cmd_plenum)
|
|
440
|
+
pl_imp = pl_sub.add_parser("import", help="Импорт карточки из JSON-файла.")
|
|
441
|
+
pl_imp.add_argument("--file", required=True)
|
|
442
|
+
pl_imp.set_defaults(func=cmd_plenum)
|
|
443
|
+
e_managers = efrsb_sub.add_parser("managers", help="Арбитражные управляющие.")
|
|
444
|
+
e_managers.add_argument("--search")
|
|
445
|
+
e_managers.add_argument("--inn")
|
|
446
|
+
e_managers.add_argument("--sro")
|
|
447
|
+
for e_sub in (e_companies, e_persons, e_pubs, e_managers):
|
|
448
|
+
e_sub.add_argument("--limit", type=int, default=5)
|
|
449
|
+
e_sub.set_defaults(func=cmd_efrsb)
|
|
450
|
+
|
|
451
|
+
# v2.14.0 ЕГРЮЛ/ЕГРИП открытая проверка контрагента (мандат владельца 24-25.10)
|
|
452
|
+
p_egrul = subparsers.add_parser(
|
|
453
|
+
"egrul", help="ЕГРЮЛ/ЕГРИП: открытая проверка контрагента (egrul.nalog.ru, ФНС).")
|
|
454
|
+
p_egrul.add_argument("--query", required=True,
|
|
455
|
+
help="ИНН, ОГРН или наименование.")
|
|
456
|
+
p_egrul.add_argument("--name-eq", action="store_true",
|
|
457
|
+
help="точное соответствие наименования.")
|
|
458
|
+
p_egrul.set_defaults(func=cmd_egrul)
|
|
459
|
+
|
|
460
|
+
# v2.15.0 S-61 ФССП по юридической проверке 25.10: (Б) ручной контур + (В) официальная статистика
|
|
461
|
+
p_fssp = subparsers.add_parser(
|
|
462
|
+
"fssp", help="ФССП БДИП: ручная проверка (human-in-the-loop) + официальная статистика.")
|
|
463
|
+
fssp_sub = p_fssp.add_subparsers(dest="fssp_cmd", required=True)
|
|
464
|
+
p_f_check = fssp_sub.add_parser(
|
|
465
|
+
"check", help="План ручной проверки: ссылка+параметры+чек-лист (капчу проходит человек).")
|
|
466
|
+
p_f_check.add_argument("--kind", required=True, choices=["fiz", "ur"])
|
|
467
|
+
p_f_check.add_argument("--name")
|
|
468
|
+
p_f_check.add_argument("--inn")
|
|
469
|
+
p_f_check.add_argument("--ogrn")
|
|
470
|
+
p_f_check.add_argument("--birth-date", dest="birth_date")
|
|
471
|
+
p_f_check.add_argument("--region")
|
|
472
|
+
p_f_check.set_defaults(func=cmd_fssp_check)
|
|
473
|
+
p_f_rec = fssp_sub.add_parser(
|
|
474
|
+
"record", help="Внести результат ручной проверки в реестр доказательств дела.")
|
|
475
|
+
p_f_rec.add_argument("--case", required=True)
|
|
476
|
+
p_f_rec.add_argument("--subject", required=True)
|
|
477
|
+
p_f_rec.add_argument("--result", required=True, choices=["none", "found", "inconclusive"])
|
|
478
|
+
p_f_rec.add_argument("--checked-by", dest="checked_by", required=True)
|
|
479
|
+
p_f_rec.add_argument("--notes")
|
|
480
|
+
p_f_rec.add_argument("--file", help="скриншот/PDF результата (путь к файлу-доказательству)")
|
|
481
|
+
p_f_rec.set_defaults(func=cmd_fssp_record)
|
|
482
|
+
p_f_st = fssp_sub.add_parser("stats", help="Официальная статистика БДИП (агрегатная, по регионам).")
|
|
483
|
+
p_f_st.add_argument("--region")
|
|
484
|
+
p_f_st.set_defaults(func=cmd_fssp_stats)
|
|
485
|
+
p_f_as = fssp_sub.add_parser(
|
|
486
|
+
"assist", help="Полу-автомат: форма заполняется скриптом через локальный браузер, капчу решает человек (скриншот->код).")
|
|
487
|
+
p_f_as.add_argument("--kind", choices=["fiz", "ur"])
|
|
488
|
+
p_f_as.add_argument("--name")
|
|
489
|
+
p_f_as.add_argument("--inn")
|
|
490
|
+
p_f_as.add_argument("--birth-date", dest="birth_date")
|
|
491
|
+
p_f_as.add_argument("--resume", help="код сессии из предыдущего вызова")
|
|
492
|
+
p_f_as.add_argument("--code", help="ответ капчи, который прочитал человек")
|
|
493
|
+
p_f_as.set_defaults(func=cmd_fssp_assist)
|
|
494
|
+
p_lic = subparsers.add_parser("license", help="Лицензия AGPL, источник сборки и флаги сборки (§13).")
|
|
495
|
+
p_lic.add_argument("--with-build-flags", dest="with_build_flags", action="store_true", default=True)
|
|
496
|
+
p_lic.set_defaults(func=cmd_license)
|
|
497
|
+
|
|
498
|
+
p_text = subparsers.add_parser("extract-text", help="Extract act text.")
|
|
499
|
+
p_text.add_argument("file", type=Path)
|
|
500
|
+
p_text.set_defaults(func=cmd_native_text)
|
|
501
|
+
|
|
502
|
+
p_card = subparsers.add_parser("import-card", help="Import a saved case card.")
|
|
503
|
+
p_card.add_argument("file", type=Path)
|
|
504
|
+
p_card.add_argument("--case", required=True, dest="case_id")
|
|
505
|
+
p_card.set_defaults(func=cmd_native_import_card)
|
|
506
|
+
|
|
507
|
+
# fees — state fee calculation (approved spec v1.0.0, ФЗ-259 от 08.08.2024)
|
|
508
|
+
p_fees = subparsers.add_parser(
|
|
509
|
+
"fees",
|
|
510
|
+
help="Расчёт госпошлины (справка, ст. 333.19/333.21 НК РФ, ред. 259-ФЗ).",
|
|
511
|
+
)
|
|
512
|
+
p_fees.add_argument(
|
|
513
|
+
"--jurisdiction",
|
|
514
|
+
required=True,
|
|
515
|
+
choices=["arbitration", "general_jurisdiction", "kas"],
|
|
516
|
+
help=(
|
|
517
|
+
"AS — арбитражные суды (АПК РФ); SOYU — суды общей юрисдикции (ГПК РФ); "
|
|
518
|
+
"KAS — административное судопроизводство (КАС РФ, тарифы ст. 333.19 НК РФ)"
|
|
519
|
+
),
|
|
520
|
+
)
|
|
521
|
+
p_fees.add_argument(
|
|
522
|
+
"--kind",
|
|
523
|
+
required=True,
|
|
524
|
+
choices=[
|
|
525
|
+
"property_claim",
|
|
526
|
+
"court_order",
|
|
527
|
+
"non_property_claim",
|
|
528
|
+
"appeal",
|
|
529
|
+
"cassation_first",
|
|
530
|
+
"cassation_supreme",
|
|
531
|
+
"bankruptcy_petition",
|
|
532
|
+
"bankruptcy_creditor_claim",
|
|
533
|
+
],
|
|
534
|
+
help="Вид требования/обжалования",
|
|
535
|
+
)
|
|
536
|
+
p_fees.add_argument(
|
|
537
|
+
"--claim-amount",
|
|
538
|
+
type=claim_amount_arg,
|
|
539
|
+
default=None,
|
|
540
|
+
help=(
|
|
541
|
+
"Цена иска в рублях, копейки допускаются и не отбрасываются "
|
|
542
|
+
"(3697453.16); нужна для имущественных исков и судебного приказа"
|
|
543
|
+
),
|
|
544
|
+
)
|
|
545
|
+
p_fees.add_argument(
|
|
546
|
+
"--applicant",
|
|
547
|
+
choices=["individual", "entity"],
|
|
548
|
+
default=None,
|
|
549
|
+
help="Заявитель: физлицо или организация (нужен для фиксированных ставок)",
|
|
550
|
+
)
|
|
551
|
+
p_fees.add_argument(
|
|
552
|
+
"--claim-class",
|
|
553
|
+
default=None,
|
|
554
|
+
help="Класс дела; bankruptcy/criminal/administrative_offence отклоняются",
|
|
555
|
+
)
|
|
556
|
+
p_fees.add_argument(
|
|
557
|
+
"--as-of",
|
|
558
|
+
dest="as_of",
|
|
559
|
+
default=None,
|
|
560
|
+
metavar="YYYY-MM-DD",
|
|
561
|
+
help=(
|
|
562
|
+
"Тариф на дату (ст. 333.19/333.21 НК РФ): 2015-01-01..2024-09-08 — "
|
|
563
|
+
"редакция 221-ФЗ, с 2024-09-09 — действующая редакция 259-ФЗ. Без флага "
|
|
564
|
+
"считается по действующему тарифу; дата раньше 2015-01-01 считается по "
|
|
565
|
+
"действующему тарифу с предупреждением (устаревший тариф не додумывается)"
|
|
566
|
+
),
|
|
567
|
+
)
|
|
568
|
+
p_fees.set_defaults(func=cmd_fees)
|
|
569
|
+
|
|
570
|
+
# deadlines — procedural deadline calculation (approved spec v1.0.0)
|
|
571
|
+
p_deadlines = subparsers.add_parser(
|
|
572
|
+
"deadlines",
|
|
573
|
+
help="Расчёт процессуального срока (АПК РФ / ГПК РФ).",
|
|
574
|
+
)
|
|
575
|
+
deadline_sub = p_deadlines.add_subparsers(dest="deadlines_command")
|
|
576
|
+
p_deadlines.add_argument(
|
|
577
|
+
"deadline_key",
|
|
578
|
+
nargs="?",
|
|
579
|
+
default=None,
|
|
580
|
+
help=(
|
|
581
|
+
"Ключ срока (legacy). АПК: as_appeal_decision, as_appeal_simplified, as_appeal_ruling, "
|
|
582
|
+
"as_cassation_district, as_court_order_cancel, as_court_order_cassation. "
|
|
583
|
+
"ГПК: soyu_appeal_decision, soyu_appeal_ruling, soyu_cassation, "
|
|
584
|
+
"soyu_court_order_cancel, soyu_court_order_cassation. "
|
|
585
|
+
"КАС: kas_appeal_decision, kas_cassation, kas_court_order_cancel"
|
|
586
|
+
),
|
|
587
|
+
)
|
|
588
|
+
p_deadlines.add_argument(
|
|
589
|
+
"--event-date",
|
|
590
|
+
default=None,
|
|
591
|
+
help="Дата события (Day 0) в формате ГГГГ-ММ-ДД",
|
|
592
|
+
)
|
|
593
|
+
p_deadlines.add_argument("--region", default=None, help="Код субъекта РФ, например 16.")
|
|
594
|
+
p_deadlines.add_argument("--tz", "--timezone", dest="tz", default="Europe/Moscow", help="IANA timezone суда.")
|
|
595
|
+
p_deadlines.add_argument("--case", "--case-id", dest="case_id", default=None, help="Привязать расчёт к делу.")
|
|
596
|
+
p_deadlines.add_argument("--confirm", action="store_true", help="Сразу подтвердить срок (anti-laches).")
|
|
597
|
+
p_deadlines.add_argument("--by", default=None, help="Актор подтверждения/anchor.")
|
|
598
|
+
p_deadlines.add_argument("--evidence", default=None, help="Доказательство (вручение и т.п.).")
|
|
599
|
+
p_deadlines.add_argument("--norm", default=None, help="Норма-ссылка для waiver.")
|
|
600
|
+
p_deadlines.add_argument("--waive", action="store_true", help="Зафиксировать waiver (требует --norm).")
|
|
601
|
+
p_deadlines.set_defaults(func=cmd_deadlines)
|
|
602
|
+
|
|
603
|
+
p_dc = deadline_sub.add_parser("calculate", aliases=["calc"], help="Рассчитать срок и (опц.) привязать к делу.")
|
|
604
|
+
p_dc.add_argument("calc_case_id", nargs="?", default=None, help="ID дела (опционально).")
|
|
605
|
+
p_dc.add_argument("calc_deadline_key", help="Ключ срока.")
|
|
606
|
+
p_dc.add_argument("--event-date", dest="event_date", default=None)
|
|
607
|
+
p_dc.add_argument("--region", default=None)
|
|
608
|
+
p_dc.add_argument("--tz", "--timezone", dest="tz", default="Europe/Moscow")
|
|
609
|
+
p_dc.add_argument("--case", "--case-id", dest="case_id", default=None)
|
|
610
|
+
p_dc.add_argument("--confirm", action="store_true")
|
|
611
|
+
p_dc.add_argument("--by", default=None)
|
|
612
|
+
p_dc.add_argument("--evidence", default=None)
|
|
613
|
+
p_dc.set_defaults(func=cmd_deadlines_calculate)
|
|
614
|
+
|
|
615
|
+
p_dconf = deadline_sub.add_parser("confirm", help="Подтвердить срок.")
|
|
616
|
+
p_dconf.add_argument("case_id")
|
|
617
|
+
p_dconf.add_argument("--key", required=True, dest="key")
|
|
618
|
+
p_dconf.add_argument("--by", required=True)
|
|
619
|
+
p_dconf.add_argument("--evidence", default=None)
|
|
620
|
+
p_dconf.add_argument("--waive", action="store_true")
|
|
621
|
+
p_dconf.add_argument("--norm", default=None)
|
|
622
|
+
p_dconf.set_defaults(func=cmd_deadlines_confirm)
|
|
623
|
+
|
|
624
|
+
p_danchor = deadline_sub.add_parser("anchor", help="Зафиксировать начало срока с доказательством.")
|
|
625
|
+
p_danchor.add_argument("case_id")
|
|
626
|
+
p_danchor.add_argument("--key", required=True, dest="key")
|
|
627
|
+
p_danchor.add_argument("--event-date", dest="event_date", required=True)
|
|
628
|
+
p_danchor.add_argument("--method", required=True, choices=["вручение", "публикация", "почта", "иное"])
|
|
629
|
+
p_danchor.add_argument("--evidence", required=True)
|
|
630
|
+
p_danchor.add_argument("--by", default=None)
|
|
631
|
+
p_danchor.set_defaults(func=cmd_deadlines_anchor)
|
|
632
|
+
|
|
633
|
+
p_dwaive = deadline_sub.add_parser("waive", help="Waiver срока с нормой-ссылкой.")
|
|
634
|
+
p_dwaive.add_argument("case_id")
|
|
635
|
+
p_dwaive.add_argument("--key", required=True, dest="key")
|
|
636
|
+
p_dwaive.add_argument("--by", required=True)
|
|
637
|
+
p_dwaive.add_argument("--norm", required=True)
|
|
638
|
+
p_dwaive.add_argument("--evidence", default=None)
|
|
639
|
+
p_dwaive.set_defaults(func=cmd_deadlines_waive)
|
|
640
|
+
|
|
641
|
+
p_daudit = deadline_sub.add_parser("audit", help="Журнал правок сроков.")
|
|
642
|
+
p_daudit.add_argument("case_id")
|
|
643
|
+
p_daudit.set_defaults(func=cmd_deadlines_audit)
|
|
644
|
+
|
|
645
|
+
p_dlist = deadline_sub.add_parser("list", help="Список состояний сроков.")
|
|
646
|
+
p_dlist.add_argument("case_id", nargs="?", default=None)
|
|
647
|
+
p_dlist.add_argument("--case", "--case-id", dest="case_id", default=None)
|
|
648
|
+
p_dlist.set_defaults(func=cmd_deadlines_list)
|
|
649
|
+
|
|
650
|
+
p_dmatrix = deadline_sub.add_parser("matrix", help="Матрица контрольных точек дела.")
|
|
651
|
+
p_dmatrix.add_argument("case_id", nargs="?", default=None)
|
|
652
|
+
p_dmatrix.add_argument("--case", "--case-id", dest="case_id", default=None)
|
|
653
|
+
p_dmatrix.add_argument("--as-of", dest="as_of", default=None, help="Дата as-of (не влияет на расчёт).")
|
|
654
|
+
p_dmatrix.add_argument("--instance", choices=["apk", "gpk", "kas"], default=None, help="Override инстанции дела.")
|
|
655
|
+
p_dmatrix.add_argument("--missing", action="store_true", help="Только точки с needs_anchor.")
|
|
656
|
+
p_dmatrix.set_defaults(func=cmd_deadlines_matrix)
|
|
657
|
+
|
|
658
|
+
# W6a §5 (S-23): автозаполнение точек из текстов актов — computed+needs_confirm, никогда confirmed.
|
|
659
|
+
p_dauto = deadline_sub.add_parser("autofill", help="Автозаполнение точек срока из текстов актов (маркеры).")
|
|
660
|
+
p_dauto.add_argument("case_id")
|
|
661
|
+
p_dauto.add_argument("--event", dest="event_id", default=None, help="Обработать только этот event_id.")
|
|
662
|
+
p_dauto.set_defaults(func=cmd_deadlines_autofill)
|
|
663
|
+
|
|
664
|
+
# the wave §1 (S-12): цепочка инстанций поверх тех же матричных ключей.
|
|
665
|
+
p_dchain = deadline_sub.add_parser(
|
|
666
|
+
"chain", help="Цепочка инстанций дела по ключам VERIFIED матрицы (справочно).")
|
|
667
|
+
p_dchain.add_argument("case_id", nargs="?", default=None)
|
|
668
|
+
p_dchain.add_argument("--case", "--case-id", dest="case_id", default=None)
|
|
669
|
+
p_dchain.add_argument("--instance", choices=["apk", "gpk", "kas"], default=None,
|
|
670
|
+
help="Override инстанции дела (без него — поле instance, "
|
|
671
|
+
"без поля — все юрисдикции матрицы).")
|
|
672
|
+
p_dchain.set_defaults(func=cmd_deadlines_chain)
|
|
673
|
+
|
|
674
|
+
p_interest = subparsers.add_parser("interest", help="Проценты по ст. 395 или 317.1 ГК РФ.")
|
|
675
|
+
p_interest.add_argument("--amount", "--principal", dest="amount", type=int, required=True)
|
|
676
|
+
p_interest.add_argument("--from", "--start", dest="start_date", required=True)
|
|
677
|
+
p_interest.add_argument("--to", "--end", dest="end_date", required=True)
|
|
678
|
+
p_interest.add_argument("--article", choices=["395", "317.1"], default="395")
|
|
679
|
+
p_interest.add_argument("--calc-fee", action="store_true")
|
|
680
|
+
p_interest.add_argument("--jurisdiction", choices=["arbitration", "general_jurisdiction", "kas"], default="arbitration")
|
|
681
|
+
p_interest.add_argument("--kind", default="property_claim")
|
|
682
|
+
p_interest.add_argument("--applicant", choices=["individual", "entity"], default="entity")
|
|
683
|
+
p_interest.add_argument("--payments", default=None, help="Частичные оплаты: YYYY-MM-DD:сумма,... или JSON-список")
|
|
684
|
+
p_interest.set_defaults(func=cmd_interest)
|
|
685
|
+
|
|
686
|
+
p_payment = subparsers.add_parser("payment", help="Реквизиты и назначение платежа.")
|
|
687
|
+
p_payment.add_argument("--jurisdiction", choices=["arbitration", "general_jurisdiction", "kas", "soyu", "eks"], required=True)
|
|
688
|
+
p_payment.add_argument("--amount", type=int, required=True)
|
|
689
|
+
p_payment.add_argument("--court", required=True)
|
|
690
|
+
p_payment.add_argument("--defendant", required=True)
|
|
691
|
+
p_payment.set_defaults(func=cmd_payment)
|
|
692
|
+
|
|
693
|
+
p_deferral = subparsers.add_parser("deferral", help="Ходатайство об отсрочке госпошлины.")
|
|
694
|
+
p_deferral.add_argument("--applicant", "--claimant", dest="applicant", required=True)
|
|
695
|
+
p_deferral.add_argument("--court", required=True)
|
|
696
|
+
p_deferral.add_argument("--amount", type=int, required=True)
|
|
697
|
+
p_deferral.add_argument("--reason", "--grounds", dest="reason", required=True)
|
|
698
|
+
p_deferral.set_defaults(func=cmd_deferral)
|
|
699
|
+
|
|
700
|
+
p_feeex = subparsers.add_parser("fee-exemption", help="Льгота по госпошлине (VERIFIED-выборка категорий ст.333.35-37).")
|
|
701
|
+
p_feeex.add_argument("--jurisdiction", required=True, choices=["apk", "gpk", "kas", "arbitration", "general_jurisdiction"])
|
|
702
|
+
p_feeex.add_argument("--category", required=True)
|
|
703
|
+
p_feeex.set_defaults(func=cmd_fee_exemption)
|
|
704
|
+
|
|
705
|
+
p_feeref = subparsers.add_parser("fee-refund", help="Возврат госпошлины по основаниям ст.333.40 НК РФ (VERIFIED).")
|
|
706
|
+
p_feeref.add_argument("--paid", required=True, help="Уплаченная сумма, Decimal")
|
|
707
|
+
p_feeref.add_argument("--ground", required=True)
|
|
708
|
+
p_feeref.add_argument("--claim-admitted", action="store_true")
|
|
709
|
+
p_feeref.add_argument("--plaintiff-default", action="store_true")
|
|
710
|
+
p_feeref.add_argument("--transferred-to-arbitration", action="store_true")
|
|
711
|
+
p_feeref.set_defaults(func=cmd_fee_refund)
|
|
712
|
+
|
|
713
|
+
p_feeidx = subparsers.add_parser("fee-indexation", help="Рамка/расчёт индексации 183 АПК / 208 ГПК / 189.1 КАС.")
|
|
714
|
+
p_feeidx.add_argument("--code", required=True, choices=["apk", "gpk", "kas"])
|
|
715
|
+
p_feeidx.add_argument("--amount")
|
|
716
|
+
p_feeidx.add_argument("--cpi-start")
|
|
717
|
+
p_feeidx.add_argument("--cpi-end")
|
|
718
|
+
p_feeidx.set_defaults(func=cmd_fee_indexation)
|
|
719
|
+
|
|
720
|
+
p_limitation = subparsers.add_parser("limitation", aliases=["laches"], help="Расчёт срока исковой давности.")
|
|
721
|
+
p_limitation.add_argument("--start", required=True)
|
|
722
|
+
p_limitation.add_argument("--type", dest="period_type", default="general",
|
|
723
|
+
choices=["general", "contract_work", "carriage", "voidable_deal", "custom_years", "custom_months"])
|
|
724
|
+
p_limitation.add_argument("--years", type=int, default=0)
|
|
725
|
+
p_limitation.add_argument("--months", type=int, default=0)
|
|
726
|
+
p_limitation.add_argument("--claim-notice-days", type=int, default=0)
|
|
727
|
+
p_limitation.add_argument("--check-date", default=None)
|
|
728
|
+
p_limitation.set_defaults(func=cmd_limitation)
|
|
729
|
+
|
|
730
|
+
p_audit = subparsers.add_parser("timeline-audit", help="Проверить процессуальную хронологию дела.")
|
|
731
|
+
p_audit.add_argument("--events", required=True, help="JSON-файл или сырой JSON-массив событий.")
|
|
732
|
+
p_audit.set_defaults(func=cmd_timeline_audit)
|
|
733
|
+
|
|
734
|
+
p_asof = subparsers.add_parser("npa-as-of", help="Получить редакцию нормы на дату события.")
|
|
735
|
+
p_asof.add_argument("--act", required=True)
|
|
736
|
+
p_asof.add_argument("--article", required=True)
|
|
737
|
+
p_asof.add_argument("--date", required=True)
|
|
738
|
+
p_asof.set_defaults(func=cmd_npa_as_of)
|
|
739
|
+
|
|
740
|
+
# verify — правило «тексты только из реестра»: валидация цитат (v2.9.0, S-52)
|
|
741
|
+
p_verify = subparsers.add_parser(
|
|
742
|
+
"verify", help="Verify a quoted fragment against an explicit local source."
|
|
743
|
+
)
|
|
744
|
+
verify_sub = p_verify.add_subparsers(dest="verify_command", required=True)
|
|
745
|
+
p_vq = verify_sub.add_parser("quote", help="Check a quotation (npa bundle | case events | text file).")
|
|
746
|
+
p_vq.add_argument("--text", required=True)
|
|
747
|
+
p_vq.add_argument("--source", required=True, choices=["npa", "case", "file"])
|
|
748
|
+
p_vq.add_argument("--act")
|
|
749
|
+
p_vq.add_argument("--article")
|
|
750
|
+
p_vq.add_argument("--date")
|
|
751
|
+
p_vq.add_argument("--case-id", dest="case_id")
|
|
752
|
+
p_vq.add_argument("--path")
|
|
753
|
+
p_vq.add_argument("--max-fuzzy", dest="max_fuzzy", type=float, default=0.0)
|
|
754
|
+
p_vq.set_defaults(func=cmd_verify_quote)
|
|
755
|
+
|
|
756
|
+
# practice-status — offline fate probe (v2.9.0, S-54). The user-facing form
|
|
757
|
+
# is `practice status <n>`: main() rewrites it here because the live
|
|
758
|
+
# `practice <query>` parser must stay flat for the MCP drift contract.
|
|
759
|
+
p_ps = subparsers.add_parser(
|
|
760
|
+
"practice-status", help="Fate probe: possibly_overturned|unknown из ЛОКАЛЬНЫХ артефактов."
|
|
761
|
+
)
|
|
762
|
+
p_ps.add_argument("case_number")
|
|
763
|
+
p_ps.add_argument("--related", default=None, help="Fixture-режим: заголовки related-инстансов через ';'.")
|
|
764
|
+
p_ps.add_argument("--store", type=Path, default=None, help="Каталог snapshot/diff артефактов case-search.")
|
|
765
|
+
p_ps.set_defaults(func=cmd_practice_status)
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
p_mcp = subparsers.add_parser("serve-mcp", help="Запустить локальный MCP JSON-RPC stdio сервер.")
|
|
769
|
+
p_mcp.set_defaults(func=cmd_serve_mcp)
|
|
770
|
+
|
|
771
|
+
# case — central case registry (v2.4.0)
|
|
772
|
+
p_case = subparsers.add_parser("case", help="Реестр дел.")
|
|
773
|
+
case_sub = p_case.add_subparsers(dest="case_command", required=True)
|
|
774
|
+
|
|
775
|
+
p_case_new = case_sub.add_parser("new", help="Создать дело.")
|
|
776
|
+
p_case_new.add_argument("--number", required=True, dest="number")
|
|
777
|
+
p_case_new.add_argument("--court", required=True)
|
|
778
|
+
p_case_new.add_argument("--plaintiff", required=True)
|
|
779
|
+
p_case_new.add_argument("--defendant", required=True)
|
|
780
|
+
p_case_new.add_argument("--owner", default=None)
|
|
781
|
+
p_case_new.add_argument("--status", default=None)
|
|
782
|
+
# the wave §1: instance дела (apk|gpk|kas) — поле, из которого матрица и
|
|
783
|
+
# цепочка берут юрисдикцию; без него — unknown.
|
|
784
|
+
p_case_new.add_argument("--instance", default=None, choices=["apk", "gpk", "kas"],
|
|
785
|
+
help="Инстанция дела: apk (арбитраж), gpk (ОЮ), kas.")
|
|
786
|
+
p_case_new.set_defaults(func=cmd_case_new)
|
|
787
|
+
|
|
788
|
+
p_case_set = case_sub.add_parser("set", help="Обновить дело.")
|
|
789
|
+
p_case_set.add_argument("case_id")
|
|
790
|
+
p_case_set.add_argument("--status", default=None)
|
|
791
|
+
p_case_set.add_argument("--owner", default=None)
|
|
792
|
+
p_case_set.add_argument("--closed-at", dest="closed_at", default=None)
|
|
793
|
+
p_case_set.add_argument("--instance", default=None, choices=["apk", "gpk", "kas"],
|
|
794
|
+
help="Инстанция дела (the wave §1).")
|
|
795
|
+
p_case_set.set_defaults(func=cmd_case_set)
|
|
796
|
+
|
|
797
|
+
p_case_amount = case_sub.add_parser("amount", help="Суммы по делу.")
|
|
798
|
+
amount_sub = p_case_amount.add_subparsers(dest="case_amount_command", required=True)
|
|
799
|
+
p_amount_add = amount_sub.add_parser("add", help="Добавить сумму.")
|
|
800
|
+
p_amount_add.add_argument("case_id")
|
|
801
|
+
p_amount_add.add_argument("--kind", required=True)
|
|
802
|
+
p_amount_add.add_argument("--amount", required=True)
|
|
803
|
+
p_amount_add.add_argument("--note", default=None)
|
|
804
|
+
p_amount_add.set_defaults(func=cmd_case_amount_add)
|
|
805
|
+
p_amount_list = amount_sub.add_parser("list", help="Список сумм.")
|
|
806
|
+
p_amount_list.add_argument("case_id")
|
|
807
|
+
p_amount_list.set_defaults(func=cmd_case_amount_list)
|
|
808
|
+
p_amount_set = amount_sub.add_parser("set", help="Статус/дата платежа по сумме (the wave).")
|
|
809
|
+
p_amount_set.add_argument("case_id")
|
|
810
|
+
p_amount_set.add_argument("amount_id", type=int)
|
|
811
|
+
p_amount_set.add_argument("--status", required=True,
|
|
812
|
+
choices=["planned", "paid", "refunded"])
|
|
813
|
+
p_amount_set.add_argument("--paid-at", dest="paid_at", default=None,
|
|
814
|
+
help="Дата платежа YYYY-MM-DD")
|
|
815
|
+
p_amount_set.set_defaults(func=cmd_case_amount_set)
|
|
816
|
+
p_amount_summary = amount_sub.add_parser("summary", help="Сводка сумм по kind x status (the wave).")
|
|
817
|
+
p_amount_summary.add_argument("case_id")
|
|
818
|
+
p_amount_summary.set_defaults(func=cmd_case_amount_summary)
|
|
819
|
+
|
|
820
|
+
p_case_doc = case_sub.add_parser("doc", help="Документы по делу.")
|
|
821
|
+
doc_sub = p_case_doc.add_subparsers(dest="case_doc_command", required=True)
|
|
822
|
+
p_doc_add = doc_sub.add_parser("add", help="Добавить документ.")
|
|
823
|
+
p_doc_add.add_argument("case_id")
|
|
824
|
+
p_doc_add.add_argument("--path", required=True)
|
|
825
|
+
p_doc_add.add_argument("--title", default=None)
|
|
826
|
+
p_doc_add.add_argument("--doc-id", dest="doc_id", type=int, default=None,
|
|
827
|
+
help="Явная ревизия: supersedes=<id> существующего документа этого дела (the wave §2)")
|
|
828
|
+
p_doc_add.set_defaults(func=cmd_case_doc_add)
|
|
829
|
+
p_doc_list = doc_sub.add_parser("list", help="Список документов.")
|
|
830
|
+
p_doc_list.add_argument("case_id")
|
|
831
|
+
p_doc_list.set_defaults(func=cmd_case_doc_list)
|
|
832
|
+
p_doc_hist = doc_sub.add_parser("history", help="Цепочка ревизий документа (newest-first, the wave §2).")
|
|
833
|
+
p_doc_hist.add_argument("case_id")
|
|
834
|
+
p_doc_hist.add_argument("--path", default=None, help="Только цепочка этого файла.")
|
|
835
|
+
p_doc_hist.set_defaults(func=cmd_case_doc_history)
|
|
836
|
+
p_doc_chk = doc_sub.add_parser(
|
|
837
|
+
"checklist", help="Структурный чек-лист комплектности (the wave §3; справка, не валиация).")
|
|
838
|
+
p_doc_chk.add_argument("case_id")
|
|
839
|
+
p_doc_chk.add_argument("doc_id", type=int)
|
|
840
|
+
p_doc_chk.add_argument("--kind", default=None,
|
|
841
|
+
help="kind из completeness_rules.json (isk_apk|claim_apk|appeal_apk|hodat)")
|
|
842
|
+
p_doc_chk.set_defaults(func=cmd_case_doc_checklist)
|
|
843
|
+
|
|
844
|
+
# the wave §4 (S-70): процессуальные формы — структурные скелеты + bus-free docx
|
|
845
|
+
p_form = subparsers.add_parser("form", help="Процессуальные формы (the wave): скелеты и docx-команда.")
|
|
846
|
+
form_sub = p_form.add_subparsers(dest="form_command", required=True)
|
|
847
|
+
p_form_new = form_sub.add_parser("new", help="Напечатать структурный скелет формы по делу.")
|
|
848
|
+
p_form_new.add_argument("kind", choices=list(FORM_KINDS))
|
|
849
|
+
p_form_new.add_argument("--case", required=True, dest="case_id")
|
|
850
|
+
p_form_new.add_argument("--format", default="md", choices=["md", "json"])
|
|
851
|
+
p_form_new.set_defaults(func=cmd_form_new)
|
|
852
|
+
p_form_docx = form_sub.add_parser(
|
|
853
|
+
"docx-command", help="Bus-free: готовая shlex-строка docx-cli build (sud её не исполняет).")
|
|
854
|
+
p_form_docx.add_argument("kind", choices=list(FORM_KINDS))
|
|
855
|
+
p_form_docx.add_argument("--case", required=True, dest="case_id")
|
|
856
|
+
p_form_docx.add_argument("--spec-dir", dest="spec_dir", default=None)
|
|
857
|
+
p_form_docx.set_defaults(func=cmd_form_docx_command)
|
|
858
|
+
|
|
859
|
+
# the wave §1 (S-73): реестр доказательств — том/лист/hash-целостность
|
|
860
|
+
p_case_ev = case_sub.add_parser("evidence", help="Реестр доказательств (the wave).")
|
|
861
|
+
ev_sub = p_case_ev.add_subparsers(dest="case_evidence_command", required=True)
|
|
862
|
+
p_ev_add = ev_sub.add_parser("add", help="Добавить доказательство.")
|
|
863
|
+
p_ev_add.add_argument("case_id")
|
|
864
|
+
p_ev_add.add_argument("--title", required=True)
|
|
865
|
+
p_ev_add.add_argument("--path", default=None,
|
|
866
|
+
help="Файл: тогда sha256 + doc_ref на строку case_documents")
|
|
867
|
+
p_ev_add.add_argument("--volume", default=None, help="Том")
|
|
868
|
+
p_ev_add.add_argument("--sheet", default=None, help="Лист")
|
|
869
|
+
p_ev_add.add_argument("--media", choices=["paper", "electronic"], default=None)
|
|
870
|
+
p_ev_add.add_argument("--note", default=None)
|
|
871
|
+
p_ev_add.set_defaults(func=cmd_case_evidence_add)
|
|
872
|
+
p_ev_list = ev_sub.add_parser("list", help="Список доказательств (том->лист, numerical-aware).")
|
|
873
|
+
p_ev_list.add_argument("case_id")
|
|
874
|
+
p_ev_list.set_defaults(func=cmd_case_evidence_list)
|
|
875
|
+
p_ev_verify = ev_sub.add_parser("verify", help="Пересчёт sha256: intact|CHANGED|MISSING_FILE|unchecked.")
|
|
876
|
+
p_ev_verify.add_argument("case_id")
|
|
877
|
+
p_ev_verify.set_defaults(func=cmd_case_evidence_verify)
|
|
878
|
+
|
|
879
|
+
# the wave §2 (S-15): представители. Проверка полномочий НЕ автоматическая.
|
|
880
|
+
p_case_rep = case_sub.add_parser("rep", help="Представители по делу (the wave).")
|
|
881
|
+
rep_sub = p_case_rep.add_subparsers(dest="case_rep_command", required=True)
|
|
882
|
+
p_rep_add = rep_sub.add_parser("add", help="Добавить представителя.")
|
|
883
|
+
p_rep_add.add_argument("case_id")
|
|
884
|
+
p_rep_add.add_argument("--side", required=True, choices=["plaintiff", "defendant"])
|
|
885
|
+
p_rep_add.add_argument("--name", required=True)
|
|
886
|
+
p_rep_add.add_argument("--authority-note", dest="authority_note", default=None)
|
|
887
|
+
p_rep_add.add_argument("--power-docs", dest="power_docs", default=None,
|
|
888
|
+
help="Свободное описание доверенности (sud её не сверяет).")
|
|
889
|
+
p_rep_add.set_defaults(func=cmd_case_rep_add)
|
|
890
|
+
p_rep_list = rep_sub.add_parser("list", help="Список представителей дела.")
|
|
891
|
+
p_rep_list.add_argument("case_id")
|
|
892
|
+
p_rep_list.set_defaults(func=cmd_case_rep_list)
|
|
893
|
+
|
|
894
|
+
# история §5 (S-07): права субъекта — экспорт дела и полное уничтожение.
|
|
895
|
+
p_case_export = case_sub.add_parser(
|
|
896
|
+
"export", help="Маш-читаемый дамп дела (json|md) со ссылкой на pdn_policy.")
|
|
897
|
+
p_case_export.add_argument("case_id")
|
|
898
|
+
p_case_export.add_argument("--format", default="json", choices=["json", "md"])
|
|
899
|
+
p_case_export.add_argument("--out", type=Path, default=None, dest="out",
|
|
900
|
+
help="Записать дамп в файл (UTF-8).")
|
|
901
|
+
p_case_export.set_defaults(func=cmd_case_export)
|
|
902
|
+
p_case_purge = case_sub.add_parser(
|
|
903
|
+
"purge", help="Уничтожить все данные дела (деструктив: требует --yes).")
|
|
904
|
+
p_case_purge.add_argument("case_id")
|
|
905
|
+
p_case_purge.add_argument("--yes", action="store_true", default=False,
|
|
906
|
+
help="Подтверждение уничтожения.")
|
|
907
|
+
p_case_purge.add_argument("--keep-files", dest="keep_files", action="store_true",
|
|
908
|
+
default=False, help="Не удалять физические файлы.")
|
|
909
|
+
p_case_purge.set_defaults(func=cmd_case_purge)
|
|
910
|
+
|
|
911
|
+
p_case_note = case_sub.add_parser("note", help="Заметки по делу.")
|
|
912
|
+
note_sub = p_case_note.add_subparsers(dest="case_note_command", required=True)
|
|
913
|
+
p_note_add = note_sub.add_parser("add", help="Добавить заметку.")
|
|
914
|
+
p_note_add.add_argument("case_id")
|
|
915
|
+
p_note_add.add_argument("--text", required=True)
|
|
916
|
+
p_note_add.add_argument("--author", default=None)
|
|
917
|
+
p_note_add.set_defaults(func=cmd_case_note_add)
|
|
918
|
+
p_note_list = note_sub.add_parser("list", help="Список заметок.")
|
|
919
|
+
p_note_list.add_argument("case_id")
|
|
920
|
+
p_note_list.set_defaults(func=cmd_case_note_list)
|
|
921
|
+
p_note_sup = note_sub.add_parser("supersede", help="Заменить заметку.")
|
|
922
|
+
p_note_sup.add_argument("note_id")
|
|
923
|
+
p_note_sup.add_argument("--text", required=True)
|
|
924
|
+
p_note_sup.add_argument("--author", default=None)
|
|
925
|
+
p_note_sup.set_defaults(func=cmd_case_note_supersede)
|
|
926
|
+
|
|
927
|
+
p_case_list = case_sub.add_parser("list", help="Список дел.")
|
|
928
|
+
p_case_list.add_argument("--status", default=None)
|
|
929
|
+
p_case_list.add_argument("--owner", default=None)
|
|
930
|
+
p_case_list.add_argument("--court", default=None)
|
|
931
|
+
p_case_list.add_argument("--party", default=None)
|
|
932
|
+
p_case_list.set_defaults(func=cmd_case_list)
|
|
933
|
+
|
|
934
|
+
p_case_card = case_sub.add_parser("card", help="Карточка дела.")
|
|
935
|
+
p_case_card.add_argument("case_id")
|
|
936
|
+
p_case_card.set_defaults(func=cmd_case_card)
|
|
937
|
+
|
|
938
|
+
# reminders — bus-free генератор напоминаний (the wave §2, S-26)
|
|
939
|
+
p_reminders = subparsers.add_parser("reminders", help="Напоминания по срокам (bus-free).")
|
|
940
|
+
reminders_sub = p_reminders.add_subparsers(dest="reminders_command", required=True)
|
|
941
|
+
p_rem_due = reminders_sub.add_parser(
|
|
942
|
+
"due", help="Сроки в окне дней + готовые a2a-строки; sud шину не вызывает.")
|
|
943
|
+
p_rem_due.add_argument("--days", type=int, default=7, help="Окно выборки, 1..90")
|
|
944
|
+
p_rem_due.set_defaults(func=cmd_reminders_due)
|
|
945
|
+
|
|
946
|
+
# conflict — conflict-of-interest heuristic (v2.4.1)
|
|
947
|
+
p_conflict = subparsers.add_parser("conflict", help="Детект конфликта интересов (name-exact).")
|
|
948
|
+
conflict_sub = p_conflict.add_subparsers(dest="conflict_command", required=True)
|
|
949
|
+
|
|
950
|
+
p_conflict_check = conflict_sub.add_parser("check", help="Проверка кандидата по локальному реестру.")
|
|
951
|
+
p_conflict_check.add_argument("--plaintiff", default=None)
|
|
952
|
+
p_conflict_check.add_argument("--defendant", default=None)
|
|
953
|
+
p_conflict_check.add_argument("--court", default=None)
|
|
954
|
+
p_conflict_check.add_argument("--case-number", dest="case_number", default=None)
|
|
955
|
+
p_conflict_check.set_defaults(func=cmd_conflict_check)
|
|
956
|
+
|
|
957
|
+
p_conflict_case = conflict_sub.add_parser("case", help="Проверка существующего дела.")
|
|
958
|
+
p_conflict_case.add_argument("case_id")
|
|
959
|
+
p_conflict_case.set_defaults(func=cmd_conflict_case)
|
|
960
|
+
|
|
961
|
+
# jurisdiction — подсудность на VERIFIED rules (v2.6.0)
|
|
962
|
+
p_jur = subparsers.add_parser("jurisdiction", help="Гейдер подсудности по VERIFIED правилам.")
|
|
963
|
+
jur_sub = p_jur.add_subparsers(dest="jurisdiction_command", required=True)
|
|
964
|
+
p_jur_check = jur_sub.add_parser("check", help="Проверка подсудности.")
|
|
965
|
+
p_jur_check.add_argument("--code", required=True, choices=["apk", "gpk", "kas"])
|
|
966
|
+
p_jur_check.add_argument("--claim", required=True)
|
|
967
|
+
p_jur_check.add_argument("--party-org", dest="party_org", action="store_true")
|
|
968
|
+
p_jur_check.add_argument("--party-phys", dest="party_phys", action="store_true")
|
|
969
|
+
p_jur_check.add_argument("--unknown-rf-location", dest="unknown_rf_location", action="store_true")
|
|
970
|
+
p_jur_check.add_argument("--immovable", action="store_true")
|
|
971
|
+
p_jur_check.add_argument("--corporate", action="store_true")
|
|
972
|
+
p_jur_check.add_argument("--bankruptcy", action="store_true")
|
|
973
|
+
p_jur_check.add_argument("--ip-rights", dest="ip_rights", action="store_true")
|
|
974
|
+
p_jur_check.add_argument("--contract-forum", dest="contract_forum", action="store_true")
|
|
975
|
+
p_jur_check.add_argument("--arbitration-clause", dest="arbitration_clause", action="store_true")
|
|
976
|
+
p_jur_check.add_argument("--multiple-defendants", dest="multiple_defendants", action="store_true")
|
|
977
|
+
p_jur_check.add_argument("--foreign-element", dest="foreign_element", action="store_true")
|
|
978
|
+
p_jur_check.add_argument("--consumer", action="store_true")
|
|
979
|
+
p_jur_check.add_argument("--alimony", action="store_true")
|
|
980
|
+
p_jur_check.set_defaults(func=cmd_jurisdiction_check)
|
|
981
|
+
|
|
982
|
+
# pretrial — претензионный порядок (v2.6.0)
|
|
983
|
+
p_pre = subparsers.add_parser("pretrial", help="Претензионный порядок по VERIFIED правилам.")
|
|
984
|
+
pre_sub = p_pre.add_subparsers(dest="pretrial_command", required=True)
|
|
985
|
+
p_pre_calc = pre_sub.add_parser("calc", help="Срок ответа на претензию.")
|
|
986
|
+
p_pre_calc.add_argument("--code", required=True, choices=["apk", "zozpp", "osago",
|
|
987
|
+
"railway", "road", "maritime",
|
|
988
|
+
"inland", "air", "freight", "postal"])
|
|
989
|
+
p_pre_calc.add_argument("--sent", required=True)
|
|
990
|
+
p_pre_calc.set_defaults(func=cmd_pretrial_calc)
|
|
991
|
+
p_pre_cons = pre_sub.add_parser("consequences", help="Последствия несоблюдения претензионного порядка.")
|
|
992
|
+
p_pre_cons.add_argument("--code", required=True, choices=["apk", "gpk", "kas"])
|
|
993
|
+
p_pre_cons.add_argument("--stage", required=True, choices=["filing", "trial", "movement"])
|
|
994
|
+
p_pre_cons.set_defaults(func=cmd_pretrial_consequences)
|
|
995
|
+
|
|
996
|
+
return parser
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
def _strip_global_options(argv):
|
|
1000
|
+
"""Return argv with global flags and their values removed.
|
|
1001
|
+
|
|
1002
|
+
This must stay in sync with the global options accepted by build_parser:
|
|
1003
|
+
--json, --state-dir PATH, --state-dir=PATH.
|
|
1004
|
+
"""
|
|
1005
|
+
result = []
|
|
1006
|
+
it = iter(argv)
|
|
1007
|
+
for token in it:
|
|
1008
|
+
if token == "--json":
|
|
1009
|
+
continue
|
|
1010
|
+
elif token == "--state-dir":
|
|
1011
|
+
try:
|
|
1012
|
+
next(it)
|
|
1013
|
+
except StopIteration:
|
|
1014
|
+
pass
|
|
1015
|
+
continue
|
|
1016
|
+
elif token.startswith("--state-dir="):
|
|
1017
|
+
continue
|
|
1018
|
+
result.append(token)
|
|
1019
|
+
return result
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
def _first_command_token(argv):
|
|
1023
|
+
"""Return the first positional command token in argv, or None."""
|
|
1024
|
+
for token in _strip_global_options(argv):
|
|
1025
|
+
if token.startswith("-"):
|
|
1026
|
+
continue
|
|
1027
|
+
return token
|
|
1028
|
+
return None
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
def _extract_global_options(argv):
|
|
1032
|
+
"""Extract --json and --state-dir from argv without enforcing full parsing."""
|
|
1033
|
+
json_mode = False
|
|
1034
|
+
state_dir = None
|
|
1035
|
+
it = iter(argv)
|
|
1036
|
+
for token in it:
|
|
1037
|
+
if token == "--json":
|
|
1038
|
+
json_mode = True
|
|
1039
|
+
elif token == "--state-dir":
|
|
1040
|
+
try:
|
|
1041
|
+
state_dir = Path(next(it))
|
|
1042
|
+
except StopIteration:
|
|
1043
|
+
pass
|
|
1044
|
+
elif token.startswith("--state-dir="):
|
|
1045
|
+
state_dir = Path(token.split("=", 1)[1])
|
|
1046
|
+
return json_mode, state_dir
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
def _rejected_tokens(argv, limit=8):
|
|
1050
|
+
"""List the offending tokens so the caller can see what was rejected.
|
|
1051
|
+
|
|
1052
|
+
Path-like tokens are dropped: the suite asserts that no output ever echoes
|
|
1053
|
+
$HOME, and an echo of an arbitrary argument is exactly how that leaks. The
|
|
1054
|
+
caller still gets the subcommand name, which is what identifies the call.
|
|
1055
|
+
"""
|
|
1056
|
+
kept = []
|
|
1057
|
+
for token in argv:
|
|
1058
|
+
if len(kept) >= limit:
|
|
1059
|
+
break
|
|
1060
|
+
if token.startswith("/") or token.startswith("~") or "://" in token:
|
|
1061
|
+
continue
|
|
1062
|
+
kept.append(token)
|
|
1063
|
+
return " ".join(kept) if kept else "(аргументы не распознаны)"
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
def main(argv=None):
|
|
1067
|
+
argv = list(argv) if argv is not None else sys.argv[1:]
|
|
1068
|
+
# Backward-compat: legacy "deadlines <key> ..." (v2.4) -> "deadlines calculate <key> ..."
|
|
1069
|
+
_DEADLINE_SUBS = {"calculate", "calc", "confirm", "anchor", "waive", "audit", "list", "matrix", "autofill", "chain"}
|
|
1070
|
+
try:
|
|
1071
|
+
i = argv.index("deadlines")
|
|
1072
|
+
except ValueError:
|
|
1073
|
+
i = -1
|
|
1074
|
+
if i >= 0 and i + 1 < len(argv):
|
|
1075
|
+
nxt = argv[i + 1]
|
|
1076
|
+
if not nxt.startswith("-") and nxt not in _DEADLINE_SUBS:
|
|
1077
|
+
argv = argv[: i + 1] + ["calculate"] + argv[i + 1:]
|
|
1078
|
+
# v2.9.0 (S-54): `practice status <n>` routes to the offline fate probe.
|
|
1079
|
+
# The live `practice <query>` parser stays flat (MCP drift contract), so
|
|
1080
|
+
# the subcommand shape is dispatched here, same precedent as deadlines.
|
|
1081
|
+
try:
|
|
1082
|
+
_ps_i = argv.index("practice")
|
|
1083
|
+
except ValueError:
|
|
1084
|
+
_ps_i = -1
|
|
1085
|
+
if _ps_i >= 0 and _ps_i + 1 < len(argv) and argv[_ps_i + 1] == "status":
|
|
1086
|
+
argv = argv[:_ps_i] + ["practice-status"] + argv[_ps_i + 2:]
|
|
1087
|
+
|
|
1088
|
+
json_requested = "--json" in argv
|
|
1089
|
+
argv = [token for token in argv if token != "--json"]
|
|
1090
|
+
# W6a §2: --explain принимается в любой позиции (как --json) — флаг
|
|
1091
|
+
# глобальный, субпарсеры его не объявляют.
|
|
1092
|
+
explain_requested = "--explain" in argv
|
|
1093
|
+
argv = [token for token in argv if token != "--explain"]
|
|
1094
|
+
|
|
1095
|
+
parser = build_parser()
|
|
1096
|
+
# A consumer parsing stdout with --json must get parseable output even when
|
|
1097
|
+
# the arguments are malformed. argparse exits 2 with human-readable usage
|
|
1098
|
+
# on stderr and nothing on stdout, which breaks the machine contract, so in
|
|
1099
|
+
# JSON mode we emit a typed error instead. Text mode keeps argparse's own
|
|
1100
|
+
# behaviour untouched.
|
|
1101
|
+
parse_failed = False
|
|
1102
|
+
if json_requested:
|
|
1103
|
+
# argparse writes its usage text to stderr the moment it rejects the
|
|
1104
|
+
# arguments, which would corrupt the JSON error we are about to emit on
|
|
1105
|
+
# the same stream. Swallow it during parsing only — the JSON error is
|
|
1106
|
+
# emitted afterwards, outside this block, so it still reaches stderr.
|
|
1107
|
+
# In text mode argparse keeps its own behaviour untouched.
|
|
1108
|
+
with contextlib.redirect_stderr(io.StringIO()):
|
|
1109
|
+
try:
|
|
1110
|
+
args = parser.parse_args(argv)
|
|
1111
|
+
except SystemExit:
|
|
1112
|
+
parse_failed = True
|
|
1113
|
+
args = None
|
|
1114
|
+
else:
|
|
1115
|
+
args = parser.parse_args(argv)
|
|
1116
|
+
|
|
1117
|
+
if parse_failed:
|
|
1118
|
+
bad_command = _first_command_token(argv)
|
|
1119
|
+
return error(
|
|
1120
|
+
{
|
|
1121
|
+
"status": "INVALID_ARGUMENTS",
|
|
1122
|
+
"detail": (
|
|
1123
|
+
f"неверные или недостающие аргументы для команды "
|
|
1124
|
+
f"'{bad_command or 'unknown'}': {_rejected_tokens(argv)}"
|
|
1125
|
+
),
|
|
1126
|
+
},
|
|
1127
|
+
meta={"command": bad_command},
|
|
1128
|
+
json_mode=True,
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1131
|
+
if args.command is None:
|
|
1132
|
+
parser.print_help()
|
|
1133
|
+
return 0
|
|
1134
|
+
|
|
1135
|
+
state_dir = args.state_dir or get_state_dir()
|
|
1136
|
+
args.state_dir = state_dir
|
|
1137
|
+
args.json_mode = json_requested
|
|
1138
|
+
args.explain = explain_requested or getattr(args, "explain", False)
|
|
1139
|
+
|
|
1140
|
+
try:
|
|
1141
|
+
return args.func(args)
|
|
1142
|
+
except Exception as exc:
|
|
1143
|
+
return error(
|
|
1144
|
+
f"unexpected internal error: {type(exc).__name__}",
|
|
1145
|
+
meta={"command": args.command, "state_dir": str(state_dir)},
|
|
1146
|
+
json_mode=args.json_mode,
|
|
1147
|
+
)
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
def cmd_providers(args):
|
|
1151
|
+
providers = list_providers()
|
|
1152
|
+
payload = [
|
|
1153
|
+
{
|
|
1154
|
+
"name": p.name,
|
|
1155
|
+
"kind": p.kind,
|
|
1156
|
+
"status": p.status,
|
|
1157
|
+
"description": p.description,
|
|
1158
|
+
}
|
|
1159
|
+
for p in providers
|
|
1160
|
+
]
|
|
1161
|
+
result(
|
|
1162
|
+
payload,
|
|
1163
|
+
meta={"command": "providers", "state_dir": str(args.state_dir)},
|
|
1164
|
+
json_mode=args.json_mode,
|
|
1165
|
+
)
|
|
1166
|
+
return 0
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
def cmd_ready(args):
|
|
1170
|
+
"""Report whether live court commands can run, and how to call them.
|
|
1171
|
+
|
|
1172
|
+
Exits non-zero when something is missing so an agent can gate on it:
|
|
1173
|
+
``sud-cli --json ready || echo "court access not configured"``.
|
|
1174
|
+
No court host is contacted — the answer is about configuration only.
|
|
1175
|
+
"""
|
|
1176
|
+
from sud_cli.readiness import check_live_readiness
|
|
1177
|
+
|
|
1178
|
+
report = check_live_readiness()
|
|
1179
|
+
ready = bool(report.get("ready"))
|
|
1180
|
+
result(
|
|
1181
|
+
report,
|
|
1182
|
+
meta={
|
|
1183
|
+
"command": "ready",
|
|
1184
|
+
"ready": ready,
|
|
1185
|
+
"state_dir": str(args.state_dir),
|
|
1186
|
+
},
|
|
1187
|
+
json_mode=args.json_mode,
|
|
1188
|
+
)
|
|
1189
|
+
return 0 if ready else 1
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
def cmd_doctor(args):
|
|
1193
|
+
from sud_cli.db import doctor_check, init_db
|
|
1194
|
+
|
|
1195
|
+
init_db(args.state_dir)
|
|
1196
|
+
report = doctor_check(args.state_dir)
|
|
1197
|
+
status = 0 if report.get("healthy") else 1
|
|
1198
|
+
result(
|
|
1199
|
+
report,
|
|
1200
|
+
meta={"command": "doctor", "state_dir": str(args.state_dir)},
|
|
1201
|
+
json_mode=args.json_mode,
|
|
1202
|
+
)
|
|
1203
|
+
return status
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
def cmd_calendar_check(args):
|
|
1207
|
+
from .production_calendar import calendar_check
|
|
1208
|
+
|
|
1209
|
+
report = calendar_check()
|
|
1210
|
+
result(report, meta={"command": "calendar check"}, json_mode=args.json_mode)
|
|
1211
|
+
return 0 if report.get("valid") else 1
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
def cmd_import(args):
|
|
1215
|
+
from sud_cli.db import get_connection, init_db
|
|
1216
|
+
from sud_cli.importing import import_fixture
|
|
1217
|
+
|
|
1218
|
+
init_db(args.state_dir)
|
|
1219
|
+
conn = get_connection(args.state_dir)
|
|
1220
|
+
try:
|
|
1221
|
+
report = import_fixture(conn, args.kind, args.file)
|
|
1222
|
+
except Exception as exc:
|
|
1223
|
+
return error(
|
|
1224
|
+
str(exc),
|
|
1225
|
+
meta={"command": "import"},
|
|
1226
|
+
json_mode=args.json_mode,
|
|
1227
|
+
)
|
|
1228
|
+
result(
|
|
1229
|
+
report,
|
|
1230
|
+
meta={"command": "import", "state_dir": str(args.state_dir)},
|
|
1231
|
+
json_mode=args.json_mode,
|
|
1232
|
+
)
|
|
1233
|
+
return 0
|
|
1234
|
+
|
|
1235
|
+
|
|
1236
|
+
def cmd_search(args):
|
|
1237
|
+
from sud_cli.db import get_connection, init_db, search_records
|
|
1238
|
+
|
|
1239
|
+
init_db(args.state_dir)
|
|
1240
|
+
conn = get_connection(args.state_dir)
|
|
1241
|
+
try:
|
|
1242
|
+
payload, meta = search_records(
|
|
1243
|
+
conn,
|
|
1244
|
+
kind=args.kind,
|
|
1245
|
+
query=args.query,
|
|
1246
|
+
limit=args.limit,
|
|
1247
|
+
since=args.since,
|
|
1248
|
+
until=args.until,
|
|
1249
|
+
)
|
|
1250
|
+
except Exception as exc:
|
|
1251
|
+
return error(
|
|
1252
|
+
str(exc),
|
|
1253
|
+
meta={"command": "search", "state_dir": str(args.state_dir)},
|
|
1254
|
+
json_mode=args.json_mode,
|
|
1255
|
+
)
|
|
1256
|
+
meta = meta or {}
|
|
1257
|
+
meta.update({"command": "search", "state_dir": str(args.state_dir)})
|
|
1258
|
+
result(payload, meta=meta, json_mode=args.json_mode)
|
|
1259
|
+
return 0
|
|
1260
|
+
|
|
1261
|
+
|
|
1262
|
+
def cmd_npa_search(args):
|
|
1263
|
+
from sud_cli.db import get_connection, init_db, search_npa
|
|
1264
|
+
|
|
1265
|
+
init_db(args.state_dir)
|
|
1266
|
+
conn = get_connection(args.state_dir)
|
|
1267
|
+
try:
|
|
1268
|
+
payload, meta = search_npa(conn, query=args.query, limit=args.limit)
|
|
1269
|
+
except Exception as exc:
|
|
1270
|
+
return error(
|
|
1271
|
+
str(exc),
|
|
1272
|
+
meta={"command": "npa search", "state_dir": str(args.state_dir)},
|
|
1273
|
+
json_mode=args.json_mode,
|
|
1274
|
+
)
|
|
1275
|
+
meta = meta or {}
|
|
1276
|
+
meta.update({"command": "npa search", "state_dir": str(args.state_dir),
|
|
1277
|
+
"source_basis": _wm.render_line("npa_source", __version__)})
|
|
1278
|
+
result(payload, meta=meta, json_mode=args.json_mode)
|
|
1279
|
+
return 0
|
|
1280
|
+
|
|
1281
|
+
|
|
1282
|
+
def cmd_npa_compare(args):
|
|
1283
|
+
from sud_cli.db import get_connection, init_db, compare_npa_editions
|
|
1284
|
+
|
|
1285
|
+
init_db(args.state_dir)
|
|
1286
|
+
conn = get_connection(args.state_dir)
|
|
1287
|
+
try:
|
|
1288
|
+
payload = compare_npa_editions(
|
|
1289
|
+
conn, doc_id=args.doc_id, edition1=args.edition1, edition2=args.edition2
|
|
1290
|
+
)
|
|
1291
|
+
except Exception as exc:
|
|
1292
|
+
return error(
|
|
1293
|
+
str(exc),
|
|
1294
|
+
meta={"command": "npa compare", "state_dir": str(args.state_dir)},
|
|
1295
|
+
json_mode=args.json_mode,
|
|
1296
|
+
)
|
|
1297
|
+
result(
|
|
1298
|
+
payload,
|
|
1299
|
+
meta={"command": "npa compare", "state_dir": str(args.state_dir)},
|
|
1300
|
+
json_mode=args.json_mode,
|
|
1301
|
+
)
|
|
1302
|
+
return 0
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
def cmd_law_search(args):
|
|
1306
|
+
from sud_cli.pravo_publication import PublicationError, query_publication_metadata
|
|
1307
|
+
|
|
1308
|
+
try:
|
|
1309
|
+
payload = query_publication_metadata(query=args.query, number=args.number)
|
|
1310
|
+
except PublicationError as exc:
|
|
1311
|
+
return error(
|
|
1312
|
+
str(exc),
|
|
1313
|
+
meta={"command": "law search"},
|
|
1314
|
+
json_mode=args.json_mode,
|
|
1315
|
+
)
|
|
1316
|
+
meta = {
|
|
1317
|
+
"command": "law search",
|
|
1318
|
+
"state_dir": str(args.state_dir),
|
|
1319
|
+
"source": payload["source"],
|
|
1320
|
+
"provider_label": payload["provider_label"],
|
|
1321
|
+
"page": payload["pagination"]["page"],
|
|
1322
|
+
"page_size": payload["pagination"]["page_size"],
|
|
1323
|
+
"total": payload["pagination"]["total"],
|
|
1324
|
+
"pages_total": payload["pagination"]["pages_total"],
|
|
1325
|
+
}
|
|
1326
|
+
result(payload["items"], meta=meta, json_mode=args.json_mode)
|
|
1327
|
+
return 0
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
def cmd_court_import(args):
|
|
1331
|
+
from sud_cli.db import get_connection, init_db
|
|
1332
|
+
from sud_cli.importing import CourtImportError, ImportError as FixtureImportError
|
|
1333
|
+
from sud_cli.importing import import_court_cases
|
|
1334
|
+
|
|
1335
|
+
init_db(args.state_dir)
|
|
1336
|
+
conn = get_connection(args.state_dir)
|
|
1337
|
+
try:
|
|
1338
|
+
report = import_court_cases(conn, args.file)
|
|
1339
|
+
except (CourtImportError, FixtureImportError) as exc:
|
|
1340
|
+
return error(
|
|
1341
|
+
str(exc),
|
|
1342
|
+
meta={"command": "court import"},
|
|
1343
|
+
json_mode=args.json_mode,
|
|
1344
|
+
)
|
|
1345
|
+
except Exception as exc:
|
|
1346
|
+
return error(
|
|
1347
|
+
f"unexpected internal error: {type(exc).__name__}",
|
|
1348
|
+
meta={"command": "court import"},
|
|
1349
|
+
json_mode=args.json_mode,
|
|
1350
|
+
)
|
|
1351
|
+
result(
|
|
1352
|
+
report,
|
|
1353
|
+
meta={"command": "court import", "state_dir": str(args.state_dir)},
|
|
1354
|
+
json_mode=args.json_mode,
|
|
1355
|
+
)
|
|
1356
|
+
return 0
|
|
1357
|
+
|
|
1358
|
+
|
|
1359
|
+
def cmd_court_search(args):
|
|
1360
|
+
from sud_cli.db import SearchError, get_connection, init_db, search_court_cases
|
|
1361
|
+
|
|
1362
|
+
init_db(args.state_dir)
|
|
1363
|
+
conn = get_connection(args.state_dir)
|
|
1364
|
+
try:
|
|
1365
|
+
payload, meta = search_court_cases(
|
|
1366
|
+
conn,
|
|
1367
|
+
query=args.query,
|
|
1368
|
+
limit=args.limit,
|
|
1369
|
+
since=args.since,
|
|
1370
|
+
until=args.until,
|
|
1371
|
+
)
|
|
1372
|
+
except SearchError as exc:
|
|
1373
|
+
return error(
|
|
1374
|
+
str(exc),
|
|
1375
|
+
meta={"command": "court search"},
|
|
1376
|
+
json_mode=args.json_mode,
|
|
1377
|
+
)
|
|
1378
|
+
except Exception as exc:
|
|
1379
|
+
return error(
|
|
1380
|
+
f"unexpected internal error: {type(exc).__name__}",
|
|
1381
|
+
meta={"command": "court search"},
|
|
1382
|
+
json_mode=args.json_mode,
|
|
1383
|
+
)
|
|
1384
|
+
meta = meta or {}
|
|
1385
|
+
meta.update({"command": "court search", "state_dir": str(args.state_dir)})
|
|
1386
|
+
result(payload, meta=meta, json_mode=args.json_mode)
|
|
1387
|
+
return 0
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
def cmd_court_case(args):
|
|
1391
|
+
from sud_cli.db import get_case_events, get_case_summary, get_connection, init_db
|
|
1392
|
+
|
|
1393
|
+
init_db(args.state_dir)
|
|
1394
|
+
conn = get_connection(args.state_dir)
|
|
1395
|
+
try:
|
|
1396
|
+
summary = get_case_summary(conn, args.case_id)
|
|
1397
|
+
if summary is None:
|
|
1398
|
+
return error(
|
|
1399
|
+
f"case not found: {args.case_id}",
|
|
1400
|
+
meta={"command": "court case"},
|
|
1401
|
+
json_mode=args.json_mode,
|
|
1402
|
+
)
|
|
1403
|
+
events = get_case_events(conn, args.case_id)
|
|
1404
|
+
payload = {
|
|
1405
|
+
**summary,
|
|
1406
|
+
"events": events,
|
|
1407
|
+
"event_count": len(events),
|
|
1408
|
+
}
|
|
1409
|
+
except Exception as exc:
|
|
1410
|
+
return error(
|
|
1411
|
+
f"unexpected internal error: {type(exc).__name__}",
|
|
1412
|
+
meta={"command": "court case", "state_dir": str(args.state_dir)},
|
|
1413
|
+
json_mode=args.json_mode,
|
|
1414
|
+
)
|
|
1415
|
+
result(
|
|
1416
|
+
payload,
|
|
1417
|
+
meta={"command": "court case", "state_dir": str(args.state_dir)},
|
|
1418
|
+
json_mode=args.json_mode,
|
|
1419
|
+
)
|
|
1420
|
+
return 0
|
|
1421
|
+
|
|
1422
|
+
|
|
1423
|
+
def _emit_bridge_result(bridge_result, label, state_dir, json_mode):
|
|
1424
|
+
"""Emit a bridge result under its own status and return the exit code.
|
|
1425
|
+
|
|
1426
|
+
The bridge distinguishes ``blocked`` ("live access is not enabled", nothing
|
|
1427
|
+
was attempted) from ``error`` (something was attempted and failed). Routing
|
|
1428
|
+
both through ``error`` told an agent that a missing env var was a crash, so
|
|
1429
|
+
it retried a call that could never succeed.
|
|
1430
|
+
"""
|
|
1431
|
+
status = bridge_result.get("status")
|
|
1432
|
+
if status == "ok":
|
|
1433
|
+
result(
|
|
1434
|
+
bridge_result["data"],
|
|
1435
|
+
meta={"command": label, "state_dir": str(state_dir)},
|
|
1436
|
+
json_mode=json_mode,
|
|
1437
|
+
)
|
|
1438
|
+
return 0
|
|
1439
|
+
emitter = blocked if status == "blocked" else error
|
|
1440
|
+
emitter(
|
|
1441
|
+
bridge_result["data"],
|
|
1442
|
+
meta={"command": label},
|
|
1443
|
+
json_mode=json_mode,
|
|
1444
|
+
)
|
|
1445
|
+
return 1
|
|
1446
|
+
|
|
1447
|
+
|
|
1448
|
+
def cmd_case_search_providers(args):
|
|
1449
|
+
bridge_result = run_bridge_command(
|
|
1450
|
+
"providers",
|
|
1451
|
+
state_dir=args.state_dir,
|
|
1452
|
+
)
|
|
1453
|
+
return _emit_bridge_result(
|
|
1454
|
+
bridge_result, "case-search providers", args.state_dir, args.json_mode
|
|
1455
|
+
)
|
|
1456
|
+
|
|
1457
|
+
|
|
1458
|
+
def cmd_case_search_import(args):
|
|
1459
|
+
bridge_result = run_bridge_command(
|
|
1460
|
+
args.cw_command,
|
|
1461
|
+
state_dir=args.state_dir,
|
|
1462
|
+
input_path=args.file,
|
|
1463
|
+
case_id=args.case_id,
|
|
1464
|
+
)
|
|
1465
|
+
return _emit_bridge_result(
|
|
1466
|
+
bridge_result,
|
|
1467
|
+
f"case-search {args.cw_command}",
|
|
1468
|
+
args.state_dir,
|
|
1469
|
+
args.json_mode,
|
|
1470
|
+
)
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
def cmd_case_search_extract(args):
|
|
1474
|
+
bridge_result = run_bridge_command(
|
|
1475
|
+
"extract-text",
|
|
1476
|
+
state_dir=args.state_dir,
|
|
1477
|
+
input_path=args.file,
|
|
1478
|
+
)
|
|
1479
|
+
return _emit_bridge_result(
|
|
1480
|
+
bridge_result, "case-search extract-text", args.state_dir, args.json_mode
|
|
1481
|
+
)
|
|
1482
|
+
|
|
1483
|
+
|
|
1484
|
+
def cmd_case_diff(args):
|
|
1485
|
+
"""Compare two saved snapshots of the same case.
|
|
1486
|
+
|
|
1487
|
+
Offline by construction: it reads two JSON files and touches no court
|
|
1488
|
+
source, so it needs no live gate and no license. The compared field set
|
|
1489
|
+
mirrors ``case_search.diff._SCALARS``, which case-search computes
|
|
1490
|
+
internally but never exposes.
|
|
1491
|
+
"""
|
|
1492
|
+
from sud_cli.case_diff import DiffError, diff_case_snapshots
|
|
1493
|
+
|
|
1494
|
+
try:
|
|
1495
|
+
payload = diff_case_snapshots(args.old, args.new)
|
|
1496
|
+
except DiffError as exc:
|
|
1497
|
+
return error(
|
|
1498
|
+
{"status": "DIFF_ERROR", "detail": str(exc)},
|
|
1499
|
+
meta={"command": "case-search case-diff"},
|
|
1500
|
+
json_mode=args.json_mode,
|
|
1501
|
+
)
|
|
1502
|
+
|
|
1503
|
+
result(
|
|
1504
|
+
payload,
|
|
1505
|
+
meta={
|
|
1506
|
+
"command": "case-search case-diff",
|
|
1507
|
+
"changed": payload["changed"],
|
|
1508
|
+
},
|
|
1509
|
+
json_mode=args.json_mode,
|
|
1510
|
+
)
|
|
1511
|
+
return 0
|
|
1512
|
+
|
|
1513
|
+
|
|
1514
|
+
def cmd_case_search_parity(args):
|
|
1515
|
+
from sud_cli.case_search_bridge import get_parity_matrix
|
|
1516
|
+
|
|
1517
|
+
matrix = get_parity_matrix()
|
|
1518
|
+
result(
|
|
1519
|
+
matrix,
|
|
1520
|
+
meta={"command": "case-search parity", "state_dir": str(args.state_dir)},
|
|
1521
|
+
json_mode=args.json_mode,
|
|
1522
|
+
)
|
|
1523
|
+
return 0
|
|
1524
|
+
|
|
1525
|
+
|
|
1526
|
+
def _run_flag_command_cli(args, command, options, label):
|
|
1527
|
+
"""Shared handler for the Phase 8 flag-driven live bridge commands."""
|
|
1528
|
+
bridge_result = run_bridge_command(
|
|
1529
|
+
command,
|
|
1530
|
+
state_dir=args.state_dir,
|
|
1531
|
+
options=options,
|
|
1532
|
+
)
|
|
1533
|
+
return _emit_bridge_result(bridge_result, label, args.state_dir, args.json_mode)
|
|
1534
|
+
|
|
1535
|
+
|
|
1536
|
+
def cmd_case_search_search_practice(args):
|
|
1537
|
+
# sudact.ru serves a "Поиск не доступен" stub to plain HTTP clients, so the
|
|
1538
|
+
# search only returns results when it goes through the Camofox browser. The
|
|
1539
|
+
# flag therefore defaults on; --no-camofox is the explicit opt-out.
|
|
1540
|
+
via_camofox = not args.no_camofox
|
|
1541
|
+
return _run_flag_command_cli(
|
|
1542
|
+
args,
|
|
1543
|
+
"search-practice",
|
|
1544
|
+
{
|
|
1545
|
+
"query": args.query,
|
|
1546
|
+
"section": args.section,
|
|
1547
|
+
"page": args.page,
|
|
1548
|
+
"limit": args.limit,
|
|
1549
|
+
"via_camofox": via_camofox,
|
|
1550
|
+
},
|
|
1551
|
+
"case-search search-practice",
|
|
1552
|
+
)
|
|
1553
|
+
|
|
1554
|
+
|
|
1555
|
+
def _run_live_command_cli(args, command, label, **kwargs):
|
|
1556
|
+
"""Shared handler for positional live commands reached through the bridge."""
|
|
1557
|
+
bridge_result = run_bridge_command(
|
|
1558
|
+
command,
|
|
1559
|
+
state_dir=args.state_dir,
|
|
1560
|
+
**kwargs,
|
|
1561
|
+
)
|
|
1562
|
+
return _emit_bridge_result(bridge_result, label, args.state_dir, args.json_mode)
|
|
1563
|
+
|
|
1564
|
+
|
|
1565
|
+
def cmd_case_search_lookup(args):
|
|
1566
|
+
return _run_live_command_cli(
|
|
1567
|
+
args,
|
|
1568
|
+
"lookup",
|
|
1569
|
+
"case-search lookup",
|
|
1570
|
+
case_id=args.case_id,
|
|
1571
|
+
options={"--source": args.source} if args.source else None,
|
|
1572
|
+
)
|
|
1573
|
+
|
|
1574
|
+
|
|
1575
|
+
def cmd_case_search_case_history(args):
|
|
1576
|
+
return _run_live_command_cli(
|
|
1577
|
+
args, "case-history", "case-search case-history", case_id=args.case_id
|
|
1578
|
+
)
|
|
1579
|
+
|
|
1580
|
+
|
|
1581
|
+
def cmd_case_search_sudrf_live(args):
|
|
1582
|
+
return _run_live_command_cli(
|
|
1583
|
+
args, "sudrf-live", "case-search sudrf-live", case_id=args.case_id
|
|
1584
|
+
)
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
def cmd_case_search_mos_live(args):
|
|
1588
|
+
return _run_live_command_cli(
|
|
1589
|
+
args,
|
|
1590
|
+
"mos-live",
|
|
1591
|
+
"case-search mos-live",
|
|
1592
|
+
case_id=args.case_id,
|
|
1593
|
+
court_slug=args.court_slug,
|
|
1594
|
+
)
|
|
1595
|
+
|
|
1596
|
+
|
|
1597
|
+
def cmd_case_search_kad_search(args):
|
|
1598
|
+
return _run_flag_command_cli(
|
|
1599
|
+
args,
|
|
1600
|
+
"kad-search",
|
|
1601
|
+
{
|
|
1602
|
+
"inn": args.inn,
|
|
1603
|
+
"ogrn": args.ogrn,
|
|
1604
|
+
"name": args.name,
|
|
1605
|
+
"page": args.page,
|
|
1606
|
+
},
|
|
1607
|
+
"case-search kad-search",
|
|
1608
|
+
)
|
|
1609
|
+
|
|
1610
|
+
|
|
1611
|
+
def cmd_case_search_get_text(args):
|
|
1612
|
+
return _run_flag_command_cli(
|
|
1613
|
+
args,
|
|
1614
|
+
"get-text",
|
|
1615
|
+
{
|
|
1616
|
+
"url_or_file": args.url,
|
|
1617
|
+
"no_cache": args.no_cache,
|
|
1618
|
+
"blocks": args.blocks,
|
|
1619
|
+
},
|
|
1620
|
+
"case-search get-text",
|
|
1621
|
+
)
|
|
1622
|
+
|
|
1623
|
+
|
|
1624
|
+
def _emit_native(outcome, label, args):
|
|
1625
|
+
payload = outcome.to_dict() if hasattr(outcome, "to_dict") else outcome
|
|
1626
|
+
status = payload.get("status", "error")
|
|
1627
|
+
emitter = result if status == "ok" else (blocked if status == "degraded" else error)
|
|
1628
|
+
emitter(payload, meta={"command": label, "state_dir": str(args.state_dir)}, json_mode=args.json_mode)
|
|
1629
|
+
return 0 if status == "ok" else 1
|
|
1630
|
+
|
|
1631
|
+
|
|
1632
|
+
def cmd_native_practice(args):
|
|
1633
|
+
from sud_cli.case_search import search_practice
|
|
1634
|
+
return _emit_native(search_practice(args.query, section=args.section, page=args.page, limit=args.limit), "practice", args)
|
|
1635
|
+
|
|
1636
|
+
|
|
1637
|
+
def cmd_native_kad(args):
|
|
1638
|
+
from sud_cli.case_search import kad_search
|
|
1639
|
+
return _emit_native(kad_search(args.inn, args.ogrn, args.name, page=args.page), "kad", args)
|
|
1640
|
+
|
|
1641
|
+
|
|
1642
|
+
def cmd_native_case_history(args):
|
|
1643
|
+
from sud_cli.case_search import case_history
|
|
1644
|
+
return _emit_native(case_history(args.case_id, section=args.section, max_related=args.max_related, limit=args.limit), "case-history", args)
|
|
1645
|
+
|
|
1646
|
+
|
|
1647
|
+
def cmd_native_sudrf(args):
|
|
1648
|
+
from sud_cli.case_search import get_sudrf_case
|
|
1649
|
+
return _emit_native(get_sudrf_case(args.case_id, host=args.host), "sudrf-live", args)
|
|
1650
|
+
|
|
1651
|
+
|
|
1652
|
+
def cmd_native_mos(args):
|
|
1653
|
+
from sud_cli.case_search import get_mos_case
|
|
1654
|
+
return _emit_native(get_mos_case(args.court_slug, args.case_id), "mos-live", args)
|
|
1655
|
+
|
|
1656
|
+
|
|
1657
|
+
def cmd_efrsb(args):
|
|
1658
|
+
"""ЕФРСБ: link/record — ручной human-in-the-loop контур (акт 25.10);
|
|
1659
|
+
сетевые companies/persons/... — legacy-эндпоинты (fail-closed)."""
|
|
1660
|
+
from sud_cli import efrsb_public as ep
|
|
1661
|
+
|
|
1662
|
+
if args.efrsb_cmd == "link":
|
|
1663
|
+
from sud_cli import efrsb_manual
|
|
1664
|
+
try:
|
|
1665
|
+
payload = efrsb_manual.link_plan(args.kind, name=args.name, inn=args.inn,
|
|
1666
|
+
ogrn=args.ogrn, birth_date=args.birth_date)
|
|
1667
|
+
except ValueError as exc:
|
|
1668
|
+
error(str(exc), meta={"command": "efrsb link"}, json_mode=args.json_mode)
|
|
1669
|
+
return 1
|
|
1670
|
+
result(payload, meta={"command": "efrsb link",
|
|
1671
|
+
"policy": "browser-адаптер запрещён актом 25.10; ссылку открывает человек"},
|
|
1672
|
+
json_mode=args.json_mode)
|
|
1673
|
+
return 0
|
|
1674
|
+
if args.efrsb_cmd == "record":
|
|
1675
|
+
from sud_cli import efrsb_manual
|
|
1676
|
+
from sud_cli.db import (DocNotFoundError, EvidenceError, add_case_evidence,
|
|
1677
|
+
get_connection, init_db)
|
|
1678
|
+
try:
|
|
1679
|
+
payload = efrsb_manual.record_payload(subject=args.subject, result=args.result,
|
|
1680
|
+
checked_by=args.checked_by,
|
|
1681
|
+
source_ref=args.source_ref, notes=args.notes)
|
|
1682
|
+
except ValueError as exc:
|
|
1683
|
+
error(str(exc), meta={"command": "efrsb record"}, json_mode=args.json_mode)
|
|
1684
|
+
return 1
|
|
1685
|
+
init_db(args.state_dir)
|
|
1686
|
+
conn = get_connection(args.state_dir)
|
|
1687
|
+
try:
|
|
1688
|
+
try:
|
|
1689
|
+
res = add_case_evidence(conn, args.case, title=payload["title"],
|
|
1690
|
+
path=None, note=payload["note"])
|
|
1691
|
+
except (DocNotFoundError, EvidenceError) as exc:
|
|
1692
|
+
return error({"status": "INVALID_ARGUMENTS", "detail": str(exc)},
|
|
1693
|
+
meta={"command": "efrsb record", "state_dir": str(args.state_dir)},
|
|
1694
|
+
json_mode=args.json_mode)
|
|
1695
|
+
finally:
|
|
1696
|
+
conn.close()
|
|
1697
|
+
if res is None:
|
|
1698
|
+
return error(f"case not found: {args.case}",
|
|
1699
|
+
meta={"command": "efrsb record"}, json_mode=args.json_mode)
|
|
1700
|
+
result(res, meta={"command": "efrsb record", "state_dir": str(args.state_dir)},
|
|
1701
|
+
json_mode=args.json_mode)
|
|
1702
|
+
return 0
|
|
1703
|
+
|
|
1704
|
+
try:
|
|
1705
|
+
if args.efrsb_cmd == "companies":
|
|
1706
|
+
payload = ep.search_companies(inn=args.inn, ogrn=args.ogrn,
|
|
1707
|
+
name=args.name, limit=args.limit)
|
|
1708
|
+
elif args.efrsb_cmd == "persons":
|
|
1709
|
+
payload = ep.search_persons(args.name, birth_date=args.birth_date,
|
|
1710
|
+
limit=args.limit)
|
|
1711
|
+
elif args.efrsb_cmd == "publications":
|
|
1712
|
+
payload = ep.case_publications(args.case_id, limit=args.limit)
|
|
1713
|
+
else:
|
|
1714
|
+
payload = ep.search_managers(search=args.search, inn=args.inn,
|
|
1715
|
+
sro=args.sro, limit=args.limit)
|
|
1716
|
+
except ep.EfrsbPublicError as exc:
|
|
1717
|
+
error(str(exc), meta={"command": "efrsb"}, json_mode=args.json_mode)
|
|
1718
|
+
return 1
|
|
1719
|
+
result(payload,
|
|
1720
|
+
meta={"command": "efrsb", "source": "bankrot.fedresurs.ru"},
|
|
1721
|
+
json_mode=args.json_mode)
|
|
1722
|
+
return 0
|
|
1723
|
+
|
|
1724
|
+
|
|
1725
|
+
def cmd_egrul(args):
|
|
1726
|
+
"""ЕГРЮЛ/ЕГРИП (egrul.nalog.ru): карточка контрагента из открытых данных ФНС."""
|
|
1727
|
+
from sud_cli import egrul_nalog
|
|
1728
|
+
|
|
1729
|
+
try:
|
|
1730
|
+
payload = egrul_nalog.search(args.query, exact_name=args.name_eq)
|
|
1731
|
+
except egrul_nalog.EgrulError as exc:
|
|
1732
|
+
error(str(exc), meta={"command": "egrul"}, json_mode=args.json_mode)
|
|
1733
|
+
return 1
|
|
1734
|
+
result(payload,
|
|
1735
|
+
meta={"command": "egrul", "source": "egrul.nalog.ru",
|
|
1736
|
+
"basis": _wm.render_line("egrul_basis", __version__),
|
|
1737
|
+
"rows": len(payload.get("rows") or [])},
|
|
1738
|
+
json_mode=args.json_mode)
|
|
1739
|
+
return 0
|
|
1740
|
+
|
|
1741
|
+
|
|
1742
|
+
def cmd_license(args):
|
|
1743
|
+
"""§13 AGPL: лицензия, источник сборки, флаги сборки (v2.17.0 §13-интерфейс)."""
|
|
1744
|
+
from sud_cli import _wm
|
|
1745
|
+
from sud_cli import __version__ as _v
|
|
1746
|
+
payload = _wm.source_info(_v)
|
|
1747
|
+
payload["build_flags"] = _wm.build_flags(_v) if getattr(args, "with_build_flags", True) else []
|
|
1748
|
+
result(payload, meta={"command": "license", "policy": "AGPL-3.0-only; сетевой сервис = обязанность отдать исходники"},
|
|
1749
|
+
json_mode=args.json_mode)
|
|
1750
|
+
return 0
|
|
1751
|
+
|
|
1752
|
+
|
|
1753
|
+
def cmd_fssp_check(args):
|
|
1754
|
+
"""План ручной проверки БДИП — офлайн, сетевых запросов нет (юридическая проверка 25.10)."""
|
|
1755
|
+
from sud_cli import fssp_manual
|
|
1756
|
+
|
|
1757
|
+
try:
|
|
1758
|
+
payload = fssp_manual.check_plan(args.kind, name=args.name, inn=args.inn,
|
|
1759
|
+
ogrn=args.ogrn, birth_date=args.birth_date,
|
|
1760
|
+
region=args.region)
|
|
1761
|
+
except ValueError as exc:
|
|
1762
|
+
error(str(exc), meta={"command": "fssp check"}, json_mode=args.json_mode)
|
|
1763
|
+
return 1
|
|
1764
|
+
payload["policy_line"] = _wm.render_line("fssp_captcha", __version__)
|
|
1765
|
+
result(payload, meta={"command": "fssp check"}, json_mode=args.json_mode)
|
|
1766
|
+
return 0
|
|
1767
|
+
|
|
1768
|
+
|
|
1769
|
+
def cmd_plenum(args):
|
|
1770
|
+
"""Корпус разъяснений ВС/ВАС (S-55): список/карточка/поиск/проверка хэшей/импорт."""
|
|
1771
|
+
from sud_cli import plenum_corpus as pc
|
|
1772
|
+
|
|
1773
|
+
try:
|
|
1774
|
+
if args.plenum_cmd == "list":
|
|
1775
|
+
payload = pc.list_cards(args.state_dir)
|
|
1776
|
+
elif args.plenum_cmd == "show":
|
|
1777
|
+
payload = pc.get_card(args.state_dir, args.doc_id)
|
|
1778
|
+
elif args.plenum_cmd == "search":
|
|
1779
|
+
payload = pc.search(args.state_dir, args.text)
|
|
1780
|
+
elif args.plenum_cmd == "verify":
|
|
1781
|
+
payload = pc.verify(args.state_dir)
|
|
1782
|
+
else: # import
|
|
1783
|
+
card = json.loads(Path(args.file).read_text(encoding="utf-8"))
|
|
1784
|
+
if "court_ruling_card" in card:
|
|
1785
|
+
card = card["court_ruling_card"]
|
|
1786
|
+
payload = pc.add_card(args.state_dir, card, rehash=True)
|
|
1787
|
+
except pc.PlenumError as exc:
|
|
1788
|
+
error(str(exc), meta={"command": "plenum " + args.plenum_cmd}, json_mode=args.json_mode)
|
|
1789
|
+
return 1
|
|
1790
|
+
except FileNotFoundError as exc:
|
|
1791
|
+
error(f"file not found: {exc}", meta={"command": "plenum import"}, json_mode=args.json_mode)
|
|
1792
|
+
return 1
|
|
1793
|
+
result(payload, meta={"command": "plenum " + args.plenum_cmd,
|
|
1794
|
+
"sources_policy": "только официальные публикации (vsrf/pravo/Бюллетень; ФКЗ 8-ФКЗ)"},
|
|
1795
|
+
json_mode=args.json_mode)
|
|
1796
|
+
return 0
|
|
1797
|
+
|
|
1798
|
+
|
|
1799
|
+
def cmd_fssp_record(args):
|
|
1800
|
+
"""Результат ручной проверки → реестр доказательств дела (provenance обязателен)."""
|
|
1801
|
+
from sud_cli import fssp_manual
|
|
1802
|
+
from sud_cli.db import (DocNotFoundError, EvidenceError, add_case_evidence,
|
|
1803
|
+
get_connection, init_db)
|
|
1804
|
+
|
|
1805
|
+
try:
|
|
1806
|
+
payload = fssp_manual.record_payload(subject=args.subject, result=args.result,
|
|
1807
|
+
checked_by=args.checked_by, notes=args.notes,
|
|
1808
|
+
evidence_file=args.file)
|
|
1809
|
+
except ValueError as exc:
|
|
1810
|
+
error(str(exc), meta={"command": "fssp record"}, json_mode=args.json_mode)
|
|
1811
|
+
return 1
|
|
1812
|
+
init_db(args.state_dir)
|
|
1813
|
+
conn = get_connection(args.state_dir)
|
|
1814
|
+
try:
|
|
1815
|
+
try:
|
|
1816
|
+
res = add_case_evidence(conn, args.case, title=payload["title"],
|
|
1817
|
+
path=payload["evidence_file"], note=payload["note"])
|
|
1818
|
+
except (DocNotFoundError, EvidenceError) as exc:
|
|
1819
|
+
return error({"status": "INVALID_ARGUMENTS", "detail": str(exc)},
|
|
1820
|
+
meta={"command": "fssp record", "state_dir": str(args.state_dir)},
|
|
1821
|
+
json_mode=args.json_mode)
|
|
1822
|
+
finally:
|
|
1823
|
+
conn.close()
|
|
1824
|
+
if res is None:
|
|
1825
|
+
return error(f"case not found: {args.case}",
|
|
1826
|
+
meta={"command": "fssp record"}, json_mode=args.json_mode)
|
|
1827
|
+
result(res, meta={"command": "fssp record", "state_dir": str(args.state_dir)},
|
|
1828
|
+
json_mode=args.json_mode)
|
|
1829
|
+
return 0
|
|
1830
|
+
|
|
1831
|
+
|
|
1832
|
+
def cmd_fssp_assist(args):
|
|
1833
|
+
"""Полу-автомат ФССП: старт или возврат с кодом капчи от человека."""
|
|
1834
|
+
from sud_cli import fssp_semi
|
|
1835
|
+
|
|
1836
|
+
try:
|
|
1837
|
+
if args.resume:
|
|
1838
|
+
if not args.code:
|
|
1839
|
+
return error("assist --resume requires --code (human-read captcha)",
|
|
1840
|
+
meta={"command": "fssp assist"}, json_mode=args.json_mode)
|
|
1841
|
+
payload = fssp_semi.resume(args.state_dir, args.resume, args.code)
|
|
1842
|
+
else:
|
|
1843
|
+
if not args.kind:
|
|
1844
|
+
return error("assist requires --kind fiz|ur (or --resume/--code)",
|
|
1845
|
+
meta={"command": "fssp assist"}, json_mode=args.json_mode)
|
|
1846
|
+
payload = fssp_semi.start(args.state_dir, kind=args.kind, name=args.name,
|
|
1847
|
+
inn=args.inn, birth_date=args.birth_date)
|
|
1848
|
+
except fssp_semi.FsspAssistError as exc:
|
|
1849
|
+
return error(str(exc), meta={"command": "fssp assist"}, json_mode=args.json_mode)
|
|
1850
|
+
# из состояния убираем служебный путь state_dir для чистоты вывода
|
|
1851
|
+
public = {k: v for k, v in payload.items() if k not in ("state_dir", "user_id", "tab_id")}
|
|
1852
|
+
result(public, meta={"command": "fssp assist",
|
|
1853
|
+
"policy": "капчу решает человек; авто.solve запрещён актом legal"},
|
|
1854
|
+
json_mode=args.json_mode)
|
|
1855
|
+
return 0
|
|
1856
|
+
|
|
1857
|
+
|
|
1858
|
+
def cmd_fssp_stats(args):
|
|
1859
|
+
"""Официальная статистика БДИП (общедоступный CSV ФССП)."""
|
|
1860
|
+
from sud_cli import fssp_stats
|
|
1861
|
+
|
|
1862
|
+
try:
|
|
1863
|
+
payload = fssp_stats.statistics(args.region)
|
|
1864
|
+
except fssp_stats.FsspStatsError as exc:
|
|
1865
|
+
error(str(exc), meta={"command": "fssp stats"}, json_mode=args.json_mode)
|
|
1866
|
+
return 1
|
|
1867
|
+
result(payload, meta={"command": "fssp stats", "source": fssp_stats.STATS_URL},
|
|
1868
|
+
json_mode=args.json_mode)
|
|
1869
|
+
return 0
|
|
1870
|
+
|
|
1871
|
+
|
|
1872
|
+
def cmd_native_text(args):
|
|
1873
|
+
from sud_cli.case_search import get_decision_text
|
|
1874
|
+
return _emit_native(get_decision_text(str(args.file)), "extract-text", args)
|
|
1875
|
+
|
|
1876
|
+
|
|
1877
|
+
def cmd_native_import_card(args):
|
|
1878
|
+
from sud_cli.case_search import import_document
|
|
1879
|
+
return _emit_native(import_document(args.file, args.case_id, store_dir=args.state_dir / "snapshots"), "import-card", args)
|
|
1880
|
+
|
|
1881
|
+
|
|
1882
|
+
def _render_disclaimer(rules, calc_date):
|
|
1883
|
+
"""The specification makes the reference-only disclaimer mandatory."""
|
|
1884
|
+
return rules["mandatory_disclaimer"].format(calc_date=calc_date)
|
|
1885
|
+
|
|
1886
|
+
|
|
1887
|
+
def _print_disclaimer(disclaimer, json_mode=False):
|
|
1888
|
+
# Printed to stderr so it survives JSON mode: a consumer parsing stdout
|
|
1889
|
+
# must still be able to show the user that this is advisory, not advice.
|
|
1890
|
+
#
|
|
1891
|
+
# In --json mode it must NOT be printed as bare text: a caller that parses
|
|
1892
|
+
# stderr as JSON would choke on the Russian notice, and the project's
|
|
1893
|
+
# convention is typed output on both streams. The disclaimer is still
|
|
1894
|
+
# carried — in the successful payload as ``disclaimer``, and for refusals
|
|
1895
|
+
# the caller gets a JSON envelope with the same rule metadata. Skipping it
|
|
1896
|
+
# here keeps the stream machine-readable; the notice is not lost, it moves
|
|
1897
|
+
# into the JSON.
|
|
1898
|
+
if json_mode:
|
|
1899
|
+
return
|
|
1900
|
+
print(disclaimer, file=sys.stderr)
|
|
1901
|
+
|
|
1902
|
+
|
|
1903
|
+
def cmd_fees(args):
|
|
1904
|
+
from datetime import date as _date
|
|
1905
|
+
|
|
1906
|
+
from .legal_engine import (
|
|
1907
|
+
STATUS_OK,
|
|
1908
|
+
calculate_fee,
|
|
1909
|
+
load_rules,
|
|
1910
|
+
parse_as_of,
|
|
1911
|
+
validate_claim_class,
|
|
1912
|
+
)
|
|
1913
|
+
|
|
1914
|
+
try:
|
|
1915
|
+
rules = load_rules()
|
|
1916
|
+
except Exception as exc: # rules file missing/corrupt must fail closed
|
|
1917
|
+
error(
|
|
1918
|
+
{"status": "RULES_UNAVAILABLE", "detail": str(exc)},
|
|
1919
|
+
meta={"command": "fees"},
|
|
1920
|
+
json_mode=args.json_mode,
|
|
1921
|
+
)
|
|
1922
|
+
return 1
|
|
1923
|
+
|
|
1924
|
+
# ``--as-of`` is parsed by the same strict validator the engine uses, so CLI
|
|
1925
|
+
# and MCP cannot disagree about which date strings are legal. A bad date is
|
|
1926
|
+
# an argument error, not a calculation result: nothing is computed and the
|
|
1927
|
+
# exit code says so.
|
|
1928
|
+
as_of_value = None
|
|
1929
|
+
if getattr(args, "as_of", None) is not None:
|
|
1930
|
+
try:
|
|
1931
|
+
as_of_value = parse_as_of(args.as_of)
|
|
1932
|
+
except ValueError as exc:
|
|
1933
|
+
error(
|
|
1934
|
+
{"status": "INVALID_ARGUMENTS", "detail": f"--as-of: {exc}"},
|
|
1935
|
+
meta={"command": "fees"},
|
|
1936
|
+
json_mode=args.json_mode,
|
|
1937
|
+
)
|
|
1938
|
+
return 1
|
|
1939
|
+
|
|
1940
|
+
if args.claim_class is not None:
|
|
1941
|
+
class_result = validate_claim_class(args.claim_class, rules, args.kind)
|
|
1942
|
+
if class_result.status != STATUS_OK:
|
|
1943
|
+
error(
|
|
1944
|
+
class_result.to_dict(),
|
|
1945
|
+
meta={"command": "fees", "rules_version": rules["version"],
|
|
1946
|
+
"basis": _wm.render_line("fees_basis", __version__)},
|
|
1947
|
+
json_mode=args.json_mode,
|
|
1948
|
+
)
|
|
1949
|
+
return 1
|
|
1950
|
+
|
|
1951
|
+
fee = calculate_fee(
|
|
1952
|
+
claim_amount=args.claim_amount,
|
|
1953
|
+
jurisdiction=args.jurisdiction,
|
|
1954
|
+
fee_kind=args.kind,
|
|
1955
|
+
applicant_type=args.applicant,
|
|
1956
|
+
rules=rules,
|
|
1957
|
+
as_of=as_of_value,
|
|
1958
|
+
)
|
|
1959
|
+
|
|
1960
|
+
calc_date = _date.today().isoformat()
|
|
1961
|
+
_print_disclaimer(_render_disclaimer(rules, calc_date), args.json_mode)
|
|
1962
|
+
|
|
1963
|
+
meta = {
|
|
1964
|
+
"command": "fees",
|
|
1965
|
+
"rules_version": rules["version"],
|
|
1966
|
+
"tariff_version": rules["tariff_version"],
|
|
1967
|
+
"calc_date": calc_date,
|
|
1968
|
+
}
|
|
1969
|
+
if as_of_value is not None:
|
|
1970
|
+
# The number's provenance belongs in the machine-readable envelope: which
|
|
1971
|
+
# edition produced it, its window, and any fallback warning.
|
|
1972
|
+
meta["as_of"] = as_of_value.isoformat()
|
|
1973
|
+
for key, meta_key in (
|
|
1974
|
+
("edition_id", "tariff_edition_id"),
|
|
1975
|
+
("tariff_effective_window", "tariff_effective_window"),
|
|
1976
|
+
("basis", "tariff_basis"),
|
|
1977
|
+
("tariff_scale", "tariff_scale"),
|
|
1978
|
+
):
|
|
1979
|
+
if fee.meta.get(key):
|
|
1980
|
+
meta[meta_key] = fee.meta[key]
|
|
1981
|
+
if fee.meta.get("warnings"):
|
|
1982
|
+
meta["tariff_warnings"] = list(fee.meta["warnings"])
|
|
1983
|
+
if not args.json_mode:
|
|
1984
|
+
for line in fee.meta["warnings"]:
|
|
1985
|
+
print(f"ПРЕДУПРЕЖДЕНИЕ: {line}", file=sys.stderr)
|
|
1986
|
+
|
|
1987
|
+
if fee.status != STATUS_OK:
|
|
1988
|
+
payload_err = fee.to_dict()
|
|
1989
|
+
if getattr(args, "explain", False):
|
|
1990
|
+
from .explain import fees_explain
|
|
1991
|
+
payload_err["explain"] = fees_explain(
|
|
1992
|
+
fee_doc=fee.to_dict(), rules=rules,
|
|
1993
|
+
as_of_iso=as_of_value.isoformat() if as_of_value else None)
|
|
1994
|
+
error(payload_err, meta=meta, json_mode=args.json_mode)
|
|
1995
|
+
return 1
|
|
1996
|
+
|
|
1997
|
+
payload = fee.to_dict()
|
|
1998
|
+
if getattr(args, "explain", False):
|
|
1999
|
+
from .explain import fees_explain
|
|
2000
|
+
payload["explain"] = fees_explain(
|
|
2001
|
+
fee_doc=payload, rules=rules,
|
|
2002
|
+
as_of_iso=as_of_value.isoformat() if as_of_value else None)
|
|
2003
|
+
payload["disclaimer"] = _render_disclaimer(rules, calc_date)
|
|
2004
|
+
payload["exemptions_note"] = rules["exemptions"]["mandatory_note"]
|
|
2005
|
+
payload["clean_room"] = rules["clean_room"]
|
|
2006
|
+
result(payload, meta=meta, json_mode=args.json_mode)
|
|
2007
|
+
return 0
|
|
2008
|
+
|
|
2009
|
+
|
|
2010
|
+
def _run_deadline_calculation(args):
|
|
2011
|
+
from datetime import date as _date, datetime, timezone
|
|
2012
|
+
|
|
2013
|
+
from .legal_deadlines import calculate_deadline
|
|
2014
|
+
from .legal_engine import STATUS_OK, load_rules
|
|
2015
|
+
from sud_cli.db import (
|
|
2016
|
+
get_latest_anchored_state,
|
|
2017
|
+
insert_deadline_state,
|
|
2018
|
+
)
|
|
2019
|
+
|
|
2020
|
+
try:
|
|
2021
|
+
rules = load_rules()
|
|
2022
|
+
except Exception as exc:
|
|
2023
|
+
error(
|
|
2024
|
+
{"status": "RULES_UNAVAILABLE", "detail": str(exc)},
|
|
2025
|
+
meta={"command": "deadlines"},
|
|
2026
|
+
json_mode=args.json_mode,
|
|
2027
|
+
)
|
|
2028
|
+
return 1
|
|
2029
|
+
|
|
2030
|
+
if getattr(args, "calc_case_id", None) and not getattr(args, "case_id", None):
|
|
2031
|
+
args.case_id = args.calc_case_id
|
|
2032
|
+
if not getattr(args, "deadline_key", None):
|
|
2033
|
+
args.deadline_key = getattr(args, "calc_deadline_key", None)
|
|
2034
|
+
|
|
2035
|
+
event_date = None
|
|
2036
|
+
if getattr(args, "event_date", None):
|
|
2037
|
+
try:
|
|
2038
|
+
event_date = datetime.strptime(args.event_date, "%Y-%m-%d").date()
|
|
2039
|
+
except ValueError:
|
|
2040
|
+
error(
|
|
2041
|
+
{
|
|
2042
|
+
"status": "MISSING_REQUIRED_PARAM",
|
|
2043
|
+
"detail": f"event_date '{args.event_date}' не в формате ГГГГ-ММ-ДД",
|
|
2044
|
+
},
|
|
2045
|
+
meta={"command": "deadlines"},
|
|
2046
|
+
json_mode=args.json_mode,
|
|
2047
|
+
)
|
|
2048
|
+
return 1
|
|
2049
|
+
|
|
2050
|
+
case_id_for_anchor = getattr(args, "case_id", None)
|
|
2051
|
+
if event_date is None and case_id_for_anchor:
|
|
2052
|
+
try:
|
|
2053
|
+
from .db import get_connection, get_latest_anchored_state, init_db
|
|
2054
|
+
init_db(args.state_dir)
|
|
2055
|
+
conn = get_connection(args.state_dir)
|
|
2056
|
+
try:
|
|
2057
|
+
anchored = get_latest_anchored_state(conn, case_id_for_anchor, args.deadline_key)
|
|
2058
|
+
finally:
|
|
2059
|
+
conn.close()
|
|
2060
|
+
if anchored:
|
|
2061
|
+
ab = anchored.get("basis") or {}
|
|
2062
|
+
if isinstance(ab, dict) and ab.get("event_date"):
|
|
2063
|
+
try:
|
|
2064
|
+
event_date = datetime.strptime(str(ab["event_date"]), "%Y-%m-%d").date()
|
|
2065
|
+
args.event_date = str(ab["event_date"])
|
|
2066
|
+
except ValueError:
|
|
2067
|
+
pass
|
|
2068
|
+
except Exception:
|
|
2069
|
+
pass
|
|
2070
|
+
|
|
2071
|
+
deadline = calculate_deadline(
|
|
2072
|
+
deadline_key=args.deadline_key,
|
|
2073
|
+
event_date=event_date,
|
|
2074
|
+
rules=rules,
|
|
2075
|
+
region=getattr(args, "region", None),
|
|
2076
|
+
tz=getattr(args, "tz", None) or "Europe/Moscow",
|
|
2077
|
+
)
|
|
2078
|
+
|
|
2079
|
+
calc_date = _date.today().isoformat()
|
|
2080
|
+
_print_disclaimer(_render_disclaimer(rules, calc_date), args.json_mode)
|
|
2081
|
+
|
|
2082
|
+
meta = {
|
|
2083
|
+
"command": "deadlines",
|
|
2084
|
+
"rules_version": rules["version"],
|
|
2085
|
+
"calc_date": calc_date,
|
|
2086
|
+
"open_questions": rules["spec_open_questions"],
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
if deadline.status != STATUS_OK:
|
|
2090
|
+
payload_err = deadline.to_dict()
|
|
2091
|
+
if getattr(args, "explain", False):
|
|
2092
|
+
from .explain import deadlines_explain
|
|
2093
|
+
payload_err["explain"] = deadlines_explain(
|
|
2094
|
+
payload=deadline.to_dict(), rules=rules,
|
|
2095
|
+
event_date=getattr(args, "event_date", None),
|
|
2096
|
+
region=getattr(args, "region", None),
|
|
2097
|
+
tz=getattr(args, "tz", None))
|
|
2098
|
+
error(payload_err, meta=meta, json_mode=args.json_mode)
|
|
2099
|
+
return 1
|
|
2100
|
+
|
|
2101
|
+
case_id = getattr(args, "case_id", None)
|
|
2102
|
+
warning_basis = None
|
|
2103
|
+
state = "computed"
|
|
2104
|
+
confirmed_by = None
|
|
2105
|
+
|
|
2106
|
+
if case_id:
|
|
2107
|
+
from sud_cli.db import get_connection, init_db
|
|
2108
|
+
init_db(args.state_dir)
|
|
2109
|
+
conn = get_connection(args.state_dir)
|
|
2110
|
+
try:
|
|
2111
|
+
anchored = get_latest_anchored_state(conn, case_id, args.deadline_key)
|
|
2112
|
+
if anchored is None:
|
|
2113
|
+
warning_basis = "event_date_assumed"
|
|
2114
|
+
basis = anchored.get("basis") if anchored else {
|
|
2115
|
+
"event_date": args.event_date,
|
|
2116
|
+
"assumed": True,
|
|
2117
|
+
"actor": "calculator",
|
|
2118
|
+
"ts": datetime.now(timezone.utc).replace(microsecond=0).isoformat(),
|
|
2119
|
+
}
|
|
2120
|
+
# calculate due_at already
|
|
2121
|
+
due_at = deadline.deadline.isoformat() if deadline.deadline else None
|
|
2122
|
+
row = insert_deadline_state(
|
|
2123
|
+
conn, case_id, args.deadline_key, due_at=due_at, basis=basis,
|
|
2124
|
+
state="computed", state_dir=None,
|
|
2125
|
+
)
|
|
2126
|
+
if getattr(args, "confirm", False):
|
|
2127
|
+
actor = getattr(args, "by", None) or "calculator"
|
|
2128
|
+
from sud_cli.db import confirm_deadline_state
|
|
2129
|
+
row = confirm_deadline_state(conn, args.state_dir, case_id, args.deadline_key, actor,
|
|
2130
|
+
evidence=getattr(args, "evidence", None))
|
|
2131
|
+
state = "confirmed"
|
|
2132
|
+
confirmed_by = actor
|
|
2133
|
+
# if waive requested
|
|
2134
|
+
if getattr(args, "waive", False) and getattr(args, "norm", None):
|
|
2135
|
+
actor = getattr(args, "by", None) or "calculator"
|
|
2136
|
+
from sud_cli.db import waive_deadline_state
|
|
2137
|
+
row = waive_deadline_state(conn, args.state_dir, case_id, args.deadline_key, actor,
|
|
2138
|
+
getattr(args, "norm"), evidence=getattr(args, "evidence", None))
|
|
2139
|
+
state = "waived"
|
|
2140
|
+
confirmed_by = actor
|
|
2141
|
+
finally:
|
|
2142
|
+
conn.close()
|
|
2143
|
+
|
|
2144
|
+
payload = deadline.to_dict()
|
|
2145
|
+
if getattr(args, "explain", False):
|
|
2146
|
+
from .explain import deadlines_explain
|
|
2147
|
+
payload["explain"] = deadlines_explain(
|
|
2148
|
+
payload=deadline.to_dict(), rules=rules,
|
|
2149
|
+
event_date=getattr(args, "event_date", None),
|
|
2150
|
+
region=getattr(args, "region", None),
|
|
2151
|
+
tz=getattr(args, "tz", None))
|
|
2152
|
+
payload["disclaimer"] = _render_disclaimer(rules, calc_date)
|
|
2153
|
+
payload["clean_room"] = rules["clean_room"]
|
|
2154
|
+
if case_id:
|
|
2155
|
+
payload["state"] = state
|
|
2156
|
+
payload["confirmed_by"] = confirmed_by
|
|
2157
|
+
if warning_basis:
|
|
2158
|
+
payload["warning_basis"] = warning_basis
|
|
2159
|
+
result(payload, meta=meta, json_mode=args.json_mode)
|
|
2160
|
+
return 0
|
|
2161
|
+
|
|
2162
|
+
|
|
2163
|
+
def cmd_deadlines(args):
|
|
2164
|
+
sub = getattr(args, "deadlines_command", None)
|
|
2165
|
+
if sub in ("calculate", "calc"):
|
|
2166
|
+
# normalize positional
|
|
2167
|
+
if getattr(args, "calc_case_id", None) and not getattr(args, "case_id", None):
|
|
2168
|
+
args.case_id = args.calc_case_id
|
|
2169
|
+
if not getattr(args, "deadline_key", None):
|
|
2170
|
+
args.deadline_key = getattr(args, "calc_deadline_key", None)
|
|
2171
|
+
# map event_date already
|
|
2172
|
+
return _run_deadline_calculation(args)
|
|
2173
|
+
if sub == "confirm":
|
|
2174
|
+
return cmd_deadlines_confirm(args)
|
|
2175
|
+
if sub == "anchor":
|
|
2176
|
+
return cmd_deadlines_anchor(args)
|
|
2177
|
+
if sub == "waive":
|
|
2178
|
+
return cmd_deadlines_waive(args)
|
|
2179
|
+
if sub == "audit":
|
|
2180
|
+
return cmd_deadlines_audit(args)
|
|
2181
|
+
if sub == "list":
|
|
2182
|
+
return cmd_deadlines_list(args)
|
|
2183
|
+
if sub == "matrix":
|
|
2184
|
+
return cmd_deadlines_matrix(args)
|
|
2185
|
+
if sub == "chain":
|
|
2186
|
+
return cmd_deadlines_chain(args)
|
|
2187
|
+
|
|
2188
|
+
# legacy: if no deadline_key but a case_id-like first? handle
|
|
2189
|
+
if getattr(args, "deadline_key", None) is None:
|
|
2190
|
+
# maybe called as: deadlines <case_id> <key>? not supported; error
|
|
2191
|
+
error({"status": "MISSING_REQUIRED_PARAM", "detail": "deadline_key required"}, meta={"command": "deadlines"}, json_mode=args.json_mode)
|
|
2192
|
+
return 1
|
|
2193
|
+
|
|
2194
|
+
# support calculate with case_id in legacy positional? If deadline_key looks like case? no.
|
|
2195
|
+
return _run_deadline_calculation(args)
|
|
2196
|
+
|
|
2197
|
+
|
|
2198
|
+
def cmd_deadlines_calculate(args):
|
|
2199
|
+
if getattr(args, "calc_case_id", None) and not getattr(args, "case_id", None):
|
|
2200
|
+
args.case_id = args.calc_case_id
|
|
2201
|
+
if not getattr(args, "deadline_key", None):
|
|
2202
|
+
args.deadline_key = getattr(args, "calc_deadline_key", None)
|
|
2203
|
+
return _run_deadline_calculation(args)
|
|
2204
|
+
|
|
2205
|
+
|
|
2206
|
+
def cmd_deadlines_confirm(args):
|
|
2207
|
+
from sud_cli.db import confirm_deadline_state, get_connection, init_db
|
|
2208
|
+
init_db(args.state_dir)
|
|
2209
|
+
conn = get_connection(args.state_dir)
|
|
2210
|
+
try:
|
|
2211
|
+
if getattr(args, "waive", False) or getattr(args, "norm", None):
|
|
2212
|
+
if not getattr(args, "norm", None):
|
|
2213
|
+
return error({"status": "INVALID_ARGUMENTS", "detail": "norm required for waive"}, meta={"command": "deadlines confirm"}, json_mode=args.json_mode)
|
|
2214
|
+
row = confirm_deadline_state(conn, args.state_dir, args.case_id, args.key, args.by,
|
|
2215
|
+
evidence=getattr(args, "evidence", None), waive=True, norm=args.norm)
|
|
2216
|
+
else:
|
|
2217
|
+
row = confirm_deadline_state(conn, args.state_dir, args.case_id, args.key, args.by,
|
|
2218
|
+
evidence=getattr(args, "evidence", None))
|
|
2219
|
+
finally:
|
|
2220
|
+
conn.close()
|
|
2221
|
+
result(row, meta={"command": "deadlines confirm", "state_dir": str(args.state_dir)}, json_mode=args.json_mode)
|
|
2222
|
+
return 0
|
|
2223
|
+
|
|
2224
|
+
|
|
2225
|
+
def cmd_deadlines_anchor(args):
|
|
2226
|
+
from sud_cli.db import anchor_deadline_state, get_connection, init_db
|
|
2227
|
+
init_db(args.state_dir)
|
|
2228
|
+
conn = get_connection(args.state_dir)
|
|
2229
|
+
try:
|
|
2230
|
+
actor = getattr(args, "by", None) or "cli"
|
|
2231
|
+
try:
|
|
2232
|
+
row = anchor_deadline_state(conn, args.state_dir, args.case_id, args.key,
|
|
2233
|
+
args.event_date, args.method, args.evidence, actor=actor)
|
|
2234
|
+
except ValueError as exc:
|
|
2235
|
+
return error(
|
|
2236
|
+
{"status": "invalid_deadline_key", "detail": str(exc)},
|
|
2237
|
+
meta={"command": "deadlines anchor", "state_dir": str(args.state_dir)},
|
|
2238
|
+
json_mode=args.json_mode,
|
|
2239
|
+
)
|
|
2240
|
+
finally:
|
|
2241
|
+
conn.close()
|
|
2242
|
+
result(row, meta={"command": "deadlines anchor", "state_dir": str(args.state_dir)}, json_mode=args.json_mode)
|
|
2243
|
+
return 0
|
|
2244
|
+
|
|
2245
|
+
|
|
2246
|
+
def cmd_deadlines_waive(args):
|
|
2247
|
+
from sud_cli.db import get_connection, init_db, waive_deadline_state
|
|
2248
|
+
init_db(args.state_dir)
|
|
2249
|
+
conn = get_connection(args.state_dir)
|
|
2250
|
+
try:
|
|
2251
|
+
row = waive_deadline_state(conn, args.state_dir, args.case_id, args.key, args.by, args.norm,
|
|
2252
|
+
evidence=getattr(args, "evidence", None))
|
|
2253
|
+
finally:
|
|
2254
|
+
conn.close()
|
|
2255
|
+
result(row, meta={"command": "deadlines waive", "state_dir": str(args.state_dir)}, json_mode=args.json_mode)
|
|
2256
|
+
return 0
|
|
2257
|
+
|
|
2258
|
+
|
|
2259
|
+
def cmd_deadlines_audit(args):
|
|
2260
|
+
from sud_cli.db import get_connection, init_db, read_deadline_audit
|
|
2261
|
+
init_db(args.state_dir)
|
|
2262
|
+
conn = get_connection(args.state_dir)
|
|
2263
|
+
try:
|
|
2264
|
+
entries = read_deadline_audit(args.state_dir, args.case_id)
|
|
2265
|
+
finally:
|
|
2266
|
+
conn.close()
|
|
2267
|
+
result(entries, meta={"command": "deadlines audit", "state_dir": str(args.state_dir)}, json_mode=args.json_mode)
|
|
2268
|
+
return 0
|
|
2269
|
+
|
|
2270
|
+
|
|
2271
|
+
def cmd_deadlines_list(args):
|
|
2272
|
+
from sud_cli.db import get_connection, init_db, list_all_deadline_states, list_deadline_states
|
|
2273
|
+
init_db(args.state_dir)
|
|
2274
|
+
conn = get_connection(args.state_dir)
|
|
2275
|
+
try:
|
|
2276
|
+
if getattr(args, "case_id", None):
|
|
2277
|
+
rows = list_deadline_states(conn, args.case_id)
|
|
2278
|
+
else:
|
|
2279
|
+
rows = list_all_deadline_states(conn)
|
|
2280
|
+
finally:
|
|
2281
|
+
conn.close()
|
|
2282
|
+
result(rows, meta={"command": "deadlines list", "state_dir": str(args.state_dir)}, json_mode=args.json_mode)
|
|
2283
|
+
return 0
|
|
2284
|
+
|
|
2285
|
+
|
|
2286
|
+
def cmd_deadlines_matrix(args):
|
|
2287
|
+
from datetime import date as _date
|
|
2288
|
+
|
|
2289
|
+
from .deadline_matrix import build_matrix
|
|
2290
|
+
from .legal_engine import load_rules
|
|
2291
|
+
|
|
2292
|
+
case_id = getattr(args, "case_id", None)
|
|
2293
|
+
if not case_id:
|
|
2294
|
+
return error({"status": "MISSING_REQUIRED_PARAM", "detail": "case_id required"},
|
|
2295
|
+
meta={"command": "deadlines matrix"}, json_mode=args.json_mode)
|
|
2296
|
+
|
|
2297
|
+
try:
|
|
2298
|
+
rules = load_rules()
|
|
2299
|
+
except Exception as exc:
|
|
2300
|
+
return error({"status": "RULES_UNAVAILABLE", "detail": str(exc)},
|
|
2301
|
+
meta={"command": "deadlines matrix"}, json_mode=args.json_mode)
|
|
2302
|
+
|
|
2303
|
+
try:
|
|
2304
|
+
instance, points = build_matrix(args.state_dir, case_id,
|
|
2305
|
+
instance_override=getattr(args, "instance", None),
|
|
2306
|
+
rules=rules, as_of=getattr(args, "as_of", None))
|
|
2307
|
+
except ValueError as exc:
|
|
2308
|
+
return error({"status": "INVALID_ARGUMENTS", "detail": str(exc)},
|
|
2309
|
+
meta={"command": "deadlines matrix", "state_dir": str(args.state_dir)},
|
|
2310
|
+
json_mode=args.json_mode)
|
|
2311
|
+
|
|
2312
|
+
if getattr(args, "missing", False):
|
|
2313
|
+
points = [p for p in points if p.get("needs_anchor")]
|
|
2314
|
+
|
|
2315
|
+
calc_date = _date.today().isoformat()
|
|
2316
|
+
disclaimer = _render_disclaimer(rules, calc_date)
|
|
2317
|
+
_print_disclaimer(disclaimer, args.json_mode)
|
|
2318
|
+
|
|
2319
|
+
payload = {
|
|
2320
|
+
"case_id": case_id,
|
|
2321
|
+
"instance": instance,
|
|
2322
|
+
"as_of": getattr(args, "as_of", None),
|
|
2323
|
+
"points": points,
|
|
2324
|
+
"matrix": points,
|
|
2325
|
+
"items": points,
|
|
2326
|
+
"deadlines": points,
|
|
2327
|
+
"entries": points,
|
|
2328
|
+
"disclaimer": disclaimer,
|
|
2329
|
+
"clean_room": rules.get("clean_room"),
|
|
2330
|
+
}
|
|
2331
|
+
meta = {
|
|
2332
|
+
"command": "deadlines matrix",
|
|
2333
|
+
"rules_version": rules["version"],
|
|
2334
|
+
"calc_date": calc_date,
|
|
2335
|
+
"state_dir": str(args.state_dir),
|
|
2336
|
+
}
|
|
2337
|
+
result(payload, meta=meta, json_mode=args.json_mode)
|
|
2338
|
+
return 0
|
|
2339
|
+
|
|
2340
|
+
|
|
2341
|
+
def cmd_deadlines_autofill(args):
|
|
2342
|
+
"""W6a §5 (S-23): автозаполнение точек из текстов актов.
|
|
2343
|
+
|
|
2344
|
+
created — только state=computed с needs_confirm; confirmed autofill не
|
|
2345
|
+
делает никогда; cure-маркеры без VERIFIED строки матрицы отдаются
|
|
2346
|
+
manual_review-hint, точка не выдумывается.
|
|
2347
|
+
"""
|
|
2348
|
+
from .autofill import autofill_case
|
|
2349
|
+
from sud_cli.db import get_connection, init_db
|
|
2350
|
+
|
|
2351
|
+
init_db(args.state_dir)
|
|
2352
|
+
conn = get_connection(args.state_dir)
|
|
2353
|
+
try:
|
|
2354
|
+
try:
|
|
2355
|
+
payload = autofill_case(conn, args.state_dir, args.case_id,
|
|
2356
|
+
event_id=getattr(args, "event_id", None))
|
|
2357
|
+
except ValueError as exc:
|
|
2358
|
+
return error({"status": "INVALID_ARGUMENTS", "detail": str(exc)},
|
|
2359
|
+
meta={"command": "deadlines autofill",
|
|
2360
|
+
"state_dir": str(args.state_dir)},
|
|
2361
|
+
json_mode=args.json_mode)
|
|
2362
|
+
finally:
|
|
2363
|
+
conn.close()
|
|
2364
|
+
result(payload, meta={"command": "deadlines autofill",
|
|
2365
|
+
"state_dir": str(args.state_dir)},
|
|
2366
|
+
json_mode=args.json_mode)
|
|
2367
|
+
return 0
|
|
2368
|
+
|
|
2369
|
+
|
|
2370
|
+
def cmd_calendar_agenda(args):
|
|
2371
|
+
from .agenda import agenda_payload
|
|
2372
|
+
|
|
2373
|
+
# W6a §1: тело вынесено в sud_cli.agenda без изменения формы конверта —
|
|
2374
|
+
# CLI и MCP вызывают один и тот же билдер (паритет S-80).
|
|
2375
|
+
payload = agenda_payload(args.state_dir, days=getattr(args, "days", 14))
|
|
2376
|
+
result(payload, meta={"command": "calendar agenda", "state_dir": str(args.state_dir)}, json_mode=args.json_mode)
|
|
2377
|
+
return 0
|
|
2378
|
+
|
|
2379
|
+
|
|
2380
|
+
def cmd_calendar_ics(args):
|
|
2381
|
+
"""W6a §3 (S-28): RFC 5545 экспорт computed/confirmed сроков."""
|
|
2382
|
+
from .calendar_ics import ics_for_state_dir
|
|
2383
|
+
|
|
2384
|
+
days = args.days
|
|
2385
|
+
if not isinstance(days, int) or days < 1 or days > 3650:
|
|
2386
|
+
return error(
|
|
2387
|
+
{"status": "INVALID_ARGUMENTS",
|
|
2388
|
+
"detail": f"--days должен быть целым 1..3650, получен {days!r}"},
|
|
2389
|
+
meta={"command": "calendar ics", "state_dir": str(args.state_dir)},
|
|
2390
|
+
json_mode=args.json_mode,
|
|
2391
|
+
)
|
|
2392
|
+
text, events = ics_for_state_dir(args.state_dir, days=days)
|
|
2393
|
+
payload = {
|
|
2394
|
+
"days": days,
|
|
2395
|
+
"count": len(events),
|
|
2396
|
+
"events": events,
|
|
2397
|
+
"ics": text,
|
|
2398
|
+
}
|
|
2399
|
+
if args.out is not None:
|
|
2400
|
+
try:
|
|
2401
|
+
args.out.parent.mkdir(parents=True, exist_ok=True)
|
|
2402
|
+
args.out.write_bytes(text.encode("utf-8"))
|
|
2403
|
+
except OSError as exc:
|
|
2404
|
+
return error({"status": "INVALID_ARGUMENTS",
|
|
2405
|
+
"detail": f"--out: {exc}"},
|
|
2406
|
+
meta={"command": "calendar ics"},
|
|
2407
|
+
json_mode=args.json_mode)
|
|
2408
|
+
payload["path"] = str(args.out)
|
|
2409
|
+
result(payload, meta={"command": "calendar ics", "state_dir": str(args.state_dir)},
|
|
2410
|
+
json_mode=args.json_mode)
|
|
2411
|
+
return 0
|
|
2412
|
+
|
|
2413
|
+
|
|
2414
|
+
def cmd_interest(args):
|
|
2415
|
+
from datetime import datetime
|
|
2416
|
+
|
|
2417
|
+
from .interest import calculate_interest
|
|
2418
|
+
|
|
2419
|
+
try:
|
|
2420
|
+
start = datetime.strptime(args.start_date, "%Y-%m-%d").date()
|
|
2421
|
+
end = datetime.strptime(args.end_date, "%Y-%m-%d").date()
|
|
2422
|
+
except ValueError:
|
|
2423
|
+
return error({"status": "MISSING_REQUIRED_PARAM", "detail": "dates must use YYYY-MM-DD"}, meta={"command": "interest"}, json_mode=args.json_mode)
|
|
2424
|
+
calc = calculate_interest(args.amount, start, end, args.article, args.payments)
|
|
2425
|
+
payload = calc.to_dict()
|
|
2426
|
+
if getattr(args, "explain", False):
|
|
2427
|
+
from .explain import interest_explain
|
|
2428
|
+
payload["explain"] = interest_explain(
|
|
2429
|
+
payload=payload, article=args.article, amount=args.amount,
|
|
2430
|
+
start=args.start_date, end=args.end_date)
|
|
2431
|
+
if args.calc_fee and calc.status == "ok":
|
|
2432
|
+
from .legal_engine import calculate_fee, load_rules
|
|
2433
|
+
claim_amount = int(calc.remaining_principal) + int(calc.amount_rub)
|
|
2434
|
+
fee = calculate_fee(claim_amount=claim_amount, jurisdiction=args.jurisdiction, fee_kind=args.kind, applicant_type=args.applicant, rules=load_rules())
|
|
2435
|
+
payload.update({"principal_rub": args.amount, "claim_amount_rub": claim_amount, "fee": fee.to_dict()})
|
|
2436
|
+
emitter = result if calc.status == "ok" else error
|
|
2437
|
+
if emitter is error:
|
|
2438
|
+
return emitter(payload, meta={"command": "interest"}, json_mode=args.json_mode)
|
|
2439
|
+
emitter(payload, meta={"command": "interest"}, json_mode=args.json_mode)
|
|
2440
|
+
return 0
|
|
2441
|
+
|
|
2442
|
+
|
|
2443
|
+
def cmd_payment(args):
|
|
2444
|
+
from .payment import payment_details
|
|
2445
|
+
try:
|
|
2446
|
+
payload = payment_details(args.jurisdiction, args.amount, args.court, args.defendant)
|
|
2447
|
+
except ValueError as exc:
|
|
2448
|
+
return error(str(exc), meta={"command": "payment"}, json_mode=args.json_mode)
|
|
2449
|
+
result(payload, meta={"command": "payment"}, json_mode=args.json_mode)
|
|
2450
|
+
return 0
|
|
2451
|
+
|
|
2452
|
+
|
|
2453
|
+
def cmd_fee_exemption(args):
|
|
2454
|
+
from .fees_extra import fee_exemption_check
|
|
2455
|
+
from .legal_engine import load_rules
|
|
2456
|
+
try:
|
|
2457
|
+
rules = load_rules()
|
|
2458
|
+
except Exception as exc:
|
|
2459
|
+
return error({"status": "RULES_UNAVAILABLE", "detail": str(exc)},
|
|
2460
|
+
meta={"command": "fee-exemption"}, json_mode=args.json_mode)
|
|
2461
|
+
payload = fee_exemption_check(rules, jurisdiction=args.jurisdiction, category_code=args.category)
|
|
2462
|
+
emitter = result if payload.get("status") == "exempt" else (error if payload.get("status") == "manual_review" else result)
|
|
2463
|
+
return emitter(payload, meta={"command": "fee-exemption", "rules_version": rules["version"]},
|
|
2464
|
+
json_mode=args.json_mode) or 0
|
|
2465
|
+
|
|
2466
|
+
|
|
2467
|
+
def cmd_fee_refund(args):
|
|
2468
|
+
from .fees_extra import fee_refund
|
|
2469
|
+
from .legal_engine import load_rules
|
|
2470
|
+
from decimal import Decimal
|
|
2471
|
+
try:
|
|
2472
|
+
rules = load_rules()
|
|
2473
|
+
paid = Decimal(args.paid)
|
|
2474
|
+
assert paid >= 0
|
|
2475
|
+
except Exception as exc:
|
|
2476
|
+
return error({"status": "INVALID_ARGUMENTS", "detail": str(exc)},
|
|
2477
|
+
meta={"command": "fee-refund"}, json_mode=args.json_mode)
|
|
2478
|
+
payload = fee_refund(rules, paid=paid, ground=args.ground,
|
|
2479
|
+
claim_admitted=args.claim_admitted,
|
|
2480
|
+
plaintiff_default=args.plaintiff_default,
|
|
2481
|
+
transferred_to_arbitration=args.transferred_to_arbitration)
|
|
2482
|
+
emitter = result if payload.get("status") == "ok" else error
|
|
2483
|
+
return emitter(payload, meta={"command": "fee-refund", "rules_version": rules["version"]},
|
|
2484
|
+
json_mode=args.json_mode) or 0
|
|
2485
|
+
|
|
2486
|
+
|
|
2487
|
+
def cmd_fee_indexation(args):
|
|
2488
|
+
from .fees_extra import indexation_compute, indexation_frame
|
|
2489
|
+
from .legal_engine import load_rules
|
|
2490
|
+
from decimal import Decimal
|
|
2491
|
+
try:
|
|
2492
|
+
rules = load_rules()
|
|
2493
|
+
amount = Decimal(args.amount) if args.amount else None
|
|
2494
|
+
cs = Decimal(args.cpi_start) if args.cpi_start else None
|
|
2495
|
+
ce = Decimal(args.cpi_end) if args.cpi_end else None
|
|
2496
|
+
except Exception as exc:
|
|
2497
|
+
return error({"status": "INVALID_ARGUMENTS", "detail": str(exc)},
|
|
2498
|
+
meta={"command": "fee-indexation"}, json_mode=args.json_mode)
|
|
2499
|
+
payload = (indexation_compute(rules, code=args.code, amount=amount, cpi_start=cs, cpi_end=ce)
|
|
2500
|
+
if args.amount else indexation_frame(rules, code=args.code))
|
|
2501
|
+
emitter = result if payload.get("status") in ("ok", "frame_only") else error
|
|
2502
|
+
return emitter(payload, meta={"command": "fee-indexation", "rules_version": rules["version"]},
|
|
2503
|
+
json_mode=args.json_mode) or 0
|
|
2504
|
+
|
|
2505
|
+
|
|
2506
|
+
def cmd_deferral(args):
|
|
2507
|
+
from .deferral import generate_deferral
|
|
2508
|
+
result(generate_deferral(args.applicant, args.court, args.amount, args.reason), meta={"command": "deferral"}, json_mode=args.json_mode)
|
|
2509
|
+
return 0
|
|
2510
|
+
|
|
2511
|
+
|
|
2512
|
+
def cmd_limitation(args):
|
|
2513
|
+
from datetime import date
|
|
2514
|
+
from .limitation import calculate_limitation
|
|
2515
|
+
try:
|
|
2516
|
+
start = date.fromisoformat(args.start)
|
|
2517
|
+
check_date = date.fromisoformat(args.check_date) if args.check_date else date.today()
|
|
2518
|
+
payload = calculate_limitation(start=start, period_type=args.period_type,
|
|
2519
|
+
claim_notice_days=args.claim_notice_days, check_date=check_date,
|
|
2520
|
+
custom_years=args.years, custom_months=args.months).to_dict()
|
|
2521
|
+
except ValueError as exc:
|
|
2522
|
+
return error({"status": "INVALID_ARGUMENTS", "detail": str(exc)}, meta={"command": "limitation"}, json_mode=args.json_mode)
|
|
2523
|
+
if getattr(args, "explain", False):
|
|
2524
|
+
from .explain import limitation_explain
|
|
2525
|
+
payload["explain"] = limitation_explain(
|
|
2526
|
+
payload=dict(payload), period_type=args.period_type,
|
|
2527
|
+
start=args.start, check_date=args.check_date)
|
|
2528
|
+
emitter = result if payload["status"] == "ok" else error
|
|
2529
|
+
return emitter(payload, meta={"command": "limitation"}, json_mode=args.json_mode) or 0
|
|
2530
|
+
|
|
2531
|
+
|
|
2532
|
+
def cmd_timeline_audit(args):
|
|
2533
|
+
import json
|
|
2534
|
+
from .timeline_audit import audit_timeline
|
|
2535
|
+
try:
|
|
2536
|
+
source = Path(args.events)
|
|
2537
|
+
raw = json.loads(source.read_text(encoding="utf-8")) if source.exists() else json.loads(args.events)
|
|
2538
|
+
events = raw.get("events", raw) if isinstance(raw, dict) else raw
|
|
2539
|
+
payload = audit_timeline(events)
|
|
2540
|
+
except (OSError, json.JSONDecodeError, TypeError, ValueError) as exc:
|
|
2541
|
+
return error({"status": "INVALID_ARGUMENTS", "detail": str(exc)}, meta={"command": "timeline-audit"}, json_mode=args.json_mode)
|
|
2542
|
+
result(payload, meta={"command": "timeline-audit"}, json_mode=args.json_mode)
|
|
2543
|
+
return 0
|
|
2544
|
+
|
|
2545
|
+
|
|
2546
|
+
def cmd_npa_as_of(args):
|
|
2547
|
+
from .npa_as_of import lookup_as_of
|
|
2548
|
+
payload = lookup_as_of(args.act, args.article, args.date)
|
|
2549
|
+
if payload.get("status") != "ok":
|
|
2550
|
+
return error(payload, meta={"command": "npa-as-of"}, json_mode=args.json_mode)
|
|
2551
|
+
result(payload, meta={"command": "npa-as-of"}, json_mode=args.json_mode)
|
|
2552
|
+
return 0
|
|
2553
|
+
|
|
2554
|
+
|
|
2555
|
+
def cmd_verify_quote(args):
|
|
2556
|
+
"""правило «тексты только из реестра» (S-52): цитата подтверждается только против явного источника.
|
|
2557
|
+
|
|
2558
|
+
exact/fuzzy/not_found/edition_unavailable — это данные (exit 0); кривые
|
|
2559
|
+
аргументы — INVALID_ARGUMENTS (exit != 0). Сети нет ни в одном источнике.
|
|
2560
|
+
"""
|
|
2561
|
+
from .verify import VerifyInputError, verify_quote
|
|
2562
|
+
try:
|
|
2563
|
+
payload = verify_quote(
|
|
2564
|
+
args.text, args.source,
|
|
2565
|
+
act=args.act, article=args.article, date=args.date,
|
|
2566
|
+
case_id=args.case_id, path=args.path,
|
|
2567
|
+
state_dir=args.state_dir, max_fuzzy=args.max_fuzzy,
|
|
2568
|
+
)
|
|
2569
|
+
except VerifyInputError as exc:
|
|
2570
|
+
return error({"status": "INVALID_ARGUMENTS", "detail": str(exc)},
|
|
2571
|
+
meta={"command": "verify quote"}, json_mode=args.json_mode)
|
|
2572
|
+
except UnicodeDecodeError as exc:
|
|
2573
|
+
return error({"status": "INVALID_ARGUMENTS",
|
|
2574
|
+
"detail": f"source file is not valid UTF-8: {exc}"},
|
|
2575
|
+
meta={"command": "verify quote"}, json_mode=args.json_mode)
|
|
2576
|
+
except OSError as exc:
|
|
2577
|
+
return error({"status": "INVALID_ARGUMENTS",
|
|
2578
|
+
"detail": f"source file unreadable: {exc}"},
|
|
2579
|
+
meta={"command": "verify quote"}, json_mode=args.json_mode)
|
|
2580
|
+
if getattr(args, "explain", False):
|
|
2581
|
+
from .explain import verify_explain
|
|
2582
|
+
payload["explain"] = verify_explain(payload=dict(payload), text_len=len(args.text or ""))
|
|
2583
|
+
result(payload, meta={"command": "verify quote"}, json_mode=args.json_mode)
|
|
2584
|
+
return 0
|
|
2585
|
+
|
|
2586
|
+
|
|
2587
|
+
def cmd_practice_status(args):
|
|
2588
|
+
"""S-54: verdict только из локальных артефактов; «действует» не выдается."""
|
|
2589
|
+
from .practice_status import assess_status
|
|
2590
|
+
store = args.store
|
|
2591
|
+
if store is None:
|
|
2592
|
+
candidate = args.state_dir / "case-search-snapshots"
|
|
2593
|
+
store = candidate if candidate.is_dir() else None
|
|
2594
|
+
try:
|
|
2595
|
+
payload = assess_status(args.case_number, related=args.related, store_dir=store)
|
|
2596
|
+
except ValueError as exc:
|
|
2597
|
+
return error({"status": "INVALID_ARGUMENTS", "detail": str(exc)},
|
|
2598
|
+
meta={"command": "practice status"}, json_mode=args.json_mode)
|
|
2599
|
+
result(payload, meta={"command": "practice status"}, json_mode=args.json_mode)
|
|
2600
|
+
return 0
|
|
2601
|
+
|
|
2602
|
+
|
|
2603
|
+
def cmd_serve_mcp(args):
|
|
2604
|
+
from .mcp_server import serve_stdio
|
|
2605
|
+
serve_stdio()
|
|
2606
|
+
return 0
|
|
2607
|
+
|
|
2608
|
+
|
|
2609
|
+
LEGAL_CASE_STATUSES = ["new", "review", "filed", "pending", "decided", "appealed", "closed", "unknown"]
|
|
2610
|
+
|
|
2611
|
+
|
|
2612
|
+
def cmd_case_new(args):
|
|
2613
|
+
from sud_cli.db import create_case, get_connection, init_db
|
|
2614
|
+
if args.status is not None and args.status not in LEGAL_CASE_STATUSES:
|
|
2615
|
+
return error(
|
|
2616
|
+
{"status": "invalid_status", "detail": f"status {args.status!r} not allowed"},
|
|
2617
|
+
meta={"command": "case new", "state_dir": str(args.state_dir)},
|
|
2618
|
+
json_mode=args.json_mode,
|
|
2619
|
+
)
|
|
2620
|
+
init_db(args.state_dir)
|
|
2621
|
+
conn = get_connection(args.state_dir)
|
|
2622
|
+
try:
|
|
2623
|
+
res = create_case(
|
|
2624
|
+
conn, args.number, args.court, args.plaintiff, args.defendant,
|
|
2625
|
+
owner=args.owner, status=args.status,
|
|
2626
|
+
# the wave §1: поле instance дела (None = не указана -> unknown).
|
|
2627
|
+
instance=getattr(args, "instance", None),
|
|
2628
|
+
)
|
|
2629
|
+
except Exception as exc:
|
|
2630
|
+
return error(
|
|
2631
|
+
str(exc),
|
|
2632
|
+
meta={"command": "case new", "state_dir": str(args.state_dir)},
|
|
2633
|
+
json_mode=args.json_mode,
|
|
2634
|
+
)
|
|
2635
|
+
finally:
|
|
2636
|
+
conn.close()
|
|
2637
|
+
if res.get("duplicate"):
|
|
2638
|
+
result(
|
|
2639
|
+
{"status": "duplicate_window", "case_id": res["case_id"]},
|
|
2640
|
+
meta={"command": "case new", "state_dir": str(args.state_dir)},
|
|
2641
|
+
json_mode=args.json_mode,
|
|
2642
|
+
)
|
|
2643
|
+
return 0
|
|
2644
|
+
result(
|
|
2645
|
+
{"case_id": res["case_id"], "case_number": res.get("case_number"),
|
|
2646
|
+
"court": res.get("court"), "status": res.get("status"),
|
|
2647
|
+
"owner": res.get("owner"), "instance": getattr(args, "instance", None)},
|
|
2648
|
+
meta={"command": "case new", "state_dir": str(args.state_dir)},
|
|
2649
|
+
json_mode=args.json_mode,
|
|
2650
|
+
)
|
|
2651
|
+
return 0
|
|
2652
|
+
|
|
2653
|
+
|
|
2654
|
+
def cmd_case_set(args):
|
|
2655
|
+
from sud_cli.db import get_connection, init_db, set_case
|
|
2656
|
+
if args.status is not None and args.status not in LEGAL_CASE_STATUSES:
|
|
2657
|
+
return error(
|
|
2658
|
+
{"status": "invalid_status", "detail": f"status {args.status!r} not allowed"},
|
|
2659
|
+
meta={"command": "case set", "state_dir": str(args.state_dir)},
|
|
2660
|
+
json_mode=args.json_mode,
|
|
2661
|
+
)
|
|
2662
|
+
init_db(args.state_dir)
|
|
2663
|
+
conn = get_connection(args.state_dir)
|
|
2664
|
+
try:
|
|
2665
|
+
fields = {}
|
|
2666
|
+
if args.status is not None:
|
|
2667
|
+
fields["status"] = args.status
|
|
2668
|
+
if args.owner is not None:
|
|
2669
|
+
fields["owner"] = args.owner
|
|
2670
|
+
if args.closed_at is not None:
|
|
2671
|
+
fields["closed_at"] = args.closed_at
|
|
2672
|
+
if getattr(args, "instance", None) is not None:
|
|
2673
|
+
fields["instance"] = args.instance
|
|
2674
|
+
updated = set_case(conn, args.case_id, **fields)
|
|
2675
|
+
except Exception as exc:
|
|
2676
|
+
return error(
|
|
2677
|
+
{"status": "invalid_status" if "invalid_status" in str(exc) else str(exc),
|
|
2678
|
+
"detail": str(exc)},
|
|
2679
|
+
meta={"command": "case set", "state_dir": str(args.state_dir)},
|
|
2680
|
+
json_mode=args.json_mode,
|
|
2681
|
+
)
|
|
2682
|
+
finally:
|
|
2683
|
+
conn.close()
|
|
2684
|
+
if updated is None:
|
|
2685
|
+
return error(
|
|
2686
|
+
f"case not found: {args.case_id}",
|
|
2687
|
+
meta={"command": "case set", "state_dir": str(args.state_dir)},
|
|
2688
|
+
json_mode=args.json_mode,
|
|
2689
|
+
)
|
|
2690
|
+
result(
|
|
2691
|
+
updated,
|
|
2692
|
+
meta={"command": "case set", "state_dir": str(args.state_dir)},
|
|
2693
|
+
json_mode=args.json_mode,
|
|
2694
|
+
)
|
|
2695
|
+
return 0
|
|
2696
|
+
|
|
2697
|
+
|
|
2698
|
+
def cmd_case_amount_add(args):
|
|
2699
|
+
from sud_cli.db import InvalidAmountError, add_case_amount, get_connection, init_db
|
|
2700
|
+
init_db(args.state_dir)
|
|
2701
|
+
conn = get_connection(args.state_dir)
|
|
2702
|
+
try:
|
|
2703
|
+
try:
|
|
2704
|
+
res = add_case_amount(conn, args.case_id, args.kind, args.amount, note=args.note)
|
|
2705
|
+
except InvalidAmountError as exc:
|
|
2706
|
+
return error(
|
|
2707
|
+
{"status": "invalid_amount", "detail": str(exc)},
|
|
2708
|
+
meta={"command": "case amount add", "state_dir": str(args.state_dir)},
|
|
2709
|
+
json_mode=args.json_mode,
|
|
2710
|
+
)
|
|
2711
|
+
finally:
|
|
2712
|
+
conn.close()
|
|
2713
|
+
if res is None:
|
|
2714
|
+
return error(
|
|
2715
|
+
f"case not found: {args.case_id}",
|
|
2716
|
+
meta={"command": "case amount add", "state_dir": str(args.state_dir)},
|
|
2717
|
+
json_mode=args.json_mode,
|
|
2718
|
+
)
|
|
2719
|
+
result(
|
|
2720
|
+
res,
|
|
2721
|
+
meta={"command": "case amount add", "state_dir": str(args.state_dir)},
|
|
2722
|
+
json_mode=args.json_mode,
|
|
2723
|
+
)
|
|
2724
|
+
return 0
|
|
2725
|
+
|
|
2726
|
+
|
|
2727
|
+
def cmd_case_amount_list(args):
|
|
2728
|
+
from sud_cli.db import get_connection, init_db, list_case_amounts
|
|
2729
|
+
init_db(args.state_dir)
|
|
2730
|
+
conn = get_connection(args.state_dir)
|
|
2731
|
+
try:
|
|
2732
|
+
amounts = list_case_amounts(conn, args.case_id)
|
|
2733
|
+
finally:
|
|
2734
|
+
conn.close()
|
|
2735
|
+
result(
|
|
2736
|
+
amounts,
|
|
2737
|
+
meta={"command": "case amount list", "state_dir": str(args.state_dir)},
|
|
2738
|
+
json_mode=args.json_mode,
|
|
2739
|
+
)
|
|
2740
|
+
return 0
|
|
2741
|
+
|
|
2742
|
+
|
|
2743
|
+
# refund 333.40 — норма уже VERIFIED (pack W4c), но применение возврата —
|
|
2744
|
+
# решение по основанию через fee-refund движок; ручная маркировка обязательна.
|
|
2745
|
+
_AMOUNT_REFUND_MR = (
|
|
2746
|
+
"manual_review: норма возврата 333.40 применена через fee-refund движок "
|
|
2747
|
+
"— сверь основание"
|
|
2748
|
+
)
|
|
2749
|
+
|
|
2750
|
+
_AMOUNTS_DISCLAIMER = (
|
|
2751
|
+
"Расчёт справочный: реестр расходов — учётные записи оператора по делу, "
|
|
2752
|
+
"суммы сверяются с платёжными документами; sud не даёт фискальной гарантии."
|
|
2753
|
+
)
|
|
2754
|
+
|
|
2755
|
+
|
|
2756
|
+
def cmd_case_amount_set(args):
|
|
2757
|
+
from datetime import date as _date
|
|
2758
|
+
from sud_cli.db import get_connection, init_db, set_case_amount
|
|
2759
|
+
paid_at = args.paid_at
|
|
2760
|
+
if paid_at is not None:
|
|
2761
|
+
try:
|
|
2762
|
+
paid_at = _date.fromisoformat(paid_at).isoformat()
|
|
2763
|
+
except ValueError:
|
|
2764
|
+
return error(
|
|
2765
|
+
{"status": "INVALID_ARGUMENTS",
|
|
2766
|
+
"detail": f"invalid paid_at {args.paid_at!r}: ожидается YYYY-MM-DD"},
|
|
2767
|
+
meta={"command": "case amount set", "state_dir": str(args.state_dir)},
|
|
2768
|
+
json_mode=args.json_mode,
|
|
2769
|
+
)
|
|
2770
|
+
init_db(args.state_dir)
|
|
2771
|
+
conn = get_connection(args.state_dir)
|
|
2772
|
+
try:
|
|
2773
|
+
row = set_case_amount(conn, args.case_id, args.amount_id,
|
|
2774
|
+
status=args.status, paid_at=paid_at)
|
|
2775
|
+
finally:
|
|
2776
|
+
conn.close()
|
|
2777
|
+
if row is None:
|
|
2778
|
+
return error(
|
|
2779
|
+
f"amount not found: {args.amount_id} for case {args.case_id}",
|
|
2780
|
+
meta={"command": "case amount set", "state_dir": str(args.state_dir)},
|
|
2781
|
+
json_mode=args.json_mode,
|
|
2782
|
+
)
|
|
2783
|
+
payload = dict(row)
|
|
2784
|
+
if args.status == "refunded":
|
|
2785
|
+
# принимается, но маркируется: возврат по 333.40 требует сверки основания
|
|
2786
|
+
payload["warnings"] = [_AMOUNT_REFUND_MR]
|
|
2787
|
+
result(
|
|
2788
|
+
payload,
|
|
2789
|
+
meta={"command": "case amount set", "state_dir": str(args.state_dir)},
|
|
2790
|
+
json_mode=args.json_mode,
|
|
2791
|
+
)
|
|
2792
|
+
return 0
|
|
2793
|
+
|
|
2794
|
+
|
|
2795
|
+
def cmd_case_amount_summary(args):
|
|
2796
|
+
from sud_cli.db import get_connection, init_db, summarize_case_amounts
|
|
2797
|
+
init_db(args.state_dir)
|
|
2798
|
+
conn = get_connection(args.state_dir)
|
|
2799
|
+
try:
|
|
2800
|
+
summary = summarize_case_amounts(conn, args.case_id)
|
|
2801
|
+
finally:
|
|
2802
|
+
conn.close()
|
|
2803
|
+
summary["disclaimer"] = _AMOUNTS_DISCLAIMER
|
|
2804
|
+
result(
|
|
2805
|
+
summary,
|
|
2806
|
+
meta={"command": "case amount summary", "state_dir": str(args.state_dir)},
|
|
2807
|
+
json_mode=args.json_mode,
|
|
2808
|
+
)
|
|
2809
|
+
return 0
|
|
2810
|
+
|
|
2811
|
+
|
|
2812
|
+
def cmd_reminders_due(args):
|
|
2813
|
+
"""the wave §2 (S-26): окна сроков + bus-free a2a-строки.
|
|
2814
|
+
|
|
2815
|
+
Здесь нет и не может быть запуска внешних процессов: модуль
|
|
2816
|
+
sud_cli.reminders чистый, строки только печатаются (as docx).
|
|
2817
|
+
"""
|
|
2818
|
+
from datetime import date as _date, timedelta
|
|
2819
|
+
from sud_cli.db import get_connection, init_db, list_all_deadline_states
|
|
2820
|
+
from sud_cli.reminders import BUS_FREE_NOTE, due_items, generate_mail_commands
|
|
2821
|
+
|
|
2822
|
+
days = args.days
|
|
2823
|
+
if not isinstance(days, int) or days < 1 or days > 90:
|
|
2824
|
+
return error(
|
|
2825
|
+
{"status": "INVALID_ARGUMENTS",
|
|
2826
|
+
"detail": f"--days должен быть целым 1..90, получен {days!r}"},
|
|
2827
|
+
meta={"command": "reminders due", "state_dir": str(args.state_dir)},
|
|
2828
|
+
json_mode=args.json_mode,
|
|
2829
|
+
)
|
|
2830
|
+
init_db(args.state_dir)
|
|
2831
|
+
conn = get_connection(args.state_dir)
|
|
2832
|
+
try:
|
|
2833
|
+
states = list_all_deadline_states(conn)
|
|
2834
|
+
finally:
|
|
2835
|
+
conn.close()
|
|
2836
|
+
today = _date.today()
|
|
2837
|
+
items = due_items(states, today, days)
|
|
2838
|
+
payload = {
|
|
2839
|
+
"days": days,
|
|
2840
|
+
"today": today.isoformat(),
|
|
2841
|
+
"window_to": (today + timedelta(days=days)).isoformat(),
|
|
2842
|
+
"items": items,
|
|
2843
|
+
"commands": generate_mail_commands(items, days),
|
|
2844
|
+
"bus_free": BUS_FREE_NOTE,
|
|
2845
|
+
}
|
|
2846
|
+
result(
|
|
2847
|
+
payload,
|
|
2848
|
+
meta={"command": "reminders due", "state_dir": str(args.state_dir)},
|
|
2849
|
+
json_mode=args.json_mode,
|
|
2850
|
+
)
|
|
2851
|
+
return 0
|
|
2852
|
+
|
|
2853
|
+
|
|
2854
|
+
def cmd_case_search_alerts(args):
|
|
2855
|
+
"""W6a §4 (S-60): алерты case-search -> bus-free a2a-строки.
|
|
2856
|
+
|
|
2857
|
+
Только чтение локальных артефактов (alert-файлы + dedup-store). sud-cli
|
|
2858
|
+
шину не вызывает и внешних процессов не запускает — паттерн reminders.py.
|
|
2859
|
+
"""
|
|
2860
|
+
from datetime import date as _date
|
|
2861
|
+
|
|
2862
|
+
from . import court_alerts
|
|
2863
|
+
|
|
2864
|
+
days = args.days
|
|
2865
|
+
if not isinstance(days, int) or days < 1 or days > 90:
|
|
2866
|
+
return error(
|
|
2867
|
+
{"status": "INVALID_ARGUMENTS",
|
|
2868
|
+
"detail": f"--days должен быть целым 1..90, получен {days!r}"},
|
|
2869
|
+
meta={"command": "case-search alerts", "state_dir": str(args.state_dir)},
|
|
2870
|
+
json_mode=args.json_mode,
|
|
2871
|
+
)
|
|
2872
|
+
if args.alerts_dir is not None:
|
|
2873
|
+
alerts_dir = args.alerts_dir
|
|
2874
|
+
else:
|
|
2875
|
+
candidates = court_alerts.alerts_search_dirs(args.state_dir)
|
|
2876
|
+
alerts_dir = next((d for d in candidates if d.is_dir()), candidates[0])
|
|
2877
|
+
payload = court_alerts.alerts_payload(alerts_dir, _date.today(), days)
|
|
2878
|
+
result(
|
|
2879
|
+
payload,
|
|
2880
|
+
meta={"command": "case-search alerts", "state_dir": str(args.state_dir)},
|
|
2881
|
+
json_mode=args.json_mode,
|
|
2882
|
+
)
|
|
2883
|
+
return 0
|
|
2884
|
+
|
|
2885
|
+
|
|
2886
|
+
def cmd_case_doc_add(args):
|
|
2887
|
+
from sud_cli.db import DocNotFoundError, add_case_document, get_connection, init_db
|
|
2888
|
+
init_db(args.state_dir)
|
|
2889
|
+
conn = get_connection(args.state_dir)
|
|
2890
|
+
try:
|
|
2891
|
+
try:
|
|
2892
|
+
res = add_case_document(conn, args.case_id, args.path, title=args.title,
|
|
2893
|
+
supersedes=args.doc_id)
|
|
2894
|
+
except DocNotFoundError as exc:
|
|
2895
|
+
code = "doc_not_found" if "doc_not_found" in str(exc) else "invalid_supersedes"
|
|
2896
|
+
return error(
|
|
2897
|
+
{"status": code, "detail": str(exc)},
|
|
2898
|
+
meta={"command": "case doc add", "state_dir": str(args.state_dir)},
|
|
2899
|
+
json_mode=args.json_mode,
|
|
2900
|
+
)
|
|
2901
|
+
finally:
|
|
2902
|
+
conn.close()
|
|
2903
|
+
if res is None:
|
|
2904
|
+
return error(
|
|
2905
|
+
f"case not found: {args.case_id}",
|
|
2906
|
+
meta={"command": "case doc add", "state_dir": str(args.state_dir)},
|
|
2907
|
+
json_mode=args.json_mode,
|
|
2908
|
+
)
|
|
2909
|
+
result(
|
|
2910
|
+
res,
|
|
2911
|
+
meta={"command": "case doc add", "state_dir": str(args.state_dir)},
|
|
2912
|
+
json_mode=args.json_mode,
|
|
2913
|
+
)
|
|
2914
|
+
return 0
|
|
2915
|
+
|
|
2916
|
+
|
|
2917
|
+
def cmd_case_doc_list(args):
|
|
2918
|
+
from sud_cli.db import get_connection, init_db, list_case_documents
|
|
2919
|
+
init_db(args.state_dir)
|
|
2920
|
+
conn = get_connection(args.state_dir)
|
|
2921
|
+
try:
|
|
2922
|
+
docs = list_case_documents(conn, args.case_id)
|
|
2923
|
+
finally:
|
|
2924
|
+
conn.close()
|
|
2925
|
+
result(
|
|
2926
|
+
docs,
|
|
2927
|
+
meta={"command": "case doc list", "state_dir": str(args.state_dir)},
|
|
2928
|
+
json_mode=args.json_mode,
|
|
2929
|
+
)
|
|
2930
|
+
return 0
|
|
2931
|
+
|
|
2932
|
+
|
|
2933
|
+
def cmd_case_doc_history(args):
|
|
2934
|
+
"""the wave §2: цепочка ревизий newest-first (хэши в каждой строке)."""
|
|
2935
|
+
from sud_cli.db import case_document_history, get_connection, init_db
|
|
2936
|
+
init_db(args.state_dir)
|
|
2937
|
+
conn = get_connection(args.state_dir)
|
|
2938
|
+
try:
|
|
2939
|
+
rows = case_document_history(conn, args.case_id, path=args.path)
|
|
2940
|
+
finally:
|
|
2941
|
+
conn.close()
|
|
2942
|
+
result(
|
|
2943
|
+
rows,
|
|
2944
|
+
meta={"command": "case doc history", "state_dir": str(args.state_dir)},
|
|
2945
|
+
json_mode=args.json_mode,
|
|
2946
|
+
)
|
|
2947
|
+
return 0
|
|
2948
|
+
|
|
2949
|
+
|
|
2950
|
+
def cmd_case_evidence_add(args):
|
|
2951
|
+
from sud_cli.db import (DocNotFoundError, EvidenceError, add_case_evidence,
|
|
2952
|
+
get_connection, init_db)
|
|
2953
|
+
init_db(args.state_dir)
|
|
2954
|
+
conn = get_connection(args.state_dir)
|
|
2955
|
+
try:
|
|
2956
|
+
try:
|
|
2957
|
+
res = add_case_evidence(
|
|
2958
|
+
conn, args.case_id, title=args.title, path=args.path,
|
|
2959
|
+
volume=args.volume, sheet=args.sheet, media=args.media, note=args.note)
|
|
2960
|
+
except (DocNotFoundError, EvidenceError) as exc:
|
|
2961
|
+
return error(
|
|
2962
|
+
{"status": "INVALID_ARGUMENTS", "detail": str(exc)},
|
|
2963
|
+
meta={"command": "case evidence add", "state_dir": str(args.state_dir)},
|
|
2964
|
+
json_mode=args.json_mode,
|
|
2965
|
+
)
|
|
2966
|
+
finally:
|
|
2967
|
+
conn.close()
|
|
2968
|
+
if res is None:
|
|
2969
|
+
return error(
|
|
2970
|
+
f"case not found: {args.case_id}",
|
|
2971
|
+
meta={"command": "case evidence add", "state_dir": str(args.state_dir)},
|
|
2972
|
+
json_mode=args.json_mode,
|
|
2973
|
+
)
|
|
2974
|
+
result(
|
|
2975
|
+
res,
|
|
2976
|
+
meta={"command": "case evidence add", "state_dir": str(args.state_dir)},
|
|
2977
|
+
json_mode=args.json_mode,
|
|
2978
|
+
)
|
|
2979
|
+
return 0
|
|
2980
|
+
|
|
2981
|
+
|
|
2982
|
+
def cmd_case_evidence_list(args):
|
|
2983
|
+
from sud_cli.db import get_connection, init_db, list_case_evidence
|
|
2984
|
+
init_db(args.state_dir)
|
|
2985
|
+
conn = get_connection(args.state_dir)
|
|
2986
|
+
try:
|
|
2987
|
+
rows = list_case_evidence(conn, args.case_id)
|
|
2988
|
+
finally:
|
|
2989
|
+
conn.close()
|
|
2990
|
+
result(
|
|
2991
|
+
rows,
|
|
2992
|
+
meta={"command": "case evidence list", "state_dir": str(args.state_dir)},
|
|
2993
|
+
json_mode=args.json_mode,
|
|
2994
|
+
)
|
|
2995
|
+
return 0
|
|
2996
|
+
|
|
2997
|
+
|
|
2998
|
+
def cmd_case_evidence_verify(args):
|
|
2999
|
+
"""the wave §1: НИКОГДА не «всё хорошо» молча — per-item verdict + summary-строка."""
|
|
3000
|
+
from sud_cli.db import (evidence_summary, get_connection, init_db,
|
|
3001
|
+
verify_case_evidence)
|
|
3002
|
+
init_db(args.state_dir)
|
|
3003
|
+
conn = get_connection(args.state_dir)
|
|
3004
|
+
try:
|
|
3005
|
+
items = verify_case_evidence(conn, args.case_id)
|
|
3006
|
+
if not items and conn.execute(
|
|
3007
|
+
"SELECT case_id FROM cases WHERE case_id = ?", (args.case_id,)
|
|
3008
|
+
).fetchone() is None:
|
|
3009
|
+
return error(
|
|
3010
|
+
f"case not found: {args.case_id}",
|
|
3011
|
+
meta={"command": "case evidence verify", "state_dir": str(args.state_dir)},
|
|
3012
|
+
json_mode=args.json_mode,
|
|
3013
|
+
)
|
|
3014
|
+
finally:
|
|
3015
|
+
conn.close()
|
|
3016
|
+
summary = evidence_summary(items)
|
|
3017
|
+
summary_line = (
|
|
3018
|
+
f"evidence verify: total={summary['total']} intact={summary['intact']} "
|
|
3019
|
+
f"CHANGED={summary['changed']} MISSING_FILE={summary['missing_file']} "
|
|
3020
|
+
f"unchecked={summary['unchecked']} problems={summary['problems']}"
|
|
3021
|
+
)
|
|
3022
|
+
payload = {
|
|
3023
|
+
"items": [{k: v for k, v in i.items() if k != "note"} for i in items],
|
|
3024
|
+
"summary": {k: v for k, v in summary.items() if k != "problem_items"},
|
|
3025
|
+
"summary_line": summary_line,
|
|
3026
|
+
}
|
|
3027
|
+
result(
|
|
3028
|
+
payload,
|
|
3029
|
+
meta={"command": "case evidence verify", "state_dir": str(args.state_dir)},
|
|
3030
|
+
json_mode=args.json_mode,
|
|
3031
|
+
)
|
|
3032
|
+
return 0
|
|
3033
|
+
|
|
3034
|
+
|
|
3035
|
+
def cmd_case_doc_checklist(args):
|
|
3036
|
+
"""the wave §3 (S-71): справочный структурный чек-лист; exit 0 всегда."""
|
|
3037
|
+
from sud_cli.completeness import checklist_payload, load_rules
|
|
3038
|
+
from sud_cli.db import get_case_document, get_connection, init_db
|
|
3039
|
+
init_db(args.state_dir)
|
|
3040
|
+
conn = get_connection(args.state_dir)
|
|
3041
|
+
try:
|
|
3042
|
+
doc = get_case_document(conn, args.doc_id, case_id=args.case_id)
|
|
3043
|
+
finally:
|
|
3044
|
+
conn.close()
|
|
3045
|
+
try:
|
|
3046
|
+
payload = checklist_payload(doc, args.kind, rules=load_rules())
|
|
3047
|
+
except ValueError as exc:
|
|
3048
|
+
return error({"status": "INVALID_ARGUMENTS", "detail": str(exc)},
|
|
3049
|
+
meta={"command": "case doc checklist", "state_dir": str(args.state_dir)},
|
|
3050
|
+
json_mode=args.json_mode)
|
|
3051
|
+
result(
|
|
3052
|
+
payload,
|
|
3053
|
+
meta={"command": "case doc checklist", "state_dir": str(args.state_dir)},
|
|
3054
|
+
json_mode=args.json_mode,
|
|
3055
|
+
)
|
|
3056
|
+
return 0
|
|
3057
|
+
|
|
3058
|
+
|
|
3059
|
+
def cmd_form_new(args):
|
|
3060
|
+
"""the wave §4: структурный скелет формы с данными дела из реестра."""
|
|
3061
|
+
from sud_cli.db import get_case, get_connection, init_db
|
|
3062
|
+
from sud_cli.forms import render_form
|
|
3063
|
+
init_db(args.state_dir)
|
|
3064
|
+
conn = get_connection(args.state_dir)
|
|
3065
|
+
try:
|
|
3066
|
+
case = get_case(conn, args.case_id)
|
|
3067
|
+
finally:
|
|
3068
|
+
conn.close()
|
|
3069
|
+
if case is None:
|
|
3070
|
+
return error(
|
|
3071
|
+
f"case not found: {args.case_id}",
|
|
3072
|
+
meta={"command": "form new", "state_dir": str(args.state_dir)},
|
|
3073
|
+
json_mode=args.json_mode,
|
|
3074
|
+
)
|
|
3075
|
+
payload = render_form(args.kind, case, fmt=args.format)
|
|
3076
|
+
result(
|
|
3077
|
+
payload,
|
|
3078
|
+
meta={"command": "form new", "state_dir": str(args.state_dir)},
|
|
3079
|
+
json_mode=args.json_mode,
|
|
3080
|
+
)
|
|
3081
|
+
return 0
|
|
3082
|
+
|
|
3083
|
+
|
|
3084
|
+
def cmd_form_docx_command(args):
|
|
3085
|
+
"""the wave §4: bus-free генератор строки docx-cli (sud её не исполняет)."""
|
|
3086
|
+
from pathlib import Path
|
|
3087
|
+
from sud_cli.db import get_case, get_connection, init_db
|
|
3088
|
+
from sud_cli.forms import docx_command
|
|
3089
|
+
init_db(args.state_dir)
|
|
3090
|
+
conn = get_connection(args.state_dir)
|
|
3091
|
+
try:
|
|
3092
|
+
case = get_case(conn, args.case_id)
|
|
3093
|
+
finally:
|
|
3094
|
+
conn.close()
|
|
3095
|
+
if case is None:
|
|
3096
|
+
return error(
|
|
3097
|
+
f"case not found: {args.case_id}",
|
|
3098
|
+
meta={"command": "form docx-command", "state_dir": str(args.state_dir)},
|
|
3099
|
+
json_mode=args.json_mode,
|
|
3100
|
+
)
|
|
3101
|
+
payload = docx_command(args.kind, case,
|
|
3102
|
+
spec_dir=Path(args.spec_dir) if args.spec_dir else None)
|
|
3103
|
+
result(
|
|
3104
|
+
payload,
|
|
3105
|
+
meta={"command": "form docx-command", "state_dir": str(args.state_dir)},
|
|
3106
|
+
json_mode=args.json_mode,
|
|
3107
|
+
)
|
|
3108
|
+
return 0
|
|
3109
|
+
|
|
3110
|
+
|
|
3111
|
+
def cmd_case_note_add(args):
|
|
3112
|
+
from sud_cli.db import add_case_note, get_connection, init_db
|
|
3113
|
+
init_db(args.state_dir)
|
|
3114
|
+
conn = get_connection(args.state_dir)
|
|
3115
|
+
try:
|
|
3116
|
+
res = add_case_note(conn, args.case_id, args.text, author=args.author)
|
|
3117
|
+
finally:
|
|
3118
|
+
conn.close()
|
|
3119
|
+
if res is None:
|
|
3120
|
+
return error(
|
|
3121
|
+
f"case not found: {args.case_id}",
|
|
3122
|
+
meta={"command": "case note add", "state_dir": str(args.state_dir)},
|
|
3123
|
+
json_mode=args.json_mode,
|
|
3124
|
+
)
|
|
3125
|
+
result(
|
|
3126
|
+
res,
|
|
3127
|
+
meta={"command": "case note add", "state_dir": str(args.state_dir)},
|
|
3128
|
+
json_mode=args.json_mode,
|
|
3129
|
+
)
|
|
3130
|
+
return 0
|
|
3131
|
+
|
|
3132
|
+
|
|
3133
|
+
def cmd_case_note_list(args):
|
|
3134
|
+
from sud_cli.db import get_connection, init_db, list_case_notes
|
|
3135
|
+
init_db(args.state_dir)
|
|
3136
|
+
conn = get_connection(args.state_dir)
|
|
3137
|
+
try:
|
|
3138
|
+
notes = list_case_notes(conn, args.case_id)
|
|
3139
|
+
finally:
|
|
3140
|
+
conn.close()
|
|
3141
|
+
result(
|
|
3142
|
+
notes,
|
|
3143
|
+
meta={"command": "case note list", "state_dir": str(args.state_dir)},
|
|
3144
|
+
json_mode=args.json_mode,
|
|
3145
|
+
)
|
|
3146
|
+
return 0
|
|
3147
|
+
|
|
3148
|
+
|
|
3149
|
+
def cmd_case_note_supersede(args):
|
|
3150
|
+
from sud_cli.db import get_connection, init_db, supersede_case_note
|
|
3151
|
+
init_db(args.state_dir)
|
|
3152
|
+
conn = get_connection(args.state_dir)
|
|
3153
|
+
try:
|
|
3154
|
+
try:
|
|
3155
|
+
note_id = int(args.note_id)
|
|
3156
|
+
except ValueError:
|
|
3157
|
+
return error(
|
|
3158
|
+
{"status": "INVALID_ARGUMENTS", "detail": "note_id must be int"},
|
|
3159
|
+
meta={"command": "case note supersede", "state_dir": str(args.state_dir)},
|
|
3160
|
+
json_mode=args.json_mode,
|
|
3161
|
+
)
|
|
3162
|
+
res = supersede_case_note(conn, note_id, args.text, author=args.author)
|
|
3163
|
+
finally:
|
|
3164
|
+
conn.close()
|
|
3165
|
+
if res is None:
|
|
3166
|
+
return error(
|
|
3167
|
+
"note not found",
|
|
3168
|
+
meta={"command": "case note supersede", "state_dir": str(args.state_dir)},
|
|
3169
|
+
json_mode=args.json_mode,
|
|
3170
|
+
)
|
|
3171
|
+
result(
|
|
3172
|
+
res,
|
|
3173
|
+
meta={"command": "case note supersede", "state_dir": str(args.state_dir)},
|
|
3174
|
+
json_mode=args.json_mode,
|
|
3175
|
+
)
|
|
3176
|
+
return 0
|
|
3177
|
+
|
|
3178
|
+
|
|
3179
|
+
def cmd_case_list(args):
|
|
3180
|
+
from sud_cli.db import get_connection, init_db, list_cases
|
|
3181
|
+
init_db(args.state_dir)
|
|
3182
|
+
conn = get_connection(args.state_dir)
|
|
3183
|
+
try:
|
|
3184
|
+
cases = list_cases(
|
|
3185
|
+
conn, status=args.status, owner=args.owner, court=args.court, party=args.party
|
|
3186
|
+
)
|
|
3187
|
+
finally:
|
|
3188
|
+
conn.close()
|
|
3189
|
+
result(
|
|
3190
|
+
cases,
|
|
3191
|
+
meta={"command": "case list", "state_dir": str(args.state_dir)},
|
|
3192
|
+
json_mode=args.json_mode,
|
|
3193
|
+
)
|
|
3194
|
+
return 0
|
|
3195
|
+
|
|
3196
|
+
|
|
3197
|
+
def cmd_case_card(args):
|
|
3198
|
+
from sud_cli.db import get_case_card, get_connection, init_db
|
|
3199
|
+
init_db(args.state_dir)
|
|
3200
|
+
conn = get_connection(args.state_dir)
|
|
3201
|
+
try:
|
|
3202
|
+
card = get_case_card(conn, args.case_id)
|
|
3203
|
+
finally:
|
|
3204
|
+
conn.close()
|
|
3205
|
+
if card is None:
|
|
3206
|
+
return error(
|
|
3207
|
+
f"case not found: {args.case_id}",
|
|
3208
|
+
meta={"command": "case card", "state_dir": str(args.state_dir)},
|
|
3209
|
+
json_mode=args.json_mode,
|
|
3210
|
+
)
|
|
3211
|
+
result(
|
|
3212
|
+
card,
|
|
3213
|
+
meta={"command": "case card", "state_dir": str(args.state_dir)},
|
|
3214
|
+
json_mode=args.json_mode,
|
|
3215
|
+
)
|
|
3216
|
+
return 0
|
|
3217
|
+
|
|
3218
|
+
|
|
3219
|
+
# ---------------------------------------------------------------------------
|
|
3220
|
+
# the wave §2 (S-15): представители
|
|
3221
|
+
# ---------------------------------------------------------------------------
|
|
3222
|
+
|
|
3223
|
+
|
|
3224
|
+
def cmd_case_rep_add(args):
|
|
3225
|
+
from sud_cli.db import REPRESENTATIVE_HINT, add_case_representative, get_connection, init_db
|
|
3226
|
+
|
|
3227
|
+
init_db(args.state_dir)
|
|
3228
|
+
conn = get_connection(args.state_dir)
|
|
3229
|
+
try:
|
|
3230
|
+
try:
|
|
3231
|
+
row = add_case_representative(
|
|
3232
|
+
conn, args.case_id, side=args.side, name=args.name,
|
|
3233
|
+
authority_note=args.authority_note, power_docs=args.power_docs,
|
|
3234
|
+
)
|
|
3235
|
+
except ValueError as exc:
|
|
3236
|
+
return error({"status": "INVALID_ARGUMENTS", "detail": str(exc)},
|
|
3237
|
+
meta={"command": "case rep add",
|
|
3238
|
+
"state_dir": str(args.state_dir)},
|
|
3239
|
+
json_mode=args.json_mode)
|
|
3240
|
+
finally:
|
|
3241
|
+
conn.close()
|
|
3242
|
+
if row is None:
|
|
3243
|
+
return error(f"case not found: {args.case_id}",
|
|
3244
|
+
meta={"command": "case rep add",
|
|
3245
|
+
"state_dir": str(args.state_dir)},
|
|
3246
|
+
json_mode=args.json_mode)
|
|
3247
|
+
payload = dict(row)
|
|
3248
|
+
payload["hint"] = REPRESENTATIVE_HINT
|
|
3249
|
+
result(payload, meta={"command": "case rep add",
|
|
3250
|
+
"state_dir": str(args.state_dir)},
|
|
3251
|
+
json_mode=args.json_mode)
|
|
3252
|
+
return 0
|
|
3253
|
+
|
|
3254
|
+
|
|
3255
|
+
def cmd_case_rep_list(args):
|
|
3256
|
+
from sud_cli.db import REPRESENTATIVE_HINT, get_case, get_connection, init_db
|
|
3257
|
+
from sud_cli.db import list_case_representatives
|
|
3258
|
+
|
|
3259
|
+
init_db(args.state_dir)
|
|
3260
|
+
conn = get_connection(args.state_dir)
|
|
3261
|
+
try:
|
|
3262
|
+
if get_case(conn, args.case_id) is None:
|
|
3263
|
+
return error(f"case not found: {args.case_id}",
|
|
3264
|
+
meta={"command": "case rep list",
|
|
3265
|
+
"state_dir": str(args.state_dir)},
|
|
3266
|
+
json_mode=args.json_mode)
|
|
3267
|
+
rows = list_case_representatives(conn, args.case_id)
|
|
3268
|
+
finally:
|
|
3269
|
+
conn.close()
|
|
3270
|
+
result(rows, meta={"command": "case rep list",
|
|
3271
|
+
"state_dir": str(args.state_dir),
|
|
3272
|
+
"hint": REPRESENTATIVE_HINT},
|
|
3273
|
+
json_mode=args.json_mode)
|
|
3274
|
+
return 0
|
|
3275
|
+
|
|
3276
|
+
|
|
3277
|
+
# ---------------------------------------------------------------------------
|
|
3278
|
+
# the wave §1 (S-12): цепочка инстанций
|
|
3279
|
+
# ---------------------------------------------------------------------------
|
|
3280
|
+
|
|
3281
|
+
|
|
3282
|
+
def cmd_deadlines_chain(args):
|
|
3283
|
+
from .chain import build_chain
|
|
3284
|
+
from .legal_engine import load_rules
|
|
3285
|
+
|
|
3286
|
+
case_id = getattr(args, "case_id", None)
|
|
3287
|
+
if not case_id:
|
|
3288
|
+
return error({"status": "MISSING_REQUIRED_PARAM",
|
|
3289
|
+
"detail": "case_id required"},
|
|
3290
|
+
meta={"command": "deadlines chain"},
|
|
3291
|
+
json_mode=args.json_mode)
|
|
3292
|
+
try:
|
|
3293
|
+
rules = load_rules()
|
|
3294
|
+
except Exception as exc:
|
|
3295
|
+
return error({"status": "RULES_UNAVAILABLE", "detail": str(exc)},
|
|
3296
|
+
meta={"command": "deadlines chain"},
|
|
3297
|
+
json_mode=args.json_mode)
|
|
3298
|
+
try:
|
|
3299
|
+
payload = build_chain(args.state_dir, case_id,
|
|
3300
|
+
instance_override=getattr(args, "instance", None),
|
|
3301
|
+
rules=rules)
|
|
3302
|
+
except ValueError as exc:
|
|
3303
|
+
return error({"status": "INVALID_ARGUMENTS", "detail": str(exc)},
|
|
3304
|
+
meta={"command": "deadlines chain",
|
|
3305
|
+
"state_dir": str(args.state_dir)},
|
|
3306
|
+
json_mode=args.json_mode)
|
|
3307
|
+
payload["rules_version"] = rules.get("version")
|
|
3308
|
+
result(payload, meta={"command": "deadlines chain",
|
|
3309
|
+
"state_dir": str(args.state_dir)},
|
|
3310
|
+
json_mode=args.json_mode)
|
|
3311
|
+
return 0
|
|
3312
|
+
|
|
3313
|
+
|
|
3314
|
+
# ---------------------------------------------------------------------------
|
|
3315
|
+
# the wave §3 (S-27): конфликты расписания
|
|
3316
|
+
# ---------------------------------------------------------------------------
|
|
3317
|
+
|
|
3318
|
+
|
|
3319
|
+
def cmd_calendar_conflicts(args):
|
|
3320
|
+
from .conflicts import conflicts_payload
|
|
3321
|
+
|
|
3322
|
+
days = args.days
|
|
3323
|
+
if not isinstance(days, int) or days < 1 or days > 3650:
|
|
3324
|
+
return error({"status": "INVALID_ARGUMENTS",
|
|
3325
|
+
"detail": f"--days должен быть целым 1..3650, получен {days!r}"},
|
|
3326
|
+
meta={"command": "calendar conflicts",
|
|
3327
|
+
"state_dir": str(args.state_dir)},
|
|
3328
|
+
json_mode=args.json_mode)
|
|
3329
|
+
payload = conflicts_payload(args.state_dir, days=days)
|
|
3330
|
+
result(payload, meta={"command": "calendar conflicts",
|
|
3331
|
+
"state_dir": str(args.state_dir)},
|
|
3332
|
+
json_mode=args.json_mode)
|
|
3333
|
+
return 0
|
|
3334
|
+
|
|
3335
|
+
|
|
3336
|
+
# ---------------------------------------------------------------------------
|
|
3337
|
+
# the wave §4 (S-84): отчёты портфеля и дела
|
|
3338
|
+
# ---------------------------------------------------------------------------
|
|
3339
|
+
|
|
3340
|
+
|
|
3341
|
+
def cmd_report_portfolio(args):
|
|
3342
|
+
from .reports import portfolio_payload, render_portfolio_text
|
|
3343
|
+
|
|
3344
|
+
days = args.days
|
|
3345
|
+
if not isinstance(days, int) or days < 1 or days > 3650:
|
|
3346
|
+
return error({"status": "INVALID_ARGUMENTS",
|
|
3347
|
+
"detail": f"--days должен быть целым 1..3650, получен {days!r}"},
|
|
3348
|
+
meta={"command": "report portfolio"},
|
|
3349
|
+
json_mode=args.json_mode)
|
|
3350
|
+
try:
|
|
3351
|
+
payload = portfolio_payload(args.state_dir, as_of=args.as_of, days=days)
|
|
3352
|
+
except ValueError as exc:
|
|
3353
|
+
return error({"status": "INVALID_ARGUMENTS",
|
|
3354
|
+
"detail": f"--as-of: {exc}"},
|
|
3355
|
+
meta={"command": "report portfolio"},
|
|
3356
|
+
json_mode=args.json_mode)
|
|
3357
|
+
if not args.json_mode:
|
|
3358
|
+
for line in render_portfolio_text(payload):
|
|
3359
|
+
print(line)
|
|
3360
|
+
return 0
|
|
3361
|
+
result(payload, meta={"command": "report portfolio",
|
|
3362
|
+
"state_dir": str(args.state_dir)},
|
|
3363
|
+
json_mode=args.json_mode)
|
|
3364
|
+
return 0
|
|
3365
|
+
|
|
3366
|
+
|
|
3367
|
+
def cmd_report_case(args):
|
|
3368
|
+
from .reports import case_payload, render_case_text
|
|
3369
|
+
|
|
3370
|
+
payload = case_payload(args.state_dir, args.case_id)
|
|
3371
|
+
if payload is None:
|
|
3372
|
+
return error(f"case not found: {args.case_id}",
|
|
3373
|
+
meta={"command": "report case",
|
|
3374
|
+
"state_dir": str(args.state_dir)},
|
|
3375
|
+
json_mode=args.json_mode)
|
|
3376
|
+
if not args.json_mode:
|
|
3377
|
+
for line in render_case_text(payload):
|
|
3378
|
+
print(line)
|
|
3379
|
+
return 0
|
|
3380
|
+
result(payload, meta={"command": "report case",
|
|
3381
|
+
"state_dir": str(args.state_dir)},
|
|
3382
|
+
json_mode=args.json_mode)
|
|
3383
|
+
return 0
|
|
3384
|
+
|
|
3385
|
+
|
|
3386
|
+
# ---------------------------------------------------------------------------
|
|
3387
|
+
# история §5 (S-85): backup / verify / restore
|
|
3388
|
+
# ---------------------------------------------------------------------------
|
|
3389
|
+
|
|
3390
|
+
|
|
3391
|
+
def cmd_backup_create(args):
|
|
3392
|
+
from .backup import create_backup
|
|
3393
|
+
|
|
3394
|
+
try:
|
|
3395
|
+
payload = create_backup(args.state_dir, out=args.out)
|
|
3396
|
+
except FileNotFoundError as exc:
|
|
3397
|
+
return error({"status": "STATE_MISSING", "detail": str(exc)},
|
|
3398
|
+
meta={"command": "backup create",
|
|
3399
|
+
"state_dir": str(args.state_dir)},
|
|
3400
|
+
json_mode=args.json_mode)
|
|
3401
|
+
except OSError as exc:
|
|
3402
|
+
return error({"status": "INVALID_ARGUMENTS", "detail": f"--out: {exc}"},
|
|
3403
|
+
meta={"command": "backup create"},
|
|
3404
|
+
json_mode=args.json_mode)
|
|
3405
|
+
payload["disclaimer"] = (
|
|
3406
|
+
"Снимок содержит ПДн реестра: файл и sidecar записаны правами 0600. "
|
|
3407
|
+
"sud-cli не копирует их никуда дальше и никуда не отправляет."
|
|
3408
|
+
)
|
|
3409
|
+
result(payload, meta={"command": "backup create",
|
|
3410
|
+
"state_dir": str(args.state_dir)},
|
|
3411
|
+
json_mode=args.json_mode)
|
|
3412
|
+
return 0
|
|
3413
|
+
|
|
3414
|
+
|
|
3415
|
+
def cmd_backup_verify(args):
|
|
3416
|
+
from .backup import verify_backup
|
|
3417
|
+
|
|
3418
|
+
try:
|
|
3419
|
+
payload = verify_backup(args.file)
|
|
3420
|
+
except FileNotFoundError as exc:
|
|
3421
|
+
return error({"status": "FILE_MISSING", "detail": str(exc)},
|
|
3422
|
+
meta={"command": "backup verify"},
|
|
3423
|
+
json_mode=args.json_mode)
|
|
3424
|
+
payload["ok"] = bool(payload["integrity_ok"] and payload["quick_ok"])
|
|
3425
|
+
if not args.json_mode:
|
|
3426
|
+
print(f"backup: {payload['path']}")
|
|
3427
|
+
print(f"integrity_check: {payload['integrity_check']}")
|
|
3428
|
+
print(f"quick_check: {payload['quick_check']}")
|
|
3429
|
+
print(f"таблиц ({payload['table_count']}): "
|
|
3430
|
+
f"{', '.join(payload['tables']) if payload['tables'] else ''}")
|
|
3431
|
+
print(f"schema_version: {payload['schema_version']}")
|
|
3432
|
+
print(f"sha256: {payload['sha256']}")
|
|
3433
|
+
print(f"sidecar: {payload['sidecar_sha256']} (match: {payload['sidecar_match']})")
|
|
3434
|
+
return 0 if payload["ok"] else 1
|
|
3435
|
+
if not payload["ok"]:
|
|
3436
|
+
return error(payload, meta={"command": "backup verify"},
|
|
3437
|
+
json_mode=args.json_mode)
|
|
3438
|
+
result(payload, meta={"command": "backup verify"}, json_mode=args.json_mode)
|
|
3439
|
+
return 0
|
|
3440
|
+
|
|
3441
|
+
|
|
3442
|
+
def cmd_backup_restore(args):
|
|
3443
|
+
from .backup import RESTORE_DISCLAIMER, restore_backup
|
|
3444
|
+
|
|
3445
|
+
try:
|
|
3446
|
+
payload = restore_backup(args.state_dir, args.file,
|
|
3447
|
+
confirmed=bool(args.yes))
|
|
3448
|
+
except FileNotFoundError as exc:
|
|
3449
|
+
return error({"status": "FILE_MISSING", "detail": str(exc)},
|
|
3450
|
+
meta={"command": "backup restore"},
|
|
3451
|
+
json_mode=args.json_mode)
|
|
3452
|
+
except Exception as exc:
|
|
3453
|
+
return error({"status": "BACKUP_UNSAFE", "detail": str(exc)},
|
|
3454
|
+
meta={"command": "backup restore"},
|
|
3455
|
+
json_mode=args.json_mode)
|
|
3456
|
+
if payload.get("status") == "needs_confirmation":
|
|
3457
|
+
# деструктив без --yes: ничего не изменено, отказ и превью в stderr
|
|
3458
|
+
return error(payload, meta={"command": "backup restore",
|
|
3459
|
+
"state_dir": str(args.state_dir)},
|
|
3460
|
+
json_mode=args.json_mode)
|
|
3461
|
+
result(payload, meta={"command": "backup restore",
|
|
3462
|
+
"state_dir": str(args.state_dir),
|
|
3463
|
+
"disclaimer": RESTORE_DISCLAIMER},
|
|
3464
|
+
json_mode=args.json_mode)
|
|
3465
|
+
return 0
|
|
3466
|
+
|
|
3467
|
+
|
|
3468
|
+
# ---------------------------------------------------------------------------
|
|
3469
|
+
# история §5 (S-86): метрики
|
|
3470
|
+
# ---------------------------------------------------------------------------
|
|
3471
|
+
|
|
3472
|
+
|
|
3473
|
+
def cmd_metrics(args):
|
|
3474
|
+
from .metrics import metrics_payload
|
|
3475
|
+
|
|
3476
|
+
payload = metrics_payload(args.state_dir)
|
|
3477
|
+
if not args.json_mode:
|
|
3478
|
+
for line in render_metrics_text(payload):
|
|
3479
|
+
print(line)
|
|
3480
|
+
return 0
|
|
3481
|
+
result(payload, meta={"command": "metrics",
|
|
3482
|
+
"state_dir": str(args.state_dir)},
|
|
3483
|
+
json_mode=args.json_mode)
|
|
3484
|
+
return 0
|
|
3485
|
+
|
|
3486
|
+
|
|
3487
|
+
def render_metrics_text(payload):
|
|
3488
|
+
"""Human-рендер метрик: только строки из payload, без «вывода»."""
|
|
3489
|
+
lines = [f"Метрики state-dir на {payload['as_of']}"]
|
|
3490
|
+
lines.append(f"дела: {payload['cases']['total']}")
|
|
3491
|
+
for status, count in sorted(payload["cases"]["by_status"].items()):
|
|
3492
|
+
lines.append(f" {status}: {count}")
|
|
3493
|
+
ds = payload["deadline_states"]
|
|
3494
|
+
lines.append(f"сроков (states): {ds['total']}")
|
|
3495
|
+
for state, count in sorted(ds["by_state"].items()):
|
|
3496
|
+
lines.append(f" {state}: {count}")
|
|
3497
|
+
win = payload["deadlines_window"]
|
|
3498
|
+
lines.append(f"окно: near<= {win['near_days']} дн. = {win['near']}, "
|
|
3499
|
+
f"просрочено = {win['overdue']}")
|
|
3500
|
+
lines.append(f"событий: {payload['events']}, документов: "
|
|
3501
|
+
f"{payload['documents']['total']} (ревизий "
|
|
3502
|
+
f"{payload['documents']['revisions']}), доказательств: "
|
|
3503
|
+
f"{payload['evidence']['total']} (без hash "
|
|
3504
|
+
f"{payload['evidence']['without_hash']})")
|
|
3505
|
+
lines.append(f"представителей: {payload['representatives']['total']}, "
|
|
3506
|
+
f"заметок: {payload['notes']}, сумм: {payload['amounts']}")
|
|
3507
|
+
lines.append(f"напоминаний к окну {payload['reminders']['pending_days']} дн.: "
|
|
3508
|
+
f"{payload['reminders']['pending']}")
|
|
3509
|
+
last = payload["backup"]["last"]
|
|
3510
|
+
lines.append(f"последний backup: {last['path']} (exists={last['exists']})")
|
|
3511
|
+
lines.append(payload["disclaimer"])
|
|
3512
|
+
return lines
|
|
3513
|
+
|
|
3514
|
+
|
|
3515
|
+
# ---------------------------------------------------------------------------
|
|
3516
|
+
# история §5 (S-07): экспорт/уничтожение данных дела
|
|
3517
|
+
# ---------------------------------------------------------------------------
|
|
3518
|
+
|
|
3519
|
+
|
|
3520
|
+
def cmd_case_export(args):
|
|
3521
|
+
from .pdn import export_case, render_export_md
|
|
3522
|
+
|
|
3523
|
+
try:
|
|
3524
|
+
payload = export_case(args.state_dir, args.case_id, fmt=args.format)
|
|
3525
|
+
except ValueError as exc:
|
|
3526
|
+
return error({"status": "INVALID_ARGUMENTS", "detail": str(exc)},
|
|
3527
|
+
meta={"command": "case export"},
|
|
3528
|
+
json_mode=args.json_mode)
|
|
3529
|
+
if payload is None:
|
|
3530
|
+
return error(f"case not found: {args.case_id}",
|
|
3531
|
+
meta={"command": "case export",
|
|
3532
|
+
"state_dir": str(args.state_dir)},
|
|
3533
|
+
json_mode=args.json_mode)
|
|
3534
|
+
text = None
|
|
3535
|
+
if args.format == "md":
|
|
3536
|
+
text = render_export_md(payload)
|
|
3537
|
+
# markdown лежит в конверте: --json + --format md не теряет рендер,
|
|
3538
|
+
# а --out пишет именно его.
|
|
3539
|
+
payload = dict(payload)
|
|
3540
|
+
payload["markdown"] = text
|
|
3541
|
+
if args.out is not None:
|
|
3542
|
+
import json as _json
|
|
3543
|
+
|
|
3544
|
+
try:
|
|
3545
|
+
args.out.parent.mkdir(parents=True, exist_ok=True)
|
|
3546
|
+
body = text if text is not None else _json.dumps(payload, ensure_ascii=False,
|
|
3547
|
+
indent=2, default=str)
|
|
3548
|
+
args.out.write_text(body + ("\n" if text is not None else ""), encoding="utf-8")
|
|
3549
|
+
except OSError as exc:
|
|
3550
|
+
return error({"status": "INVALID_ARGUMENTS", "detail": f"--out: {exc}"},
|
|
3551
|
+
meta={"command": "case export"},
|
|
3552
|
+
json_mode=args.json_mode)
|
|
3553
|
+
payload = dict(payload)
|
|
3554
|
+
payload["written_to"] = str(args.out)
|
|
3555
|
+
if text is not None and not args.json_mode:
|
|
3556
|
+
print(text, end="")
|
|
3557
|
+
return 0
|
|
3558
|
+
result(payload, meta={"command": "case export",
|
|
3559
|
+
"state_dir": str(args.state_dir)},
|
|
3560
|
+
json_mode=args.json_mode)
|
|
3561
|
+
return 0
|
|
3562
|
+
|
|
3563
|
+
|
|
3564
|
+
def cmd_case_purge(args):
|
|
3565
|
+
from .pdn import purge_apply, purge_preview
|
|
3566
|
+
|
|
3567
|
+
if not args.yes:
|
|
3568
|
+
preview = purge_preview(args.state_dir, args.case_id)
|
|
3569
|
+
if preview is None:
|
|
3570
|
+
return error(f"case not found: {args.case_id}",
|
|
3571
|
+
meta={"command": "case purge",
|
|
3572
|
+
"state_dir": str(args.state_dir)},
|
|
3573
|
+
json_mode=args.json_mode)
|
|
3574
|
+
return error(
|
|
3575
|
+
{"status": "needs_confirmation",
|
|
3576
|
+
"detail": "purge уничтожает данные дела безвозвратно: нужен --yes",
|
|
3577
|
+
"preview": preview},
|
|
3578
|
+
meta={"command": "case purge", "state_dir": str(args.state_dir)},
|
|
3579
|
+
json_mode=args.json_mode,
|
|
3580
|
+
)
|
|
3581
|
+
payload = purge_apply(args.state_dir, args.case_id, confirmed=True,
|
|
3582
|
+
actor="cli", remove_files=not args.keep_files)
|
|
3583
|
+
if payload.get("status") == "not_found":
|
|
3584
|
+
return error(f"case not found: {args.case_id}",
|
|
3585
|
+
meta={"command": "case purge",
|
|
3586
|
+
"state_dir": str(args.state_dir)},
|
|
3587
|
+
json_mode=args.json_mode)
|
|
3588
|
+
result(payload, meta={"command": "case purge",
|
|
3589
|
+
"state_dir": str(args.state_dir)},
|
|
3590
|
+
json_mode=args.json_mode)
|
|
3591
|
+
return 0
|
|
3592
|
+
|
|
3593
|
+
_CONFLICT_DISCLAIMER = (
|
|
3594
|
+
"Отсутствие совпадений не означает отсутствие конфликта: покрытие — "
|
|
3595
|
+
"только имена локального реестра; внешние стороны и аффилированные лица "
|
|
3596
|
+
"не охвачены. Решение остаётся за человеком (boundary IX-3)."
|
|
3597
|
+
)
|
|
3598
|
+
|
|
3599
|
+
|
|
3600
|
+
def _conflict_payload(hits):
|
|
3601
|
+
return {
|
|
3602
|
+
"clean": len(hits) == 0,
|
|
3603
|
+
"hits": hits,
|
|
3604
|
+
"method": "name_exact",
|
|
3605
|
+
"coverage": "local registry names only",
|
|
3606
|
+
"disclaimer": _CONFLICT_DISCLAIMER,
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
|
|
3610
|
+
def cmd_conflict_check(args):
|
|
3611
|
+
from sud_cli.conflict import check as conflict_check
|
|
3612
|
+
from sud_cli.db import get_connection, init_db, list_cases
|
|
3613
|
+
|
|
3614
|
+
init_db(args.state_dir)
|
|
3615
|
+
conn = get_connection(args.state_dir)
|
|
3616
|
+
try:
|
|
3617
|
+
portfolio = list_cases(conn)
|
|
3618
|
+
candidate = {
|
|
3619
|
+
"parties": [],
|
|
3620
|
+
"court": args.court,
|
|
3621
|
+
"case_number": args.case_number,
|
|
3622
|
+
}
|
|
3623
|
+
if args.plaintiff:
|
|
3624
|
+
candidate["parties"].append(args.plaintiff)
|
|
3625
|
+
if args.defendant:
|
|
3626
|
+
candidate["parties"].append(args.defendant)
|
|
3627
|
+
hits = conflict_check(portfolio, candidate)
|
|
3628
|
+
finally:
|
|
3629
|
+
conn.close()
|
|
3630
|
+
result(
|
|
3631
|
+
_conflict_payload(hits),
|
|
3632
|
+
meta={"command": "conflict check", "state_dir": str(args.state_dir)},
|
|
3633
|
+
json_mode=args.json_mode,
|
|
3634
|
+
)
|
|
3635
|
+
return 0
|
|
3636
|
+
|
|
3637
|
+
|
|
3638
|
+
def cmd_conflict_case(args):
|
|
3639
|
+
from sud_cli.conflict import check as conflict_check
|
|
3640
|
+
from sud_cli.db import get_case, get_connection, init_db, list_cases
|
|
3641
|
+
|
|
3642
|
+
init_db(args.state_dir)
|
|
3643
|
+
conn = get_connection(args.state_dir)
|
|
3644
|
+
try:
|
|
3645
|
+
case = get_case(conn, args.case_id)
|
|
3646
|
+
if case is None:
|
|
3647
|
+
return error(
|
|
3648
|
+
f"case not found: {args.case_id}",
|
|
3649
|
+
meta={"command": "conflict case", "state_dir": str(args.state_dir)},
|
|
3650
|
+
json_mode=args.json_mode,
|
|
3651
|
+
)
|
|
3652
|
+
portfolio = [c for c in list_cases(conn) if c.get("case_id") != args.case_id]
|
|
3653
|
+
candidate = {
|
|
3654
|
+
"parties": [],
|
|
3655
|
+
"case_id": args.case_id,
|
|
3656
|
+
"court": case.get("court"),
|
|
3657
|
+
"case_number": case.get("case_number"),
|
|
3658
|
+
}
|
|
3659
|
+
for key in ("plaintiff", "defendant"):
|
|
3660
|
+
val = case.get(key)
|
|
3661
|
+
if val:
|
|
3662
|
+
candidate["parties"].append(val)
|
|
3663
|
+
hits = conflict_check(portfolio, candidate)
|
|
3664
|
+
finally:
|
|
3665
|
+
conn.close()
|
|
3666
|
+
result(
|
|
3667
|
+
_conflict_payload(hits),
|
|
3668
|
+
meta={"command": "conflict case", "state_dir": str(args.state_dir)},
|
|
3669
|
+
json_mode=args.json_mode,
|
|
3670
|
+
)
|
|
3671
|
+
return 0
|
|
3672
|
+
|
|
3673
|
+
|
|
3674
|
+
def cmd_jurisdiction_check(args):
|
|
3675
|
+
from sud_cli.jurisdiction import check_jurisdiction
|
|
3676
|
+
|
|
3677
|
+
payload = check_jurisdiction(
|
|
3678
|
+
code=args.code,
|
|
3679
|
+
claim=args.claim,
|
|
3680
|
+
party_org=args.party_org,
|
|
3681
|
+
party_phys=args.party_phys,
|
|
3682
|
+
unknown_rf_location=args.unknown_rf_location,
|
|
3683
|
+
immovable=args.immovable,
|
|
3684
|
+
corporate=args.corporate,
|
|
3685
|
+
bankruptcy=args.bankruptcy,
|
|
3686
|
+
ip_rights=args.ip_rights,
|
|
3687
|
+
contract_forum=args.contract_forum,
|
|
3688
|
+
arbitration_clause=args.arbitration_clause,
|
|
3689
|
+
multiple_defendants=args.multiple_defendants,
|
|
3690
|
+
foreign_element=args.foreign_element,
|
|
3691
|
+
consumer=args.consumer,
|
|
3692
|
+
alimony=args.alimony,
|
|
3693
|
+
)
|
|
3694
|
+
if getattr(args, "explain", False):
|
|
3695
|
+
from .explain import jurisdiction_explain
|
|
3696
|
+
payload["explain"] = jurisdiction_explain(payload=dict(payload), code=args.code, claim=args.claim)
|
|
3697
|
+
result(payload, meta={"command": "jurisdiction check", "state_dir": str(args.state_dir)}, json_mode=args.json_mode)
|
|
3698
|
+
return 0
|
|
3699
|
+
|
|
3700
|
+
|
|
3701
|
+
def cmd_pretrial_calc(args):
|
|
3702
|
+
from sud_cli.pretrial import calc_pretrial
|
|
3703
|
+
|
|
3704
|
+
payload = calc_pretrial(code=args.code, sent=args.sent)
|
|
3705
|
+
if getattr(args, "explain", False):
|
|
3706
|
+
from .explain import pretrial_explain
|
|
3707
|
+
payload["explain"] = pretrial_explain(payload=dict(payload), code=args.code, sent=args.sent)
|
|
3708
|
+
result(payload, meta={"command": "pretrial calc", "state_dir": str(args.state_dir)}, json_mode=args.json_mode)
|
|
3709
|
+
return 0
|
|
3710
|
+
|
|
3711
|
+
|
|
3712
|
+
def cmd_pretrial_consequences(args):
|
|
3713
|
+
from sud_cli.pretrial import consequences
|
|
3714
|
+
|
|
3715
|
+
payload = consequences(code=args.code, stage=args.stage)
|
|
3716
|
+
if getattr(args, "explain", False):
|
|
3717
|
+
from .explain import pretrial_consequences_explain
|
|
3718
|
+
payload["explain"] = pretrial_consequences_explain(
|
|
3719
|
+
payload=dict(payload), code=args.code, stage=args.stage)
|
|
3720
|
+
result(payload, meta={"command": "pretrial consequences", "state_dir": str(args.state_dir)}, json_mode=args.json_mode)
|
|
3721
|
+
return 0
|