diff --git a/docs/ISSUE6_PHASE1_RESEARCH.md b/docs/ISSUE6_PHASE1_RESEARCH.md new file mode 100644 index 0000000..be6b82d --- /dev/null +++ b/docs/ISSUE6_PHASE1_RESEARCH.md @@ -0,0 +1,291 @@ +# Issue #6 — фаза 1: живые замеры + +Дата замеров: **2026-08-21**. Интерфейс: `https://wordstat.yandex.ru/`, +авторизованный Chrome через CDP `http://127.0.0.1:9223`, фраза +`курсы английского языка`, регион `Россия`. Chrome не закрывался. Это только +исследование: `collector.py`, `wordstat.toml` и код приложения не менялись. + +## Краткий результат + +Блокирующая для потребителя дневная детализация доступна. В текущем +интерфейсе она автоматически выставила окно **22.06.2026—20.08.2026** и +показала сообщение, что дневная статистика доступна только за 60 дней. +Окно в 56 дней целиком входит в это окно. Недельная детализация выдаёт +полные календарные недели понедельник—воскресенье. + +## Подтверждено руками + +### Формат `Период` + +После переключения `Динамика` → `По дням` я скачал именно пункт +`Скачать` → `Таблицу (CSV)`. Файл имеет следующие байтовые свойства: + +* UTF-8; +* BOM `EF BB BF` в начале; +* разделитель полей `;`; +* терминатор строк — одиночный `CR` (`0D`), без `LF`; +* 59 строк всего (заголовок + 58 данных) и 177 разделителей `;`. + +Первые байты после BOM, декодированные UTF-8: + +```text +Дата;Число запросов;Доля от всех запросов, %;Динамика частотности запросов «курсы английского языка», по дням, 22.06.2026 — 19.08.2026, Россия, все устройства\r +22.06.2026;1 680;0,00047;\r +23.06.2026;1 626;0,00047; +``` + +Главное расхождение с DOM: в **файле** поле называется `Дата`, а значение +имеет формат `DD.MM.YYYY`, например `22.06.2026`. Год в выгрузке есть. +DOM показывал локализованное короткое представление `22 июня`, поэтому DOM +нельзя использовать как описание CSV. + +После переключения на `По неделям` я скачал второй CSV тем же способом. Его +байтовые свойства: UTF-8, BOM `EF BB BF`, разделитель `;`, одиночный `CR` +(`0D`) без `LF`, 9 строк всего (заголовок + 8 данных). Файл начинается так: + +```text +Неделя с;Число запросов;Доля от всех запросов, %;Динамика частотности запросов «курсы английского языка», по неделям, 22.06.2026 — 16.08.2026, Россия, все устройства\r +22.06.2026;9 799;0,00042;\r +29.06.2026;8 839;0,00039; +``` + +В **файле** недельное поле называется `Неделя с` и содержит только дату +начала недели в формате `DD.MM.YYYY`; диапазона и номера недели нет. По DOM +та же строка отображалась как диапазон `22 июня 2026 – 28 июня 2026`. + +### Реакция окна на детализацию + +При смене `По месяцам` на `По дням` интерфейс сам изменил окно с месячного на +дневное и показал дословно: + +> Диапазон дат был изменён: статистика «по дням» доступна только для 60 дней. + +Результат: `22.06.2026 — 20.08.2026` (60 календарных дней включительно). +Следовательно, окно из 56 дней доступно целиком. При смене на недели окно +стало `22.06.2026 — 16.08.2026` и появилось сообщение: + +> Диапазон дат был изменён: статистика «по неделям» доступна только для полных календарных недель. + +### Типизация по фактическим значениям + +Фактические значения прогнаны через `src/wordstat/dtypes.py:parse_number`: + +| Значение | Результат | +|---|---:| +| `22 июня` | `NotImplemented` | +| `22 июня 2026 – 28 июня 2026` | `NotImplemented` | +| `1 680` | `1680` (`int`) | +| `0,00047` | `0.00047` (`float`) | +| `9 799` | `9799` (`int`) | +| `0,00042` | `0.00042` (`float`) | + +Дополнительно проверены значения **из скачанных файлов**: `parse_number` +возвращает `NotImplemented` для `22.06.2026` и для обоих заголовков +(`Дата`, `Неделя с`). Это подтверждает, что фактические даты не станут +числами. + +`infer_column(["22 июня", "23 июня"])` и +`infer_column(["22 июня 2026 – 28 июня 2026", "29 июня 2026 – 5 июля 2026"])` +вернули `None`: оба периода остаются строковой колонкой. + +### Селекторы и уникальность + +Проверка выполнялась в консоли страницы через +`document.querySelectorAll(SEL).length`: + +| Контрол | CSS-селектор | count | +|---|---|---:| +| Выпадающий список детализации | `.wordstat__content-type_select > button` | 1 | +| Диапазон дат | `.range-datepicker__selected-dates > button` | 1 | +| Кнопка скачивания | `.save-button` | 1 | + +После раскрытия списка детализации реально видны ровно три опции: `По дням`, +`По неделям`, `По месяцам`. Для строгого гейта интерфейса пригодны первые два +селекторы; селектор кнопки скачивания проверен в том же состоянии. + +### Нижняя граница + +В месячном календаре виден год 2018, а при выборе `Январь 2018` интерфейс +автоматически установил `Январь 2018 — Март 2018` и показал дословно: + +> Диапазон дат был изменён: минимальный период для построения графика с выбранной детализацией — три календарных месяца. + +## Взято из справки, не проверено руками + +Официальная [справка Яндекса по новому интерфейсу Wordstat](https://yandex.ru/support2/wordstat/ru/interface/new) +утверждает: для месячной и недельной детализации диапазон — от трёх месяцев +или трёх недель до пяти лет; самая ранняя дата — январь 2018; для дней — до +60 дней от текущей даты. В ходе этого замера я подтвердил нижнюю границу +январь 2018 и месячный минимум три месяца, но **не подтвердил руками предел +пяти лет**. Поэтому пять лет не следует пока превращать в локальную +валидацию как проверенный факт. + +## Минимальные окна и эксперимент с пятью годами + +### День — подтверждено руками + +При выборе дневной детализации из более короткого недельного окна интерфейс +выставил полный доступный дневной диапазон `22.06.2026 — 20.08.2026` и +показал сообщение про доступность только 60 дней. Попытка выбрать отдельный +день в календаре не завершила второй клик: календарь оставался в состоянии +выбора диапазона. Поэтому это подтверждает **верхнюю границу 60 дней**, но не +минимум одного дня. Минимум дневного окна остаётся ниже непроверенного. + +### Неделя — подтверждено руками частично + +При переходе из дневной детализации в недельную интерфейс автоматически +перевёл окно в `27.07.2026 — 16.08.2026`, то есть ровно три полные недели. +Попытка выбрать одну неделю отдельным вторым кликом не завершилась: выбор +даты в уже выделенном диапазоне не менял окно. Это живое подтверждение +автоматического минимума в наблюдённом переходе, но не самостоятельный +эксперимент с одной неделей. Официальная справка ниже говорит о трёх +неделях; в отчёте это оставлено как справочное подтверждение, а не выдано +за полностью завершённый UI-тест. + +### Пять лет — не подтверждено; причина зафиксирована + +Я выбрал `Январь 2018`: интерфейс автоматически поставил минимум +`Январь 2018 — Март 2018`. Далее при переходе через селектор года popup +закрывался после выбора года, а последующий клик месяца менял начало +текущего диапазона вместо завершения выбора конца. Поэтому получить +однозначное состояние «январь 2018 → позже пяти лет» не удалось; факта о +пределе пяти лет из этого эксперимента нет. + +## Не проверено + +* Минимум дневного окна (отдельный день) и независимый выбор одной недели — + эксперименты не завершились из-за поведения календаря, описанного выше. +* Фактический отказ/усечение при попытке задать месячный диапазон длиннее + пяти лет — UI-эксперимент неоднозначен, доказательства нет. + +## Уточнение о хвосте дневного ряда + +Последний доступный день не является фиксированным лагом сервиса. По +сравнению запросов, переданному после первоначального замера, наблюдались +разные глубины хвоста: иногда в выгрузке есть вчерашний день, иногда +отсутствуют последние четыре дня. Поэтому нельзя зашивать в потребителе или +в collector константу «лаг N дней» и нельзя считать короткий хвост ошибкой. + +Для исходного замера 21.08 заголовок CSV заявлял период до 19.08, а реальные +строки заканчивались 18.08. При дополнительной проверке на том же аккаунте +были сопоставлены 10 общих дат с ранее сохранённой выгрузкой — значения не +изменились. Это не является проверкой задним числом: замеры сделаны в тот же +день и не дают временного среза, по которому можно установить, растут ли +значения уже выгруженных дат позже. Вопрос «отсутствуют последние дни или +присутствуют заниженными» остаётся неустановленным. + +Безопасный контракт для потребителя: хвост и его глубина определяются только +по фактическим строкам файла; последние даты нужно читать из ряда и отрезать +по факту, а не по константе. Пустой хвост делает ряд короче заявленного, но +не создаёт искусственного спада; заниженные значения в присутствующем хвосте +были бы отдельным риском тихой порчи данных и требуют отдельного временного +сравнения. + +## Вывод для фазы 2 + +Дневной и недельный периоды нельзя валидировать как числа по аналогии с +гипотетическим `01.2024`: фактические даты `DD.MM.YYYY` отвергаются строгой +регуляркой чисел и остаются строками. Важно не перепутать DOM и выгрузку: +в DOM дневное значение теряет год, а CSV его содержит; недельный DOM рисует +диапазон, а CSV хранит дату начала. При реализации управляемого окна +нужно поддержать произвольные даты, учитывать автоматическое ограничение +дней 60 и для недель передавать/выбирать полные календарные недели. + +Отдельный подтверждённый факт из текущего дерева: `src/wordstat/dtypes.py` +описывает несуществующий продовый формат `01.2024`, а +`tests/test_dtypes.py` и `tests/test_dataset_io.py` закрепляют его. Реальный +месячный формат — `август 2024`; исправление docstring, фикстуры и регрессии +оставлено фазе 2 и в этот PR не включено. + +## Обновление по итогам фазы 2: недельный период применяется, баг был в нашей проверке готовности + +Первая версия фазы 2 (реализация в PR #21) содержала ошибочный вывод: +«Wordstat игнорирует явно запрошенный недельный период и всегда возвращает +дефолтное окно». Вывод был построен на прогоне, где `date_from`/`date_to` +по факту не передавались (`requested_period: null` в логе того прогона) — +дефолтное окно оказалось дефолтным именно потому, что период не +запрашивался, а не потому что Wordstat его отверг. Автор вывода не +перепроверил формулировку, унаследованную из хендоффа предыдущей сессии, +прежде чем строить на ней запрет валидации. + +Пользователь опроверг это напрямую живым скриншотом UI: `По неделям`, +период `24.12.2018 — 13.01.2019` — явно применённый вручную исторический +период, далеко не дефолтные последние ~2 года. После этого недельный +период был проверен нашим кодом (`_set_period`/`_select_calendar_date`) с +теми же датами через `--resume-dir`-трюк (см. «Метод живой проверки» ниже) +— и тоже не применился, с тем же дефолтным результатом. Это на короткое +время выглядело как подтверждение исходного вывода другим путём, но +причина оказалась не в платформе, а **в нашей же проверке готовности**. + +### Настоящая причина: `_wait_for_table_granularity` проверял формат, не значение + +`_wait_for_table_granularity` (уже существовавшая функция, используемая +после смены грануляции) проверяла только то, что первая ячейка таблицы +*выглядит* как ожидаемая грануляция (регулярка вида «похоже на диапазон +недель»). После смены периода `_set_period` вызывала ту же проверку — но +таблица к этому моменту всё ещё показывала *старые* данные, которые уже +были в правильном недельном формате, поэтому проверка формата +срабатывала мгновенно, ложно сигнализируя готовность. Реальная +перерисовка данных под новый период происходила позже (от долей секунды +до нескольких секунд, по замерам). `_download_current_view` в этом окне +скачивал CSV за прежний (дефолтный) период — молча, без ошибки, ровно тот +«худший исход», которого запрет и пытался избежать, только раньше в +конвейере. + +Живой прогон с добавленным `RegExp(...).test(...)` по значению ячейки (не +только формату) подтвердил: кнопка диапазона дат обновляется мгновенно, +первая строка таблицы — с задержкой, а до тех пор `_wait_for_table_granularity` +уже возвращает «готово». + +### Первая версия фикса содержала свою ошибку: падеж месяца + +Первая версия проверки по значению ячейки сравнивала текст ячейки с +`RUSSIAN_MONTHS` в именительном падеже (`декабрь`) — тем же списком, что +используется для кликов по выпадающему списку месяца в календаре. Но +первая ячейка дневного/недельного ряда использует **родительный** падеж +(`22 июня`, `24 декабря` — «числа июня», «числа декабря»); только +месячная грануляция (`август 2024`) — именительный. `"24 декабря +2018".startsWith("24 декабрь 2018")` никогда не возвращает `true`, так что +первая версия проверки не ловила ложное готово-состояние раньше, а просто +ждала полный таймаут (у которого не было условия совпасть) на каждом +запросе с явным периодом — маскируясь под «Wordstat не успевает». +Добавлен отдельный список `RUSSIAN_MONTHS_GENITIVE`; сравнение теперь +точное (день, месяц и год, а не только день и год — иначе «тот же день, +другой месяц» молча проходил бы проверку). + +### Подтверждено живым прогоном: `requested_period` применяется + +* **Weekly**, запрошено `2018-12-24 — 2019-01-13`: `actual_period` + `24.12.2018 — 07.01.2019`. `07.01.2019` — начало последней полной + недели, заканчивающейся `13.01.2019` (поле `Неделя с` хранит начало + недели, не диапазон) — период применился точно, без усечения, 3 полные + недели. +* **Daily**, запрошено `2026-07-22 — 2026-08-20`: `actual_period` + `22.07.2026 — 19.08.2026`, 29 из 30 строк — тот же вариативный хвост + дневного ряда, что уже задокументирован выше в этом документе, не новый + дефект. + +Запрет явного недельного периода из первой версии фазы 2 полностью +отменён (revert коммита в PR #21) вместе с последовавшим за ним удалением +«мёртвого» кода недельного пикера — тот код не был мёртвым, а +недоделанным. + +### Метод живой проверки + +Полный CLI-прогон по-прежнему недоступен для этой проверки: `top_popular` +пуст всегда (issue #11), и fail-closed на пустых top-экспортах (PR #19) +корректно останавливает прогон до `dynamics`. Проверка велась напрямую +через `WordstatCollector.collect_many` с `--resume-dir`, где +`top_popular`/`top_related`/`regions` заранее отмечены собранными в +manifest (с заглушечными `.parquet`), так что реально у живого Wordstat +запрашивался только вид `dynamics` — тот же метод, что и в исходном +прогоне фазы 2. + +### Урок для дальнейшей работы + +Кнопка/DOM-индикатор выбора (дата в контроле, активный radio) и +фактическое содержимое таблицы/экспорта могут расходиться во времени на +живом Wordstat — это не первый такой случай в этом проекте (issue #3, тот +же класс проблемы для переключения вида вместо периода). Любая новая +проверка готовности должна сверяться с реальным значением в DOM, а не +только с его форматом или с состоянием отдельного контрола.