ПТИЦА: у изъятий на участке появляется номер постановления, а таблица не удваивается #3568
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#3568
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/ptica-reservation-act-number"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Refs #2982
Что было
На проде в
land_reservation27 строк, иact_numberпуст у всех 27. Замер 17.09.2026, только SELECT: все строки из источникаizyatie_ekb_ocr, 5 документов,count(DISTINCT cad_num) = 27.Номера в документах есть, но регекс их не видел. Он требовал суффикс областного акта:
У постановлений Администрации Екатеринбурга суффикса нет: № 1413, 259, 2687, 863, 1504. Тот же обязательный суффикс стоял во втором месте, в
page_reservation_parser.py(об этом комментарий от 28.08).Что сделано
1.
izyatie_ocr.py: номер берётся у того же акта, что и дата. Просто сделать суффикс необязательным недостаточно. Первое «№» в теле документа относится к чужому акту: там стоят «Решение Думы … № 60/1» (Генплан) и «Приказ Министерства … № 746-П». Для даты это уже решено в #2981: берётся первое «от DD.MM.YYYY», перед которым стоит слово «постановлени». Теперь номер берётся из «№» вплотную после этой даты, а если там его нет, то вплотную перед ней («Постановление № 509-ПП от …»). Если рядом с датой номера нет, в поле пишется None, а не номер соседнего документа. Эту связку проверил мутацией. Если заменить её на «первое подходящее №», на двух прод-фрагментах вместо1413и259получается746-П.2.
page_reservation_parser.py(pravo.gov66, ручной запуск): суффикс стал необязательным. Раньше номер без суффикса в шапке пропускался, и бралось следующее «№ …-ПП», то есть номер цитируемого постановления. Номер с суффиксом не из списка теперь отбрасывается целиком, а не обрезается: из «№ 218-ФЗ», «№ 746-П» и «№ 60/1» не получится ни «218», ни «746», ни «60».3.
izyatie_ocr_ingest.py: прогон не удваивает таблицу.act_numberвходит в ключUNIQUE NULLS NOT DISTINCT (cad_num, act_number). Поэтому строка (участок, «1413») не конфликтует со строкой (участок, NULL). Без очистки пятничный прогон положил бы 27 строк с номером рядом с 27 старыми, иreservation_lookupпоказывал бы каждое изъятие дважды. Теперь в том же SAVEPOINT перед записью выполняетсяDELETE … WHERE cad_num = :cad AND doc_url = :doc AND act_number IS DISTINCT FROM :act. Почему условие именно такое: один документ содержит один акт (номер разбирается один раз на весь PDF), значит строка с тем же (участок, документ) и другим номером — это старый разбор той же записи. Участок из другого документа не затрагивается, это проверено тестом и мутацией.Единица учёта остаётся (участок, акт), как объявлено в м.136 («1 строка = 1 кад-номер в 1 постановлении»).
is_activeне трогал. На текущих данных смысл таблицы не меняется: у 27 участков по одному документу, так что после прогона строк останется 27. Миграции нет: старые строки исправит сам прогон.Тесты
tests/services/scrapers/test_2982_act_number.py, 14 тестов. Пять дословных OCR-фрагментов прод-документов изtest_2464_act_date_citationпроходят черезextract_izyatie_records. Ожидаемые пары: (1413, 2022-05-27), (259, 2020-02-12), (2687, 2015-09-29), (863, 2019-04-19), (1504, 2019-06-24). Те же фрагменты проходят черезextract_reservations. Контроли: одни цитаты дают None; номер привязан к дате; собственный номер в шапке важнее цитаты «№ 100-ПП»; «509-ПП» по-прежнему извлекается.tests/sql/test_2982_land_reservation_reparse.py, 3 теста. Это_upsert_recordsна временной таблице с тем же констрейнтом. Состояние как на проде (NULL) плюс прогон с номером дают одну строку, и повторный прогон идемпотентен. Участок в другом документе остаётся. Соседний участок того же документа остаётся.TEST_DATABASE_URL):test_2982_*,test_2464_*(3 файла),test_izyatie_ocr,test_page_reservation_parser,test_reservation_ingest,test_reservation_lookup: 114 passed, rc=0 (после rebase на9cc23c66).backend:uv run python -m pytest tests/ -q --ignore=tests/smoke: 5046 passed, 87 skipped, 4 failed, rc=1. Все 4 падения — известныеtests/ops/test_2203_backup_trailer_grep_dashdash.pyна macOS (BSD mktemp), к изменению отношения не имеют.uv run ruff check: All checks passed.ruff format --checkпо изменённым файлам чистый.Фальсификация
A. Парсеры откатил до origin/main (
git show origin/main:… >оба файла), затем восстановил и сверил черезdiff -q:(Зелёными остались 2 контроля, которые и на main верны: «одни цитаты дают None» и «509-ПП».)
B. Убрал привязку номера к дате (в теле ищется первое подходящее «№»):
C. Убрал
DELETEиз_upsert_records:D. Расширил
DELETE, убрав условие поdoc_url:Чего здесь НЕТ (честно)
rus, а исполнять код в прод-контейнере мне нельзя. Тесты идут по дословным OCR-фрагментам, которые собрали в #2981 тем же трактом (ocr_pdf_textв прод-контейнере). Поэтому PR ссылается на issue без автозакрытия: issue закрывается после приёмки на проде (см. ниже).page_reservation_parser._parse_act_dateостался дефект #2981: берётся первое «от DD.MM.YYYY» без контекста. На тех же фрагментах он даёт 2004-07-06 (Генплан). Загрузчик pravo.gov66 запускается вручную и на проде строк не дал, поэтому здесь я это не чинил.Деплой
worker, а его деплой пересоздаёт отдельным guard-блоком (#3029). Если вkn_scrape_runsилиobjective_scrape_runsесть свежие прогоны соstatus='running', пересоздание worker пропускается (в логе деплоя будетWORKER RECREATE SKIPPED), и пятничный прогон пойдёт на старом коде.docker exec gendesign-worker-1 grep -c _DELETE_STALE_PARSE_SQL /app/app/workers/tasks/izyatie_ocr_ingest.pyдолжен вернуть ≥ 1 (в старом коде этого имени нет). Если grep возвращает 0, нужен ручной recreate worker после окончания прогонов.izyatie-ocr-ingest-weekly: OCR пяти PDF занимает несколько минут.Приёмка на проде
Срок: после первого пятничного прогона
izyatie-ocr-ingest-weeklyпосле деплоя. Это 18.09.2026 07:00 МСК, если деплой будет до этого времени, иначе 25.09.2026. Ручной запуск таски — решение владельца.Как читать результат:
count(*) > 27значит, что очистка не сработала.count(act_number) < 27значит, что настоящий OCR расходится с фрагментами. Обе величины 27, но max(fetched_at) всё ещё 07.06: прогон не состоялся, и нули ничего не доказывают. В логахgendesign-worker-1за время прогона должны быть строкиextract_izyatie_records: … (act=1413)и так далее.🤖 Generated with Claude Code