[SF-v2 #961] 961-C3: Персист + версионирование прогонов #994

Closed
opened 2026-06-02 19:25:56 +00:00 by lekss361 · 3 comments
Owner

Часть EPIC #961 (Site Finder v2 / GG-форсайт). Декомпозиция критпути — ground-truth (прод-БД + код) + web-research, 2026-06-02.

Scope: Писать строку в analysis_runs на каждый /analyze; добавить GET /parcels/{cad}/runs и GET /runs/{id}.

Файлы: parcels.py L2505 (после сборки) + new backend/app/services/analysis_runs.py

DoD:

  • каждый /analyze пишет строку (SAVEPOINT-safe); 2 GET-эндпоинта; schema_version const

Зависит от: 961-C1, 961-C2
Effort: M

**Часть EPIC #961 (Site Finder v2 / GG-форсайт).** Декомпозиция критпути — ground-truth (прод-БД + код) + web-research, 2026-06-02. **Scope:** Писать строку в analysis_runs на каждый /analyze; добавить GET /parcels/{cad}/runs и GET /runs/{id}. **Файлы:** parcels.py L2505 (после сборки) + new backend/app/services/analysis_runs.py **DoD:** - каждый /analyze пишет строку (SAVEPOINT-safe); 2 GET-эндпоинта; schema_version const **Зависит от:** 961-C1, 961-C2 **Effort:** M
lekss361 added this to the Site Finder v2 — Foresight (GG-форсайт) milestone 2026-06-02 19:26:01 +00:00
lekss361 added the
GG-форсайт
scope/backend
site-finder
labels 2026-06-02 19:26:02 +00:00
lekss361 reopened this issue 2026-06-03 17:59:37 +00:00
Author
Owner

🟡 Reopen (verify) — фикс #1029 безопасен, но его root-cause описан неверно → реальная причина прод-500 могла остаться незакрытой. Валидация #1028 / #1029.

Несоответствие. #1029 заменил parcels.py:2672 result_payload["district"]["district_name"].get("district_name") и описал причину как «KeyError из district-dict без ключа (частичный district-lookup)». Но: result_payload["district"] = dict(district_row) if district_row else None (parcels.py:2550), а district_row приходит из запроса, который всегда селектит d.district_name (parcels.py:1331). → ключ district_name всегда присутствует (значение может быть NULL), исходный ["district_name"] вернул бы None, а не кинул KeyError на этом пути. Случай district_row is None уже прикрыт isinstance(...dict).

Вывод: либо фикс закрыл несуществующий триггер (и реальная причина живого 500 всё ещё открыта), либо описание просто mislabeled. Рекомендация: сверить с оригинальным GlitchTip-трейсом 500 ДО окончательного закрытия #994.

Сам код-фикс (.get() + lazy WeasyPrint import + revival by-bbox тестов) корректен и безопасен. Персист #1028 (SAVEPOINT-safe, CAST(:x AS …), never-run→NULL) — чистый. Миграция #1027 / view v_analysis_runs_latest — идемпотентны, deployed-схема совпадает byte-for-byte.
Прим.: #1033/#1034 фиксят отдельный pre-existing класс NULL-geometry 500 (cadastre_fetch.py, ~964 строки с NULL geom) — не связаны с персист-PR.

🟡 **Reopen (verify) — фикс #1029 безопасен, но его root-cause описан неверно → реальная причина прод-500 могла остаться незакрытой.** Валидация #1028 / #1029. **Несоответствие.** #1029 заменил `parcels.py:2672` `result_payload["district"]["district_name"]` → `.get("district_name")` и описал причину как «KeyError из district-dict без ключа (частичный district-lookup)». Но: `result_payload["district"] = dict(district_row) if district_row else None` (`parcels.py:2550`), а `district_row` приходит из запроса, который **всегда** селектит `d.district_name` (`parcels.py:1331`). → ключ `district_name` всегда присутствует (значение может быть NULL), исходный `["district_name"]` вернул бы `None`, а **не кинул KeyError** на этом пути. Случай `district_row is None` уже прикрыт `isinstance(...dict)`. Вывод: либо фикс закрыл несуществующий триггер (и реальная причина живого 500 всё ещё открыта), либо описание просто mislabeled. **Рекомендация:** сверить с оригинальным GlitchTip-трейсом 500 ДО окончательного закрытия #994. _Сам код-фикс (`.get()` + lazy WeasyPrint import + revival by-bbox тестов) корректен и безопасен. Персист #1028 (SAVEPOINT-safe, CAST(:x AS …), never-run→NULL) — чистый. Миграция #1027 / view `v_analysis_runs_latest` — идемпотентны, deployed-схема совпадает byte-for-byte._ _Прим.: #1033/#1034 фиксят отдельный pre-existing класс NULL-geometry 500 (`cadastre_fetch.py`, ~964 строки с NULL geom) — не связаны с персист-PR._
Collaborator

VERIFY result — you were right about the #1029 attribution.

#1029's commit/vault described the prod-500 cause as result['district']['district_name'] KeyError — WRONG, as you noted: district_name is always SELECTed, so that key can't KeyError on prod. The KeyError #1029 'fixed' only fired on a TEST mock (district dict missing the key). #1029 is harmless but did NOT fix a prod-500.

REAL prod-500 cause (found via live traceback this session): TypeError float() not NoneType at parcels.py centroid — ST_Centroid(NULL) on ~964 NULL-geometry parcels. Fixed + verified: #1033 (route NULL-geom to graceful fetch + value-guard) + #1034 (cad_exists_in_db requires non-NULL geom). Prod re-check: 66:41:0309091:40 500->202; batch zero-500s.

Cross-check done: prod backend logs last 2h have NO other analyze 500/traceback class. Real cause closed. Will correct the #1029 vault attribution.

VERIFY result — you were right about the #1029 attribution. #1029's commit/vault described the prod-500 cause as result['district']['district_name'] KeyError — WRONG, as you noted: district_name is always SELECTed, so that key can't KeyError on prod. The KeyError #1029 'fixed' only fired on a TEST mock (district dict missing the key). #1029 is harmless but did NOT fix a prod-500. REAL prod-500 cause (found via live traceback this session): TypeError float() not NoneType at parcels.py centroid — ST_Centroid(NULL) on ~964 NULL-geometry parcels. Fixed + verified: #1033 (route NULL-geom to graceful fetch + value-guard) + #1034 (cad_exists_in_db requires non-NULL geom). Prod re-check: 66:41:0309091:40 500->202; batch zero-500s. Cross-check done: prod backend logs last 2h have NO other analyze 500/traceback class. Real cause closed. Will correct the #1029 vault attribution.
Collaborator

Статус (триаж 2026-06-06): ЧАСТИЧНО. Персист есть — /analyze пишет в analysis_runs (persist_analysis_run, schema_version='1.0', SAVEPOINT-safe) + GET /{cad}/forecast читает последний §22-ран. 🔴 Нет 2 read-эндпоинтов из DoD: GET /parcels/{cad}/runs и GET /runs/{id}. Остаётся: эти 2 эндпоинта. Объём S. Оставляю open.

**Статус (триаж 2026-06-06): ЧАСТИЧНО.** ✅ Персист есть — `/analyze` пишет в `analysis_runs` (`persist_analysis_run`, `schema_version='1.0'`, SAVEPOINT-safe) + `GET /{cad}/forecast` читает последний §22-ран. 🔴 Нет 2 read-эндпоинтов из DoD: `GET /parcels/{cad}/runs` и `GET /runs/{id}`. Остаётся: эти 2 эндпоинта. Объём S. Оставляю open.
Sign in to join this conversation.
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#994
No description provided.