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/pretrial.py ADDED
@@ -0,0 +1,151 @@
1
+ """Pretrial (claim) helper for sud-cli (v2.6.0).
2
+
3
+ Pure read of sud_cli/jurisdiction_rules.json. Calendar arithmetic via
4
+ production_calendar/holidays_rf.
5
+ """
6
+ import json
7
+ from datetime import date, timedelta
8
+ from pathlib import Path
9
+
10
+ RULES_PATH = Path(__file__).with_name("jurisdiction_rules.json")
11
+
12
+ CODE_TO_KEY = {"apk": "APK_30d", "zozpp": "ZOZPP_10d", "osago": "OSAGO_30d"}
13
+ CHARTER_CODES = {
14
+ "railway": "railway_ujt", "ujt": "railway_ujt",
15
+ "road": "road_uat", "uat": "road_uat",
16
+ "maritime": "maritime_ktm", "ktm": "maritime_ktm",
17
+ "inland": "inland_water_kvvt", "kvvt": "inland_water_kvvt",
18
+ "air": "air_code_vk", "vk": "air_code_vk",
19
+ "freight": "freight_forwarding", "expedition": "freight_forwarding",
20
+ "postal": "postal",
21
+ }
22
+
23
+
24
+ def load_rules(path=None):
25
+ return json.loads(Path(path or RULES_PATH).read_text(encoding="utf-8"))
26
+
27
+
28
+ def _shift_to_working(day):
29
+ from .production_calendar import is_working_day
30
+ cur = day
31
+ while not is_working_day(cur):
32
+ cur = cur + timedelta(days=1)
33
+ return cur
34
+
35
+
36
+ def calc_pretrial(code, sent, rules=None):
37
+ rules = rules or load_rules()
38
+ key = CODE_TO_KEY.get(str(code).lower())
39
+ mode = str(code).lower()
40
+ if mode in CHARTER_CODES:
41
+ key = CHARTER_CODES[mode]
42
+ rule = rules["pretrial"].get("charters_transport", {}).get(key, {})
43
+ if not rule:
44
+ return {"code": mode, "error": "charter_rule_absent",
45
+ "warnings": ["нет записи в VERIFIED rules — manual_review"]}
46
+ else:
47
+ rule = rules["pretrial"].get(CODE_TO_KEY.get(mode, ""), {})
48
+ days = int(rule.get("days", 0))
49
+ sent_d = date.fromisoformat(sent) if isinstance(sent, str) else sent
50
+ base_due = sent_d + timedelta(days=days)
51
+ exclude = bool(rule.get("exclude_nonworking_holidays"))
52
+ if exclude:
53
+ due = _shift_to_working(base_due)
54
+ else:
55
+ due = base_due
56
+ right_to_sue = due
57
+ # VERIFIED charter-нормы (юридическая проверка 2026.10.24): postal — manual_review (30/5), остальные hard
58
+ mr = []
59
+ if rule.get("confidence") == "manual_review" or rule.get("status") == "NOT_SERIALIZED_NEEDS_TEXT":
60
+ mr.append({"reason": rule.get("confidence", "manual_review"),
61
+ "norm": rule.get("norms") or rule.get("norm", ""),
62
+ "scope": rule.get("scope", "")})
63
+ warnings = ["disclaimer: " + rules.get("disclaimer", "")]
64
+ payload = {
65
+ "code": str(code).lower(),
66
+ "rule": key,
67
+ "norm": rule.get("norm") or rule.get("norms", ""),
68
+ "sent": sent_d.isoformat(),
69
+ "sent_date": sent_d.isoformat(),
70
+ "days": days,
71
+ "calendar": bool(rule.get("calendar", True)),
72
+ "exclude_nonworking_holidays": exclude,
73
+ "response_deadline": due.isoformat(),
74
+ "response_deadline_date": due.isoformat(),
75
+ "answer_deadline": due.isoformat(),
76
+ "deadline": due.isoformat(),
77
+ "deadline_date": due.isoformat(),
78
+ "due_date": due.isoformat(),
79
+ "due": due.isoformat(),
80
+ "right_to_sue_date": right_to_sue.isoformat(),
81
+ "right_to_sue": right_to_sue.isoformat(),
82
+ "right_to_claim_date": right_to_sue.isoformat(),
83
+ "right_to_claim": right_to_sue.isoformat(),
84
+ "sue_date": right_to_sue.isoformat(),
85
+ "claim_date": right_to_sue.isoformat(),
86
+ "warnings": warnings,
87
+ "manual_review": mr,
88
+ "disclaimer": rules.get("disclaimer", ""),
89
+ }
90
+ return payload
91
+
92
+
93
+ def consequences(code, stage, rules=None):
94
+ rules = rules or load_rules()
95
+ code_u = str(code).upper()
96
+ cons = rules["consequences_noncompliance"]
97
+ stage_map = {
98
+ "filing": "return_at_filing",
99
+ "trial": "left_without_consideration",
100
+ "movement": "left_without_movement",
101
+ }
102
+ cons_key = stage_map.get(str(stage).lower(), str(stage).lower())
103
+
104
+ norms = {}
105
+ for k, v in cons.items():
106
+ if k == "confidence":
107
+ continue
108
+ if isinstance(v, dict) and code_u in v:
109
+ norms[k] = v[code_u]
110
+
111
+ selected = cons.get(cons_key, {})
112
+ selected_norm = None
113
+ basis = None
114
+ confidence = "hard"
115
+ if isinstance(selected, dict) and code_u in selected:
116
+ selected_val = selected[code_u]
117
+ if isinstance(selected_val, dict):
118
+ selected_norm = selected_val.get("norm", selected_val)
119
+ basis = selected_val.get("basis")
120
+ else:
121
+ selected_norm = selected_val
122
+ confidence = selected.get("confidence", confidence)
123
+ elif isinstance(selected, dict):
124
+ selected_norm = None
125
+ basis = None
126
+ confidence = selected.get("confidence", confidence)
127
+ else:
128
+ selected_norm = selected
129
+
130
+ lwm_full = cons.get("left_without_movement", {}).get(code_u)
131
+ if isinstance(lwm_full, dict):
132
+ norms["left_without_movement"] = lwm_full
133
+
134
+ out = {
135
+ "code": str(code).lower(),
136
+ "stage": str(stage).lower(),
137
+ "consequence": cons_key,
138
+ "norm": selected_norm,
139
+ "norms": norms,
140
+ "return_at_filing": norms.get("return_at_filing"),
141
+ "left_without_consideration": norms.get("left_without_consideration"),
142
+ "left_without_movement": lwm_full if isinstance(lwm_full, dict) else norms.get("left_without_movement"),
143
+ "confidence": confidence,
144
+ "disclaimer": rules.get("disclaimer", ""),
145
+ "warnings": [rules.get("disclaimer", "")],
146
+ }
147
+ if basis:
148
+ out["basis"] = basis
149
+ if isinstance(lwm_full, dict) and "basis" in lwm_full and "basis" not in out and cons_key == "left_without_movement":
150
+ out["basis"] = lwm_full["basis"]
151
+ return out
@@ -0,0 +1,233 @@
1
+ """Production calendar for federal deadlines (addendum 1.1.0, п. Б2).
2
+
3
+ The calendar carries three layers, and each is recorded with its own legal
4
+ source so a reader can tell how much of the answer is statute and how much is
5
+ decree:
6
+
7
+ * **ст. 112 ТК РФ** — the fixed federal non-working holidays. These repeat every
8
+ year, so a year with no calendar entry still gets them.
9
+ * **ч. 2 ст. 112 ТК РФ** — automatic moves. A holiday landing on a weekend moves
10
+ to the next working day on its own, except for the holidays named in абз. 2
11
+ and 3 (1–8 января, 23 февраля), where only the Government may move the day off.
12
+ * **Постановления Правительства РФ** — the yearly transfer decrees.
13
+
14
+ The transfer decrees used to be absent here: they are published per-year and no
15
+ verified copy was supplied, and transcribing a calendar from memory would be a
16
+ silent legal guess inside a deadline calculator. The 2026 decree is now encoded
17
+ because it was verified against the official portal scan (№ 1466 от 24.09.2025,
18
+ publication N 0001202509240023). 2026 is the only verified year, so the
19
+ ``CALENDAR_WARNING`` is now **per-year** rather than global: a deadline landing in
20
+ a year whose decree is encoded carries a note saying so, and every other year
21
+ still carries the full warning. Failing a year loudly is the point — the tool
22
+ says what it does not know instead of pretending.
23
+ """
24
+
25
+ import json
26
+ from datetime import date, timedelta
27
+ from functools import lru_cache
28
+ from pathlib import Path
29
+ from typing import Dict, List, Optional, Set
30
+
31
+ from .regional_holidays import holidays_for_year as regional_holidays, normalize_region, region_name
32
+
33
+ CALENDAR_PATH = Path(__file__).with_name("holidays_rf.json")
34
+ TRANSFERS_PATH = Path(__file__).with_name("government_transfers.json")
35
+
36
+ REGIONAL_NOTE = (
37
+ "Региональные нерабочие праздничные дни субъектов РФ не учитываются."
38
+ )
39
+
40
+
41
+ class CalendarError(Exception):
42
+ """The calendar file is missing or structurally invalid."""
43
+
44
+
45
+ @lru_cache(maxsize=1)
46
+ def load_calendar(path: Optional[str] = None) -> dict:
47
+ target = Path(path) if path else CALENDAR_PATH
48
+ try:
49
+ data = json.loads(target.read_text(encoding="utf-8"))
50
+ except (OSError, json.JSONDecodeError) as exc:
51
+ raise CalendarError(f"cannot load production calendar: {exc}") from exc
52
+ if "federal_holidays" not in data:
53
+ raise CalendarError("production calendar missing federal_holidays")
54
+ return data
55
+
56
+
57
+ def _parse(token: str, year: int) -> date:
58
+ month, day = (int(part) for part in token.split("-"))
59
+ return date(year, month, day)
60
+
61
+
62
+ @lru_cache(maxsize=1)
63
+ def _holiday_index(calendar_path: Optional[str] = None) -> Dict[int, Set[date]]:
64
+ data = load_calendar(calendar_path)
65
+ index: Dict[int, Set[date]] = {}
66
+ for year, days in data["federal_holidays"].items():
67
+ index[int(year)] = {_parse(token, int(year)) for token in days}
68
+ return index
69
+
70
+
71
+ def _statutory_pattern(calendar_path: Optional[str] = None) -> List[str]:
72
+ """The ст. 112 ТК РФ day pattern, taken from any populated year.
73
+
74
+ The statutory dates repeat annually, so they are used as the fallback for a
75
+ year that has no calendar entry. Only the fixed holidays are used; the
76
+ automatic weekend moves are applied separately by ``holidays_for_year``.
77
+ """
78
+ data = load_calendar(calendar_path)
79
+ for days in data["federal_holidays"].values():
80
+ return list(days)
81
+ raise CalendarError("production calendar has no years to copy the pattern from")
82
+
83
+
84
+ def holidays_for_year(year: int, calendar_path: Optional[str] = None, region=None) -> Set[date]:
85
+ """Every federal non-working day in ``year``.
86
+
87
+ This is the union of the ст. 112 ТК РФ holidays and any day the Government
88
+ declared a day off by decree. Days moved *out* of a holiday by a decree are
89
+ not representable in a set of non-working days; that is why
90
+ ``days_restored_to_working`` is applied by :func:`is_working_day` rather
91
+ than here.
92
+ """
93
+ index = _holiday_index(calendar_path)
94
+ base = index.get(year) or {_parse(token, year) for token in _statutory_pattern(calendar_path)}
95
+ transfer = load_calendar(calendar_path).get("transfers", {}).get(str(year), {})
96
+ # ч. 2 ст. 112 ТК РФ: a holiday landing on a weekend moves to the next working
97
+ # day on its own. These are derived from the statute, not from the decree, and
98
+ # are recorded per-year because the set depends on which weekdays that year
99
+ # happens to start on.
100
+ cases = load_calendar(calendar_path).get("auto_transfers_st112", {}).get(str(year), [])
101
+ for case in cases:
102
+ base.add(_parse(case["moved_to"], year))
103
+ # v2.3.2 (sa-1 F-12): ч.2 ст.112 переносы выводятся алгоритмически для ЛЮБОГО
104
+ # года, а не только записанных (23.02.2025 воскресенье -> 24.02.2025 отсутствовал).
105
+ # Явные записи выше имеют приоритет: праздник, уже перенесённый записью, не
106
+ # сдвигается повторно. Не переносим: январский блок (каникулы поглощают перенос,
107
+ # позиция Роструда) и даты вне фиксированного statutory-списка (декретные дни).
108
+ _ST112_FIXED = {"02-23", "03-08", "05-01", "05-09", "06-12", "11-04"}
109
+ _explicit_holidays = {case.get("holiday") for case in cases}
110
+ derived: Set[date] = set()
111
+ for token in _statutory_pattern(calendar_path):
112
+ if token not in _ST112_FIXED or token in _explicit_holidays:
113
+ continue
114
+ h = _parse(token, year)
115
+ if h.weekday() < 5:
116
+ continue
117
+ cand = h + timedelta(days=1)
118
+ while cand.weekday() >= 5 or cand in base or cand in derived:
119
+ cand += timedelta(days=1)
120
+ derived.add(cand)
121
+ base |= derived
122
+ return (base | {_parse(token, year) for token in transfer.get("extra_days_off", [])} |
123
+ regional_holidays(year, region))
124
+
125
+
126
+ def days_restored_to_working(year: int, calendar_path: Optional[str] = None) -> Set[date]:
127
+ """Days a decree moved a day off back onto a working day."""
128
+ return {
129
+ _parse(token, year)
130
+ for token in (
131
+ load_calendar(calendar_path)
132
+ .get("transfers", {})
133
+ .get(str(year), {})
134
+ .get("days_restored_to_working", [])
135
+ )
136
+ }
137
+
138
+
139
+ def is_working_day(day: date, calendar_path: Optional[str] = None, region=None) -> bool:
140
+ """True when ``day`` counts as a working day for АПК/ГПК deadlines."""
141
+ if day in days_restored_to_working(day.year, calendar_path):
142
+ return True
143
+ if day.weekday() >= 5:
144
+ return False
145
+ return day not in holidays_for_year(day.year, calendar_path, region)
146
+
147
+
148
+ def auto_transfers_encoded(calendar_path: Optional[str] = None) -> bool:
149
+ """Whether ч. 2 ст. 112 ТК РФ automatic moves are encoded for ``year``."""
150
+ return bool(load_calendar(calendar_path).get("auto_transfers_st112"))
151
+
152
+
153
+ def transfers_encoded_for(year: int, calendar_path: Optional[str] = None) -> bool:
154
+ """Whether the Government transfer decree for ``year`` is actually encoded.
155
+
156
+ Per-year on purpose: encoding 2026 must not silence the warning for 2027.
157
+ """
158
+ data = load_calendar(calendar_path)
159
+ verified = data.get("years_with_verified_transfers", [])
160
+ return year in verified or str(year) in verified
161
+
162
+
163
+ def calendar_warnings(year: int, calendar_path: Optional[str] = None, region=None) -> List[str]:
164
+ """Warnings that must accompany any deadline computed for ``year``."""
165
+ data = load_calendar(calendar_path)
166
+ warnings: List[str] = []
167
+ if transfers_encoded_for(year, calendar_path):
168
+ decree = data.get("transfers", {}).get(str(year), {}).get("decree", "не указан")
169
+ warnings.append(
170
+ data["no_warning_template"].format(year=year, decree=decree)
171
+ )
172
+ if data.get("transfers", {}).get(str(year), {}).get("amendments_found") is False:
173
+ warnings.append(
174
+ "Проверено: изменяющих актов к постановлению о переносах не найдено "
175
+ "(скан официального портала)."
176
+ )
177
+ else:
178
+ warnings.append(data["warning_template"].format(year=year))
179
+ if region is None:
180
+ warnings.append(REGIONAL_NOTE + " (укажите --region <код> при необходимости)")
181
+ else:
182
+ code = normalize_region(region)
183
+ warnings.append(f"Учтены региональные нерабочие праздничные дни: {region_name(code)} (код {code}).")
184
+ return warnings
185
+
186
+
187
+ def unknown_years(years, calendar_path: Optional[str] = None) -> Set[int]:
188
+ """Years in the span that have no declared holiday set at all."""
189
+ index = _holiday_index(calendar_path)
190
+ return {y for y in years if y not in index}
191
+
192
+
193
+ def calendar_check() -> dict:
194
+ """Validate the isolated transfer manifest and report its coverage."""
195
+ try:
196
+ data = json.loads(TRANSFERS_PATH.read_text(encoding="utf-8"))
197
+ through = int(data["verified_through"])
198
+ years = sorted(int(year) for year in data["years"])
199
+ if years != list(range(min(years), through + 1)):
200
+ raise CalendarError("government transfer years are not contiguous")
201
+ return {"status": "ok", "valid": True, "verified_through": through, "years": years, "source": data["source"]}
202
+ except (OSError, json.JSONDecodeError, KeyError, TypeError, ValueError, CalendarError) as exc:
203
+ return {"status": "error", "valid": False, "detail": str(exc)}
204
+
205
+
206
+ def add_working_days(start: date, count: int, calendar_path: Optional[str] = None, region=None) -> date:
207
+ """Advance ``count`` working days per Б1 (ч. 3 + ч. 4 ст. 113 АПК РФ).
208
+
209
+ ``start`` is Day 1 — the next calendar day after the event — and it counts
210
+ as day 1 when it is a working day. Non-working days are skipped without
211
+ incrementing the counter. So a Friday event (start = Saturday) skips
212
+ Saturday and Sunday and returns the following Friday for ``count=3``,
213
+ while a Thursday event (start = Friday) counts that Friday as day 1.
214
+
215
+ Getting this off by one silently shifts every АПК deadline by a day, so the
216
+ distinction is asserted in tests on both a mid-week and a weekend start.
217
+ """
218
+ if count <= 0:
219
+ return start
220
+ current = start
221
+ counted = 0
222
+ while counted < count:
223
+ if is_working_day(current, calendar_path, region):
224
+ counted += 1
225
+ if counted == count:
226
+ break
227
+ current += timedelta(days=1)
228
+ return current
229
+
230
+
231
+ def add_calendar_days(start: date, count: int) -> date:
232
+ """Advance ``count`` calendar days (ГПК/КАС)."""
233
+ return start + timedelta(days=count - 1)
sud_cli/providers.py ADDED
@@ -0,0 +1,95 @@
1
+ """Source/provider registry for sud-cli."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import List
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class Provider:
9
+ name: str
10
+ kind: str # 'offline' | 'live'
11
+ status: str # 'available' | 'unavailable' | 'blocked'
12
+ description: str
13
+
14
+
15
+ REGISTRY: List[Provider] = [
16
+ Provider(
17
+ name="local-json-court",
18
+ kind="offline",
19
+ status="available",
20
+ description="Synthetic local JSON import for legacy court records.",
21
+ ),
22
+ Provider(
23
+ name="local-json-court-case",
24
+ kind="offline",
25
+ status="available",
26
+ description="Synthetic local JSON import for native court case/event timelines.",
27
+ ),
28
+ Provider(
29
+ name="local-json-npa",
30
+ kind="offline",
31
+ status="available",
32
+ description="Synthetic local JSON import for NPA document fixtures.",
33
+ ),
34
+ Provider(
35
+ name="case-search-live",
36
+ kind="live",
37
+ status="unavailable",
38
+ description="Live court source adapter; not implemented (Phase 3 offline-only).",
39
+ ),
40
+ Provider(
41
+ name="case-search-bridge",
42
+ kind="bridge",
43
+ status="available",
44
+ description=(
45
+ "Transitional read-only bridge to the case-search CLI for allowlisted "
46
+ "offline import/extraction commands; not a live source or parity claim."
47
+ ),
48
+ ),
49
+ Provider(
50
+ name="kad-arbitr-ru",
51
+ kind="live",
52
+ status="unavailable",
53
+ description=(
54
+ "KAD/arbitr.ru live case search; response contract is unverified "
55
+ "(HTML response, CAPTCHA gate, HTTP 451 probe) and no live adapter is implemented."
56
+ ),
57
+ ),
58
+ Provider(
59
+ name="pravo-gov-ru",
60
+ kind="live",
61
+ status="available",
62
+ description=(
63
+ "Official publication.pravo.gov.ru metadata-only search; "
64
+ "one bounded GET per invocation, HTTPS only."
65
+ ),
66
+ ),
67
+ Provider(
68
+ name="garant",
69
+ kind="live",
70
+ status="unavailable",
71
+ description="Commercial legal source metadata-only access; not implemented in Phase 1.",
72
+ ),
73
+ Provider(
74
+ name="consultantplus",
75
+ kind="live",
76
+ status="unavailable",
77
+ description="Commercial legal source metadata-only access; not implemented in Phase 1.",
78
+ ),
79
+ Provider(
80
+ name="fees-calculator",
81
+ kind="calculator",
82
+ status="blocked",
83
+ description="Court fee calculation; blocked in Phase 1.",
84
+ ),
85
+ Provider(
86
+ name="deadlines-calculator",
87
+ kind="calculator",
88
+ status="blocked",
89
+ description="Procedural deadline calculation; blocked in Phase 1.",
90
+ ),
91
+ ]
92
+
93
+
94
+ def list_providers() -> List[Provider]:
95
+ return list(REGISTRY)