# ACO: приёмочная документация — что программа обязана выдать, когда она сделала свою работу

Дата: 16.10.2026. Срез: `a26bb5a` (ветка `claude/release-1.0-fixes`). Разбор:
[DEEP-ANALYSIS-2026-10-16-SELECTION-MAPS.md](DEEP-ANALYSIS-2026-10-16-SELECTION-MAPS.md). Макеты:
[mockups/plan-2026-10-16-selection/index.html](../mockups/plan-2026-10-16-selection/index.html).

Документ отвечает на один вопрос: **по каким признакам считать, что программа отработала правильно.** Он не про то,
как устроен код, а про то, что должно оказаться на экране, в базе и в готовом отчёте. Каждая проверка написана так,
чтобы её мог выполнить человек, который видит ACO впервые, и чтобы результат нельзя было истолковать двояко.

Как читать статус:

| Знак | Смысл |
|---|---|
| ✓ | выполняется на срезе `a26bb5a`, подтверждено автоматической проверкой или чтением кода |
| ◐ | выполняется частично — в столбце «Сегодня» написано, чем именно отличается |
| ✗ | не выполняется; закрывается фазой из столбца «Фаза» |

---

## 0. Условия прогона

| Что | Значение |
|---|---|
| Среда | прод `aco.24oros.ru` (учётная запись «проверяющий», роль auditor) или свой стенд `docker compose -p aco-<версия>` |
| Объект-образец А | Рязань, 2-комнатная квартира, 48 м², 3-й этаж из 5, 1972 г., кирпич, ипотека |
| Объект-образец Б | село в Рязанской области, жилой дом 96 м² с участком 12 соток |
| Объект-образец В | Москва, 1-комнатная, 38 м², 12-й этаж из 17, 2015 г. (проверка плотного рынка) |
| Данные | вымышленные; настоящие дела оценщиков для приёмки не используются |
| Автоматические проверки | `./scripts/run_tests.sh`, `cd frontend && npm run lint && npm run build && npm run test:e2e` |

Измеренное в этой сессии на `a26bb5a` (эталон для сравнения после правок):

| Проверка | Результат 16.10.2026 |
|---|---|
| Тесты бэкенда | 929 прошли, 7 пропущены, 119 с |
| Сквозные проверки (e2e) | 91 прошла, 2,1 мин |
| Правила кода и типы | 0 замечаний |
| Сборка фронтенда | успешно; карта 287 кБ gzip, воркер 144 кБ, вход 118 кБ |

---

## 1. Что программа обязана выдать на каждом шаге

Это договор о результате: не «кнопка нажалась», а «получено то, ради чего нажимали».

| Шаг оценщика | Что обязано получиться | Чем доказывается |
|---|---|---|
| Создал дело по адресу | адрес распознан до дома, точка дома найдена, на карте виден объект и кольца поиска | Р-1, Р-2 |
| Загрузил документы | поля-кандидаты извлечены и помечены как требующие проверки; первоисточник не подменён | Р-3 |
| Нажал «Подобрать аналоги» | список сопоставимых предложений в выбранном кольце, отсортированный так же, как его увидит оценщик; у каждого — цена 1 м², расстояние и причина, почему он здесь | В-01…В-06 |
| Взял «5 лучших» | пять разных объектов (не одна квартира дважды), ни одного с невыясненной ценой-выбросом | В-07, В-08 |
| Запустил расчёт | итог, устойчивый к удалению любого одного аналога; выбросы отсеяны и показаны; доля одного аналога ≤ 40 % | Ц-01…Ц-05 |
| Открыл «Местоположение» | окружение, расстояние до каждого аналога, схема с подложкой; если подложки нет — об этом сказано | К-01…К-05 |
| Подтвердил и выгрузил | DOCX и PDF, где каждое утверждение о выборке соответствует данным расчёта | Д-01…Д-05 |

---

## 2. Приёмочные проверки

### 2.1. Сбор и отбор объявлений (В)

