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/__init__.py
ADDED
sud_cli/__main__.py
ADDED
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
|
+
}
|