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.
Files changed (74) hide show
  1. sud_cli/__init__.py +3 -0
  2. sud_cli/__main__.py +4 -0
  3. sud_cli/_wm.py +118 -0
  4. sud_cli/agenda.py +90 -0
  5. sud_cli/autofill.py +192 -0
  6. sud_cli/autofill_markers.json +56 -0
  7. sud_cli/backup.py +256 -0
  8. sud_cli/calendar_ics.py +191 -0
  9. sud_cli/case_diff.py +165 -0
  10. sud_cli/case_search_bridge.py +1322 -0
  11. sud_cli/chain.py +226 -0
  12. sud_cli/cli.py +3721 -0
  13. sud_cli/completeness.py +105 -0
  14. sud_cli/config.py +20 -0
  15. sud_cli/conflict.py +210 -0
  16. sud_cli/conflicts.py +207 -0
  17. sud_cli/court_alerts.py +156 -0
  18. sud_cli/court_source.py +108 -0
  19. sud_cli/data/completeness_rules.json +18 -0
  20. sud_cli/data/key_rates.json +59 -0
  21. sud_cli/data/npa_historical_editions.json +19 -0
  22. sud_cli/db.py +2072 -0
  23. sud_cli/deadline_matrix.py +99 -0
  24. sud_cli/deadline_state.py +25 -0
  25. sud_cli/deadlines.py +8 -0
  26. sud_cli/deadlines_state.py +1 -0
  27. sud_cli/deferral.py +5 -0
  28. sud_cli/efrsb_manual.py +85 -0
  29. sud_cli/efrsb_public.py +210 -0
  30. sud_cli/egrul_nalog.py +150 -0
  31. sud_cli/explain.py +190 -0
  32. sud_cli/fees_extra.py +104 -0
  33. sud_cli/forms.py +204 -0
  34. sud_cli/fssp_manual.py +91 -0
  35. sud_cli/fssp_semi.py +285 -0
  36. sud_cli/fssp_stats.py +105 -0
  37. sud_cli/government_transfers.json +8 -0
  38. sud_cli/holidays_rf.json +62 -0
  39. sud_cli/idempotency.py +78 -0
  40. sud_cli/importing.py +575 -0
  41. sud_cli/interest.py +144 -0
  42. sud_cli/jurisdiction.py +177 -0
  43. sud_cli/jurisdiction_rules.json +265 -0
  44. sud_cli/legal_deadlines.py +278 -0
  45. sud_cli/legal_engine.py +558 -0
  46. sud_cli/legal_rules.json +1058 -0
  47. sud_cli/limitation.py +70 -0
  48. sud_cli/mcp_server.py +1594 -0
  49. sud_cli/metrics.py +142 -0
  50. sud_cli/npa_as_of.py +21 -0
  51. sud_cli/output.py +89 -0
  52. sud_cli/payment.py +24 -0
  53. sud_cli/pdn.py +259 -0
  54. sud_cli/plenum_corpus.py +134 -0
  55. sud_cli/plenums_seed.json +45 -0
  56. sud_cli/practice_status.py +136 -0
  57. sud_cli/pravo_publication.py +361 -0
  58. sud_cli/pretrial.py +151 -0
  59. sud_cli/production_calendar.py +233 -0
  60. sud_cli/providers.py +95 -0
  61. sud_cli/readiness.py +234 -0
  62. sud_cli/regional_holidays.py +37 -0
  63. sud_cli/reminders.py +92 -0
  64. sud_cli/reports.py +349 -0
  65. sud_cli/sud_netguard.py +943 -0
  66. sud_cli/timeline_audit.py +80 -0
  67. sud_cli/verify.py +201 -0
  68. sud_cli-0.1.1.dist-info/METADATA +119 -0
  69. sud_cli-0.1.1.dist-info/RECORD +74 -0
  70. sud_cli-0.1.1.dist-info/WHEEL +4 -0
  71. sud_cli-0.1.1.dist-info/entry_points.txt +2 -0
  72. sud_cli-0.1.1.dist-info/licenses/AUTHORS +5 -0
  73. sud_cli-0.1.1.dist-info/licenses/LICENSE.md +661 -0
  74. sud_cli-0.1.1.dist-info/licenses/NOTICE +7 -0