| № | Что проверяем | Как | Ожидаемый результат | Сегодня | Фаза |
|---|---|---|---|---|---|
| В-01 | Радиус соблюдается | Объект А, кольцо 500 м, «Подобрать» | Ни одного объявления с расстоянием > 500 м в списке «в радиусе». Объявления без точки — отдельной группой с пометкой «расстояние неизвестно» | ✓ (`tests/test_search_radius.py`) | — |
| В-02 | **Порядок по близости** | Объект А. В выдаче найти объявление ближе 300 м с площадью в пределах ±10 % и объявление дальше 3 км | Близкое стоит **выше** дальнего. Порядок с сервера совпадает с порядком на экране | ✗ сервер сортирует без расстояния: дальний за 4,8 км встаёт первым | С1 |
| В-03 | Порядок одинаков на сервере и в браузере | Сравнить порядок в ответе `/directory/market-search` и на экране | Совпадает полностью | ✗ две разные формулы (`_unlikeness` и `methodSimilarity`) | С1 |
| В-04 | Обрезка не теряет лучших | Объект В (плотный рынок, > 100 предложений) | В сохранённых 15 на площадку — те же, что были бы первыми по баллу сходства | ✗ обрезка идёт по формуле без расстояния и цены | С1 |
| В-05 | Глубина у Яндекса хватает | Объект А, кольцо 500 м | В кольце набирается минимум формы (5) либо честно сказано «мало, расширьте кольцо» | ◐ берётся только первая страница каталога (20), порядок — Яндекса | С1 |
| В-06 | Причина видна у каждой строки | Любой объект | У строки: цена 1 м², расстояние, кольцо, ожидаемые поправки; у отклоняющихся — словесный флаг | ◐ цена и расстояние есть; кольцо и причина отбора — нет | С1, С2 |
| В-07 | **Приманка помечена с первой выдачи** | Подложить объявление с ценой 1 м² вдвое ниже медианы | Флаг «цена на N % ниже медианы — проверьте» виден сразу, до того как что-либо собрано; «Взять 5 лучших» его не берёт | ✗ медиана считается по «Собранным»; меньше трёх — флага нет | С2 |
| В-08 | **Одна квартира — одно наблюдение** | Два объявления одной квартиры с Яндекса и Циана | В ленте одна строка с двумя ссылками; «5 лучших» берёт её один раз; при выпуске — предупреждение, если в расчёте обе | ◐ предупреждение есть только в карточках; выпуск не блокирует | С2 |
| В-09 | Допуск площади соответствует типу | Объект Б (участок 12 соток) | Допуск шире, чем у квартиры, и назван на экране | ✗ жёстко ±25 % у подбора и ±30 % у подсказок | С1 |
| В-10 | Отказ площадки не рушит остальные | Отметить 4 площадки без ключа поставщика | Яндекс отработал, остальные — «нужен ключ поставщика»; список не пуст | ✓ | — |
| В-11 | Устаревшие предложения видны | Аналог с датой предложения старше 3 месяцев | Предупреждение при выпуске с номерами аналогов | ✓ (`release_rules.py`) | — |
| В-12 | Ручной ввод не хуже автоматического | Внести 5 аналогов вручную, без подбора | Все проверки выпуска работают так же; «нет ссылки» блокирует | ✓ | — |

### 2.2. Цены и расчёт (Ц)

| № | Что проверяем | Как | Ожидаемый результат | Сегодня | Фаза |
|---|---|---|---|---|---|
| Ц-01 | Итог не держится на одном аналоге | Расчёт на 5 аналогах, открыть таблицу устойчивости | Для каждого аналога показан итог без него; разброс не больше 10 % | ✓ методика 1.6.2 | — |
| Ц-02 | Доля одного аналога ограничена | Аналог без поправок среди прочих | Его доля ≤ 40 %, в данных отчёта записан параметр `max_share` | ✓ | — |
| Ц-03 | Выброс отсеивается и остаётся виден | Аналог с ценой втрое выше прочих, выборка ≥ 4 | Отсеян по Хампелю, но напечатан в таблице с пометкой | ✓ | — |
| Ц-04 | Повтор расчёта даёт то же число | Пересчитать без правок | Итог до рубля тот же; расчёт и перерасчёт идут одной функцией | ✓ 8 эталонов | — |
| Ц-05 | Неизвестное не двигает итог | Оставить у объекта пустым материал стен | Поправка не вводится, программа просит заполнить | ✓ методика 1.6.2 | — |
| Ц-06 | Скидка на торг применена ко всем | Любой расчёт | Торг одинаков у всех аналогов и не входит в вес | ✓ | — |
| Ц-07 | Источник каждой поправки назван | Открыть отчёт, раздел поправок | У каждой — источник: справочник с выходными данными либо честное «до калибровки» | ◐ все пометки «до калибровки» | С6 |
| Ц-08 | Площадь объекта правдоподобна | Ввести площадь 3,2 м² при аналогах 45–55 м² | Выпуск заблокирован проверкой правдоподобия | ✓ (`plausibility.py`) | — |

