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