From cefb4494a5affa7f4399dcc2933e27a252868974 Mon Sep 17 00:00:00 2001 From: IK Date: Wed, 9 Sep 2026 20:22:50 +0700 Subject: [PATCH] docs: audit regressions and prioritize recovery plan --- README.md | 4 +- docs/REGRESSION_AUDIT_2026-09-09.md | 144 ++++++++++++++++++++++++++++ docs/ROADMAP.md | 30 +++++- 3 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 docs/REGRESSION_AUDIT_2026-09-09.md diff --git a/README.md b/README.md index 155f5ad..2258ba9 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ RF4 Spotter — неофициальный сервис свежих точек ## Статус разработки +**Повторная приёмка 9 сентября 2026 (`9ae05ef`): к деплою пока не готов.** Найдены регрессии после последних исправлений: невалидный Caddyfile, несовместимость activity API с detail-страницами, поломка research cooldown CLI и незавершённые UI/SEO/защита интервалов. Python: **10 failed, 86 passed, 1 skipped**; Astro check/build и web unit проходят, но не покрывают эти сценарии. [Отчёт с доказательствами](docs/REGRESSION_AUDIT_2026-09-09.md), [план R01–R15](docs/ROADMAP.md#повторная-приёмка-9-сентября-2026). Следующие задачи — R01 и R02. До R05 не запускать текущий production bootstrap: он наследует реальные источники и публичные порты. Ниже описаны реализованные возможности; прежние успешные проверки не означают приёмку текущей ревизии. + Web Docker-образ устанавливает зависимости через `npm ci` по lock-файлу и удаляет devDependencies после сборки. Локальные `.env` исключены из web build context. Очередь внешних наблюдений выбирает только ожидающие проверки записи на сервере и показывает их страницами по 50. Обработанные записи не скрывают более старые необработанные наблюдения. @@ -18,7 +20,7 @@ Web Docker-образ устанавливает зависимости чере Идёт исправление аудита: актуальные изменения и ограничения перечислены в [AUDIT_FIXES.md](docs/AUDIT_FIXES.md). Production Compose включает community scheduler; страницы rules/privacy реализованы. Для запуска остаются сервер, DNS/TLS, секреты, внешний backup и контакты. Шкала 72 часов использует полную выборку по времени поступления; одинаковые поля разных источников больше не считаются доказательством одного события. Фоновая публикация обновляет кэш API в пределах TTL, не мгновенно. -Полный аудит 8 сентября: [отчёт](docs/PROJECT_AUDIT_2026-09-08.md), [приоритетный план](docs/ROADMAP.md#ближайший-рабочий-пакет). T01/T02 выполнены: исправлены маршруты формы и конфликт admin-аутентификации. Проверка `sh deploy/test-proxy-routing.sh` на запущенном локальном Compose проверяет Astro redirects, API, Basic-доступ к страницам и Bearer-доступ к API, включая отказы. Следующий блокер — исходящая сеть community scheduler (T03). Затем — клиентский rate limit, надёжность парсеров, согласованность фильтров/оценок и HTTP/SEO-контракт ошибок. +Предыдущий полный аудит 8 сентября: [отчёт](docs/PROJECT_AUDIT_2026-09-08.md). T01/T02 исправили маршруты формы и конфликт admin-аутентификации; `sh deploy/test-proxy-routing.sh` проверял Astro redirects, API, Basic/Bearer и отказы на прежней ревизии. На текущей ревизии повторный запуск блокирует R01. Подключение scheduler к исходящей сети добавлено, но интеграционная приёмка T03 ещё нужна. Актуальная последовательность работ находится в [плане повторной приёмки](docs/ROADMAP.md#повторная-приёмка-9-сентября-2026). На ширинах 320, 390, 768 и 1280 px ранее проверено отсутствие горизонтального переполнения основных страниц. Это не полная визуальная приёмка: аудит обнаружил неверную desktop-компоновку фильтров; наполненные карточки, длинные названия, клавиатура и zoom остаются отдельной задачей. diff --git a/docs/REGRESSION_AUDIT_2026-09-09.md b/docs/REGRESSION_AUDIT_2026-09-09.md new file mode 100644 index 0000000..51be23a --- /dev/null +++ b/docs/REGRESSION_AUDIT_2026-09-09.md @@ -0,0 +1,144 @@ +# Повторный аудит после исправлений — 9 сентября 2026 + +## Вывод и границы проверки + +Текущий `9ae05ef` не готов к production. Проверены изменения `c6fdc96..9ae05ef`: три коммита, 19 файлов, 208 добавленных и 80 удалённых строк. Изменений тестов, README и ROADMAP в этом диапазоне нет. Поэтому по доступной ветке нельзя подтвердить выполнение всего предыдущего аудита: часть исправлений полезна, часть неполна, есть новые блокирующие регрессии. + +Это проверка кода и локальных команд, а не повторная визуальная приёмка в браузере или проверка рабочего сервера. Парсеры в сеть не запускались: разрешённый интервал не менее 30 минут на площадку сохраняется. Production/bootstrap не запускался; для Caddy использован только одноразовый контейнер без сети. Неотслеживаемые `.gigacode/`, `.gigaide/`, `.idea/` не изменялись. + +### Фактические проверки + +| Проверка | Результат | +| --- | --- | +| `.venv/bin/pytest -q --tb=short` | **10 failed, 86 passed, 1 skipped**, 3,54 с | +| `.venv/bin/pytest -q tests/test_community_cli.py --tb=short` | **2 failed, 1 passed** | +| `npm --prefix apps/web run check` | 40 файлов, 0 ошибок/предупреждений | +| `npm --prefix apps/web run test:unit` | 1 успешный test-file subtest | +| `npm --prefix apps/web run build` | Успех | +| Caddy 2.10.2, `caddy adapt` с текущим Caddyfile | **Ошибка: строка 8, unrecognized directive: body_limit** | + +Из 10 падений Python: два относятся к research cooldown; четыре — к изменённому activity-контракту; два — к новой зависимости scheduler helpers от внешней БД; по одному — к изменению сигнатуры rate limit и состава readiness. Последние два сами по себе не доказывают поломку функциональности: старые проверки не обновлены под новый контракт. Но зелёной приёмки изменений нет. Успешная сборка Astro не проверяет соответствие JSON реальному типу `api`. + +## Подтверждённые регрессии и недоработки + +### R01 · P0 · Production proxy не может запуститься + +- Доказательство: [Caddyfile](../deploy/Caddyfile), строка 8, `body_limit 10M`; локальный `caddy adapt` завершается ошибкой неизвестной директивы. +- Последствие: недоступны сайт, API и файловый домен при запуске с этим конфигом. Прежняя успешная проверка маршрутизации не подтверждает текущий HEAD. +- Исправление: поддерживаемая конфигурация лимита тела, затем проверка конфигурации и маршрутов; проверить 413 для JSON и multipart. Не ограничиться удалением строки: требование защиты от больших запросов остаётся. + +### R02 · P0 · Activity API изменён без миграции всех страниц + +- [main.py](../apps/api/app/main.py): `/api/v1/activity` теперь возвращает `{items,total,limit,offset}`. На новый формат переведена только главная. +- [fish/[slug].astro](../apps/web/src/pages/fish/[slug].astro) и [waterbodies/[slug].astro](../apps/web/src/pages/waterbodies/[slug].astro) вызывают `items.map` вне `try` у объекта, а не массива: путь рендера заканчивается TypeError. +- [waterbodies/[slug]/[fish].astro](../apps/web/src/pages/waterbodies/[slug]/[fish].astro) проверяет `items.length` у объекта и выбирает ложное пустое состояние. +- [spots/[id].astro](../apps/web/src/pages/spots/[id].astro) вызывает `activityRows.find` у объекта; исключение перехватывается как 503, запрос timeline не выполняется. +- Четыре существующих API-теста падают после смены контракта. Перечисленные последствия страниц установлены по коду; отдельный SSR-прогон ещё нужен. +- Приёмка: один общий контракт и все потребители, SSR для четырёх маршрутов с данными и пустым `items`, полная пагинация без потерь; старый контракт либо явно мигрирован, либо сохранена совместимость. + +### R03 · P1 · Research CLI не читает и не создаёт cooldown state + +- [community_cli.py](../rf4_research/community_cli.py): `enforce_fetch_interval` открывает отсутствующий файл в `r`, `mark_fetch` — в `r+`; оба падают при первом запуске. Подтверждено двумя тестами. +- Для существующего файла `f.read(encoding="utf-8")` тоже ошибочен: encoding задаётся при открытии, а не в `TextIOWrapper.read`. +- Раздельные shared/exclusive lock не делают проверку и резервирование атомарными. Разблокировка перед закрытием/flush записи оставляет дополнительное окно гонки; миграции старых ключей нет. +- Приёмка: первый запуск, существующий и повреждённый state, миграция ключей, конкурентные процессы, неудачный HTTP расходует интервал. Production и исследовательский путь не должны обходить общий лимит. + +### R04 · P1 · Disabled-фильтрация может обойти общий интервал сайта + +- [community_scheduler.py](../apps/api/app/community_scheduler.py): `configured_sources()` теперь оставляет только enabled. Из этого списка строится и `site_sources` для поиска недавних попыток. +- Сценарий по коду: RF4-STAT fishing только что запрошен → его отключают → posts больше не учитывает fishing в истории сайта и может запросить тот же сайт раньше 1800 секунд. Исключать отключённые endpoint нужно из кандидатов, **не из истории площадки**. +- `run_source(disabled)` теперь может получить KeyError до прежнего безопасного `return False`; повторные снимки registry открывают гонку при переключении enabled. +- Приёмка: выключение/включение endpoint не сбрасывает site cooldown; конкурентные запуски видят общую историю и только актуальных кандидатов. + +### R05 · P1 · Bootstrap перестал быть изолированным и безопасным для источников + +- [test-production-bootstrap.sh](../deploy/test-production-bootstrap.sh) добавляет production `proxy community-scheduler`, но [compose.bootstrap.yaml](../deploy/compose.bootstrap.yaml) переопределяет только API/web. +- Наследуются host-порты 80/443, production домены/TLS и реальные URL парсеров. Свежая БД bootstrap не знает историю запросов другого стека; повторные проверки способны нарушать 30-минутное ограничение. Импорт также конфликтует с проверкой `catch_report = 0`. +- Проверки curl/Playwright продолжают ходить напрямую на API/web, а не через новый proxy: запуск контейнера не равен проверке маршрутизации. +- Приёмка: loopback high ports, локальные HTTP-домены, fixture HTTP-источник и запрещённый внешний egress; сценарии формы/модерации проходят через Caddy. Реальные источники — только отдельный согласованный smoke с общей историей интервалов. + +### R06 · P1 · Фильтры сигналов сравнивают slug с названием + +- [index.astro](../apps/web/src/pages/index.astro) отправляет выбранные slug `fish`/`waterbody`. +- [main.py](../apps/api/app/main.py), `community_observations`, сравнивает их с `fish_name`/`waterbody_name` (названия источника). Наблюдение «Щука» не совпадает с `pike` и исчезает при фильтрации. +- Приёмка: единая семантика slug/ID с подтверждёнными mapping/alias; отдельно определить поведение несопоставленных наблюдений. Проверить источник, неполноту и выбранные фильтры вместе, не теряя атрибуцию. + +### R07 · P1 · Ошибка главной остаётся HTTP 200 и индексируемой + +- [index.astro](../apps/web/src/pages/index.astro): `if (unavailable)` находится **до** запросов, сразу после присвоения `false`; в `catch` меняется только флаг. Ветка 503/Retry-After/noindex никогда не срабатывает при сбое API. +- На detail-страницах `noindex` зависит от найденной сущности, не от `unavailable`: при успешном справочнике и сбое activity ошибочная страница не закрывается через этот флаг. +- Приёмка: после завершения загрузки согласовать status, noindex, Cache-Control, Retry-After и JSON-LD для 200-empty/404/422/503; аварийная страница не изображает полноценный Dataset. + +### R08 · P1 · Фильтры UI не доведены до рабочего responsive-состояния + +- [global.css](../apps/web/src/styles/global.css): `.filter-advanced-field{display:contents}` применяется уже к `label`: текст подписи и select теряют общий grid-контейнер и становятся отдельными grid items. +- На mobile поля скрыты CSS, но JS добавляет класс `filter-compact-hidden`, который задаёт противоположное — `display:contents!important`. Управления раскрытием больше нет. Без JS период и сортировка недоступны. +- [index.astro](../apps/web/src/pages/index.astro): только вариант 24 часов имеет `selected`; после запроса 6/12/72 форма показывает первый вариант вместо фактического фильтра и может менять период при повторной отправке. +- Приёмка: связная label/select компоновка, сохранённые выбранные значения, доступные все фильтры с JS и без него; desktop/mobile, клавиатура и zoom. Это статические находки; визуальную приёмку нужно выполнить после исправления R02. + +### R09 · P1 · Публичной пагинации по-прежнему нет + +- [index.astro](../apps/web/src/pages/index.astro) запрашивает всегда `limit=20&offset=0`, добавлен total, но нет перехода к следующей странице активности. +- Рекорды/каталоги также не получили полноценную публичную навигацию по страницам в проверенном diff. Рост лимита полевых сигналов до 48 не решает задачу остальных списков. +- Приёмка: 21+ результатов достижимы, фильтры сохраняются, счётчик различает показанное/общее; определить SEO-политику пагинации. + +### R10 · P1 · Ошибки формы теряют черновик, timeout распознаётся неверно + +- [report.astro](../apps/web/src/pages/report.astro) восстанавливает черновик и фокус только при `create_error`, но не при новых `rate_limited`, `server_error`, `timeout`. +- [api/report.ts](../apps/web/src/pages/api/report.ts) ищет timeout как `TypeError` с текстом `abort`; `AbortSignal.timeout` использует TimeoutError, поэтому новое состояние обычно недостижимо. +- Отключение кнопки не даёт серверной идемпотентности: при таймауте создания неизвестно, сохранил ли API заявку; повтор может создать дубль. Обработка multipart retry-маршрута и единая обработка 413 также требуют завершения. +- Приёмка: восстановление полей/фокуса при каждом отказе, безопасное поведение при недоступном sessionStorage, корректная классификация timeout и повтор без дублирования сохранённого улова. + +### R11 · P1 · Проверка URL происходит после сетевого обращения + +- [community_cli.py](../rf4_research/community_cli.py), `fetch_html`: сначала `urlopen`, затем allowlist конечного `response.url`. Запрос к запрещённому initial URL или redirect уже произошёл до отказа. +- Лимит чтения 5 МБ полезен, но не исправляет эту проблему. `fetch_site_key` не объединяет `rf4db.com` и `download.rf4db.com` в площадку. +- Приёмка: validate scheme/host/port до I/O и на каждом redirect, общий ключ площадки, ограничение ответа; тесты только на mock/fixture transport. + +### R12 · P1 · Мониторинг не сигнализирует о зависшем импорте + +- [readiness.py](../apps/api/app/readiness.py) добавляет community-компонент, но его failed/stale/unknown не влияют на итог `ready`. Выбирается одна последняя попытка всех источников: успех одного маскирует проблемы других. +- [monitor.sh](../deploy/monitor.sh) проверяет лишь running контейнер и наличие текста `"status":"ready"` где-либо в JSON. Он не проверяет component status/свежесть каждого источника. +- Приёмка: разделить готовность API обслуживать данные и здоровье сбора, но обязательно выдавать мониторинговую тревогу по enabled source/site с учётом ротации и backoff. Проверить running без завершения, stale и failed при исправных DB/MinIO. + +### R13 · P1 · Доверие к IP клиента не ограничено proxy boundary + +- [main.py](../apps/api/app/main.py), `_check_rate_limit`: первый `X-Forwarded-For` принимается от любого peer без проверки доверенного прокси и валидности IP. +- [api/report.ts](../apps/web/src/pages/api/report.ts) при отсутствии заголовков передаёт общий `unknown`. Это вновь общий bucket для прямого доступа к Astro. +- Публичный обход через штатный Caddy здесь **не доказан**: proxy может очищать входной XFF. Подтверждено отсутствие защиты на уровне API и зависимость от внешнего trust boundary. +- Приёмка: определить допустимые peer/proxy, проверять IP/цепочку, доказать независимость двух клиентов через proxy и игнорирование подделанного XFF на недоверенном входе. + +### R14 · P2 · Registry источников стал зависеть от БД при разборе CLI + +- [cli.py](../apps/api/app/cli.py) вызывает `configured_sources()` при построении argparse `choices`, до разбора команды, включая `--help`. Теперь это открывает внешнее соединение с БД. +- Два прежних scheduler unit-теста падают с connection refused к localhost:5432: helpers перестали быть автономными. +- Приёмка: отделить статический registry от выборки enabled в переданной сессии; `--help` работает без БД; unit-тесты не требуют поднятого PostgreSQL, отдельная интеграционная проверка покрывает конкуренцию. + +### R15 · P1 · Заявленные D04/D06/D07/D08 выполнены не полностью + +- D04: [community_importer.py](../apps/api/app/community_importer.py) добавляет fallback только водоёма по `Waterbody.name_ru`; рыба по-прежнему требует external ID и alias. `review_note` говорит о проверенных алиасах даже при новом name fallback. Нужны единые правила и точное объяснение автоматического соответствия. +- D06: на главной остаётся «Один игрок не может искусственно поднять уверенность». Формула [activity.py](../apps/api/app/activity.py) при 10 отчётах одного игрока с source confidence 100 даёт 72%: 45 + 7 + 20. Ограничения вклада по-прежнему нет. +- D07: замена `last_confirmed_at` на `reported_at` не разделяет все события. [community_review.py](../apps/api/app/community_review.py) всё ещё присваивает `caught_at=published_at`. Неизвестное время улова нельзя выдавать за известное. +- D08: запрос точки теперь ограничен водоёмом, но остаётся top-100; поиск только по spot_id не различает виды рыбы. После R02 нужен адресный агрегат/явный список видов на точке, а не поиск внутри страницы рейтинга. +- Приёмка: fixture-набор с отсутствующим ID, публикацией позже улова, неизвестным временем, одним игроком и несколькими рыбами одной точки; публичные подписи соответствуют фактическим данным. + +## Что полезного уже добавлено + +- Community scheduler подключён к `backend` и `edge` в production Compose (направление T03 верное; egress-проверка текущего стека ещё нужна). +- Публичный `/imports` использует отдельный DTO без диагностических подробностей (T07; добавить regression-проверку полей). +- Введены лимит ответа источника 5 МБ, таймауты POST, отдельные сообщения 429/5xx, счётчик activity total и 503/noindex для рекордов. Это частичные улучшения, не основание закрывать связанные задачи целиком. +- Прежние исправления разделения Astro/API и Basic/Bearer сохранены в коде, но новый невалидный Caddyfile блокирует их применение. + +## План исправления и дальнейших работ + +Рабочие чекбоксы находятся в [ROADMAP](ROADMAP.md#повторная-приёмка-9-сентября-2026). Порядок: R01 → R02 → R03/R04/R05/R11 → R06–R10 → R12–R15. Не нужно перезапускать Docker после каждой правки. + +1. Восстановить deploy и контракт страниц. Адресные offline-тесты + один Caddy adapt. +2. Устранить риски интервала и произвольных HTTP-запросов. Bootstrap перевести на fixtures до его следующего запуска. +3. Закрыть пользовательские/SEO-регрессии и правдивость данных. Затем единая SSR/браузерная матрица с наполненными страницами. +4. Восстановить зелёный Python suite, расширить web tests проверками потребителей API и добавить их в CI. Не подменять исправление регрессий ослаблением assertions. +5. Один изолированный production acceptance: Caddy → Astro → API, форма → модерация → публикация с обязательным источником, неполные сигналы и мониторинг. Без живого scraping. +6. Продолжить незавершённый предыдущий аудит: D05/S02 (полный каталог, lookup по slug, очередь detail URL); V01–V04/U05–U07 (идентичность, токены, плотность карточек, координаты, иконки, визуальная приёмка); S03–S06 (canonical, полезные постоянные страницы, sitemap, performance); T08–T10/D09–D10 (воспроизводимость, нагрузка, release/migration, ограниченные права хранения, история импорта/backoff). +7. После сервера: внешний launch checklist, S07; после обратной связи — V05. Astro/FastAPI/PostgreSQL сохраняются: смена стека эти дефекты не исправит. + +Каждый пункт закрывается только с доказательством своей приёмки, обновлением README при изменении поведения и отдельным коммитом. Старые отметки о завершении относятся к прежним ревизиям, а не автоматически к текущему HEAD. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 704413e..bfca521 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,10 +1,10 @@ # План работ RF4 Spotter -Приоритет после полного аудита: [отчёт 8 сентября 2026](PROJECT_AUDIT_2026-09-08.md). Новый пакет ниже имеет приоритет над историческими чекбоксами; [AUDIT_FIXES.md](AUDIT_FIXES.md) сохраняет историю исправлений. +Приоритет: [повторный аудит 9 сентября 2026](REGRESSION_AUDIT_2026-09-09.md) после сторонних исправлений. Пакет R01–R15 ниже имеет приоритет над [аудитом 8 сентября](PROJECT_AUDIT_2026-09-08.md) и историческими чекбоксами; [AUDIT_FIXES.md](AUDIT_FIXES.md) сохраняет историю исправлений. Этот файл — рабочий источник правды по развитию проекта. После завершения задачи её чекбокс меняется с `[ ]` на `[x]`, рядом добавляется ссылка на коммит или короткое подтверждение проверки. Новые задачи добавляются в соответствующий этап, а не хранятся только в переписке. -Последняя сверка кода, production-конфигурации, UI/UX, визуальной идентичности и SEO: 8 сентября 2026 года (база `486b4e9`). +Последняя сверка изменений кода и production-конфигурации: 9 сентября 2026 года (`c6fdc96..9ae05ef`). Python: 10 failed / 86 passed / 1 skipped; Astro check/build и web unit — успешно; Caddy adapt — ошибка. Визуальная приёмка обновлённого UI ещё не выполнена. Обозначения: @@ -180,7 +180,31 @@ ## Ближайший рабочий пакет -Production-контур требует исправлений до запуска. Доказательства и критерии приёмки каждого ID находятся в [полном аудите](PROJECT_AUDIT_2026-09-08.md). Следующая задача — **T03**. Инфраструктурные изменения можно объединить в один проверочный Compose-прогон. +Production-контур требует исправлений до запуска. Следующая задача — **R01**, затем **R02**. Текущая сборка не считается принятой по результатам проверок прежних коммитов. Старые T/D/U/V/S сохраняются как тематический backlog, а не параллельная очередь дублирующих исправлений. + +### Повторная приёмка 9 сентября 2026 + +Доказательства и критерии приёмки: [повторный аудит](REGRESSION_AUDIT_2026-09-09.md). В этом проходе изменена только документация, исправления ниже ещё не выполнены. + +- [ ] R01 (P0, T05): исправить невалидный Caddyfile, проверить adapt, 413 и маршруты. +- [ ] R02 (P0, U03/D08): согласовать activity envelope со всеми страницами; SSR populated/empty и API regression tests. +- [ ] R03 (P1, D02): восстановить CLI state; атомарная проверка/резервирование, flush, миграция и конкурентные тесты. +- [ ] R04 (P1, D01/D02): исключать disabled из ротации, но сохранять всю историю сайта для cooldown; убрать гонки registry. +- [ ] R05 (P1, T06): изолировать bootstrap-порты/домены и fixture-источники; проверить сценарии через proxy без внешних запросов. +- [ ] R11 (P1, D03): проверять URL до запроса и каждый redirect; единый ключ сайта и ограниченное чтение. +- [ ] R06 (P1, U02): согласовать slug-фильтры полевых сигналов с alias/mapping, сохранить источник и пометки неполноты. +- [ ] R07 (P1, S01): единый HTTP/noindex/cache/JSON-LD контракт ошибок после загрузки данных. +- [ ] R08 (P1, U01): восстановить label/select сетку, selected всех периодов и mobile/no-JS управление. +- [ ] R09 (P1, U03): добавить настоящую пагинацию activity/records/catalogs с сохранением фильтров и честным total. +- [ ] R10 (P1, U04/T05): черновик и фокус для всех ошибок, TimeoutError, безопасный повтор и обработка multipart/413. +- [ ] R12 (P1, T06/D10): мониторить здоровье каждого enabled источника/площадки, stale/running/failed; разбирать JSON статусов. +- [ ] R13 (P1, T04): определить proxy trust boundary, валидировать IP и проверить независимость rate limit двух клиентов. +- [ ] R14 (P2, D01/T08): отделить registry от БД; CLI help и unit-тесты работают без внешнего PostgreSQL. +- [ ] R15 (P1, D04/D06/D07/D08): единые правила alias fallback, честная уверенность/время, адресный агрегат точки и рыбы. +- [ ] Приёмка пакета: зелёный Python suite, web unit в CI, SSR и одна общая desktop/mobile проверка populated/empty/error/long/keyboard/zoom; один изолированный production acceptance без живого scraping. +- [ ] Сверить T03/T07 и остальные частично реализованные старые задачи с критериями; только после проверки перенести отметки `[x]` и обновить README. + +После пакета: D05/S02 → V01–V04/U05–U07 → S03–S06 → T08–T10/D09–D10. Серверные S07 и launch checklist остаются отдельной стадией; смена Astro-стека не планируется. ### P0 — блокеры production