### 2.3. Карты и местоположение (К)

| № | Что проверяем | Как | Ожидаемый результат | Сегодня | Фаза |
|---|---|---|---|---|---|
| К-01 | Карта открывается | Экран объекта, 1280 и 390 px | Подложка с русскими подписями, объект, кольца; воркер стартовал (в консоли нет ошибок CSP) | ✓ проверено 15.10 на проде | — |
| К-02 | OneMaps недоступен — карта остаётся | Заблокировать `/onemaps/*` | Через ≤ 6 с та же карта с OpenFreeMap; расчёт не страдает | ✓ | — |
| К-03 | Расстояния до аналогов определены | Расчёт с 5 аналогами | Расстояние у каждого; не нашлось — честное «не определено» и имя аналога | ✓ | — |
| К-04 | **Схема без подложки видна оценщику** | Расчёт при загруженном сервере | На экране «схема без подложки» с кнопкой «Обновить карту»; в документе — подпись под рисунком | ✗ признак есть только в API | С4 |
| К-05 | Схема с подложкой успевает | 10 расчётов при load > 50 | Не меньше 9 из 10 — с подложкой | ◐ правка 16.10 спасает раздел, но подложка теряется | С4 |
| К-06 | Повтор не ходит в сеть зря | Пересчитать то же дело | 0 новых запросов слоёв окна к OneMaps (кэш) | ✗ кэша у `window_layers` нет | С4 |
| К-07 | Номера совпадают везде | Схема, таблица аналогов, приложения | Номер 3 — один и тот же объект во всех трёх местах | ✓ | — |
| К-08 | Атрибуция напечатана | Схема в DOCX | «OneMaps (onemaps.ru), © участники OpenStreetMap (ODbL)» | ✓ | — |

### 2.4. Документ (Д)

| № | Что проверяем | Как | Ожидаемый результат | Сегодня | Фаза |
|---|---|---|---|---|---|
| Д-01 | Контроль документа проходит | Выгрузить эталонное дело | 51 из 51 проверки | ✓ | — |
| Д-02 | **Обоснование по местоположению — правда** | Объект Б (село, аналоги по области) | Напечатано фактическое: населённые пункты и расстояния; «тот же населённый пункт» — только когда это так | ✗ печатается всегда один текст | С3 |
| Д-03 | Расстояния в тексте и в таблице сходятся | Любой отчёт | Максимальное расстояние в тексте равно максимуму в таблице источников | ✗ в тексте расстояний нет | С3 |
| Д-04 | Служебных фраз нет | Поиск по документу | Нет «до калибровки», «система не составляет», «заменить» в тексте для заказчика | ✓ (`plausibility.FORBIDDEN_PHRASES`) | — |
| Д-05 | Бета-вид помечен и признан | Выпуск по бета-виду | Признание при подтверждении, запись в журнал; в документе пометки нет | ◐ метки есть, признания нет | П2 |
| Д-06 | PDF получается под нагрузкой | 90-страничный отчёт при load 80 | Готов за ≤ 60 с либо понятное сообщение со сроком | ◐ замер не сделан | П4 |

### 2.5. Эксплуатация (Э)

| № | Что проверяем | Ожидаемый результат | Сегодня |
|---|---|---|---|
| Э-01 | `GET /health`, `/health/ready`, `/health/release` | 200, база доступна, предпроверка без раскрытия секретов | ✓ |
| Э-02 | Доступность снаружи 08–22 | ≥ 99,5 % за 7 дней подряд | ✗ окна по 15 минут около 05/13/21 (П1) |
| Э-03 | Ночная копия | 7 суток без пропуска, проверка восстановления по воскресеньям — OK | ◐ сбой 14.10 не разобран (П1) |
| Э-04 | Оповещения | Сбой доходит в Telegram | ◐ нужен ключ владельца (П1) |
| Э-05 | Откат | `deploy.sh` возвращает прежний образ, данные целы | ✓ |

