From ec743d57e8fd5c0c57dd3789bc4f8022167fb42e Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Mon, 26 Oct 2026 10:20:21 +0400 Subject: [PATCH] =?UTF-8?q?=D0=90=D1=80=D1=85=D0=B8=D0=B2=20OpenSpec:=20hu?= =?UTF-8?q?b-backend-fixes=20(#129)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../.openspec.yaml | 0 .../2026-10-26-hub-backend-fixes}/design.md | 0 .../2026-10-26-hub-backend-fixes}/proposal.md | 0 .../specs/commands/spec.md | 0 .../specs/data-model/spec.md | 0 .../specs/hub-api/spec.md | 0 openspec/specs/commands/spec.md | 49 ++++++++++++++++++ openspec/specs/data-model/spec.md | 12 +++-- openspec/specs/hub-api/spec.md | 50 +++++++++++++++++++ 9 files changed, 107 insertions(+), 4 deletions(-) rename openspec/changes/{hub-backend-fixes => archive/2026-10-26-hub-backend-fixes}/.openspec.yaml (100%) rename openspec/changes/{hub-backend-fixes => archive/2026-10-26-hub-backend-fixes}/design.md (100%) rename openspec/changes/{hub-backend-fixes => archive/2026-10-26-hub-backend-fixes}/proposal.md (100%) rename openspec/changes/{hub-backend-fixes => archive/2026-10-26-hub-backend-fixes}/specs/commands/spec.md (100%) rename openspec/changes/{hub-backend-fixes => archive/2026-10-26-hub-backend-fixes}/specs/data-model/spec.md (100%) rename openspec/changes/{hub-backend-fixes => archive/2026-10-26-hub-backend-fixes}/specs/hub-api/spec.md (100%) diff --git a/openspec/changes/hub-backend-fixes/.openspec.yaml b/openspec/changes/archive/2026-10-26-hub-backend-fixes/.openspec.yaml similarity index 100% rename from openspec/changes/hub-backend-fixes/.openspec.yaml rename to openspec/changes/archive/2026-10-26-hub-backend-fixes/.openspec.yaml diff --git a/openspec/changes/hub-backend-fixes/design.md b/openspec/changes/archive/2026-10-26-hub-backend-fixes/design.md similarity index 100% rename from openspec/changes/hub-backend-fixes/design.md rename to openspec/changes/archive/2026-10-26-hub-backend-fixes/design.md diff --git a/openspec/changes/hub-backend-fixes/proposal.md b/openspec/changes/archive/2026-10-26-hub-backend-fixes/proposal.md similarity index 100% rename from openspec/changes/hub-backend-fixes/proposal.md rename to openspec/changes/archive/2026-10-26-hub-backend-fixes/proposal.md diff --git a/openspec/changes/hub-backend-fixes/specs/commands/spec.md b/openspec/changes/archive/2026-10-26-hub-backend-fixes/specs/commands/spec.md similarity index 100% rename from openspec/changes/hub-backend-fixes/specs/commands/spec.md rename to openspec/changes/archive/2026-10-26-hub-backend-fixes/specs/commands/spec.md diff --git a/openspec/changes/hub-backend-fixes/specs/data-model/spec.md b/openspec/changes/archive/2026-10-26-hub-backend-fixes/specs/data-model/spec.md similarity index 100% rename from openspec/changes/hub-backend-fixes/specs/data-model/spec.md rename to openspec/changes/archive/2026-10-26-hub-backend-fixes/specs/data-model/spec.md diff --git a/openspec/changes/hub-backend-fixes/specs/hub-api/spec.md b/openspec/changes/archive/2026-10-26-hub-backend-fixes/specs/hub-api/spec.md similarity index 100% rename from openspec/changes/hub-backend-fixes/specs/hub-api/spec.md rename to openspec/changes/archive/2026-10-26-hub-backend-fixes/specs/hub-api/spec.md diff --git a/openspec/specs/commands/spec.md b/openspec/specs/commands/spec.md index 32719a9..4aad8c0 100644 --- a/openspec/specs/commands/spec.md +++ b/openspec/specs/commands/spec.md @@ -390,3 +390,52 @@ SHALL перевести её в `confirmed_by_state`: `ensure_fields` — вс #### Scenario: Перенаправление на другой хост - **WHEN** Jira на `GET /secure/attachment/31337/…` ответила 302 на `https://files.example/…` - **THEN** агент туда не ходит, команда `failed` «Jira перенаправила скачивание на files.example» + +### Requirement: Нормализация параметров при постановке +При постановке пакета (`POST /batches`, MCP `batch_create`, повтор команды) хаб SHALL сохранять параметры команд в +каноническом виде. Нормализация SHALL выполняться сразу после проверки типов значений, до остальных проверок: +обязательный параметр, ставший пустым после обрезки, даёт 422 «нет <параметр>»; правила типов, временные ключи +`new:` и проверки `ensure_attachment` работают по нормализованным значениям; элемент списка, ставший пустым, даёт +422 «<параметр> — без пустых элементов». Необязательное поле `ensure_fields` из одних пробелов (`priority`, `duedate`, +`planned_start`, `planned_end`, `epic_key`, `assignee`) даёт 422 «<параметр> — из одних пробелов; для очистки — "" или +null»: после обрезки оно стало бы пустой строкой, которую агент читает как «очистить поле в Jira» (R-8). Строковые параметры обрезаются по краям, кроме текстовых `summary`, +`description`, `body` и `goal`, которые хранятся как заданы. Элементы списков `labels`, `labels_add`, `labels_remove` +и `keys`, а также `target` у `ensure_status`, если это список, обрезаются поэлементно. Числа, логические значения и +`null` не меняются. `filename` у `ensure_attachment` SHALL дополнительно приводиться к нормальной форме Unicode NFC. +Агент SHALL получать параметры в том виде, в каком они сохранены. + +#### Scenario: Ключ с пробелами +- **WHEN** `POST /batches` с `ensure_rank {key: "A-1", before: " A-2 "}` +- **THEN** пакет создан, параметры команды `{key: "A-1", before: "A-2"}`; агент при `claim` получает `before = "A-2"` + +#### Scenario: Текст не обрезается +- **WHEN** `POST /batches` с `ensure_comment {key: " A-1", marker: "Итог ", body: " отступ\n"}` +- **THEN** сохранено `key = "A-1"`, `marker = "Итог"`, `body = " отступ\n"` + +#### Scenario: Временный ключ с пробелами +- **WHEN** пакет `ensure_issue {temp_key: "new:1", …}`, затем `ensure_fields {key: "A-5", epic_key: " new:1 "}` +- **THEN** пакет создан, `epic_key` сохранён как `new:1` и подставляется настоящим ключом после выполнения + `ensure_issue` + +#### Scenario: Ключ из пробелов +- **WHEN** `POST /batches` с `ensure_status {key: " ", target: "Готово"}` +- **THEN** 422 «команда 1 (ensure_status): нет key; ожидаются key, target» + +#### Scenario: Поле из пробелов не очищает Jira +- **WHEN** `POST /batches` с `ensure_fields {key: "A-1", assignee: " "}` +- **THEN** 422 «команда 1 (ensure_fields): assignee — из одних пробелов; для очистки — "" или null», пакет не создан + +#### Scenario: Имя вложения в NFD +- **WHEN** `ensure_attachment` с `filename` «отчёт.pdf» в NFD («е» + U+0308) +- **THEN** сохранён `filename` «отчёт.pdf» в NFC; повторная постановка того же файла с именем в NFC даёт ту же строку + `filename` + +### Requirement: Идентификаторы из цифр — только ASCII +Хаб SHALL считать строку числовым идентификатором (`attachment_id` в `fetch_attachment`, `id` вложения в состоянии +агента, ссылка `#` в MCP, версия агента), только если она непуста и состоит из символов `0-9`. Строка из других +цифр Unicode (`"²"`, `"١٢"`) — не идентификатор: при постановке 422, в состоянии агента 422 `bad_state`, в MCP — +поиск по имени. Ответ 500 MUST NOT возникать. + +#### Scenario: Надстрочная цифра +- **WHEN** `POST /batches` с `fetch_attachment {key: "A-1", attachment_id: "²"}` +- **THEN** 422 «attachment_id — цифры id вложения из issue», пакет не создан diff --git a/openspec/specs/data-model/spec.md b/openspec/specs/data-model/spec.md index 37a78d7..f8fb7d4 100644 --- a/openspec/specs/data-model/spec.md +++ b/openspec/specs/data-model/spec.md @@ -212,10 +212,9 @@ SHALL обнулить `issues_count` и `links_count` её `sync_run` (`started относительный путь и не-строка при записи — 422; агенту не передаётся), `closed_statuses` (список имён статусов Jira, считающихся закрытыми, по умолчанию `["Готово", "Снято"]`; не-список при записи — 422; агенту не передаётся; на существующей базе появляется со значением по умолчанию при открытии хаба). Базовая JQL вычисляется из проектов, доска — свойство проекта (требование -«Проекты»); на новой базе ключей `base_jql` и `board_id` нет. Оставшиеся от v3 ключи хаб SHALL NOT использовать, кроме -передачи `board_id` агенту как запасной доски для команд без доски в параметрах (поставленных до миграции); -Ключи, которые модуль объявил устаревшими (у jira — `status_labels`, `transitions_map`), хаб SHALL удалять при каждом -заполнении умолчаний, не меняя `settings_version`. `GET /settings` SHALL отдавать вычисленную `base_jql` только для чтения, а запись `base_jql` или `board_id` через +«Проекты»); на новой базе ключей `base_jql` и `board_id` нет. Ключи, которые модуль объявил устаревшими (у jira — `status_labels`, `transitions_map`, оставшиеся от v3 +`base_jql` и `board_id`), хаб SHALL удалять при каждом заполнении умолчаний, не меняя `settings_version`; агенту они +не передаются. `GET /settings` SHALL отдавать вычисленную `base_jql` только для чтения, а запись `base_jql` или `board_id` через `PUT /settings` — 422 с пояснением. Спринты агент читает с досок проектов, отдельной настройки имён спринтов нет. Начальные значения SHALL импортироваться из `admin/jira-sync/config.example.json` командой `sdlc-hub init`; пароль и логин Jira в настройках MUST NOT присутствовать. `issue_types` — объект `{task, epic, bug}`, у каждого `{name, id}` (`id` может быть `null`); `init` берёт их из ключей конфига @@ -256,6 +255,11 @@ SHALL обнулить `issues_count` и `links_count` её `sync_run` (`started - **THEN** `GET /settings` не содержит `status_labels` и `transitions_map`; `GET /agent/settings` агенту `jira` их не отдаёт +#### Scenario: Ключи v3 в живой базе +- **WHEN** в `setting` лежат `base_jql` и `board_id` (база, поднятая с v3), хаб открыл базу +- **THEN** ключей `base_jql` и `board_id` в `setting` нет, `settings_version` прежний; `GET /settings` отдаёт вычисленную + `base_jql`, `board_id` в ответе нет; `PUT /settings {board_id: 1}` — 422 «доска Jira — свойство проекта» + ### Requirement: Проекты Таблица `project` SHALL хранить проекты моста — подмножества задач Jira-проекта `project_key`, выделенные меткой: `label` (PK, метка Jira), `name`, `created_at`. Метка SHALL быть непустой строкой до 100 символов без пробельных и diff --git a/openspec/specs/hub-api/spec.md b/openspec/specs/hub-api/spec.md index 9e25466..b4520bb 100644 --- a/openspec/specs/hub-api/spec.md +++ b/openspec/specs/hub-api/spec.md @@ -790,3 +790,53 @@ from_string, to, to_string}`. До записи хаб SHALL проверить #### Scenario: Проект без доски - **WHEN** `GET /agile?project=STOCKS_DRRK`, у проекта нет доски - **THEN** 200, `board = null`, `sprints = []`, `backlog = null`, `board_issues = null` + +### Requirement: Форма тела запроса +На тело неверной формы хаб SHALL отвечать 422, а не 500. В `POST /agent/state` поле `issues` SHALL быть списком, +каждый его элемент — объектом с непустым строковым `key`. Иначе 422 `bad_state` с номером элемента, и пачка не +применяется. В `POST /wiki/sync` поле `project` в теле, если задано, SHALL быть строкой. Иначе 422 `bad_request` +«project — строка». + +#### Scenario: Элемент состояния не объект +- **WHEN** агент шлёт `POST /agent/state {issues: ["A-1"]}` +- **THEN** 422 `bad_state` «issues[0] — объект задачи с key», зеркало не изменилось + +#### Scenario: Проект вики не строкой +- **WHEN** `POST /wiki/sync {project: 5}` +- **THEN** 422 `bad_request` «project — строка», пакет не создан + +### Requirement: Ошибка доступа к файлу хранилища +Если файл хранилища вложений не читается (`PermissionError`, `IsADirectoryError` или другая ошибка ОС), хаб SHALL +обрабатывать его как отсутствующий: при `claim` `ensure_attachment` получает `size = null`, `GET /files/` и +скачивание вложения отвечают 404, как для удалённого файла. В лог хаба SHALL писаться предупреждение с путём и +ошибкой. `FileNotFoundError` в лог не пишется. + +#### Scenario: Нет прав на файл +- **WHEN** у файла `files/` сняты права на чтение, агент берёт `ensure_attachment` с этим `file` +- **THEN** `claim` отвечает 200, у команды `size = null`; в логе — предупреждение с путём + +### Requirement: Пустой SDLC_HUB_HOME +Переменная `SDLC_HUB_HOME`, заданная пустой строкой, SHALL трактоваться как незаданная: каталог данных — +`~/.local/share/sdlc-hub/`, проверка прежнего каталога выполняется. Хаб MUST NOT писать данные в рабочий каталог +процесса из-за пустой переменной. + +#### Scenario: Пустая переменная +- **WHEN** `SDLC_HUB_HOME= sdlc-hub tokens` +- **THEN** база открыта из `~/.local/share/sdlc-hub/hub.sqlite`; в рабочем каталоге не появился `hub.sqlite` + +### Requirement: Восстановление прерванной публикации агента +Перед публикацией версии агента модуля хаб SHALL приводить `release/<модуль>/` к опубликованному состоянию по +`manifest.json`. Если `agent.py.prev` есть, а `agent.py` нет или не совпадает по sha256 с манифестом, тогда как +`agent.py.prev` совпадает, `agent.py.prev` SHALL возвращаться на место `agent.py`. Если `agent.py` совпадает с +манифестом, оставшийся `agent.py.prev` удаляется. В других сочетаниях файлы не трогаются, в лог пишется +предупреждение. Годный `agent.py.prev`, совпадающий с манифестом, MUST NOT перезаписываться другим содержимым. + +#### Scenario: Сбой между переименованием и записью +- **WHEN** опубликована v5, прошлая публикация упала после переименования: `agent.py` нет, `agent.py.prev` — v5, + манифест — v5; новая публикация v6 снова падает при записи +- **THEN** в `release/jira/` — `agent.py` v5 и манифест v5, опубликованной остаётся v5 + +#### Scenario: Сбой поверх неудачного возврата +- **WHEN** `agent.py` — недописанный v6, `agent.py.prev` — v5, манифест — v5; запущена публикация v6 +- **THEN** перед записью `agent.py` восстановлен из `.prev` (v5); при сбое записи опубликованной остаётся v5, а `.prev` + не затёрт недописанным файлом -- 2.47.2