Andrey Epifancev e786428a1d Спека: пакет C бэкенда хаба hub-backend-fixes (#129)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 02:06:19 +04:00

9.3 KiB
Raw Blame History

Context

Пакет C сверки follow-ups 25.10.2026 (sdlc-hub#129). Все правки — в Python-коде хаба, без смены схемы базы и контракта с агентами. Владелец пропустил вопросы brainstorming («не сталкивался и не хочу»): решения ниже приняты агентом и продублированы в «Decisions made without discussion».

Decisions

  1. Нормализация параметров — один шаг в validate. После всех проверок validate кладёт в пакет параметры, приведённые функцией normalize(ctype, params) -> dict (modules/jira/commands.py). Проверки на пустоту (.strip()) остаются как есть. Обрезаются строковые параметры всех типов, кроме текстовых summary, description, body, goal. Это ключи, temp_key, sprint, rename_to, move_open_to, link_type, assignee, priority, даты, marker, attachment_id, file, filename, issue_type, target (строка или элементы списка). Элементы списков labels, labels_add, labels_remove, keys тоже обрезаются. Числа, логические значения и null не меняются. Нормализация возвращает новый словарь и не меняет вход.
  2. filename — NFC. normalize приводит filename у ensure_attachment к unicodedata.normalize("NFC", …). MCP подставляет имя по умолчанию из Path(path).name и тоже приводит его к NFC; на macOS имена из пути приходят в NFD. Поиск вложения по имени в MCP-инструменте attachment сравнивает имена в NFC с обеих сторон.
  3. Проверки hooks.check_batch идут по нормализованным параметрам. validate вызывается раньше, поэтому filename с пробелами по краям проверяется уже обрезанным.
  4. ASCII-цифры. В core/http.py (рядом с int_group) появляется функция is_digits(s) -> bool: s — непустая строка только из 0-9 (s.isascii() and s.isdigit()). Модуль jira использует её вместо str.isdigit() везде, где за проверкой идёт int() или где строка — идентификатор: hooks.py (attachment_id), mirror.py (check_attachments), attachments.py (сортировка), fetch.py (версия агента), mcp.py (ссылка #<id>). В разборе дат history.py и mcp.py значение уже прошло форматную проверку, но для единообразия там тоже is_digits.
  5. Форма элементов состояния. mirror.check_issues(issues) вызывается до check_comments и check_attachments. Она требует: issues — список (иначе сейчас 500 на итерации), каждый элемент — объект с непустым строковым key. Иначе ValueError с номером элемента. Обработчик POST /agent/state уже превращает ValueError в 422 bad_state, как для комментариев.
  6. POST /wiki/sync. Если project в теле задан и это не строка, ответ 422 bad_request «project — строка» до обращения к проектам.
  7. Файлы хранилища. files.size и files.read ловят OSError. FileNotFoundError по-прежнему тихо даёт None. Остальные ошибки тоже дают None, но пишут в лог предупреждение с путём и ошибкой. Дальше всё идёт по существующему пути «файла нет»: size = null при claim, 404 при скачивании.
  8. SDLC_HUB_HOME="". paths.home() берёт os.environ.get("SDLC_HUB_HOME") or <по умолчанию>. Пустая строка равна незаданной переменной; home_problem/home_warning уже считают так.
  9. Восстановление прерванной публикации. В начале release.publish, до проверки версии, вызывается _recover(rd). Если agent.py.prev есть, а agent.py нет или его sha256 не совпадает с манифестом, тогда как sha256 agent.py.prev совпадает, agent.py.prev возвращается на место agent.py с предупреждением в лог. Если agent.py совпадает с манифестом, а .prev лежит, .prev удаляется. Во всех остальных случаях ничего не трогается, а в лог пишется предупреждение. Так закрываются T2.2 и M-6: после _recover в agent.py лежит опубликованная версия, и os.replace(agent, prev) не затирает годный .prev.
  10. Устаревшие ключи настроек. base_jql и board_id добавляются в modules/jira/settings.OBSOLETE. ensure_defaults удаляет их при каждом открытии, не меняя settings_version, как status_labels. Запись через PUT /settings по-прежнему даёт 422 с пояснением. Агенту board_id давно не передаётся: его нет в AGENT_KEYS.

Risks / Trade-offs

  • Нормализация меняет то, что раньше уходило агенту дословно. Пробелы по краям идентификатора в Jira не значимы, а текстовые поля не трогаются.
  • _recover срабатывает только при совпадении sha256 с манифестом, поэтому неизвестное содержимое не перезаписывается.
  • Удаление board_id из setting необратимо, но значение нигде не читается. Копия базы есть в backups/.

Decisions made without discussion

  • R-1 (proposal, 26.10.2026): O-3 (check_confirmed считает обратную связь симметричного типа конфликтом) не входит в пакет и переносится в пакет B. Хаб не знает, какие типы симметричны: это знает агент по /issueLinkType. Ручная настройка списка типов в хабе отвергнута, она дублировала бы Jira. Цена ошибки: при потерянном отчёте команда ensure_link к «связанной» задаче, связанной в обратную сторону, подтверждается не по состоянию, а довыдачей агенту, и агент отвечает existed. Лишний вызов Jira, данные не страдают.
  • R-2 (design #1): обрезаются все строковые параметры, кроме summary, description, body, goal. Обоснование: пробелы по краям идентификатора всегда ошибка ввода, а в тексте могут быть значимы. Цена ошибки: тема с пробелами по краям уходит в Jira как есть, Jira обрезает её сама.
  • R-3 (design #2): имя вложения приводится к NFC, а не к NFD. NFC — форма, в которой имена приходят из Jira и набираются с клавиатуры. Цена ошибки: имя вложения в Jira будет в NFC, визуально то же самое.
  • R-4 (design #7): ошибка доступа к файлу хранилища трактуется как «файла нет», а не как отдельная ошибка API. Обоснование: путь «файла нет» уже понятен агенту и клиентам. Цена ошибки: причина видна только в логе хаба.
  • R-5 (design #9): _recover чинит только состояния, однозначно определяемые по sha256 манифеста. Остальные оставляет как есть с предупреждением в лог. Цена ошибки: в редком сочетании сбоев владелец публикует ещё раз.
  • R-6 (design #10): base_jql и board_id удаляются через механизм устаревших ключей, без новой версии схемы. Это тот же путь, что у status_labels, и при каждом старте ключи, вставленные старым процессом, убираются снова. Цена ошибки: нет.