---

## 3. Связь проверок с находками и фазами

| Находка разбора | Проверки | Фаза | Закрыта, когда |
|---|---|---|---|
| В-1 отбор не знает расстояния и цены | В-02, В-03, В-04, В-05 | С1 | проба из разбора даёт порядок «180 м → 4,8 км → приманка» |
| В-2 две формулы сходства | В-03 | С1 | тест сравнивает порядок сервера и браузера на одном наборе |
| В-3 допуски площади врозь | В-09 | С1 | допуск берётся из формы категории |
| В-4 медиана по «Собранным» | В-07 | С2 | флаг виден на первой выдаче |
| В-5 нулевая поправка без проверки | Д-02, Д-03, К-03 | С3 | текст собирается из расстояний, расхождение ловит проверка |
| В-6 двойники | В-08 | С2 | одно наблюдение в ленте, предупреждение при выпуске |
| В-7 полугодовое окно | В-11 | — | окно и разброс показаны в «Анализе рынка» |
| В-8 коэффициенты без калибровки | Ц-07 | С6 | ни одной пометки «до калибровки» для Рязани и Москвы |
| К-1 схема без подложки молча | К-04 | С4 | видно на экране и подписано в документе |
| К-2 слои не кэшируются | К-06 | С4 | повтор не делает сетевых запросов |
| К-3 нет предела объектов | К-05 | С4 | 9 схем с подложкой из 10 при load > 50 |
| К-4 карта тяжёлая | — | П4 | вход ≤ 80 кБ gzip |
| К-5 бюджет константами | К-03, К-05 | С4 | расстояния не теряются из-за схемы |

---

## 4. Регрессия: что обязано быть зелёным перед каждой выкладкой

```bash
./scripts/run_tests.sh            # ожидание: не меньше 929 прошли, 0 упали
./scripts/run_critical.sh         # ожидание: 3/3
cd frontend && npm run lint       # ожидание: 0 замечаний
cd frontend && npm run build      # ожидание: успешно, check-map-worker: ok
cd frontend && npm run test:e2e   # ожидание: не меньше 91 прошла, 0 упали
```

Новые проверки, которые обязаны появиться вместе с фазами (без них фаза не принимается):

| Файл теста | Что доказывает | Фаза |
|---|---|---|
| `backend/tests/test_listing_rank.py` | каждое слагаемое балла и итоговый порядок на наборе из 6 объявлений | С1 |
| `frontend/e2e/similarity-method.spec.ts` (дополнить) | тот же набор даёт тот же порядок в браузере | С1 |
| `backend/tests/test_market_base.py` (дополнить) | две площадки об одной квартире — одно наблюдение | С2 |
| `backend/tests/test_release_rules.py` (дополнить) | предупреждения: двойник, дальше кольца, цена-выброс | С2, С3 |
| `backend/tests/test_location_partial.py` (дополнить) | `with_layers=false` доходит до API и до подписи в документе | С4 |
| `backend/tests/golden/cases` | эталон «село»: другое обоснование местоположения | С3 |

---

## 5. Протокол прогона

Заполняется при каждой приёмке; строка без подписи и даты не считается.

| Дата | Срез | Среда | В | Ц | К | Д | Э | Кто | Замечания |
|---|---|---|---|---|---|---|---|---|---|
| 16.10.2026 | `a26bb5a` | локально | 4 из 12 | 7 из 8 | 5 из 8 | 2 из 6 | 2 из 5 | сессия разбора | прод живыми запросами не проверялся: защита среды не пропустила |
| | | | | | | | | | |

**Итог на 16.10.2026: 20 проверок из 39 выполняются полностью, 9 — частично.** Больше всего не хватает в сборе аналогов (В) —
именно её закрывают фазы С1–С2, и именно она сильнее всего влияет на число, которое оценщик подписывает.
