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/explain.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""W6a §2 (S-81): --explain — человекочитаемая цепочка расчёта.
|
|
2
|
+
|
|
3
|
+
Конверт команды с флагом получает блок ``explain`` из четырёх секций:
|
|
4
|
+
какие НОРМЫ применялись (norm_ref из rules; verbatim legal_text там, где он
|
|
5
|
+
есть в правилах), какие ДАННЫЕ (тарифная редакция, ставки, штампы
|
|
6
|
+
производственного календаря), какие ФОРМУЛЫ (шаги breakdown/trace движка) и
|
|
7
|
+
что осталось РУЧНЫМ (manual_review причины).
|
|
8
|
+
|
|
9
|
+
Zero-граница: explain не печатает тексты норм вне правил — для
|
|
10
|
+
reference_only-записей отдаётся fetch_hint="npa_as_of" (тот же контракт, что
|
|
11
|
+
W2b в jurisdiction/fees_extra). Байт-стабильность старых конвертов: без
|
|
12
|
+
флага --explain этот модуль к конверту не прикасается.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
EXPLAIN_POLICY = ("explain собран только из rules-данных движка; тексты норм вне правил "
|
|
19
|
+
"не выводятся — reference_only даёт fetch_hint=npa_as_of")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def envelope(command: str, norms: Optional[List[dict]] = None,
|
|
23
|
+
data: Optional[List[dict]] = None, formulas: Optional[List[str]] = None,
|
|
24
|
+
manual_review: Optional[List[Any]] = None) -> dict:
|
|
25
|
+
return {
|
|
26
|
+
"command": command,
|
|
27
|
+
"norms": norms or [],
|
|
28
|
+
"data": data or [],
|
|
29
|
+
"formulas": formulas or [],
|
|
30
|
+
"manual_review": manual_review or [],
|
|
31
|
+
"policy": EXPLAIN_POLICY,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _norm_from_formula(formula: str) -> Optional[str]:
|
|
36
|
+
if not formula:
|
|
37
|
+
return None
|
|
38
|
+
head = formula.split(":", 1)[0].strip()
|
|
39
|
+
return head or None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def fees_explain(*, fee_doc: dict, rules: dict, as_of_iso: Optional[str]) -> dict:
|
|
43
|
+
basis = rules.get("tariff_basis") or {}
|
|
44
|
+
norms = []
|
|
45
|
+
art = _norm_from_formula(fee_doc.get("formula") or "")
|
|
46
|
+
if art:
|
|
47
|
+
norms.append({"norm_ref": art, "source": "legal_rules.json"})
|
|
48
|
+
norms.append({"norm_ref": "ст. 333.19, 333.21 НК РФ",
|
|
49
|
+
"source": "legal_rules.json (tariff_basis)"})
|
|
50
|
+
data = [
|
|
51
|
+
{"item": "rules", "version": rules.get("version"),
|
|
52
|
+
"tariff_version": rules.get("tariff_version"),
|
|
53
|
+
"legal_status": rules.get("legal_status")},
|
|
54
|
+
{"item": "tariff_basis", "federal_law": basis.get("federal_law"),
|
|
55
|
+
"effective_from": basis.get("effective_from")},
|
|
56
|
+
{"item": "rounding", **{k: (rules.get("rounding") or {}).get(k)
|
|
57
|
+
for k in ("article", "mode")}},
|
|
58
|
+
]
|
|
59
|
+
meta = fee_doc.get("meta") or {}
|
|
60
|
+
if as_of_iso:
|
|
61
|
+
data.append({"item": "tariff_edition", "as_of": as_of_iso,
|
|
62
|
+
"edition_id": meta.get("edition_id"),
|
|
63
|
+
"window": meta.get("tariff_effective_window") or meta.get("window"),
|
|
64
|
+
"warnings": meta.get("warnings") or []})
|
|
65
|
+
formulas = []
|
|
66
|
+
if fee_doc.get("formula"):
|
|
67
|
+
formulas.append(fee_doc["formula"])
|
|
68
|
+
for step in fee_doc.get("breakdown") or []:
|
|
69
|
+
if isinstance(step, dict) and step.get("step"):
|
|
70
|
+
formulas.append(step["step"])
|
|
71
|
+
manual = []
|
|
72
|
+
if fee_doc.get("status") != "ok":
|
|
73
|
+
manual.append({"reason": "status != ok", "detail": fee_doc.get("detail")})
|
|
74
|
+
if meta.get("manual_reason"):
|
|
75
|
+
manual.append({"reason": meta["manual_reason"]})
|
|
76
|
+
return envelope("fees", norms=norms, data=data, formulas=formulas, manual_review=manual)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def deadlines_explain(*, payload: dict, rules: dict, event_date: Optional[str],
|
|
80
|
+
region: Optional[str], tz: Optional[str]) -> dict:
|
|
81
|
+
cal = rules.get("production_calendar") or {}
|
|
82
|
+
entry = (rules.get("deadline_matrix") or {})
|
|
83
|
+
norms = []
|
|
84
|
+
if payload.get("legal_basis"):
|
|
85
|
+
norms.append({"norm_ref": payload["legal_basis"], "source": "legal_rules.json (deadline_matrix)"})
|
|
86
|
+
data = [
|
|
87
|
+
{"item": "spec", "spec_version": rules.get("spec_version"),
|
|
88
|
+
"legal_status": rules.get("legal_status")},
|
|
89
|
+
{"item": "calendar", "status": cal.get("status"), "as_of": cal.get("as_of"),
|
|
90
|
+
"shift_enabled": cal.get("shift_enabled"),
|
|
91
|
+
"transfers_encoded_through": cal.get("government_transfers_encoded_through")},
|
|
92
|
+
{"item": "inputs", "event_date": event_date, "region": region, "tz": tz},
|
|
93
|
+
]
|
|
94
|
+
formulas = [t.get("step") for t in payload.get("trace") or [] if isinstance(t, dict) and t.get("step")]
|
|
95
|
+
manual = []
|
|
96
|
+
if payload.get("status") != "ok":
|
|
97
|
+
manual.append({"reason": "status != ok", "detail": payload.get("detail")})
|
|
98
|
+
for w in payload.get("warnings") or []:
|
|
99
|
+
manual.append({"reason": "warning", "detail": w})
|
|
100
|
+
return envelope("deadlines", norms=norms, data=data, formulas=[str(f) for f in formulas], manual_review=manual)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def limitation_explain(*, payload: dict, period_type: str, start: str,
|
|
104
|
+
check_date: Optional[str]) -> dict:
|
|
105
|
+
norms = [{"norm_ref": n, "source": "limitation.PERIODS (ст.-ссылки движка)"}
|
|
106
|
+
for n in payload.get("legal_basis") or []]
|
|
107
|
+
data = [{"item": "inputs", "period_type": period_type, "start": start,
|
|
108
|
+
"check_date": check_date}]
|
|
109
|
+
formulas = [t.get("step") for t in payload.get("trace") or [] if isinstance(t, dict) and t.get("step")]
|
|
110
|
+
manual = []
|
|
111
|
+
if payload.get("status") != "ok":
|
|
112
|
+
manual.append({"reason": "status != ok", "detail": payload.get("detail")})
|
|
113
|
+
return envelope("limitation", norms=norms, data=data, formulas=[str(f) for f in formulas], manual_review=manual)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def interest_explain(*, payload: dict, article: str, amount, start: str, end: str) -> dict:
|
|
117
|
+
norms = [{"norm_ref": f"ст. {article} ГК РФ", "source": "движок interest.py (ст.-ссылка)"}]
|
|
118
|
+
data = [
|
|
119
|
+
{"item": "inputs", "amount": amount, "start": start, "end": end},
|
|
120
|
+
{"item": "key_rates", **(payload.get("rates_meta") or {})},
|
|
121
|
+
]
|
|
122
|
+
formulas = []
|
|
123
|
+
for p in payload.get("periods") or []:
|
|
124
|
+
if isinstance(p, dict):
|
|
125
|
+
formulas.append(
|
|
126
|
+
f"{p.get('from')}..{p.get('to')}: {p.get('principal_rub')} × {p.get('days')}/{p.get('year_days', '—')} × {p.get('rate')}% = {p.get('amount_rub')}")
|
|
127
|
+
manual = [{"reason": "rates warning", "detail": w} for w in payload.get("rates_warnings") or []]
|
|
128
|
+
return envelope("interest", norms=norms, data=data, formulas=formulas, manual_review=manual)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def pretrial_explain(*, payload: dict, code: str, sent: Optional[str]) -> dict:
|
|
132
|
+
norms = []
|
|
133
|
+
if payload.get("norm"):
|
|
134
|
+
norms.append({"norm_ref": payload["norm"], "source": "jurisdiction_rules.json (pretrial)"})
|
|
135
|
+
data = [{"item": "rule", "code": code, "rule": payload.get("rule"),
|
|
136
|
+
"days": payload.get("days"), "calendar": payload.get("calendar"),
|
|
137
|
+
"exclude_nonworking_holidays": payload.get("exclude_nonworking_holidays")}]
|
|
138
|
+
formulas = []
|
|
139
|
+
if payload.get("sent") and payload.get("deadline"):
|
|
140
|
+
formulas.append(f"{payload['sent']} + {payload.get('days')} кал.дн -> {payload['deadline']}"
|
|
141
|
+
+ (" (с переносом на рабочий день)" if payload.get("exclude_nonworking_holidays") else ""))
|
|
142
|
+
manual = [{"reason": m.get("reason"), "norm": m.get("norm"), "scope": m.get("scope")}
|
|
143
|
+
for m in payload.get("manual_review") or [] if isinstance(m, dict)]
|
|
144
|
+
if payload.get("error"):
|
|
145
|
+
manual.append({"reason": payload["error"], "detail": str(payload.get("warnings") or "")})
|
|
146
|
+
return envelope("pretrial", norms=norms, data=data, formulas=formulas, manual_review=manual)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def pretrial_consequences_explain(*, payload: dict, code: str, stage: str) -> dict:
|
|
150
|
+
norms = []
|
|
151
|
+
if payload.get("norm"):
|
|
152
|
+
norms.append({"norm_ref": payload["norm"], "source": "jurisdiction_rules.json (consequences)"})
|
|
153
|
+
data = [{"item": "selection", "code": code, "stage": stage,
|
|
154
|
+
"consequence": payload.get("consequence"), "confidence": payload.get("confidence")}]
|
|
155
|
+
manual = []
|
|
156
|
+
if payload.get("confidence") not in (None, "hard"):
|
|
157
|
+
manual.append({"reason": "confidence != hard", "detail": payload.get("confidence")})
|
|
158
|
+
return envelope("pretrial consequences", norms=norms, data=data, formulas=[], manual_review=manual)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def jurisdiction_explain(*, payload: dict, code: str, claim: Optional[str]) -> dict:
|
|
162
|
+
norms = []
|
|
163
|
+
for cand in payload.get("candidates") or []:
|
|
164
|
+
if not isinstance(cand, dict):
|
|
165
|
+
continue
|
|
166
|
+
entry = {"norm_ref": cand.get("venue_basis"), "confidence": cand.get("confidence")}
|
|
167
|
+
if cand.get("legal_text"):
|
|
168
|
+
entry["verbatim"] = cand["legal_text"] # verbatim берётся из правил
|
|
169
|
+
if cand.get("fetch_hint"):
|
|
170
|
+
entry["fetch_hint"] = cand["fetch_hint"]
|
|
171
|
+
norms.append(entry)
|
|
172
|
+
data = [{"item": "inputs", "code": code, "claim": claim,
|
|
173
|
+
"rules_version_key": "jurisdiction_rules.json"}]
|
|
174
|
+
formulas = [f"кандидат #{i + 1}: {c.get('venue_basis')} ({c.get('confidence')})"
|
|
175
|
+
for i, c in enumerate(payload.get("candidates") or []) if isinstance(c, dict)]
|
|
176
|
+
manual = [{"reason": m.get("reason"), "norm": m.get("norm")}
|
|
177
|
+
for m in payload.get("manual_review") or [] if isinstance(m, dict)]
|
|
178
|
+
return envelope("jurisdiction check", norms=norms, data=data, formulas=formulas, manual_review=manual)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def verify_explain(*, payload: dict, text_len: int) -> dict:
|
|
182
|
+
data = [{"item": "source", "source": payload.get("source"),
|
|
183
|
+
"location": payload.get("location"),
|
|
184
|
+
"path": payload.get("path"), "case_id": payload.get("case_id"),
|
|
185
|
+
"max_fuzzy": payload.get("max_fuzzy"), "requested_len": text_len}]
|
|
186
|
+
formulas = [f"verdict={payload.get('status')} offset={payload.get('offset')} ratio={payload.get('ratio')}"]
|
|
187
|
+
manual = []
|
|
188
|
+
if payload.get("status") in ("not_found", "edition_unavailable"):
|
|
189
|
+
manual.append({"reason": payload.get("status"), "detail": payload.get("detail")})
|
|
190
|
+
return envelope("verify quote", norms=[], data=data, formulas=formulas, manual_review=manual)
|
sud_cli/fees_extra.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""W4c: льготы 333.35-37, возврат 333.40, рамка индексации 183/208/189.1.
|
|
2
|
+
|
|
3
|
+
Основания — ТОЛЬКО fee_exemptions_and_refunds из legal_rules.json
|
|
4
|
+
(VERIFIED pack colleague-legal, акт 2026.10.24). Список категорий в акте —
|
|
5
|
+
выборка, а не полный перечень статьи: совпадения нет => manual_review с
|
|
6
|
+
fetch_hint, права додумывать категории движок не имеет.
|
|
7
|
+
Индексация считается ТОЛЬКО по ИПЦ, явно переданному оператором (данных
|
|
8
|
+
Росстата в репозитории нет; выдумывать коэффициент запрещено).
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from decimal import Decimal, ROUND_HALF_UP
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
DISCLAIMER = ("Расчёт справочный: льготы применяются судом по представленным "
|
|
16
|
+
"документам; категории — VERIFIED-выборка legal, не полный перечень статьи.")
|
|
17
|
+
|
|
18
|
+
_JURIS_TO_EXEMPT_KEY = {
|
|
19
|
+
"apk": "exemptions_333_37",
|
|
20
|
+
"gpk": "exemptions_333_36",
|
|
21
|
+
"sayu": "exemptions_333_36",
|
|
22
|
+
"kas": "exemptions_333_36",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _pack(rules: dict) -> dict:
|
|
27
|
+
return rules.get("fee_exemptions_and_refunds", {})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def fee_exemption_check(rules: dict, *, jurisdiction: str, category_code: str) -> dict:
|
|
31
|
+
pack = _pack(rules)
|
|
32
|
+
keys = [_JURIS_TO_EXEMPT_KEY.get(str(jurisdiction).lower()), "exemptions_333_35"]
|
|
33
|
+
hits = []
|
|
34
|
+
for k in keys:
|
|
35
|
+
if not k:
|
|
36
|
+
continue
|
|
37
|
+
for cat in pack.get(k, {}).get("categories", []):
|
|
38
|
+
if cat.get("code") == category_code:
|
|
39
|
+
hits.append({**cat, "statute": pack[k]["statute"]})
|
|
40
|
+
if hits:
|
|
41
|
+
full = [h for h in hits if h.get("exempt_level") == "full"] or hits
|
|
42
|
+
h = full[0]
|
|
43
|
+
return {"status": "exempt", "exempt_level": h.get("exempt_level"),
|
|
44
|
+
"basis": h.get("basis"), "description": h.get("description"),
|
|
45
|
+
"statute": h.get("statute"), "warnings": [DISCLAIMER]}
|
|
46
|
+
return {"status": "manual_review",
|
|
47
|
+
"detail": f"категория '{category_code}' не в VERIFIED-выборке; полный текст "
|
|
48
|
+
f"ст.333.35-37 — npa_as_of, не догадка движка",
|
|
49
|
+
"fetch_hint": "npa_as_of", "warnings": [DISCLAIMER]}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def fee_refund(rules: dict, *, paid: Decimal, ground: str,
|
|
53
|
+
claim_admitted: bool = False, plaintiff_default: bool = False,
|
|
54
|
+
transferred_to_arbitration: bool = False) -> dict:
|
|
55
|
+
pack = _pack(rules)
|
|
56
|
+
grounds = {g["code"]: g for g in pack.get("refund_333_40", {}).get("grounds", [])}
|
|
57
|
+
if ground not in grounds:
|
|
58
|
+
return {"status": "manual_review", "detail": f"основание '{ground}' не из VERIFIED списка",
|
|
59
|
+
"known": sorted(grounds), "warnings": [DISCLAIMER]}
|
|
60
|
+
g = grounds[ground]
|
|
61
|
+
share = Decimal(str(g.get("refund_share", 1)))
|
|
62
|
+
notes = [g.get("condition", "")]
|
|
63
|
+
if ground == "dismissed_no_consideration" and (plaintiff_default or transferred_to_arbitration):
|
|
64
|
+
share = Decimal("0")
|
|
65
|
+
notes.append("исключение VERIFIED-условия: неявка истца / передача в третейский суд — возврат не положен")
|
|
66
|
+
if ground.startswith("settlement") and claim_admitted:
|
|
67
|
+
share = Decimal("0")
|
|
68
|
+
notes.append("исключение VERIFIED-условия: признание иска ответчиком — 70/50/30% не применяются")
|
|
69
|
+
amount = (paid * share).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
|
70
|
+
return {"status": "ok", "ground": ground, "basis": g["basis"], "share": str(share),
|
|
71
|
+
"paid": str(paid), "refund_amount": str(amount), "conditions": [n for n in notes if n],
|
|
72
|
+
"statute": pack.get("refund_333_40", {}).get("statute", "ст. 333.40 НК РФ"),
|
|
73
|
+
"warnings": [DISCLAIMER]}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def indexation_frame(rules: dict, *, code: str) -> dict:
|
|
77
|
+
idx = _pack(rules).get("interest_and_indexation", {}).get("indexation_procedural", {})
|
|
78
|
+
frame = idx.get(str(code).lower())
|
|
79
|
+
if not frame:
|
|
80
|
+
return {"status": "manual_review", "detail": f"нет VERIFIED рамки для '{code}'",
|
|
81
|
+
"known": sorted(idx)}
|
|
82
|
+
return {"status": "frame_only", "index": frame.get("index"),
|
|
83
|
+
"period_start": frame.get("period_start"), "period_end": frame.get("period_end"),
|
|
84
|
+
"statute": frame.get("statute"),
|
|
85
|
+
"calc_hint": "sud-cli fees indexation --code X --amount S --cpi-start A --cpi-end B "
|
|
86
|
+
"(ИПЦ явно оператором; данных Росстата в пакете нет)",
|
|
87
|
+
"warnings": ["ИПЦ Росстата в репозитории нет: расчёт только по явно переданному "
|
|
88
|
+
"официальному значению; выдуманный коэффициент = фальсификация (правило «тексты только из реестра»)."]}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def indexation_compute(rules: dict, *, code: str, amount: Decimal,
|
|
92
|
+
cpi_start: Optional[Decimal], cpi_end: Optional[Decimal]) -> dict:
|
|
93
|
+
f = indexation_frame(rules, code=code)
|
|
94
|
+
if f["status"] != "frame_only":
|
|
95
|
+
return f
|
|
96
|
+
if cpi_start is None or cpi_end is None or cpi_start <= 0:
|
|
97
|
+
return {**f, "status": "data_unavailable",
|
|
98
|
+
"detail": "нужны --cpi-start/--cpi-end (официальные значения ИПЦ Росстата)"}
|
|
99
|
+
indexed = (amount * (cpi_end / cpi_start)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
|
100
|
+
return {"status": "ok", "basis": f["statute"], "amount": str(amount),
|
|
101
|
+
"cpi_start": str(cpi_start), "cpi_end": str(cpi_end),
|
|
102
|
+
"indexed_amount": str(indexed), "provenance": "user-supplied CPI, unverified against Росстат",
|
|
103
|
+
"period": {"start": f["period_start"], "end": f["period_end"]},
|
|
104
|
+
"warnings": [DISCLAIMER]}
|
sud_cli/forms.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""the wave §4 (S-70): структурные скелеты процессуальных форм + bus-free docx-команда.
|
|
2
|
+
|
|
3
|
+
Границы:
|
|
4
|
+
- только структурные скелеты с плейсхолдерами [ФИО]/[дело]/[дата] и данными
|
|
5
|
+
дела из реестра (обезличенно: case_number/court/plaintiff/defendant);
|
|
6
|
+
- НИКАКИХ нормативных цитат: в модуле нет ни одной ссылки «ст.» с номером,
|
|
7
|
+
и тесты гейтят это regex'ом и по исходнику, и по выводу;
|
|
8
|
+
- sud никогда не запускает docx-конвертер: docx_command собирает готовую
|
|
9
|
+
shlex-экранированную строку (паттерн reminders/alerts) и пишет spec-файл;
|
|
10
|
+
модуль не импортирует ничего, что умеет поднимать внешние процессы;
|
|
11
|
+
- deferral-шаблон госпошлины НЕ дублируется: вид otsrochka даёт структуру и
|
|
12
|
+
ссылку-команду на существующий генератор `sud-cli deferral`.
|
|
13
|
+
|
|
14
|
+
SYNC ТОЧКА §3<->§4: заголовки markdown-секций ниже = маркеры
|
|
15
|
+
data/completeness_rules.json (hodat: Шапка/Просьба/Подпись/Дата;
|
|
16
|
+
appeal: +Обжалуется/Основания/Приложения). Меняешь маркер — меняй заголовок,
|
|
17
|
+
тесты test_v2120_3 расходятся первыми.
|
|
18
|
+
"""
|
|
19
|
+
import json
|
|
20
|
+
import shlex
|
|
21
|
+
import tempfile
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
FORM_KINDS = ("vosstanovlenie_sroka", "otsrochka", "appeal_structure")
|
|
25
|
+
|
|
26
|
+
#: вид формы -> checklist-kind из completeness_rules.json (единый набор маркеров)
|
|
27
|
+
CHECKLIST_KIND_FOR_FORM = {
|
|
28
|
+
"vosstanovlenie_sroka": "hodat",
|
|
29
|
+
"otsrochka": "hodat",
|
|
30
|
+
"appeal_structure": "appeal_apk",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
BUS_FREE_NOTE = (
|
|
34
|
+
"bus-free: sud-cli не запускает docx-конвертер; ниже готовая экранированная "
|
|
35
|
+
"строка команды — исполнение остаётся за оператором"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
VERIFY_CMD_TEMPLATE = '/usr/bin/sud-cli form new {kind} --case {case_id} --format md'
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _case_fields(case):
|
|
42
|
+
case = case or {}
|
|
43
|
+
return {
|
|
44
|
+
"case_id": case.get("case_id"),
|
|
45
|
+
"case_number": case.get("case_number") or "[дело]",
|
|
46
|
+
"court": case.get("court") or "[суд]",
|
|
47
|
+
"plaintiff": case.get("plaintiff") or "[истец]",
|
|
48
|
+
"defendant": case.get("defendant") or "[ответчик]",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _header(c):
|
|
53
|
+
return (
|
|
54
|
+
"## Шапка\n"
|
|
55
|
+
f"В [суд]: {c['court']}\n"
|
|
56
|
+
f"Дело [дело] N {c['case_number']}\n"
|
|
57
|
+
f"[истец]: {c['plaintiff']}\n"
|
|
58
|
+
f"[ответчик]: {c['defendant']}\n"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _skeleton_vosstanovlenie(c):
|
|
63
|
+
return (
|
|
64
|
+
"# Ходатайство о восстановлении процессуального срока (структурный скелет)\n\n"
|
|
65
|
+
+ _header(c)
|
|
66
|
+
+ "\n## Просьба\n"
|
|
67
|
+
"Прошу восстановить пропущенный срок для [действие].\n"
|
|
68
|
+
"[причина пропуска]: ...; [момент, с которым связан срок]: ...\n"
|
|
69
|
+
"[доказательства уважительности]: ...\n\n"
|
|
70
|
+
"## Подпись\n"
|
|
71
|
+
"[ФИО] ________________________\n\n"
|
|
72
|
+
"## Дата\n"
|
|
73
|
+
"[ДД.ММ.ГГГГ]\n"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _skeleton_otsrochka(c):
|
|
78
|
+
return (
|
|
79
|
+
"# Ходатайство об отсрочке (рассрочке) уплаты государственной пошлины (структурный скелет)\n\n"
|
|
80
|
+
+ _header(c)
|
|
81
|
+
+ "\n## Просьба\n"
|
|
82
|
+
"Прошу предоставить отсрочку (рассрочку) уплаты пошлины в размере [сумма] до [ДД.ММ.ГГГГ].\n"
|
|
83
|
+
"[обстоятельства имущественного положения]: ...\n\n"
|
|
84
|
+
"## Приложения\n"
|
|
85
|
+
"[справка банка об остатках]\n[иные документы]\n\n"
|
|
86
|
+
"## Подпись\n"
|
|
87
|
+
"[ФИО] ________________________\n\n"
|
|
88
|
+
"## Дата\n"
|
|
89
|
+
"[ДД.ММ.ГГГГ]\n\n"
|
|
90
|
+
"---\n"
|
|
91
|
+
"Существующий шаблон ходатайства об отсрочке пошлины НЕ дублируется: "
|
|
92
|
+
"полный текст даёт команда `sud-cli deferral --applicant ... --court ... "
|
|
93
|
+
"--amount ... --reason ...`.\n"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _skeleton_appeal(c):
|
|
98
|
+
return (
|
|
99
|
+
"# Апелляционная жалоба (структурный скелет)\n\n"
|
|
100
|
+
+ _header(c)
|
|
101
|
+
+ "\n## Обжалуется\n"
|
|
102
|
+
"Судебный акт от [ДД.ММ.ГГГГ] по делу N " + str(c["case_number"]) + ".\n\n"
|
|
103
|
+
"## Основания\n"
|
|
104
|
+
"[довод 1]: ...\n[довод 2]: ...\n\n"
|
|
105
|
+
"## Просьба\n"
|
|
106
|
+
"Прошу [решение по результату рассмотрения].\n\n"
|
|
107
|
+
"## Приложения\n"
|
|
108
|
+
"[копия судебного акта]\n[документы, подтверждающие доводы]\n\n"
|
|
109
|
+
"## Подпись\n"
|
|
110
|
+
"[ФИО] ________________________\n\n"
|
|
111
|
+
"## Дата\n"
|
|
112
|
+
"[ДД.ММ.ГГГГ]\n"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
_BUILDERS = {
|
|
117
|
+
"vosstanovlenie_sroka": _skeleton_vosstanovlenie,
|
|
118
|
+
"otsrochka": _skeleton_otsrochka,
|
|
119
|
+
"appeal_structure": _skeleton_appeal,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def render_skeleton(kind, case):
|
|
124
|
+
if kind not in _BUILDERS:
|
|
125
|
+
raise ValueError(f"unknown form kind: {kind}")
|
|
126
|
+
return _BUILDERS[kind](_case_fields(case))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _as_sections(kind, case):
|
|
130
|
+
"""json-представление: список {heading, body} без потери структуры md."""
|
|
131
|
+
text = render_skeleton(kind, case)
|
|
132
|
+
sections = []
|
|
133
|
+
current = None
|
|
134
|
+
for line in text.splitlines():
|
|
135
|
+
if line.startswith("#"):
|
|
136
|
+
if current:
|
|
137
|
+
sections.append(current)
|
|
138
|
+
current = {"heading": line.lstrip("# ").strip(), "body": ""}
|
|
139
|
+
elif current is not None:
|
|
140
|
+
current["body"] = (current["body"] + "\n" + line).strip("\n")
|
|
141
|
+
if current:
|
|
142
|
+
sections.append(current)
|
|
143
|
+
return sections
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def render_form(kind, case, fmt="md"):
|
|
147
|
+
"""Формы: markdown|json. Только чтение данных дела, без сайд-эффектов."""
|
|
148
|
+
if fmt not in ("md", "json"):
|
|
149
|
+
raise ValueError(f"unknown format: {fmt}")
|
|
150
|
+
skeleton = render_skeleton(kind, case)
|
|
151
|
+
if fmt == "md":
|
|
152
|
+
return {
|
|
153
|
+
"status": "ok",
|
|
154
|
+
"kind": kind,
|
|
155
|
+
"format": "md",
|
|
156
|
+
"checklist_kind": CHECKLIST_KIND_FOR_FORM[kind],
|
|
157
|
+
"case": _case_fields(case),
|
|
158
|
+
"text": skeleton,
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
"status": "ok",
|
|
162
|
+
"kind": kind,
|
|
163
|
+
"format": "json",
|
|
164
|
+
"checklist_kind": CHECKLIST_KIND_FOR_FORM[kind],
|
|
165
|
+
"case": _case_fields(case),
|
|
166
|
+
"sections": _as_sections(kind, case),
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def build_docx_spec(kind, case):
|
|
171
|
+
return {
|
|
172
|
+
"generator": "sud-cli form docx-command",
|
|
173
|
+
"kind": kind,
|
|
174
|
+
"case": _case_fields(case),
|
|
175
|
+
"content_md": render_skeleton(kind, case),
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def docx_command(kind, case, spec_dir=None):
|
|
180
|
+
"""Готовая shlex-строка docx-cli build; sud НИКОГДА не вызывает её сам.
|
|
181
|
+
|
|
182
|
+
Паттерн reminders/alerts: сборка строки + запись spec-файла. Никаких
|
|
183
|
+
внешних процессов: исполнение — осознанный шаг оператора.
|
|
184
|
+
"""
|
|
185
|
+
case_id = _case_fields(case)["case_id"] or "[case]"
|
|
186
|
+
spec = build_docx_spec(kind, case)
|
|
187
|
+
if spec_dir is None:
|
|
188
|
+
spec_dir = Path(tempfile.mkdtemp(prefix="sud-form-"))
|
|
189
|
+
else:
|
|
190
|
+
spec_dir = Path(spec_dir)
|
|
191
|
+
spec_dir.mkdir(parents=True, exist_ok=True)
|
|
192
|
+
spec_path = spec_dir / f"{kind}-{case_id}-spec.json"
|
|
193
|
+
spec_path.write_text(json.dumps(spec, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
194
|
+
verify = VERIFY_CMD_TEMPLATE.format(kind=kind, case_id=case_id)
|
|
195
|
+
parts = ["docx-cli", "build", "--spec", str(spec_path), "--verify", verify, "--verify-run"]
|
|
196
|
+
command = " ".join(shlex.quote(p) for p in parts)
|
|
197
|
+
return {
|
|
198
|
+
"status": "ok",
|
|
199
|
+
"kind": kind,
|
|
200
|
+
"command": command,
|
|
201
|
+
"spec_path": str(spec_path),
|
|
202
|
+
"verify_command": verify,
|
|
203
|
+
"bus_free": BUS_FREE_NOTE,
|
|
204
|
+
}
|
sud_cli/fssp_manual.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""S-61 (Б) ручной контур ФССП — human-in-the-loop проверка БДИП.
|
|
2
|
+
|
|
3
|
+
Акт legal 2026.10.25 (проверено): официальный публичный API
|
|
4
|
+
ФССП отключён с 10.03.2022; обход капчи/лимитов запрещён. Утверждён базовый путь:
|
|
5
|
+
sud-cli готовит ссылку и чек-лист, капчу проходит человек с личного устройства,
|
|
6
|
+
результат фиксируется в досье как доказательство с provenance (кто/когда/чем
|
|
7
|
+
подтверждено). Внешние действия — за человеком (канон S-62).
|
|
8
|
+
|
|
9
|
+
Этот модуль НЕ выполняет сетевых запросов: только план проверки и оформление
|
|
10
|
+
результата. Данные ФССП носит ориентирующий характер (ст. 7 8-ФЗ о достоверности
|
|
11
|
+
оперативной информации): при споре подтверждать первоисточником/запросом.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
FSSP_BASE_URL = "https://fssp.gov.ru/iss/ip/"
|
|
18
|
+
|
|
19
|
+
_KINDS = {"fiz", "ur"}
|
|
20
|
+
|
|
21
|
+
CHECKLIST = [
|
|
22
|
+
"Откройте ссылку на личном устройстве (не с серверного IP: ФССП лимитирует частые подключения).",
|
|
23
|
+
"Выберите раздел: поиск по физическим лицам / юридических лицах.",
|
|
24
|
+
"Введите параметры из плана ниже; при возможности сузьте регионом.",
|
|
25
|
+
"Дождитесь ответа капчи/проверки; сделайте скриншот результата (или сохраните PDF).",
|
|
26
|
+
"Вернитесь в sud-cli: fssp record — результат попадёт в реестр доказательств дела с отметкой provenance.",
|
|
27
|
+
"Для процессуального использования: данные БДИП ориентирующие; при споре подтвердите запросом в ОСП/первоисточником.",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _require(condition: bool, message: str) -> None:
|
|
32
|
+
if not condition:
|
|
33
|
+
raise ValueError(message)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def check_plan(kind: str, *, name: Optional[str] = None, inn: Optional[str] = None,
|
|
37
|
+
ogrn: Optional[str] = None, birth_date: Optional[str] = None,
|
|
38
|
+
region: Optional[str] = None) -> dict:
|
|
39
|
+
"""Собрать план ручной проверки БДИП ФССП (без сетевых запросов)."""
|
|
40
|
+
kind = (kind or "").strip().lower()
|
|
41
|
+
_require(kind in _KINDS, f"kind must be one of {sorted(_KINDS)}")
|
|
42
|
+
params: dict[str, str] = {}
|
|
43
|
+
if kind == "fiz":
|
|
44
|
+
_require(bool(name), "for kind=fiz provide --name (ФИО)")
|
|
45
|
+
params["ФИО"] = str(name).strip()
|
|
46
|
+
if birth_date:
|
|
47
|
+
params["дата рождения"] = birth_date.strip()
|
|
48
|
+
else:
|
|
49
|
+
_require(bool(name or inn or ogrn),
|
|
50
|
+
"for kind=ur provide --name or --inn/--ogrn")
|
|
51
|
+
if name:
|
|
52
|
+
params["наименование"] = name.strip()
|
|
53
|
+
if inn:
|
|
54
|
+
params["ИНН"] = inn.strip()
|
|
55
|
+
if ogrn:
|
|
56
|
+
params["ОГРН"] = ogrn.strip()
|
|
57
|
+
if region:
|
|
58
|
+
params["регион"] = region.strip()
|
|
59
|
+
return {
|
|
60
|
+
"url": FSSP_BASE_URL,
|
|
61
|
+
"kind": kind,
|
|
62
|
+
"params": params,
|
|
63
|
+
"checklist": list(CHECKLIST),
|
|
64
|
+
"disclaimer": ("Данные Банка данных исполнительных производств носят ориентирующий характер; "
|
|
65
|
+
"проверку выполняет человек с личного устройства; результат фиксируется "
|
|
66
|
+
"в досье с provenance. Автоматический обход капчи/лимитов запрещён (юридическая проверка 25.10)."),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def record_payload(*, subject: str, result: str, checked_by: str,
|
|
71
|
+
notes: Optional[str] = None,
|
|
72
|
+
evidence_file: Optional[str] = None) -> dict:
|
|
73
|
+
"""Оформить результат ручной проверки для реестра доказательств.
|
|
74
|
+
|
|
75
|
+
title/note — без чужих ПДн pass-through сверх введённых самим оператором;
|
|
76
|
+
файл доказательства (скрин/PDF) хранится отдельно, здесь только путь.
|
|
77
|
+
"""
|
|
78
|
+
result = (result or "").strip().lower()
|
|
79
|
+
_require(bool(subject and subject.strip()), "subject required")
|
|
80
|
+
_require(bool(checked_by and checked_by.strip()), "checked_by required")
|
|
81
|
+
_require(result in {"none", "found", "inconclusive"},
|
|
82
|
+
"result must be none|found|inconclusive")
|
|
83
|
+
verdict = {"none": "производств не найдено",
|
|
84
|
+
"found": "производства найдены",
|
|
85
|
+
"inconclusive": "проверка не завершена/неоднозначно"}[result]
|
|
86
|
+
title = f"Ручная проверка ФССП БДИП: {subject.strip()}"
|
|
87
|
+
note = f"Результат: {verdict}. Проверил(а): {checked_by.strip()}. Источник: {FSSP_BASE_URL} (ориентирующие данные)."
|
|
88
|
+
if notes:
|
|
89
|
+
note += f" Примечание: {notes.strip()}"
|
|
90
|
+
return {"title": title, "note": note, "result": result,
|
|
91
|
+
"evidence_file": evidence_file}
|