From 5dd078566c3bb0b2b91e18276608a143bdb60ba9 Mon Sep 17 00:00:00 2001 From: Andrew Pimenov Date: Sun, 23 Aug 2026 19:16:08 +0300 Subject: [PATCH] docs: sync ARCHITECTURE line counts and format list with code, gitignore speedtest binary Line-count exceptions had drifted 18-80% behind actual file sizes, storage section undercounted --format all as three formats instead of five, test count was stale at 880 vs actual 930. --- .gitignore | 2 ++ ARCHITECTURE.md | 17 +++++++++-------- ARCHITECTURE.ru.md | 17 +++++++++-------- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 6db7640..ed2fe69 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ bugs.md llm_context* TASKS.md docs/superpowers/ +speedtest.exe +speedtest.md plans/ .venv/ diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 27cdc89..ad8613d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -49,15 +49,16 @@ src/netsleuth/ **File-size exceptions.** The ~200-line guideline (Global Constraints) held for most modules, but these grew past it during implementation and were kept as single files rather than split, because each one is a single cohesive responsibility that splitting would only scatter across more files with a thinner reason to draw the line anywhere in particular: -- `exporter.py` (~910 lines) — JSON and both Markdown languages are renderers of the same report shape; keeping them together is what guarantees the English and Russian reports can never drift on which sections or rows exist. `render_markdown(report, lang="en"|"ru")` and a handful of inline `_t(lang, en, ru)` calls at each string site render both languages from one code path, deliberately, instead of a second renderer file that could silently fall out of sync. This grew by five sections (TLS, DNS advanced, path diversity, prefix bench, DPI check) without changing that argument. -- `cli.py` (~805 lines) — the Typer app, every flag, and the full `diagnose()` phase pipeline; splitting flag parsing from what the flags toggle would just add an import hop. -- `models.py` (~490 lines) — every shared dataclass plus `to_jsonable()`; this is deliberately the one place that defines the report's vocabulary. -- `interpret.py` (~600 lines) — threshold bands, the latency/path/speed/TLS/DNS/DPI/anycast/prefix finding generators, VPN scoring and bufferbloat grading all reason over the same `Finding[]` shape. +- `exporter.py` (~1100 lines) — JSON and both Markdown languages are renderers of the same report shape; keeping them together is what guarantees the English and Russian reports can never drift on which sections or rows exist. `render_markdown(report, lang="en"|"ru")` and a handful of inline `_t(lang, en, ru)` calls at each string site render both languages from one code path, deliberately, instead of a second renderer file that could silently fall out of sync. This grew by five sections (TLS, DNS advanced, path diversity, prefix bench, DPI check) without changing that argument. +- `cli.py` (~1078 lines) — the Typer app, every flag, and the full `diagnose()` phase pipeline; splitting flag parsing from what the flags toggle would just add an import hop. +- `models.py` (~576 lines) — every shared dataclass plus `to_jsonable()`; this is deliberately the one place that defines the report's vocabulary. +- `interpret.py` (~1079 lines) — threshold bands, the latency/path/speed/TLS/DNS/DPI/anycast/prefix finding generators, VPN scoring and bufferbloat grading all reason over the same `Finding[]` shape. - `bgp.py` (~305 lines) — four independent providers (RIPEstat, CAIDA ASRank, Team Cymru, PeeringDB) plus the disk cache they share. - `ip_geo.py` (~280 lines) — six independent provider normalizers plus the field-wise merge that reconciles them. -- `netinfo.py`, `speed.py` (~260 lines each) — OS-specific capability probing and the four-tier speedtest cascade both fan out into several backends that don't split cleanly by single responsibility. +- `netinfo.py` (~310 lines) — OS-specific capability probing fans out into several backends that don't split cleanly by single responsibility. +- `speed.py` (~260 lines) — the four-tier speedtest cascade fans out into several backends that don't split cleanly by single responsibility. - `reputation.py` (~250 lines) — FireHOL netsets, Shodan InternetDB and DNSBL decoding are three unrelated reputation sources sharing one result shape. -- `probes/traceroute.py` (~225 lines) — the four-tier cascade plus the opt-in `tcp_trace` tier. +- `probes/traceroute.py` (~284 lines) — the four-tier cascade plus the opt-in `tcp_trace` tier. ## Data flow @@ -190,7 +191,7 @@ The AS prefix benchmark (`probes/prefix_benchmark.py`) gets the same treatment f ## Storage -- `./logs///
/report__.{md,ru.md,json,prom,csv}` — the artifacts selected by `--format`/`output.formats` (default: English Markdown only; `all` writes all three: English Markdown, a full Russian translation with identical structure, and the JSON dump — English only, no `.ru.json`). When both Markdown languages are written they cross-link each other at the top, the same way `README.md`/`README.ru.md` do; writing only one skips the cross-link rather than pointing at a file that doesn't exist. `` is the target's subject in target mode (an ASN, IP or domain) and the local egress ASN in auto mode. The run's start date becomes the directory (`exporter.report_date_dir()`), so browsing `logs/` means walking year, then month, then day; the filename itself only needs a readable time-of-day (`exporter.readable_timestamp()`, e.g. `19-12-00Z`) since the date is already the path — no `:` because Windows forbids it in filenames, dashes instead. Falls back to a flat `report_unknown_unknown.…` at the `logs/` root when `started_at` doesn't parse. Written temp-file-plus-`os.replace()`, always atomic — `atomic_write()` creates the whole `YYYY/MM/DD` chain via `mkdir(parents=True)`. Gitignored: reports contain the external IP, city, coordinates and ISP name. +- `./logs///
/report__.{md,ru.md,json,prom,csv}` — the artifacts selected by `--format`/`output.formats` (default: English Markdown only; `all` writes all five: English Markdown, a full Russian translation with identical structure, the JSON dump, the Prometheus text-format dump, and the CSV dump — English only, no `.ru.json`). When both Markdown languages are written they cross-link each other at the top, the same way `README.md`/`README.ru.md` do; writing only one skips the cross-link rather than pointing at a file that doesn't exist. `` is the target's subject in target mode (an ASN, IP or domain) and the local egress ASN in auto mode. The run's start date becomes the directory (`exporter.report_date_dir()`), so browsing `logs/` means walking year, then month, then day; the filename itself only needs a readable time-of-day (`exporter.readable_timestamp()`, e.g. `19-12-00Z`) since the date is already the path — no `:` because Windows forbids it in filenames, dashes instead. Falls back to a flat `report_unknown_unknown.…` at the `logs/` root when `started_at` doesn't parse. Written temp-file-plus-`os.replace()`, always atomic — `atomic_write()` creates the whole `YYYY/MM/DD` chain via `mkdir(parents=True)`. Gitignored: reports contain the external IP, city, coordinates and ISP name. - `./logs///
/watch__.json` — one time-series artifact per `--watch` session, not one report per tick, dated by the session's `started_at` the same way one-shot reports are. - `./.cache/firehol/*.netset` — downloaded blocklists, refreshed per `providers.firehol_refresh_hours`. - `./.cache/pdb-net-.json`, `./.cache/pdb-netixlan-.json` — PeeringDB responses, valid for `providers.peeringdb_cache_hours`. @@ -204,4 +205,4 @@ Several sections back the L4-L7 probes, all opt-in at the CLI level; the ones wh ## Tests -`uv run pytest -q`. Roughly 880 tests covering the parsers, normalizers, verdict engine, DNSBL decoding, throughput math, ping statistics, jitter percentiles, serialization and the report diff. Glue is deliberately untested: live HTTP, real ICMP, Typer wiring, `psutil`/`ctypes` calls and the `--watch` timing loop. Traceroute fixtures live in `tests/fixtures/traceroute/{windows,linux,darwin}/` and include a cp866-encoded Russian Windows sample, because that encoding is exactly what breaks naive parsers. DoH response fixtures (RFC 8484 JSON form) live in `tests/fixtures/api/doh_*.json` alongside the other provider fixtures. +`uv run pytest -q`. Roughly 930 tests covering the parsers, normalizers, verdict engine, DNSBL decoding, throughput math, ping statistics, jitter percentiles, serialization and the report diff. Glue is deliberately untested: live HTTP, real ICMP, Typer wiring, `psutil`/`ctypes` calls and the `--watch` timing loop. Traceroute fixtures live in `tests/fixtures/traceroute/{windows,linux,darwin}/` and include a cp866-encoded Russian Windows sample, because that encoding is exactly what breaks naive parsers. DoH response fixtures (RFC 8484 JSON form) live in `tests/fixtures/api/doh_*.json` alongside the other provider fixtures. diff --git a/ARCHITECTURE.ru.md b/ARCHITECTURE.ru.md index e43d0b3..5310070 100644 --- a/ARCHITECTURE.ru.md +++ b/ARCHITECTURE.ru.md @@ -49,15 +49,16 @@ src/netsleuth/ **Исключения по размеру файла.** Лимит ~200 строк (Общие ограничения) выдержан для большинства модулей, но эти выросли за него в процессе реализации и остались одним файлом, а не были разбиты, потому что каждый — единая цельная ответственность, и разбиение только раскидало бы её по файлам без чёткой причины проводить границу где-то конкретно: -- `exporter.py` (~910 строк) — JSON и оба языка Markdown — рендеры одной и той же формы отчёта; то, что они вместе, гарантирует, что английский и русский отчёты никогда не разойдутся в том, какие разделы или строки существуют. `render_markdown(report, lang="en"|"ru")` и точечные вызовы `_t(lang, en, ru)` в местах со строками рендерят оба языка из одного пути кода — намеренно, вместо отдельного файла-рендера, который мог бы незаметно разойтись. Файл вырос на пять разделов (TLS, DNS advanced, path diversity, prefix bench, DPI check), не меняя этот принцип. -- `cli.py` (~805 строк) — приложение Typer, все флаги и весь конвейер фаз `diagnose()`; разделение разбора флагов и того, что они переключают, добавило бы только лишний импорт. -- `models.py` (~490 строк) — все общие датаклассы плюс `to_jsonable()`; это намеренно единственное место, определяющее словарь отчёта. -- `interpret.py` (~600 строк) — пороговые полосы, генераторы находок по задержке/маршруту/скорости/TLS/DNS/DPI/anycast/префиксам, скоринг VPN и оценка bufferbloat — всё рассуждает над одной формой `Finding[]`. +- `exporter.py` (~1100 строк) — JSON и оба языка Markdown — рендеры одной и той же формы отчёта; то, что они вместе, гарантирует, что английский и русский отчёты никогда не разойдутся в том, какие разделы или строки существуют. `render_markdown(report, lang="en"|"ru")` и точечные вызовы `_t(lang, en, ru)` в местах со строками рендерят оба языка из одного пути кода — намеренно, вместо отдельного файла-рендера, который мог бы незаметно разойтись. Файл вырос на пять разделов (TLS, DNS advanced, path diversity, prefix bench, DPI check), не меняя этот принцип. +- `cli.py` (~1078 строк) — приложение Typer, все флаги и весь конвейер фаз `diagnose()`; разделение разбора флагов и того, что они переключают, добавило бы только лишний импорт. +- `models.py` (~576 строк) — все общие датаклассы плюс `to_jsonable()`; это намеренно единственное место, определяющее словарь отчёта. +- `interpret.py` (~1079 строк) — пороговые полосы, генераторы находок по задержке/маршруту/скорости/TLS/DNS/DPI/anycast/префиксам, скоринг VPN и оценка bufferbloat — всё рассуждает над одной формой `Finding[]`. - `bgp.py` (~305 строк) — четыре независимых источника (RIPEstat, CAIDA ASRank, Team Cymru, PeeringDB) плюс общий для них дисковый кэш. - `ip_geo.py` (~280 строк) — шесть независимых нормализаторов провайдеров плюс пополевое слияние, которое их примиряет. -- `netinfo.py`, `speed.py` (~260 строк каждый) — определение возможностей по ОС и каскад замера скорости из четырёх уровней оба веерятся по нескольким бэкендам, которые не разбиваются чисто по единой ответственности. +- `netinfo.py` (~310 строк) — определение возможностей по ОС веерится по нескольким бэкендам, которые не разбиваются чисто по единой ответственности. +- `speed.py` (~260 строк) — каскад замера скорости из четырёх уровней веерится по нескольким бэкендам, которые не разбиваются чисто по единой ответственности. - `reputation.py` (~250 строк) — netset'ы FireHOL, Shodan InternetDB и декодирование DNSBL — три несвязанных источника репутации с одной общей формой результата. -- `probes/traceroute.py` (~225 строк) — каскад из четырёх уровней плюс опциональный уровень `tcp_trace`. +- `probes/traceroute.py` (~284 строки) — каскад из четырёх уровней плюс опциональный уровень `tcp_trace`. ## Поток данных @@ -190,7 +191,7 @@ src/netsleuth/ ## Хранение -- `./logs///
/report__.{md,ru.md,json,prom,csv}` — артефакты, выбранные `--format`/`output.formats` (по умолчанию: только английский Markdown; `all` пишет все три: английский Markdown, его полный перевод на русский с идентичной структурой и JSON-дамп — только английский, `.ru.json` нет). Когда пишутся оба языка Markdown, они ссылаются друг на друга вверху, так же как `README.md`/`README.ru.md`; если пишется только один — перекрёстная ссылка не ставится, чтобы не указывать на несуществующий файл. `` — это цель в режиме target (ASN, IP или домен) и локальный ASN исходящего адреса в авто-режиме. Дата старта прогона становится директорией (`exporter.report_date_dir()`), так что просмотр `logs/` — это спуск по году, затем месяцу, затем дню; самому имени файла достаточно читаемого времени суток (`exporter.readable_timestamp()`, например `19-12-00Z`), поскольку дата уже есть в пути — без `:`, потому что Windows запрещает его в именах файлов, вместо него дефисы. При непарсящемся `started_at` — плоский `report_unknown_unknown.…` прямо в корне `logs/`. Запись через временный файл и `os.replace()`, всегда атомарно — `atomic_write()` создаёт всю цепочку `YYYY/MM/DD` через `mkdir(parents=True)`. В gitignore: отчёты содержат внешний IP, город, координаты и название провайдера. +- `./logs///
/report__.{md,ru.md,json,prom,csv}` — артефакты, выбранные `--format`/`output.formats` (по умолчанию: только английский Markdown; `all` пишет все пять: английский Markdown, его полный перевод на русский с идентичной структурой, JSON-дамп, дамп в формате Prometheus text и CSV-дамп — только английский, `.ru.json` нет). Когда пишутся оба языка Markdown, они ссылаются друг на друга вверху, так же как `README.md`/`README.ru.md`; если пишется только один — перекрёстная ссылка не ставится, чтобы не указывать на несуществующий файл. `` — это цель в режиме target (ASN, IP или домен) и локальный ASN исходящего адреса в авто-режиме. Дата старта прогона становится директорией (`exporter.report_date_dir()`), так что просмотр `logs/` — это спуск по году, затем месяцу, затем дню; самому имени файла достаточно читаемого времени суток (`exporter.readable_timestamp()`, например `19-12-00Z`), поскольку дата уже есть в пути — без `:`, потому что Windows запрещает его в именах файлов, вместо него дефисы. При непарсящемся `started_at` — плоский `report_unknown_unknown.…` прямо в корне `logs/`. Запись через временный файл и `os.replace()`, всегда атомарно — `atomic_write()` создаёт всю цепочку `YYYY/MM/DD` через `mkdir(parents=True)`. В gitignore: отчёты содержат внешний IP, город, координаты и название провайдера. - `./logs///
/watch__.json` — один тайм-ряд на сессию `--watch`, а не отдельный отчёт на каждый тик, датирован по `started_at` сессии так же, как и одноразовые отчёты. - `./.cache/firehol/*.netset` — скачанные блоклисты, обновляются по `providers.firehol_refresh_hours`. - `./.cache/pdb-net-.json`, `./.cache/pdb-netixlan-.json` — ответы PeeringDB, действительны `providers.peeringdb_cache_hours`. @@ -204,4 +205,4 @@ src/netsleuth/ ## Тесты -`uv run pytest -q`. Около 880 тестов: парсеры, нормализаторы, движок вердиктов, декодирование DNSBL, математика пропускной способности, статистика пингов, перцентили джиттера, сериализация и сравнение отчётов. Клей намеренно не тестируется: живой HTTP, настоящий ICMP, обвязка Typer, вызовы `psutil`/`ctypes` и цикл `--watch`. Фикстуры трассировок лежат в `tests/fixtures/traceroute/{windows,linux,darwin}/` и включают образец русской Windows в cp866 — именно эта кодировка ломает наивные парсеры. Фикстуры DoH-ответов (формат RFC 8484 JSON) лежат в `tests/fixtures/api/doh_*.json` рядом с фикстурами других провайдеров. +`uv run pytest -q`. Около 930 тестов: парсеры, нормализаторы, движок вердиктов, декодирование DNSBL, математика пропускной способности, статистика пингов, перцентили джиттера, сериализация и сравнение отчётов. Клей намеренно не тестируется: живой HTTP, настоящий ICMP, обвязка Typer, вызовы `psutil`/`ctypes` и цикл `--watch`. Фикстуры трассировок лежат в `tests/fixtures/traceroute/{windows,linux,darwin}/` и включают образец русской Windows в cp866 — именно эта кодировка ломает наивные парсеры. Фикстуры DoH-ответов (формат RFC 8484 JSON) лежат в `tests/fixtures/api/doh_*.json` рядом с фикстурами других провайдеров.