Andrey Epifancev 98faabad67 Спека: рулинги R-8, R-9, R-10 по итогам финального ревью (#129)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 03:10:20 +04:00

13 KiB
Raw Blame History

Context

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

Decisions

  1. Нормализация параметров — один шаг в validate. Сразу после проверки типов значений (_type_error) validate заменяет параметры результатом normalize(ctype, params) -> dict (modules/jira/commands.py), и все дальнейшие проверки (обязательные параметры, _rule_error, временные ключи, повтор темы) идут по нормализованным значениям. Иначе epic_key: " new:1 " прошёл бы как постоянный ключ и стал временным уже после проверки объявления, а ключ из пробелов прошёл бы проверку на пустоту. Обязательный параметр, ставший пустым, даёт ту же ошибку «нет <параметр>», пустой элемент списка — «<параметр> — без пустых элементов». Существующие .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, и при каждом старте ключи, вставленные старым процессом, убираются снова. Цена ошибки: нет.
  • R-7 (design #7, 26.10.2026): files.size для каталога на месте файла возвращает None (проверка S_ISREG), а не размер каталога. План предписывал только except OSError, но Path.stat() на каталоге не бросает исключение, и claim отдал бы агенту размер каталога. Решение согласовано с files.exists (is_file). Цена ошибки: нет, поведение совпадает с целью design #7 («файла нет»); read для каталога пишет предупреждение, size молчит (T4.1).
  • R-8 (design #1, 26.10.2026; финальное ревью I-1): необязательный параметр ensure_fields (поля FIELD_PARAMS: assignee, epic_key, priority, даты, оценки), заданный строкой из одних пробелов, отвергается с 422 «<параметр> — из одних пробелов; для очистки — "" или null». Иначе обрезка превращала бы его в "", а агент читает "" как «очистить поле в Jira». Для before/after/move_open_to пробелы по-прежнему означают «не задано». Цена ошибки: ввод из одних пробелов отвергается хабом, а не Jira; очистка полей работает как раньше через "" или null.
  • R-9 (design #7, уточняет R-7; финальное ревью I-3): files.size открывает файл и берёт размер через fstat, _intact ловит OSError. Причина: Path.stat() не требует права на чтение, и chmod 0 давал настоящий размер при read → None; повторная загрузка того же файла давала 500. Теперь size для нечитаемого файла и каталога — None с предупреждением, put перезаписывает нечитаемый файл. Цена ошибки: лишнее открытие файла при claim.
  • R-10 (design #10; финальное ревью M-3, T6.2): на базе v7 с ключами v3 migrate_v7 удаляет base_jql и board_id и поднимает settings_version; неразобранный base_jql теряется вместе с ключом (значение остаётся в предупреждении migrate_v3 и в backups/schema-v3). Цена ошибки: потеря неразобранного значения, которое нигде не читается.