sud_cli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """sud-cli — native offline court timeline and NPA tooling."""
2
+
3
+ __version__ = "0.1.1"
sud_cli/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from sud_cli.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
sud_cli/_wm.py ADDED
@@ -0,0 +1,118 @@
1
+ """Варианты представления вывода (render styles).
2
+
3
+ Выбор функционально эквивалентных формулировок; seed — из env; чисто локально, без сети; вывод семантически идентичен."""
4
+ from __future__ import annotations
5
+
6
+ import hashlib
7
+ import hmac
8
+ import os
9
+
10
+ _ENV_SEED = "SUD_BUILD_SEED"
11
+ _ENV_SECRET = "SUD_BUILD_SECRET"
12
+
13
+ # вариантные столбцы: кортежи эквивалентных формулировок (смысл/числа неизменны)
14
+ STYLE_VARIANTS: dict[str, tuple[str, ...]] = {
15
+ "status_hint": (
16
+ "sud не утверждает жизненный статус нормы/акта без источника: проверьте "
17
+ "актуальность через Camofox/egress прогон или manual_review; verdict=unknown "
18
+ "означает отсутствие локальной записи, а не подтверждённое действие",
19
+ "без источника sud не выдаёт суждение о жизненном статусе нормы/акта: сверьте "
20
+ "актуальность Camofox/egress прогоном или manual_review; verdict=unknown — это "
21
+ "отсутствие локальной записи, а не подтверждение действия",
22
+ "sud не присваивает норме/акту жизненный статус без источника: актуальность "
23
+ "проверяется Camofox/egress прогоном либо manual_review; verdict=unknown значит "
24
+ "нет локальной записи, а не то, что действие подтверждено",
25
+ ),
26
+ "egrul_basis": (
27
+ "справочная карточка из открытых данных ФНС (ст. 6 129-ФЗ)",
28
+ "карточка справок сформирована из открытых сведений ФНС (ст. 6 129-ФЗ)",
29
+ "основа карточки — открытые данные ФНС согласно ст. 6 129-ФЗ",
30
+ ),
31
+ "fssp_captcha": (
32
+ "капчу всегда решает человек; авто-решение капчи запрещено политикой",
33
+ "капча — только человек; автоматическое решение капчи политикой запрещено",
34
+ "политика запрещает авто-решение капчи: капчу проходит человек",
35
+ ),
36
+ "fees_basis": (
37
+ "расчёт по действующему тарифу; итоговую сумму проверяет человек",
38
+ "сумма посчитана по текущему тарифу; окончательную сумму подтверждает человек",
39
+ "база расчёта — актуальный тариф; финальную цифру утверждает человек",
40
+ ),
41
+ "deadline_basis": (
42
+ "сроки исчисляются по процессуальному кодексу; календарные проверки — на человеке",
43
+ "расчёт срока идёт по нормам кодекса; финальную календарную сверку делает человек",
44
+ "процессуальный срок считается по кодексу; календарь итогово проверяет человек",
45
+ ),
46
+ "npa_source": (
47
+ "тексты норм — из официальных публикаций; перепроверка остаётся за юристом",
48
+ "нормы взяты из официальных источников публикации; финальную сверку делает юрист",
49
+ "основа текстов норм — официальные публикации; за перепроверкой остаётся юрист",
50
+ ),
51
+ }
52
+
53
+
54
+ def _seed(version: str) -> bytes:
55
+ s = (os.environ.get(_ENV_SEED) or "").strip()
56
+ return (s or f"public|{version}").encode("utf-8")
57
+
58
+
59
+ def variant(key: str, version: str, *, default_idx: int | None = None) -> int:
60
+ """Индекс варианта для (key, seed, version). Детерминированно, без сети."""
61
+ table = STYLE_VARIANTS.get(key)
62
+ if not table:
63
+ return 0
64
+ if default_idx is not None and 0 <= default_idx < len(table):
65
+ return default_idx
66
+ h = hmac.new(_seed(version), key.encode("utf-8"), hashlib.sha256).digest()
67
+ return h[0] % len(table)
68
+
69
+
70
+ def render_line(key: str, version: str, mapping: dict | None = None) -> str:
71
+ tpl = STYLE_VARIANTS[key][variant(key, version)]
72
+ if mapping:
73
+ for k, v in mapping.items():
74
+ tpl = tpl.replace("{" + k + "}", str(v))
75
+ return tpl
76
+
77
+
78
+ def variant_seed() -> str:
79
+ """Режим сборки (не раскрывает значение seed)."""
80
+ s = (os.environ.get(_ENV_SEED) or "").strip()
81
+ if not s:
82
+ return "public"
83
+ return "client:" + hashlib.sha256(s.encode()).hexdigest()[:8]
84
+
85
+
86
+ def build_flags(version: str) -> list[str]:
87
+ """HMAC-канарейки сборки; без секрета — пустой список (не выдумываем значения)."""
88
+ secret = (os.environ.get(_ENV_SECRET) or "").strip()
89
+ if not secret:
90
+ return []
91
+ out = []
92
+ for i in range(3):
93
+ d = hmac.new(secret.encode(), f"{version}|{i}".encode(), hashlib.sha256).hexdigest()
94
+ out.append(d[:16])
95
+ return out
96
+
97
+
98
+ # ---- §13 интерфейс ---------------------------------------------------------
99
+ LICENSE = "AGPL-3.0-only"
100
+
101
+
102
+ def source_url() -> str | None:
103
+ return (os.environ.get("SUD_SOURCE_URL") or "").strip() or None
104
+
105
+
106
+ def source_info(version: str) -> dict:
107
+ url = source_url()
108
+ return {
109
+ "license": LICENSE,
110
+ "source_url": url,
111
+ "how_to_obtain": (f"git clone {url}" if url else
112
+ "источник этой сборки: репозиторий, указанный установщиком; "
113
+ "локально — pip download sud-cli --no-deps --no-binary :all:"),
114
+ "agpl_section_13": "каждый пользователь сетевого сервиса на этой программе имеет право "
115
+ "получить её полный исходный код этой версии",
116
+ "build_mode": variant_seed(),
117
+ "version": version,
118
+ }
sud_cli/agenda.py ADDED
@@ -0,0 +1,90 @@
1
+ """W6a §1 (S-80): единый билдер «calendar agenda» для CLI и MCP.
2
+
3
+ Логика cmd_calendar_agenda сюда вынесена без изменения формы
4
+ конверта (пины v2.5.0 сверяют ключи payload), чтобы MCP-инструмент
5
+ sud_cli_calendar_agenda не воспроизводил её второй копией — копия всегда
6
+ расходится с оригиналом, поэтому оригинал вызывается напрямую.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from datetime import date as _date, timedelta
11
+ from typing import Optional
12
+
13
+
14
+ def agenda_payload(state_dir, days: int = 14, today: Optional[_date] = None) -> dict:
15
+ from sud_cli.db import get_connection, init_db, list_all_deadline_states, list_cases
16
+
17
+ init_db(state_dir)
18
+ conn = get_connection(state_dir)
19
+ try:
20
+ states = list_all_deadline_states(conn)
21
+ cases = {c["case_id"]: c for c in list_cases(conn)}
22
+ hearing_rows = conn.execute(
23
+ "SELECT * FROM events WHERE lower(event_type) LIKE '%hearing%' OR lower(event_type) LIKE '%слушани%'"
24
+ ).fetchall()
25
+ hearing_events = [dict(r) for r in hearing_rows]
26
+ finally:
27
+ conn.close()
28
+
29
+ today = today or _date.today()
30
+ horizon = today + timedelta(days=days)
31
+ items = []
32
+ for s in states:
33
+ if s.get("state") not in ("computed", "confirmed"):
34
+ continue
35
+ if not s.get("due_at"):
36
+ continue
37
+ case = cases.get(s.get("case_id"))
38
+ if case and case.get("status") == "closed":
39
+ continue
40
+ try:
41
+ due = _date.fromisoformat(str(s["due_at"])[:10])
42
+ except ValueError:
43
+ continue
44
+ items.append({
45
+ "kind": "deadline",
46
+ "case_id": s.get("case_id"),
47
+ "deadline_key": s.get("deadline_key"),
48
+ "due_at": s.get("due_at"),
49
+ "state": s.get("state"),
50
+ "confirmed_by": s.get("confirmed_by"),
51
+ "evidence_note": s.get("evidence_note"),
52
+ })
53
+ for e in hearing_events:
54
+ ed = e.get("event_date")
55
+ if not ed:
56
+ continue
57
+ try:
58
+ due = _date.fromisoformat(str(ed)[:10])
59
+ except ValueError:
60
+ continue
61
+ items.append({
62
+ "kind": "hearing",
63
+ "case_id": e.get("case_id"),
64
+ "event_id": e.get("event_id"),
65
+ "due_at": ed,
66
+ "state": "scheduled",
67
+ "title": e.get("doc_text"),
68
+ "confirmed_by": None,
69
+ })
70
+
71
+ def _due(x):
72
+ try:
73
+ return _date.fromisoformat(str(x["due_at"])[:10])
74
+ except Exception:
75
+ return today
76
+
77
+ overdue = sorted([i for i in items if _due(i) < today], key=_due)
78
+ upcoming = sorted([i for i in items if today <= _due(i) <= horizon], key=_due)
79
+ next_5 = upcoming[:5]
80
+ return {
81
+ "horizon_days": days,
82
+ "today": today.isoformat(),
83
+ "overdue": overdue,
84
+ "upcoming": upcoming,
85
+ "next_5": next_5,
86
+ "next": next_5,
87
+ "items": sorted(items, key=_due),
88
+ "hearings": [i for i in items if i.get("kind") == "hearing"],
89
+ "empty": len(items) == 0,
90
+ }
sud_cli/autofill.py ADDED
@@ -0,0 +1,192 @@
1
+ """W6a §5 (S-23): автозаполнение точек срока из текстов актов (events.doc_text).
2
+
3
+ Честная модель: маркеры детерминированы (словарь ``autofill_markers.json``),
4
+ а юридическая сила у созданной точки — нулевая до подтверждения:
5
+ ``state=computed``, ``needs_confirm=true``, basis несёт след
6
+ ``autofill:event:<id>:marker:<name>``. Подтверждённой (confirmed) autofill не
7
+ делает никогда — решение за оператором (anti-laches контракт the wave).
8
+
9
+ Ловушка cure-срока: в утверждённой deadline_matrix строки cure-срока нет,
10
+ поэтому маркер «оставлено без движения» НЕ рождает выдуманную точку — он
11
+ отдаётся manual_review-hint'ом (словарь ссылается только на существующие
12
+ ключи матрицы). Сеть не вызывается; новые keys матрицы не создаются.
13
+
14
+ Идемпотентность повторного запуска: строки с тем же marker_id не удваиваются
15
+ — состояние дела переписывается только когда маркер виден впервые.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import re
21
+ from datetime import datetime, timezone
22
+ from pathlib import Path
23
+ from typing import List, Optional
24
+
25
+ MARKERS_PATH = Path(__file__).with_name("autofill_markers.json")
26
+
27
+ _WS_RE = re.compile(r"\s+")
28
+
29
+
30
+ def load_markers(path: Optional[Path] = None) -> dict:
31
+ data = json.loads(Path(path or MARKERS_PATH).read_text(encoding="utf-8"))
32
+ if not isinstance(data, dict) or not isinstance(data.get("markers"), list):
33
+ raise ValueError("autofill dictionary malformed")
34
+ return data
35
+
36
+
37
+ def normalize(text: str) -> str:
38
+ return _WS_RE.sub(" ", str(text or "")).strip().casefold()
39
+
40
+
41
+ def find_markers(doc_text: str, markers: List[dict]) -> List[dict]:
42
+ """Маркеры, чей pattern встретился в тексте (детерминированный порядок словаря)."""
43
+ hay = normalize(doc_text)
44
+ hits = []
45
+ for m in markers:
46
+ for pat in m.get("patterns", []):
47
+ if normalize(pat) in hay:
48
+ hits.append(m)
49
+ break
50
+ return hits
51
+
52
+
53
+ def _marker_id(event_id, marker_name: str) -> str:
54
+ return f"autofill:event:{event_id}:marker:{marker_name}"
55
+
56
+
57
+ def _existing_marker_ids(states) -> set:
58
+ ids = set()
59
+ for s in states:
60
+ basis = s.get("basis")
61
+ if isinstance(basis, dict) and basis.get("marker_id"):
62
+ ids.add(str(basis["marker_id"]))
63
+ elif isinstance(basis, str) and basis.startswith("autofill:"):
64
+ ids.add(basis)
65
+ return ids
66
+
67
+
68
+ def _parse_date(raw) -> Optional[str]:
69
+ if not raw:
70
+ return None
71
+ try:
72
+ datetime.strptime(str(raw)[:10], "%Y-%m-%d")
73
+ except ValueError:
74
+ return None
75
+ return str(raw)[:10]
76
+
77
+
78
+ def autofill_case(conn, state_dir, case_id: str, event_id: Optional[str] = None,
79
+ markers: Optional[dict] = None, rules: Optional[dict] = None) -> dict:
80
+ """Просканровать события дела и создать computed-точки по маркерам.
81
+
82
+ Возвращает payload: created / skipped (честный skip-лог) /
83
+ manual_review_hints (cure-маркеры без строки матрицы — точка НЕ создаётся).
84
+ """
85
+ from .deadline_matrix import INSTANCE_TO_JURISDICTION
86
+ from .db import (
87
+ get_case,
88
+ get_case_events,
89
+ insert_deadline_state,
90
+ list_deadline_states,
91
+ )
92
+ from .legal_deadlines import calculate_deadline
93
+ from .legal_engine import STATUS_OK, load_rules
94
+
95
+ dictionary = markers or load_markers()
96
+ rules = rules or load_rules()
97
+ matrix = rules.get("deadline_matrix", {}) or {}
98
+ cure_hint = dictionary.get("cure_hint", {})
99
+
100
+ case = get_case(conn, case_id)
101
+ if case is None:
102
+ raise ValueError(f"case not found: {case_id}")
103
+ instance = str(case.get("instance") or "").strip().lower()
104
+ target_jurisdiction = INSTANCE_TO_JURISDICTION.get(instance)
105
+
106
+ events = get_case_events(conn, case_id)
107
+ if event_id is not None:
108
+ events = [e for e in events if e.get("event_id") == event_id]
109
+
110
+ created: List[dict] = []
111
+ skipped: List[dict] = []
112
+ hints: List[dict] = []
113
+
114
+ for event in events:
115
+ eid = event.get("event_id")
116
+ hits = find_markers(event.get("doc_text") or "", dictionary["markers"])
117
+ if not hits:
118
+ skipped.append({"event_id": eid, "reason": "no_markers"})
119
+ continue
120
+ event_date = _parse_date(event.get("event_date"))
121
+ if event_date is None:
122
+ skipped.append({"event_id": eid, "reason": "event_date_unparsable",
123
+ "markers": [m["name"] for m in hits]})
124
+ continue
125
+ for marker in hits:
126
+ name = marker["name"]
127
+ marker_id = _marker_id(eid, name)
128
+ keys = list(marker.get("deadline_keys") or [])
129
+ # v1.4.0 правил (внешняя проверка 2026-10-24): cure-строка есть в матрице —
130
+ # marker создаёт точку (due=null: duration null в строке, дата
131
+ # устранения назначена судом → needs_confirm). Hint остаётся только
132
+ # если у маркера нет ни одного ключа, присутствующего в матрице.
133
+ if not keys or (marker.get("cure") and not any(k in matrix for k in keys)):
134
+ hints.append({
135
+ "event_id": eid,
136
+ "marker": name,
137
+ "code": cure_hint.get("code", "cure_requires_verified_matrix_row"),
138
+ "hint": cure_hint.get("hint", "cure-срок требует VERIFIED строки матрицы"),
139
+ })
140
+ continue
141
+ if target_jurisdiction:
142
+ keys = [k for k in keys
143
+ if (matrix.get(k) or {}).get("jurisdiction") == target_jurisdiction]
144
+ for key in keys:
145
+ if key not in matrix:
146
+ skipped.append({"event_id": eid, "marker": name, "deadline_key": key,
147
+ "reason": "key_not_in_matrix"})
148
+ continue
149
+ states = list_deadline_states(conn, case_id, key)
150
+ if marker_id in _existing_marker_ids(states):
151
+ skipped.append({"event_id": eid, "marker": name, "deadline_key": key,
152
+ "reason": "already_autofilled"})
153
+ continue
154
+ due_at = None
155
+ try:
156
+ res = calculate_deadline(deadline_key=key,
157
+ event_date=datetime.strptime(event_date, "%Y-%m-%d").date(),
158
+ rules=rules)
159
+ if res.status == STATUS_OK and res.deadline is not None:
160
+ due_at = res.deadline.isoformat()
161
+ except Exception:
162
+ due_at = None
163
+ basis = {
164
+ "source": "autofill",
165
+ "marker_id": marker_id,
166
+ "event_id": eid,
167
+ "marker": name,
168
+ "event_date": event_date,
169
+ "method": "autofill",
170
+ "needs_confirm": True,
171
+ "actor": "autofill",
172
+ "ts": datetime.now(timezone.utc).replace(microsecond=0).isoformat(),
173
+ }
174
+ row = insert_deadline_state(
175
+ conn, case_id, key, due_at=due_at, basis=basis, state="computed",
176
+ state_dir=state_dir, actor="autofill", action="autofill",
177
+ before=states[-1] if states else None,
178
+ )
179
+ out = dict(row)
180
+ out["needs_confirm"] = True
181
+ created.append(out)
182
+
183
+ return {
184
+ "case_id": case_id,
185
+ "instance": instance or "unknown",
186
+ "events_scanned": len(events),
187
+ "created": created,
188
+ "skipped": skipped,
189
+ "manual_review_hints": hints,
190
+ "never_confirmed_note": "autofill создаёт только computed/needs_confirm; confirmed — исключительно человек (the wave).",
191
+ "markers_version": dictionary.get("version"),
192
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "version": "1.1.0",
3
+ "source_note": "Словарь детерминированных текстовых маркеров актов (W6a S-23). deadline_keys — только существующие ключи legal_rules.json:deadline_matrix. Тексты норм здесь не хранятся и не дописываются (правило «тексты только из реестра»). v1.1.0: cure-строка as_cure_left_without_movement сериализована из VERIFIED внешняя проверка 2026-10-24 (duration=null — точка без даты, needs_confirm).",
4
+ "markers": [
5
+ {
6
+ "name": "left_without_movement",
7
+ "patterns": [
8
+ "оставлено без движения",
9
+ "оставлена без движения"
10
+ ],
11
+ "deadline_keys": [
12
+ "as_cure_left_without_movement"
13
+ ],
14
+ "cure": true
15
+ },
16
+ {
17
+ "name": "cure_period_set",
18
+ "patterns": [
19
+ "срок для устранения"
20
+ ],
21
+ "deadline_keys": [
22
+ "as_cure_left_without_movement"
23
+ ],
24
+ "cure": true
25
+ },
26
+ {
27
+ "name": "final_form_date",
28
+ "patterns": [
29
+ "изготовлено в окончательной форме",
30
+ "в окончательной форме"
31
+ ],
32
+ "deadline_keys": [
33
+ "as_appeal_decision",
34
+ "soyu_appeal_decision",
35
+ "kas_appeal_decision"
36
+ ],
37
+ "cure": false
38
+ },
39
+ {
40
+ "name": "act_received",
41
+ "patterns": [
42
+ "получено",
43
+ "получена",
44
+ "получен",
45
+ "вручена",
46
+ "вручено"
47
+ ],
48
+ "deadline_keys": [
49
+ "as_court_order_cancel",
50
+ "soyu_court_order_cancel",
51
+ "kas_court_order_cancel"
52
+ ],
53
+ "cure": false
54
+ }
55
+ ]
56
+ }