Добавить 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/
|
||||
src/core/Deal.Api/data/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
+6
-6
@@ -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<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-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 |
|
||||
|
||||
## 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-проверки (нужны внешние условия)
|
||||
|
||||
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