diff --git a/.gitignore b/.gitignore index bd1235b..8466703 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,7 @@ deploy/certs/ # === Рантайм-данные (БД, объектное хранилище, ключи шифрования) === archive/leadradar-legacy/data/ src/core/Deal.Api/data/ + +# Python +__pycache__/ +*.pyc diff --git a/backlog.md b/backlog.md index 016a19d..e6f04ad 100644 --- a/backlog.md +++ b/backlog.md @@ -14,9 +14,9 @@ | BL-TG-MULTI | Мультиаккаунтность Telegram (сейчас 1 аккаунт на тенант) | ТЗ §12 | P2 | DEFERRED | | 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`; в 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_*`), удалены блоки ``, `` сжаты до короткой фразы; `//`-комментарии со ссылками удалены, в `.proto` — тоже. Строк комментариев 27 210 → ~19 100 | запрос владельца 2026-09-11 | P2 | DONE | -| TD-COMMENTS-IFACE | Привести код к правилам код-стайла (`docs/spec/Код-стайл-Дейл.md`). **Сделано (2026-09-11):** (1) `` только блочно — исправлено 5286 шт. в 833 файлах; (2) комментарии только на public/protected — понижено 2028 XML-доков с private/internal (359 файлов). **Осталось:** (3) не дублировать `` интерфейса в реализации (нужен 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) `` только блочно — исправлено 5286 шт. в 833 файлах; (2) комментарии только на public/protected — понижено 2028 XML-доков с private/internal (359 файлов); (3) codemod `scripts/dedup_summary_inheritdoc.py` (dry-run) — в продакшн-коде простых дублей `` уже нет (интерфейсные реализации используют ``). **Осталось:** блоки с ``+дублирующим 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`), дедупликация ``→`` (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. Инфраструктура и эксплуатация @@ -51,16 +51,16 @@ | ID | Пункт | Источник | Приоритет | Статус | |---|---|---|---|---| -| TD-SETTINGS-UI | Вынос оставшихся вкладок `SettingsView` в отдельные компоненты (частично сделано) | ревью 2026-09-08 | P3 | TECHDEBT | -| TD-VIRT | Полная виртуализация длинных колонок (сейчас — прогрессивный рендер «Показать ещё») | ревью, этап 12 | P3 | TECHDEBT | +| TD-SETTINGS-UI | Вынос вкладок `SettingsView` в компоненты. **Сделано (2026-09-11):** `SettingsView.vue` — только набор вкладок/QR-опрос, все 10 вкладок — отдельные компоненты (`components/settings/*`) | ревью 2026-09-08 | P3 | DONE | +| 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-DBL-CLICK | **Сделано (2026-09-11):** перезагрузка доски при batch-переклассификации коалесцируется `scheduleBoardReload()` (ответ + SSE → один запрос) | этап 12, E | P3 | DONE | | 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-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-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 | ## 6. Manual-проверки (нужны внешние условия) diff --git a/scripts/__pycache__/_slim_comments.cpython-312.pyc b/scripts/__pycache__/_slim_comments.cpython-312.pyc deleted file mode 100644 index 843531d..0000000 Binary files a/scripts/__pycache__/_slim_comments.cpython-312.pyc and /dev/null differ diff --git a/scripts/dedup_summary_inheritdoc.py b/scripts/dedup_summary_inheritdoc.py new file mode 100644 index 0000000..2cc5769 --- /dev/null +++ b/scripts/dedup_summary_inheritdoc.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +"""Заменить дублирующий реализации на (код-стайл Дейла). + +Консервативно: + * собираются тексты членов интерфейсов (блоки, состоящие только из ); + * в типах с интерфейсной базой такой же по тексту блок-док заменяется на `/// `; + * блоки с ///... и 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"", re.IGNORECASE) +SUMMARY_CLOSE_RE = re.compile(r"", 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}/// ") + 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())