Compare commits

..

No commits in common. "5aebc91f13bda2b6e34a91db471f6f0ddf54be76" and "b975451b537d232ee610ec03fa85abbc25a561c0" have entirely different histories.

3 changed files with 10 additions and 145 deletions

View file

@ -70,32 +70,10 @@ _TABLE_CAPTION_RE = {
def _page_contains_table(text: str, table_no: int) -> bool: def _page_contains_table(text: str, table_no: int) -> bool:
"""Возвращает True если на странице встречается строка «Таблица N». """Возвращает True если текст страницы содержит заголовок таблицы N.
Это поиск подстроки, без разбора контекста. Строка ОГЛАВЛЕНИЯ Требуем явный caption «Таблица N» hint-только режим (оглавление, перекрёстные ссылки)
«Таблица 11 Баланс территории ....... 34» от настоящей подписи не отличается даёт false positive и подавляется. Если caption присутствует достаточно.
падеж тот же, именительный, и вызывающий код ставит found_start=True прямо на
странице содержания, начиная выдирать таблицы оттуда.
Прежняя редакция этой докстроки утверждала обратное будто ложные срабатывания
оглавления и перекрёстных ссылок здесь отсеиваются. Такого кода никогда не было
(#2464). Обещание защиты, которой нет, опаснее её отсутствия: читающий не станет
её добавлять. (Старая формулировка тут намеренно пересказана, а не процитирована:
гейт test_2464_tep_docstring_truth ищет обещание по тексту и не отличил бы цитату
от утверждения.)
Замер 20.08.2026 уточняет и границы проблемы: перекрёстные ссылки в косвенных
падежах регекс НЕ ловит он требует именительное «Таблица N», поэтому
«приведены в таблице 12», «см. Таблицу 12», «табл. 12» дают False. Опасно
ровно оглавление, а не любое упоминание.
Почему подавление не реализовано здесь и сейчас: таблица `ekb_ppt_tep` на проде
пуста (0 строк) URL в `_SEED_DOCS` заглушка, живых PDF нет, эвристику отсева
оглавления не на чем откалибровать. Правило, придуманное без образцов, ловит
ровно те случаи, которые придумали вместе с ним. Условие для реализации хотя
бы один настоящий документ (#1136); целиться следует в признаки оглавления
(точки-выноски, номер страницы в конце строки, несколько подписей на одной
странице), а не в падежи.
""" """
cap = _TABLE_CAPTION_RE[table_no] cap = _TABLE_CAPTION_RE[table_no]
return bool(cap.search(text)) return bool(cap.search(text))
@ -325,7 +303,9 @@ def _parse_table13(raw_rows: list[list[str | None]]) -> list[dict[str, Any]]:
seen: set[tuple[str, str, str, str]] = set() seen: set[tuple[str, str, str, str]] = set()
result: list[dict[str, Any]] = [] result: list[dict[str, Any]] = []
for rec in rows_clean: for rec in rows_clean:
area_key = f"{rec['area_ha_num']:.4f}" if rec["area_ha_num"] is not None else rec["area_ha"] area_key = (
f"{rec['area_ha_num']:.4f}" if rec["area_ha_num"] is not None else rec["area_ha"]
)
key = (rec["phase"], rec["composition"], rec["zone"], area_key) key = (rec["phase"], rec["composition"], rec["zone"], area_key)
if key in seen: if key in seen:
continue continue

View file

@ -22,22 +22,10 @@ logger = logging.getLogger(__name__)
# ── Seed-документы ───────────────────────────────────────────────────────────── # ── Seed-документы ─────────────────────────────────────────────────────────────
# Образец: ППТ 22823 (2018), пояснительная записка. # Образец: ППТ 22823 (2018), пояснительная записка.
# #
# URL — placeholder, ingest скипает с WARNING, метрики остаются {"docs": 0}. # URL — placeholder. Реальный URL пояснительной записки лежит на ГИСОГД ЕКБ
# Прод 20.08.2026: в таблице ekb_ppt_tep 0 строк — то есть загрузчик не отработал # (https://gisogd.ekburg.ru/) под номером проекта планировки, но прямой PDF-линк
# ни разу. # требует ручного поиска через UI (#1136). До тех пор — ingest скипает с
# # WARNING и метрики остаются {"docs": 0}.
# ВАЖНО (#2464): хост `gisogd.ekburg.ru`, названный ниже как место, где «лежит
# реальный URL», НЕ СУЩЕСТВУЕТ — DNS не резолвит его ни с рабочей машины, ни с
# прод-хоста (проверено 20.08.2026). Прежняя редакция этого комментария отправляла
# искать документ вручную на портале, которого нет.
#
# Живой портал ГИСОГД Свердловской области — `gisogd66.midural.ru` (его использует
# загрузчик РНС/РВЭ, см. services/scrapers/gisogd66.py). Раздел 13 там — документы
# по земельному участку; проекты планировки лежат в других разделах, перечисление
# групп доступно через `/api/v1/{schema}/gisogddocgroups/{razdel}`. Перебор
# razdel3/4/5 показал разделы «Генеральный план», «Местные нормативы», «Правила
# землепользования и застройки» — точный раздел ППТ и формат ссылки на PDF
# пояснительной записки ещё предстоит найти (#1136).
# #
# Override-пути для прогона: # Override-пути для прогона:
# 1. Передать в task: ingest_ppt_tep([{"doc_ref": "...", "url": "...", # 1. Передать в task: ingest_ppt_tep([{"doc_ref": "...", "url": "...",

View file

@ -1,103 +0,0 @@
"""Докстрока `_page_contains_table` не обещает защиты, которой нет (#2464).
Прежняя редакция утверждала:
«Требуем явный caption «Таблица N» hint-только режим (оглавление,
перекрёстные ссылки) даёт false positive и подавляется.»
Кода подавления в функции никогда не было это `bool(cap.search(text))`, то есть
обычный поиск подстроки. Строка оглавления «Таблица 11 Баланс территории ..... 34» от
настоящей подписи не отличается: падеж тот же, именительный.
Замер 20.08.2026 уточнил и сам пункт эпика: приведённый в нём пример перекрёстной
ссылки «показатели приведены в таблице 12» регекс НЕ ловит он требует именительное
«Таблица N». Опасно ровно оглавление, а не любое упоминание. Это зафиксировано
характеризующим тестом, чтобы следующая попытка подавления целилась в признаки
оглавления, а не в падежи.
Обещание защиты, которой нет, опаснее её отсутствия: читающий не станет её
добавлять. Тот же класс, что «объявлено исполняется».
Почему подавление не реализовано, а докстрока приведена к коду: таблица
`ekb_ppt_tep` на проде пуста (0 строк) URL в `_SEED_DOCS` заглушка, живых PDF нет,
эвристику отсева оглавления не на чем откалибровать. Правило, придуманное без
образцов, ловит ровно те случаи, которые придумали вместе с ним.
"""
from __future__ import annotations
import inspect
from app.services.scrapers import ekb_ppt_tep_parser as mod
def test_docstring_does_not_promise_suppression_that_is_absent() -> None:
"""Головной: докстрока не должна обещать подавление false positive.
На origin/main она содержит «даёт false positive и подавляется», а в теле
функции один `cap.search(text)`. Красное значение конкретно: обещание есть,
реализации нет.
"""
doc = inspect.getdoc(mod._page_contains_table) or ""
src = inspect.getsource(mod._page_contains_table)
тело = src.split('"""')[-1]
# Ищем именно ОБЕЩАНИЕ («подавляется»), а не разговор о подавлении: новая
# редакция объясняет, почему подавление НЕ реализовано, и не должна краснеть.
обещает = "подавляется" in doc
реализует = any(kw in тело for kw in ("оглавлен", "leader", "count(", "finditer", "rsplit"))
assert not (обещает and not реализует), (
"докстрока обещает подавление false positive, а в теле только поиск "
f"подстроки:\n{тело.strip()}"
)
def test_docstring_names_the_actual_behaviour() -> None:
"""Докстрока обязана честно называть false positive, а не умалчивать о нём.
Просто убрать обещание было бы недостаточно: молчание читается как «всё
хорошо». Ловит «починку» через вычёркивание неудобной фразы.
"""
doc = (inspect.getdoc(mod._page_contains_table) or "").lower()
assert (
"false positive" in doc or "оглавлен" in doc
), "докстрока не предупреждает о ложных срабатываниях на оглавлении"
def test_toc_line_really_is_a_false_positive() -> None:
"""Характеризующий: строка оглавления действительно проходит как подпись.
Зелёный с обеих сторон он фиксирует ФАКТИЧЕСКОЕ поведение, о котором теперь
говорит докстрока. Если кто-то реализует подавление, тест покраснеет и
заставит обновить и докстроку, и его.
"""
оглавление = "СОДЕРЖАНИЕ\nТаблица 11 Баланс территории .......... 34\nТаблица 12 ТЭП .... 36"
assert mod._page_contains_table(оглавление, 11) is True, "оглавление обязано срабатывать"
# А вот перекрёстные ссылки в косвенных падежах регекс НЕ ловит: он требует
# именительное «Таблица N». Пункт эпика приводил «приведены в таблице 12» как
# пример false positive — замер это опроверг, и докстрока теперь говорит точнее.
for мимо in (
"Основные показатели приведены в таблице 12 настоящей записки.",
"см. Таблицу 12 ниже",
"Данные в табл. 12",
):
assert mod._page_contains_table(мимо, 12) is False, f"неожиданно сработало: {мимо!r}"
assert mod._page_contains_table("Никаких таблиц тут нет", 11) is False
def test_seed_comment_does_not_send_to_a_dead_host() -> None:
"""Комментарий сида не должен отправлять искать документ на несуществующем хосте.
`gisogd.ekburg.ru` не резолвится ни с рабочей машины, ни с прод-хоста
(проверено 20.08.2026), а прежний комментарий называл его местом, где «лежит
реальный URL». Живой портал gisogd66.midural.ru.
"""
from pathlib import Path
src = Path(inspect.getsourcefile(mod)).resolve().parents[3]
текст = (src / "app" / "workers" / "tasks" / "ekb_ppt_tep_sync.py").read_text(encoding="utf-8")
assert (
"НЕ СУЩЕСТВУЕТ" in текст or "не резолв" in текст.lower()
), "комментарий сида не предупреждает, что gisogd.ekburg.ru мёртв"
assert "gisogd66.midural.ru" in текст, "не назван живой портал ГИСОГД-СО"