78 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 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`, и при каждом старте ключи, вставленные старым процессом, убираются снова.
Цена ошибки: нет.