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/reports.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""the wave §4 (S-84): отчёты портфеля и дела — только счётчики из реальных таблиц.
|
|
2
|
+
|
|
3
|
+
Правило «не выдуманных метрик»: здесь нет ни «успешности», ни «рисков», ни
|
|
4
|
+
«средних» — только то, что читается из cases/events/amounts/deadline_states/
|
|
5
|
+
evidence_items/case_representatives. Горизонт near/overdue переиспользует
|
|
6
|
+
логику agenda (W6a) — вторая копия окна не заводится, копии всегда расходятся.
|
|
7
|
+
Пустые секции выводятся явно [] / {}, а не «нет данных».
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from datetime import date as _date
|
|
12
|
+
from decimal import Decimal
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
from .db import (
|
|
16
|
+
LEGAL_CASE_STATUSES,
|
|
17
|
+
get_case,
|
|
18
|
+
get_case_card,
|
|
19
|
+
get_connection,
|
|
20
|
+
init_db,
|
|
21
|
+
list_cases,
|
|
22
|
+
summarize_case_amounts,
|
|
23
|
+
representatives_card_section,
|
|
24
|
+
verify_case_evidence,
|
|
25
|
+
evidence_summary,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
NEAR_HORIZON_DAYS = 30
|
|
29
|
+
|
|
30
|
+
REPORT_DISCLAIMER = (
|
|
31
|
+
"Отчёт справочный: считает только строки локального реестра sud-cli. "
|
|
32
|
+
"Статусы дел — внесённые оператором, не факт суда; суммы — внесённые "
|
|
33
|
+
"расходы; сроки — computed|confirmed из deadline_states. Метрик успеха, "
|
|
34
|
+
"риска или «качества ведения» здесь нет и быть не может: sud-cli их не "
|
|
35
|
+
"наблюдает."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _zero() -> Decimal:
|
|
40
|
+
return Decimal("0")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _near_overdue(state_dir: Any, today: _date, days: int):
|
|
44
|
+
"""Сроки в окне [today, today+days] и просроченные — из agenda-билдера (W6a).
|
|
45
|
+
|
|
46
|
+
Окно не переопределяется второй копией: near/overdue берутся из
|
|
47
|
+
agenda_payload — там же живёт правило «закрытое дело не плодит пунктов»
|
|
48
|
+
и то же определение computed|confirmed. Здесь только суждение до
|
|
49
|
+
kind=deadline и подстановка days_left.
|
|
50
|
+
"""
|
|
51
|
+
from .agenda import agenda_payload
|
|
52
|
+
|
|
53
|
+
payload = agenda_payload(state_dir, days=days, today=today)
|
|
54
|
+
|
|
55
|
+
def _pick(items):
|
|
56
|
+
out = []
|
|
57
|
+
for item in items:
|
|
58
|
+
if item.get("kind") != "deadline":
|
|
59
|
+
continue
|
|
60
|
+
out.append({
|
|
61
|
+
"case_id": item.get("case_id"),
|
|
62
|
+
"deadline_key": item.get("deadline_key"),
|
|
63
|
+
"due_at": item.get("due_at"),
|
|
64
|
+
"state": item.get("state"),
|
|
65
|
+
"days_left": (_date.fromisoformat(str(item["due_at"])[:10])
|
|
66
|
+
- today).days,
|
|
67
|
+
})
|
|
68
|
+
return out
|
|
69
|
+
|
|
70
|
+
return _pick(payload["upcoming"]), _pick(payload["overdue"]), payload
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _closed_dropped_deadlines(conn) -> List[dict]:
|
|
74
|
+
"""Сроки закрытых дел, которые agenda-правило не показывает.
|
|
75
|
+
|
|
76
|
+
Переиспользование окна (W6a) означает, что у закрытого дела его computed/
|
|
77
|
+
confirmed сроки в near/overdue не попадают. Это не повод терять их молча:
|
|
78
|
+
они считаются здесь и выводятся отдельным списком с причиной, чтобы
|
|
79
|
+
приёмщик видел, что именно скрыло правило, а не «сроков нет».
|
|
80
|
+
"""
|
|
81
|
+
from .db import list_all_deadline_states
|
|
82
|
+
from .reminders import _latest_per_key
|
|
83
|
+
|
|
84
|
+
closed = {c["case_id"] for c in list_cases(conn) if c.get("status") == "closed"}
|
|
85
|
+
dropped = []
|
|
86
|
+
for s in _latest_per_key(list_all_deadline_states(conn)):
|
|
87
|
+
if s.get("case_id") not in closed:
|
|
88
|
+
continue
|
|
89
|
+
if s.get("state") not in ("computed", "confirmed") or not s.get("due_at"):
|
|
90
|
+
continue
|
|
91
|
+
dropped.append({
|
|
92
|
+
"case_id": s.get("case_id"),
|
|
93
|
+
"deadline_key": s.get("deadline_key"),
|
|
94
|
+
"due_at": s.get("due_at"),
|
|
95
|
+
"state": s.get("state"),
|
|
96
|
+
"reason": "agenda-правило: закрытое дело не участвует в near/overdue",
|
|
97
|
+
})
|
|
98
|
+
dropped.sort(key=lambda i: (str(i["due_at"]), str(i["case_id"]),
|
|
99
|
+
str(i["deadline_key"])))
|
|
100
|
+
return dropped
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def portfolio_payload(state_dir: Any, as_of: Optional[str] = None,
|
|
104
|
+
days: int = NEAR_HORIZON_DAYS) -> Dict[str, Any]:
|
|
105
|
+
init_db(state_dir)
|
|
106
|
+
conn = get_connection(state_dir)
|
|
107
|
+
try:
|
|
108
|
+
cases = list_cases(conn)
|
|
109
|
+
today = _date.today()
|
|
110
|
+
if as_of:
|
|
111
|
+
today = _date.fromisoformat(str(as_of)[:10])
|
|
112
|
+
|
|
113
|
+
by_status: Dict[str, int] = {}
|
|
114
|
+
for c in cases:
|
|
115
|
+
key = c.get("status") or "unknown"
|
|
116
|
+
by_status[key] = by_status.get(key, 0) + 1
|
|
117
|
+
# статусы enum'а всегда присутствуют в выводе: пустой портфель даёт
|
|
118
|
+
# нули по всем LEGAL_CASE_STATUSES, а не «секции нет».
|
|
119
|
+
statuses = {s: int(by_status.get(s, 0)) for s in LEGAL_CASE_STATUSES}
|
|
120
|
+
for s, n in by_status.items(): # значения вне enum — честно видно
|
|
121
|
+
if s not in statuses:
|
|
122
|
+
statuses[s] = n
|
|
123
|
+
|
|
124
|
+
spent = _zero()
|
|
125
|
+
planned = _zero()
|
|
126
|
+
refunded = _zero()
|
|
127
|
+
by_kind: Dict[str, str] = {}
|
|
128
|
+
amounts_cases = 0
|
|
129
|
+
for c in cases:
|
|
130
|
+
summary = summarize_case_amounts(conn, c["case_id"])
|
|
131
|
+
rows = summary["rows"]
|
|
132
|
+
if rows:
|
|
133
|
+
amounts_cases += 1
|
|
134
|
+
for r in rows:
|
|
135
|
+
dec = Decimal(str(r["total"]))
|
|
136
|
+
by_kind[r["kind"]] = str(Decimal(by_kind.get(r["kind"], "0")) + dec)
|
|
137
|
+
status = r["status"] or "planned"
|
|
138
|
+
if status == "paid":
|
|
139
|
+
spent += dec
|
|
140
|
+
elif status == "refunded":
|
|
141
|
+
refunded += dec
|
|
142
|
+
else:
|
|
143
|
+
planned += dec
|
|
144
|
+
|
|
145
|
+
reps_total = 0
|
|
146
|
+
reps_by_side: Dict[str, int] = {}
|
|
147
|
+
for c in cases:
|
|
148
|
+
section = representatives_card_section(conn, c["case_id"])
|
|
149
|
+
reps_total += section["count"]
|
|
150
|
+
for side, n in section["by_side"].items():
|
|
151
|
+
reps_by_side[side] = reps_by_side.get(side, 0) + n
|
|
152
|
+
|
|
153
|
+
evidence_total = int(conn.execute(
|
|
154
|
+
"SELECT COUNT(*) AS n FROM evidence_items").fetchone()["n"])
|
|
155
|
+
evidence_problems = int(conn.execute(
|
|
156
|
+
"SELECT COUNT(*) AS n FROM evidence_items WHERE sha256 IS NULL").fetchone()["n"])
|
|
157
|
+
documents_total = int(conn.execute(
|
|
158
|
+
"SELECT COUNT(*) AS n FROM case_documents").fetchone()["n"])
|
|
159
|
+
documents_revisions = int(conn.execute(
|
|
160
|
+
"SELECT COUNT(*) AS n FROM case_documents WHERE supersedes IS NOT NULL"
|
|
161
|
+
).fetchone()["n"])
|
|
162
|
+
events_total = int(conn.execute(
|
|
163
|
+
"SELECT COUNT(*) AS n FROM events").fetchone()["n"])
|
|
164
|
+
|
|
165
|
+
near, overdue, _agenda = _near_overdue(state_dir, today, days)
|
|
166
|
+
closed_dropped = _closed_dropped_deadlines(conn)
|
|
167
|
+
# problems — только счётчики из таблиц состояния (ТЗ §4: без verify,
|
|
168
|
+
# который пересчитывает файлы: это уже не «counts из state tables»).
|
|
169
|
+
problems = [
|
|
170
|
+
{
|
|
171
|
+
"case_id": c["case_id"],
|
|
172
|
+
"status": c.get("status") or "unknown",
|
|
173
|
+
"events": int(conn.execute(
|
|
174
|
+
"SELECT COUNT(*) AS n FROM events WHERE case_id = ?",
|
|
175
|
+
(c["case_id"],)).fetchone()["n"]),
|
|
176
|
+
"deadline_states_without_due": int(conn.execute(
|
|
177
|
+
"SELECT COUNT(*) AS n FROM deadline_states "
|
|
178
|
+
"WHERE case_id = ? AND due_at IS NULL",
|
|
179
|
+
(c["case_id"],)).fetchone()["n"]),
|
|
180
|
+
}
|
|
181
|
+
for c in cases
|
|
182
|
+
]
|
|
183
|
+
finally:
|
|
184
|
+
conn.close()
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
"as_of": today.isoformat(),
|
|
188
|
+
"horizon_days": days,
|
|
189
|
+
"cases": {
|
|
190
|
+
"total": len(cases),
|
|
191
|
+
"with_amounts": amounts_cases,
|
|
192
|
+
"by_status": statuses,
|
|
193
|
+
},
|
|
194
|
+
"events": {"total": events_total},
|
|
195
|
+
"deadlines": {
|
|
196
|
+
"near": near,
|
|
197
|
+
"near_count": len(near),
|
|
198
|
+
"overdue": overdue,
|
|
199
|
+
"overdue_count": len(overdue),
|
|
200
|
+
# окно — agenda-правило (W6a): у закрытого дела сроки в near/overdue
|
|
201
|
+
# не попадают; они перечислены здесь, а не потеряны молча.
|
|
202
|
+
"closed_excluded": closed_dropped,
|
|
203
|
+
"closed_excluded_count": len(closed_dropped),
|
|
204
|
+
},
|
|
205
|
+
"amounts": {
|
|
206
|
+
"cases_with_amounts": amounts_cases,
|
|
207
|
+
"by_kind": by_kind,
|
|
208
|
+
"totals": {
|
|
209
|
+
"paid": str(spent),
|
|
210
|
+
"planned": str(planned),
|
|
211
|
+
"refunded": str(refunded),
|
|
212
|
+
},
|
|
213
|
+
"currency": "RUB",
|
|
214
|
+
},
|
|
215
|
+
"representatives": {
|
|
216
|
+
"total": reps_total,
|
|
217
|
+
"by_side": {s: int(reps_by_side.get(s, 0)) for s in ("plaintiff", "defendant")},
|
|
218
|
+
},
|
|
219
|
+
"documents": {"total": documents_total, "revisions": documents_revisions},
|
|
220
|
+
"evidence": {"total": evidence_total, "without_hash": evidence_problems},
|
|
221
|
+
"problems": problems,
|
|
222
|
+
"portfolio": [
|
|
223
|
+
{
|
|
224
|
+
"case_id": c["case_id"],
|
|
225
|
+
"case_number": c.get("case_number"),
|
|
226
|
+
"court": c.get("court"),
|
|
227
|
+
"status": c.get("status"),
|
|
228
|
+
"instance": c.get("instance"),
|
|
229
|
+
"owner": c.get("owner"),
|
|
230
|
+
}
|
|
231
|
+
for c in cases
|
|
232
|
+
],
|
|
233
|
+
"disclaimer": REPORT_DISCLAIMER,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def case_payload(state_dir: Any, case_id: str) -> Optional[Dict[str, Any]]:
|
|
238
|
+
"""Печатная развёртка дела: секции card + матрица сроков + evidence-итог."""
|
|
239
|
+
from .deadline_matrix import build_matrix
|
|
240
|
+
|
|
241
|
+
init_db(state_dir)
|
|
242
|
+
conn = get_connection(state_dir)
|
|
243
|
+
try:
|
|
244
|
+
if get_case(conn, case_id) is None:
|
|
245
|
+
return None
|
|
246
|
+
card = get_case_card(conn, case_id)
|
|
247
|
+
amounts = summarize_case_amounts(conn, case_id)
|
|
248
|
+
items = verify_case_evidence(conn, case_id)
|
|
249
|
+
summary = evidence_summary(items)
|
|
250
|
+
finally:
|
|
251
|
+
conn.close()
|
|
252
|
+
|
|
253
|
+
instance, points = build_matrix(state_dir, case_id)
|
|
254
|
+
matrix_summary = {
|
|
255
|
+
"instance": instance,
|
|
256
|
+
"points_total": len(points),
|
|
257
|
+
"anchored": [p["key"] for p in points if not p.get("needs_anchor")],
|
|
258
|
+
"needs_anchor": [p["key"] for p in points if p.get("needs_anchor")],
|
|
259
|
+
"computed": [p["key"] for p in points if p.get("due_at")],
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
sections = {
|
|
263
|
+
"case": card.get("case"),
|
|
264
|
+
"parties": card.get("parties"),
|
|
265
|
+
"amounts": card.get("amounts"),
|
|
266
|
+
"documents": card.get("documents"),
|
|
267
|
+
"documents_current": card.get("documents_current"),
|
|
268
|
+
"notes": card.get("notes"),
|
|
269
|
+
"events": card.get("events"),
|
|
270
|
+
"deadlines": card.get("deadlines"),
|
|
271
|
+
"evidence": card.get("evidence"),
|
|
272
|
+
"representatives": card.get("representatives"),
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
"case_id": case_id,
|
|
276
|
+
"sections": sections,
|
|
277
|
+
"amounts_summary": amounts,
|
|
278
|
+
"evidence_verify": {k: v for k, v in summary.items() if k != "problem_items"},
|
|
279
|
+
"evidence_problems": summary["problem_items"],
|
|
280
|
+
"matrix": matrix_summary,
|
|
281
|
+
"disclaimer": REPORT_DISCLAIMER,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def render_portfolio_text(payload: Dict[str, Any]) -> List[str]:
|
|
286
|
+
"""Human-рендер портфеля: строки по фактическим секциям, без «красивых» выводов."""
|
|
287
|
+
lines = [
|
|
288
|
+
f"Портфель на {payload['as_of']} (горизонт {payload['horizon_days']} дн.)",
|
|
289
|
+
f"дел: {payload['cases']['total']}",
|
|
290
|
+
]
|
|
291
|
+
lines.append("по статусам:")
|
|
292
|
+
for status, count in sorted(payload["cases"]["by_status"].items()):
|
|
293
|
+
lines.append(f" {status}: {count}")
|
|
294
|
+
lines.append(f"сроки near (<= {payload['horizon_days']} дн.): "
|
|
295
|
+
f"{payload['deadlines']['near_count']}")
|
|
296
|
+
for item in payload["deadlines"]["near"]:
|
|
297
|
+
lines.append(f" {item['due_at']} {item['case_id']} {item['deadline_key']} "
|
|
298
|
+
f"{item['state']} ({item['days_left']} дн.)")
|
|
299
|
+
lines.append(f"просрочено: {payload['deadlines']['overdue_count']}")
|
|
300
|
+
for item in payload["deadlines"]["overdue"]:
|
|
301
|
+
lines.append(f" {item['due_at']} {item['case_id']} {item['deadline_key']} "
|
|
302
|
+
f"{item['state']}")
|
|
303
|
+
dropped = payload["deadlines"].get("closed_excluded") or []
|
|
304
|
+
lines.append(f"вне окна (закрытые дела, правило agenda): {len(dropped)}")
|
|
305
|
+
for item in dropped:
|
|
306
|
+
lines.append(f" {item['due_at']} {item['case_id']} {item['deadline_key']} "
|
|
307
|
+
f"{item['state']}")
|
|
308
|
+
totals = payload["amounts"]["totals"]
|
|
309
|
+
lines.append(f"расходы: paid={totals['paid']} planned={totals['planned']} "
|
|
310
|
+
f"refunded={totals['refunded']} RUB")
|
|
311
|
+
lines.append(f"представители: {payload['representatives']['total']} "
|
|
312
|
+
f"(plaintiff={payload['representatives']['by_side']['plaintiff']}, "
|
|
313
|
+
f"defendant={payload['representatives']['by_side']['defendant']})")
|
|
314
|
+
lines.append(f"документы: {payload['documents']['total']} "
|
|
315
|
+
f"(ревизий: {payload['documents']['revisions']})")
|
|
316
|
+
lines.append(f"доказательства: {payload['evidence']['total']} "
|
|
317
|
+
f"(без hash: {payload['evidence']['without_hash']})")
|
|
318
|
+
lines.append(f"problems (по делам): {len(payload['problems'])}")
|
|
319
|
+
for p in payload["problems"]:
|
|
320
|
+
if p["deadline_states_without_due"] or p["events"]:
|
|
321
|
+
lines.append(
|
|
322
|
+
f" {p['case_id']}: status={p['status']} events={p['events']} "
|
|
323
|
+
f"сроков без due_at={p['deadline_states_without_due']}"
|
|
324
|
+
)
|
|
325
|
+
lines.append(payload["disclaimer"])
|
|
326
|
+
return lines
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def render_case_text(payload: Dict[str, Any]) -> List[str]:
|
|
330
|
+
case = payload["sections"].get("case") or {}
|
|
331
|
+
lines = [
|
|
332
|
+
f"Дело {payload['case_id']}: {case.get('case_number')} ({case.get('court')})",
|
|
333
|
+
f"статус: {case.get('status')} | instance: {case.get('instance')} "
|
|
334
|
+
f"| owner: {case.get('owner')}",
|
|
335
|
+
f"стороны: истец {case.get('plaintiff')} / ответчик {case.get('defendant')}",
|
|
336
|
+
f"событий: {len(payload['sections'].get('events') or [])}",
|
|
337
|
+
f"заметок: {len(payload['sections'].get('notes') or [])}",
|
|
338
|
+
f"документов: {len(payload['sections'].get('documents') or [])} "
|
|
339
|
+
f"(текущих: {len(payload['sections'].get('documents_current') or [])})",
|
|
340
|
+
f"доказательств: {payload['sections']['evidence']['count']} "
|
|
341
|
+
f"(проблемных: {len(payload['evidence_problems'])})",
|
|
342
|
+
f"представителей: {payload['sections']['representatives']['count']}",
|
|
343
|
+
f"расходы: {payload['amounts_summary']['totals_by_status']}",
|
|
344
|
+
f"матрица: instance={payload['matrix']['instance']} "
|
|
345
|
+
f"точек={payload['matrix']['points_total']} "
|
|
346
|
+
f"нужен якорь={len(payload['matrix']['needs_anchor'])}",
|
|
347
|
+
payload["disclaimer"],
|
|
348
|
+
]
|
|
349
|
+
return lines
|