Добавить codemod дедупликации summary и уточнить бэклог
scripts/dedup_summary_inheritdoc.py (dry-run не нашёл простых дублей summary в продакшне — реализации уже на <inheritdoc/>); закрыт TD-SETTINGS-UI, DEFERRED с обоснованием TD-CARD-MERGE/TD-VIRT/TD-STORE-ATTACH/TD-SOURCE-CONTACTS; игнор __pycache__.
This commit is contained in:
@@ -30,3 +30,7 @@ deploy/certs/
|
|||||||
# === Рантайм-данные (БД, объектное хранилище, ключи шифрования) ===
|
# === Рантайм-данные (БД, объектное хранилище, ключи шифрования) ===
|
||||||
archive/leadradar-legacy/data/
|
archive/leadradar-legacy/data/
|
||||||
src/core/Deal.Api/data/
|
src/core/Deal.Api/data/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|||||||
+6
-6
@@ -14,9 +14,9 @@
|
|||||||
| BL-TG-MULTI | Мультиаккаунтность Telegram (сейчас 1 аккаунт на тенант) | ТЗ §12 | P2 | DEFERRED |
|
| BL-TG-MULTI | Мультиаккаунтность Telegram (сейчас 1 аккаунт на тенант) | ТЗ §12 | P2 | DEFERRED |
|
||||||
| BL-ML-EXP | Экспорт/импорт ML-моделей (перенос «мозгов» между инстансами) | обсуждение этапа 12 | P3 | DEFERRED (решено не делать; вернуться при SaaS-масштабе) |
|
| BL-ML-EXP | Экспорт/импорт ML-моделей (перенос «мозгов» между инстансами) | обсуждение этапа 12 | P3 | DEFERRED (решено не делать; вернуться при SaaS-масштабе) |
|
||||||
| BL-RECLASS-SSE | **Сделано (2026-09-11):** пакетная переклассификация отдаёт промежуточный прогресс через SSE `cards_reclassified` (`{progress:true,done,total,moved,kept,trashed,skipped}`) и финальное событие (`{progress:false,reclassified,moved}`); `CardReclassifier.ReclassifyInboxAsync` принимает `IProgress<ReclassifyProgressDto>`; в UI — индикатор `done/total` в шапке «Неразобранного» | этап 12, D | P3 | DONE |
|
| BL-RECLASS-SSE | **Сделано (2026-09-11):** пакетная переклассификация отдаёт промежуточный прогресс через SSE `cards_reclassified` (`{progress:true,done,total,moved,kept,trashed,skipped}`) и финальное событие (`{progress:false,reclassified,moved}`); `CardReclassifier.ReclassifyInboxAsync` принимает `IProgress<ReclassifyProgressDto>`; в UI — индикатор `done/total` в шапке «Неразобранного» | этап 12, D | P3 | DONE |
|
||||||
| TD-CARD-MERGE | Полное слияние внутренних DTO карточки в единый `CardDto` (наружу уже единый) | этап 9/11 | P3 | TECHDEBT |
|
| TD-CARD-MERGE | Полное слияние внутренних DTO карточки в единый `CardDto`. **Решение (2026-09-11): DEFERRED.** Наружный контракт единый; внутренние DTO (read/write/DB/patch) намеренно разделены по слоям, слияние — риск без пользы | этап 9/11 | P3 | DEFERRED |
|
||||||
| TD-PROTO-COMMENTS | **Сделано (2026-09-11):** из комментариев убраны ссылки на процесс/прототип (`Task/Ruling/этап/python L…/main.py/прототип/LEADRADAR_*`), удалены блоки `<remarks>`, `<summary>` сжаты до короткой фразы; `//`-комментарии со ссылками удалены, в `.proto` — тоже. Строк комментариев 27 210 → ~19 100 | запрос владельца 2026-09-11 | P2 | DONE |
|
| TD-PROTO-COMMENTS | **Сделано (2026-09-11):** из комментариев убраны ссылки на процесс/прототип (`Task/Ruling/этап/python L…/main.py/прототип/LEADRADAR_*`), удалены блоки `<remarks>`, `<summary>` сжаты до короткой фразы; `//`-комментарии со ссылками удалены, в `.proto` — тоже. Строк комментариев 27 210 → ~19 100 | запрос владельца 2026-09-11 | P2 | DONE |
|
||||||
| TD-COMMENTS-IFACE | Привести код к правилам код-стайла (`docs/spec/Код-стайл-Дейл.md`). **Сделано (2026-09-11):** (1) `<summary>` только блочно — исправлено 5286 шт. в 833 файлах; (2) комментарии только на public/protected — понижено 2028 XML-доков с private/internal (359 файлов). **Осталось:** (3) не дублировать `<summary>` интерфейса в реализации (нужен Roslyn-анализ); (4) явная реализация интерфейсов там, где возможно (61 интерфейс, точечный ревью). Скрипты: `scripts/fix_summary_blocks.py`, `scripts/fix_private_docs.py`. Детали — `docs/spec/Код-стайл-аудит-2026-09-11.md` | запрос владельца 2026-09-11 | P2 | TECHDEBT (1,2 — DONE; 3,4 — BACKLOG) |
|
| TD-COMMENTS-IFACE | Привести код к правилам код-стайла (`docs/spec/Код-стайл-Дейл.md`). **Сделано (2026-09-11):** (1) `<summary>` только блочно — исправлено 5286 шт. в 833 файлах; (2) комментарии только на public/protected — понижено 2028 XML-доков с private/internal (359 файлов); (3) codemod `scripts/dedup_summary_inheritdoc.py` (dry-run) — в продакшн-коде простых дублей `<summary>` уже нет (интерфейсные реализации используют `<inheritdoc/>`). **Осталось:** блоки с `<param>`+дублирующим summary (нужен Roslyn) и (4) явная реализация интерфейсов (61 интерфейс, точечный ревью). Скрипты: `scripts/fix_summary_blocks.py`, `scripts/fix_private_docs.py`, `scripts/dedup_summary_inheritdoc.py`. Детали — `docs/spec/Код-стайл-аудит-2026-09-11.md` | запрос владельца 2026-09-11 | P2 | TECHDEBT (1,2 — DONE; 3 — проверено codemod'ом; 4 — BACKLOG) |
|
||||||
| TD-STYLE-ANALYZERS | Остаток мягких правил код-стайла: `var` для встроенных/неочевидных типов (1529, сейчас `silent`), дедупликация `<summary>`→`<inheritdoc/>` (Roslyn), решение по переводам строк (`.editorconfig` = CRLF, фактически 231 CRLF / 697 LF). Уже закрыто в `.editorconfig` (+build-проверка): запрет `this.` и именование приватных полей (`_camelCase`; `const`/`static readonly` — Pascal). Детали — `docs/spec/Код-стайл-аудит-2026-09-11.md` | аудит 2026-09-11 | P3 | BACKLOG |
|
| TD-STYLE-ANALYZERS | Остаток мягких правил код-стайла: `var` для встроенных/неочевидных типов (1529, сейчас `silent`), дедупликация `<summary>`→`<inheritdoc/>` (Roslyn), решение по переводам строк (`.editorconfig` = CRLF, фактически 231 CRLF / 697 LF). Уже закрыто в `.editorconfig` (+build-проверка): запрет `this.` и именование приватных полей (`_camelCase`; `const`/`static readonly` — Pascal). Детали — `docs/spec/Код-стайл-аудит-2026-09-11.md` | аудит 2026-09-11 | P3 | BACKLOG |
|
||||||
|
|
||||||
## 2. Инфраструктура и эксплуатация
|
## 2. Инфраструктура и эксплуатация
|
||||||
@@ -51,16 +51,16 @@
|
|||||||
|
|
||||||
| ID | Пункт | Источник | Приоритет | Статус |
|
| ID | Пункт | Источник | Приоритет | Статус |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| TD-SETTINGS-UI | Вынос оставшихся вкладок `SettingsView` в отдельные компоненты (частично сделано) | ревью 2026-09-08 | P3 | TECHDEBT |
|
| TD-SETTINGS-UI | Вынос вкладок `SettingsView` в компоненты. **Сделано (2026-09-11):** `SettingsView.vue` — только набор вкладок/QR-опрос, все 10 вкладок — отдельные компоненты (`components/settings/*`) | ревью 2026-09-08 | P3 | DONE |
|
||||||
| TD-VIRT | Полная виртуализация длинных колонок (сейчас — прогрессивный рендер «Показать ещё») | ревью, этап 12 | P3 | TECHDEBT |
|
| TD-VIRT | Полная виртуализация длинных колонок. **Решение (2026-09-11): DEFERRED** — прогрессивный рендер «Показать ещё» покрывает текущие объёмы; виртуализация — при росте списков | ревью, этап 12 | P3 | DEFERRED |
|
||||||
| TD-SSE-DEAD | **Сделано (2026-09-11):** мёртвые SSE-ветки фронта `boards_changed`/`pipeline_stats` удалены из `store/lifecycle.js` (core их не публикует) | этап 12, E | P3 | DONE |
|
| TD-SSE-DEAD | **Сделано (2026-09-11):** мёртвые SSE-ветки фронта `boards_changed`/`pipeline_stats` удалены из `store/lifecycle.js` (core их не публикует) | этап 12, E | P3 | DONE |
|
||||||
| TD-DBL-CLICK | **Сделано (2026-09-11):** перезагрузка доски при batch-переклассификации коалесцируется `scheduleBoardReload()` (ответ + SSE → один запрос) | этап 12, E | P3 | DONE |
|
| TD-DBL-CLICK | **Сделано (2026-09-11):** перезагрузка доски при batch-переклассификации коалесцируется `scheduleBoardReload()` (ответ + SSE → один запрос) | этап 12, E | P3 | DONE |
|
||||||
| TD-TEST-HARNESS | Историческая гонка `FreeTcpPort` — устранена; следить за новыми хост-хелперами | этап 12, E | P3 | TECHDEBT |
|
| TD-TEST-HARNESS | Историческая гонка `FreeTcpPort` — устранена; следить за новыми хост-хелперами | этап 12, E | P3 | TECHDEBT |
|
||||||
| TD-OLD-DOCS | Исторические доки несут старые термины под пометками. **Проверено (2026-09-11):** `docs/superpowers/plans/*` и старые `docs/architecture/2026-09-0*` имеют шапку «Исторический документ»; переписывать не нужно | docs sweep | P3 | DONE |
|
| TD-OLD-DOCS | Исторические доки несут старые термины под пометками. **Проверено (2026-09-11):** `docs/superpowers/plans/*` и старые `docs/architecture/2026-09-0*` имеют шапку «Исторический документ»; переписывать не нужно | docs sweep | P3 | DONE |
|
||||||
| TD-SOURCE-PROVIDER | Провайдеры содержимого источников. **Сделано (2026-09-11):** `TelegramSourceContentProvider` + `ReadSource` RPC + `GET /api/cards/{id}/source` + UI «Обновить из источника». Осталось: провайдеры прочих источников по мере появления | generic source 2026-09-11 | P2 | TECHDEBT |
|
| TD-SOURCE-PROVIDER | Провайдеры содержимого источников. **Сделано (2026-09-11):** `TelegramSourceContentProvider` + `ReadSource` RPC + `GET /api/cards/{id}/source` + UI «Обновить из источника». Осталось: провайдеры прочих источников по мере появления | generic source 2026-09-11 | P2 | TECHDEBT |
|
||||||
| TD-STORE-ATTACH | Выгрузка вложений источника в Storage-сервис адаптером. **Решение (2026-09-11): медиа-посты Telegram пропускаем** — извлечение/выгрузка не делаются. Остались на будущее: вложения прочих источников (файл/диск/таблица) и `ISourceContentProvider` для remote-просмотра | generic source 2026-09-11 | P3 | BACKLOG |
|
| TD-STORE-ATTACH | Выгрузка вложений источника в Storage-сервис адаптером. **Решение (2026-09-11): медиа-посты Telegram пропускаем** — извлечение/выгрузка не делаются; вложений у прочих источников пока нет — **DEFERRED** (контракт `DataRef` готов, включается при появлении такого источника) | generic source 2026-09-11 | P3 | DEFERRED |
|
||||||
| TD-TG-CORE-SPLIT | Перенос оставшейся Telegram-специфики ядра в telegram-сервис. **Закрыто (2026-09-11): не требуется.** Задача «дашборды/карточки не знают о Telegram» решена generic-контрактом источника; оставшиеся `TelegramStore`/`Dialogs`/`TgMessages`/Discovery — это состояние тенанта (ядро — владелец данных, telegram-service — stateless-шлюз), перенос отдал бы шлюзу доступ к схеме тенанта | generic source 2026-09-11 | — | CLOSED |
|
| TD-TG-CORE-SPLIT | Перенос оставшейся Telegram-специфики ядра в telegram-сервис. **Закрыто (2026-09-11): не требуется.** Задача «дашборды/карточки не знают о Telegram» решена generic-контрактом источника; оставшиеся `TelegramStore`/`Dialogs`/`TgMessages`/Discovery — это состояние тенанта (ядро — владелец данных, telegram-service — stateless-шлюз), перенос отдал бы шлюзу доступ к схеме тенанта | generic source 2026-09-11 | — | CLOSED |
|
||||||
| TD-SOURCE-CONTACTS | Квалификатор контактов знает форматы профилей (t.me/`@handle`) — вынести в расширяемые правила источников | generic source 2026-09-11 | P3 | BACKLOG |
|
| TD-SOURCE-CONTACTS | Квалификатор контактов знает форматы профилей (t.me/`@handle`). **Решение (2026-09-11): DEFERRED** — форматы стабильны, расширяемость под источник добавляется при конкретной потребности | generic source 2026-09-11 | P3 | DEFERRED |
|
||||||
| TD-APIMAP-COUNT | Ручной подсчёт числа ручек в `api-map`. **Сделано (2026-09-11):** сверил счётчики §3.1–§3.8 с фактическими строками (рассинхрон §3.5 — 14→15 из-за `GET /cards/{id}/source`); в §3 добавлено правило обновлять счётчики | docs sweep | P3 | DONE |
|
| TD-APIMAP-COUNT | Ручной подсчёт числа ручек в `api-map`. **Сделано (2026-09-11):** сверил счётчики §3.1–§3.8 с фактическими строками (рассинхрон §3.5 — 14→15 из-за `GET /cards/{id}/source`); в §3 добавлено правило обновлять счётчики | docs sweep | P3 | DONE |
|
||||||
|
|
||||||
## 6. Manual-проверки (нужны внешние условия)
|
## 6. Manual-проверки (нужны внешние условия)
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,190 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Заменить дублирующий <summary> реализации на <inheritdoc/> (код-стайл Дейла).
|
||||||
|
|
||||||
|
Консервативно:
|
||||||
|
* собираются тексты <summary> членов интерфейсов (блоки, состоящие только из <summary>);
|
||||||
|
* в типах с интерфейсной базой такой же по тексту блок-док заменяется на `/// <inheritdoc />`;
|
||||||
|
* блоки с <param>/<returns>/<remarks>/... и summary самих типов не трогаются.
|
||||||
|
|
||||||
|
Запуск: python scripts/dedup_summary_inheritdoc.py [--apply]
|
||||||
|
Без --apply — dry-run со счётчиками.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
|
ROOTS = [
|
||||||
|
REPO / "src" / "core",
|
||||||
|
REPO / "src" / "telegram-service",
|
||||||
|
REPO / "src" / "ai-service",
|
||||||
|
REPO / "src" / "ml-service",
|
||||||
|
REPO / "src" / "storage-service",
|
||||||
|
REPO / "src" / "grpc-hosting",
|
||||||
|
]
|
||||||
|
|
||||||
|
INTERFACE_RE = re.compile(
|
||||||
|
r"^\s*(?:public|internal|protected|private)?\s*"
|
||||||
|
r"(?:static\s+|sealed\s+|abstract\s+|partial\s+|unsafe\s+)*interface\s+\w+"
|
||||||
|
)
|
||||||
|
TYPE_BASE_RE = re.compile(
|
||||||
|
r"^\s*(?:public|internal|protected|private)?\s*"
|
||||||
|
r"(?:static\s+|sealed\s+|abstract\s+|partial\s+|unsafe\s+)*"
|
||||||
|
r"(?:class|struct|record)\s+\w+[^{;]*:\s*(.+?)(?:\{|$)"
|
||||||
|
)
|
||||||
|
IFACE_NAME_RE = re.compile(r"\bI[A-Z]\w*")
|
||||||
|
EXTRA_TAG_RE = re.compile(r"<(?:param|typeparam|returns|remarks|exception|example|value)\b", re.IGNORECASE)
|
||||||
|
SUMMARY_OPEN_RE = re.compile(r"<summary>", re.IGNORECASE)
|
||||||
|
SUMMARY_CLOSE_RE = re.compile(r"</summary>", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def iter_files() -> list[Path]:
|
||||||
|
files: list[Path] = []
|
||||||
|
for root in ROOTS:
|
||||||
|
if not root.exists():
|
||||||
|
continue
|
||||||
|
for path in root.rglob("*.cs"):
|
||||||
|
parts = set(path.parts)
|
||||||
|
if "obj" in parts or "bin" in parts:
|
||||||
|
continue
|
||||||
|
if "tests" in path.as_posix().lower():
|
||||||
|
continue
|
||||||
|
files.append(path)
|
||||||
|
return sorted(files)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize(block: list[str]) -> str:
|
||||||
|
text = " ".join(line.strip().lstrip("/").strip() for line in block)
|
||||||
|
text = SUMMARY_CLOSE_RE.sub("", SUMMARY_OPEN_RE.sub("", text))
|
||||||
|
return re.sub(r"\s+", " ", text).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def is_summary_only(block: list[str]) -> bool:
|
||||||
|
joined = "\n".join(block)
|
||||||
|
return (
|
||||||
|
bool(SUMMARY_OPEN_RE.search(joined))
|
||||||
|
and bool(SUMMARY_CLOSE_RE.search(joined))
|
||||||
|
and not EXTRA_TAG_RE.search(joined)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def depth_snapshot(lines: list[str]) -> list[int]:
|
||||||
|
"""Глубина фигурных скобок перед каждой строкой (приблизительно, без строк/комментариев)."""
|
||||||
|
depths: list[int] = []
|
||||||
|
depth = 0
|
||||||
|
for line in lines:
|
||||||
|
depths.append(depth)
|
||||||
|
depth += line.count("{") - line.count("}")
|
||||||
|
return depths
|
||||||
|
|
||||||
|
|
||||||
|
def type_scopes(lines: list[str]):
|
||||||
|
"""Список (start_line, end_line, kind) для интерфейсов и типов с интерфейсной базой.
|
||||||
|
|
||||||
|
kind: "interface" | "type". Границы — по фигурным скобкам объявления.
|
||||||
|
"""
|
||||||
|
depths = depth_snapshot(lines)
|
||||||
|
scopes: list[tuple[int, int, str]] = []
|
||||||
|
stack: list[tuple[int, str, int]] = [] # (depth, kind, start_line)
|
||||||
|
for idx, line in enumerate(lines):
|
||||||
|
depth_before = depths[idx]
|
||||||
|
if INTERFACE_RE.match(line):
|
||||||
|
stack.append((depth_before, "interface", idx))
|
||||||
|
else:
|
||||||
|
m = TYPE_BASE_RE.match(line)
|
||||||
|
if m and IFACE_NAME_RE.search(m.group(1)):
|
||||||
|
stack.append((depth_before, "type", idx))
|
||||||
|
# закрыть блоки на строке закрывающей скобки (глубина после неё возвращается к уровню объявления)
|
||||||
|
while (
|
||||||
|
stack
|
||||||
|
and depths[idx] <= stack[-1][0]
|
||||||
|
and lines[idx].strip().startswith("}")
|
||||||
|
and idx > stack[-1][2]
|
||||||
|
):
|
||||||
|
depth, kind, start = stack.pop()
|
||||||
|
scopes.append((start, idx, kind))
|
||||||
|
for depth, kind, start in stack:
|
||||||
|
scopes.append((start, len(lines) - 1, kind))
|
||||||
|
return scopes
|
||||||
|
|
||||||
|
|
||||||
|
def containing_kinds(scope_kinds: list[tuple[int, int, str]], line_idx: int, depth: int) -> set[str]:
|
||||||
|
kinds: set[str] = set()
|
||||||
|
for start, end, kind in scope_kinds:
|
||||||
|
if start <= line_idx <= end and start != line_idx:
|
||||||
|
kinds.add(kind)
|
||||||
|
return kinds
|
||||||
|
|
||||||
|
|
||||||
|
def collect_interface_summaries(files: list[Path]) -> set[str]:
|
||||||
|
summaries: set[str] = set()
|
||||||
|
for path in files:
|
||||||
|
lines = path.read_text(encoding="utf-8").splitlines()
|
||||||
|
depths = depth_snapshot(lines)
|
||||||
|
scopes = type_scopes(lines)
|
||||||
|
idx = 0
|
||||||
|
while idx < len(lines):
|
||||||
|
if not lines[idx].lstrip().startswith("///"):
|
||||||
|
idx += 1
|
||||||
|
continue
|
||||||
|
start = idx
|
||||||
|
while idx < len(lines) and lines[idx].lstrip().startswith("///"):
|
||||||
|
idx += 1
|
||||||
|
block = lines[start:idx]
|
||||||
|
kinds = containing_kinds(scopes, start, depths[start])
|
||||||
|
if "interface" in kinds and is_summary_only(block):
|
||||||
|
summaries.add(normalize(block))
|
||||||
|
return summaries
|
||||||
|
|
||||||
|
|
||||||
|
def process_file(path: Path, iface_summaries: set[str]) -> tuple[int, list[str]]:
|
||||||
|
lines = path.read_text(encoding="utf-8").splitlines()
|
||||||
|
depths = depth_snapshot(lines)
|
||||||
|
scopes = type_scopes(lines)
|
||||||
|
result: list[str] = []
|
||||||
|
replaced = 0
|
||||||
|
idx = 0
|
||||||
|
while idx < len(lines):
|
||||||
|
if not lines[idx].lstrip().startswith("///"):
|
||||||
|
result.append(lines[idx])
|
||||||
|
idx += 1
|
||||||
|
continue
|
||||||
|
start = idx
|
||||||
|
while idx < len(lines) and lines[idx].lstrip().startswith("///"):
|
||||||
|
idx += 1
|
||||||
|
block = lines[start:idx]
|
||||||
|
kinds = containing_kinds(scopes, start, depths[start])
|
||||||
|
if "type" in kinds and is_summary_only(block) and normalize(block) in iface_summaries:
|
||||||
|
indent = block[0][: len(block[0]) - len(block[0].lstrip())]
|
||||||
|
result.append(f"{indent}/// <inheritdoc />")
|
||||||
|
replaced += 1
|
||||||
|
else:
|
||||||
|
result.extend(block)
|
||||||
|
return replaced, result
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
apply = "--apply" in sys.argv
|
||||||
|
files = iter_files()
|
||||||
|
iface_summaries = collect_interface_summaries(files)
|
||||||
|
total = 0
|
||||||
|
changed = 0
|
||||||
|
for path in files:
|
||||||
|
replaced, new_lines = process_file(path, iface_summaries)
|
||||||
|
if not replaced:
|
||||||
|
continue
|
||||||
|
total += replaced
|
||||||
|
changed += 1
|
||||||
|
if apply:
|
||||||
|
original = path.read_text(encoding="utf-8")
|
||||||
|
text = "\n".join(new_lines) + ("\n" if original.endswith("\n") else "")
|
||||||
|
path.write_text(text, encoding="utf-8")
|
||||||
|
mode = "применено" if apply else "dry-run"
|
||||||
|
print(f"{mode}: замен {total} в {changed} файлах (интерфейсных summary: {len(iface_summaries)})")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user