Переименование jira-hub → sdlc-hub (#97) #99
2
openspec/changes/sdlc-rename/.openspec.yaml
Normal file
2
openspec/changes/sdlc-rename/.openspec.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-24
|
||||
103
openspec/changes/sdlc-rename/design.md
Normal file
103
openspec/changes/sdlc-rename/design.md
Normal file
@ -0,0 +1,103 @@
|
||||
## Context
|
||||
|
||||
`main` bb54d0a: схема v7, модули `jira` и `wiki`, агенты jira v14 и wiki v1. Имя `jira-hub` живёт в:
|
||||
|
||||
- **коде:** пакет `src/jira_hub/` (≈30 файлов импортируют `jira_hub`), `pyproject.toml` (дистрибутив и скрипт
|
||||
`jira-hub`), `core/paths.py` (`JIRA_HUB_HOME`, `~/.local/share/jira-hub`), `core/cli.py` (`prog`, модуль MCP по
|
||||
умолчанию `jira`), `core/mcp.py` (`mcp_json`: серверы `jira-hub`/`jira-hub-<модуль>`), `FastMCP("jira-hub")` и
|
||||
`FastMCP("jira-hub-wiki")`, подсказки «jira-hub serve», `api.py` (текст «make web в каталоге jira-hub», строка запуска);
|
||||
- **тестах:** `JIRA_HUB_HOME` в фикстурах, эталон `tests/fixtures/baseline/mcp_wiki_tools.expected.json`, проверки имён
|
||||
серверов и фрагментов `.mcp.json`, пути `src/jira_hub` в `test_layout`, `test_registry`, `test_web_app`,
|
||||
`test_wiki_projects`;
|
||||
- **вебе:** `<title>Хаб Jira</title>`, бренд в `shell/Sidebar.tsx`, ключи `jira-hub-token|sidebar|legend`,
|
||||
`JIRA_HUB_URL` в `vite.config.ts`, `web/scripts/test-hub.sh`, `package.json` (`jira-hub-web`), подсказка входа;
|
||||
- **документации:** README (launchd, MCP, выкатка), CLAUDE.md, ARCHITECTURE.md, Makefile, `openspec/specs/` (18
|
||||
требований в 7 спеках);
|
||||
- **проектах-потребителях** (не git): `awp-ca` — `.mcp.json`, `enabledMcpjsonServers` в `.claude/settings.local.json`,
|
||||
`CLAUDE.md`, скилл `building-customer-error-report` (`mcp__jira-hub__*`); `stocks_drrk` — `.mcp.json`,
|
||||
`enabledMcpjsonServers`;
|
||||
- **живой установке:** launchd `ru.aepif.jira-hub` (`uv run --project …/jira-bridge/jira-hub jira-hub serve`, лог
|
||||
`~/Library/Logs/jira-hub.log`), данные `~/.local/share/jira-hub/`, `.mcp.json` в `~/projects/awp-ca` и
|
||||
`~/projects/stocks_drrk` (серверы `jira-hub`, `jira-hub-wiki`), user scope MCP `jira-hub`, репозиторий Gitea
|
||||
`aep/jira-hub`, checkout `~/projects/jira-bridge/jira-hub` с worktree в `.wt/`;
|
||||
- **соседях:** jira-agent и eva-agent упоминают `jira-hub` только в документации, `agent.example.json` и e2e-скрипте
|
||||
eva-agent (`JIRA_HUB_HOME`, `jira-hub` CLI). Протокол агента имени хаба не содержит: агенты ходят через общую папку.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:** одно имя `sdlc-hub` в коде, CLI, данных, MCP, вебе и документации; живой хаб переключается одним скриптом
|
||||
владельца с сухим прогоном; новый код не может молча работать с пустым каталогом данных рядом с живым.
|
||||
|
||||
**Non-Goals и Anti-Goals:** см. proposal.md.
|
||||
|
||||
## Decisions
|
||||
|
||||
1. **Имя — `sdlc-hub`** (решение владельца 23.10.2026). Бренд в вебе — «SDLC Hub».
|
||||
2. **Псевдонима `jira-hub` нет.** Старые записи MCP и launchd после переключения должны падать явно, а не работать
|
||||
«почти». Переключение делает скрипт сразу для всех известных потребителей.
|
||||
3. **MCP-серверы `sdlc-<модуль>`**, модуль в `sdlc-hub mcp` обязателен. Модуль по умолчанию был нужен только ради
|
||||
неизменных записей `.mcp.json` в hub-modules; при переименовании записи меняются всё равно. Имена инструментов в
|
||||
Claude станут `mcp__sdlc-jira__*` и `mcp__sdlc-wiki__*`.
|
||||
4. **Каталог данных не переносится кодом.** Хаб, MCP-серверы открытых сессий Claude и CLI открывают одну базу; перенос
|
||||
«на лету» при старте любого из них рискует WAL и гонкой. Код только отказывает с подсказкой (требование «Каталог
|
||||
данных прежнего имени»), перенос — шаг скрипта при остановленном хабе.
|
||||
5. **Скрипт переключения в репозитории** (`scripts/cutover.sh`), сухой прогон по умолчанию. Шагов много, они касаются
|
||||
launchd, двух чужих репозиториев и user scope Claude; руками их легко сделать не в том порядке (I-2 wiki-module).
|
||||
Скрипт одноразовый, после переключения остаётся в истории как образец; тесты — на сухой прогон и проверки шага 1
|
||||
(во временном `HOME`), без launchd.
|
||||
6. **Переименование репозитория в Gitea — руками владельца** в интерфейсе (Settings → Repository name). Gitea оставляет
|
||||
перенаправление со старого адреса; `tea` и `origin` после этого работают по новому адресу. Скрипт проверяет, что
|
||||
переименование сделано, и меняет `origin`.
|
||||
7. **Checkout переезжает в `jira-bridge/sdlc-hub`**, workspace `jira-bridge` остаётся: от его пути зависят каталог
|
||||
памяти Claude, workspace `CLAUDE.md`, путь общей папки реле (`…/utm/rshb/jira-bridge`) и настройки агентов на
|
||||
виртуалке. Переименование workspace — отдельно, если понадобится.
|
||||
8. **Ключи `localStorage` — с разовым переносом** из `jira-hub-*`: браузер владельца не должен снова просить токен и
|
||||
терять свёрнутость панели и легенды. Ключи доски `graph-*` имени хаба не содержат и не меняются.
|
||||
9. **История не переписывается**: `docs/superpowers/plans/`, `openspec/changes/archive/` остаются со старыми именами.
|
||||
Раздел `Purpose` спек `hub-api`, `mcp`, `web-app` (дельтами не меняется) правится в PR архива.
|
||||
10. **Агенты без релиза.** `src/agent.py` и `src/run.py` jira-agent и eva-agent не трогаются (строки документации — со
|
||||
следующим релизом); README, CLAUDE.md, ARCHITECTURE.md, `agent.example.json` и `scripts/e2e-hub.sh` eva-agent —
|
||||
мелкими PR в своих репозиториях (bounded, без change).
|
||||
11. **Порядок с #98 (веб под модули).** Переименование сливается первым; ветка #98 перебазируется на него. Бренд и
|
||||
ключи хранилища меняет этот change, устройство боковой панели — #98.
|
||||
|
||||
## Переключение (для владельца)
|
||||
|
||||
1. Слить PR этого change; переименовать репозиторий в Gitea `aep/jira-hub` → `aep/sdlc-hub`.
|
||||
2. Закрыть или не трогать сессии Claude с MCP хаба (после переключения — `/mcp` reconnect).
|
||||
3. `bash ~/projects/jira-bridge/jira-hub/scripts/cutover.sh` — сухой прогон, проверить вывод.
|
||||
4. `bash ~/projects/jira-bridge/jira-hub/scripts/cutover.sh --apply`.
|
||||
5. Просмотреть правки в awp-ca и stocks_drrk (`.mcp.json`, `.claude/`, `CLAUDE.md` awp-ca; копии `*.pre-sdlc` рядом) и
|
||||
удалить копии. Это не git-репозитории — коммитить нечего.
|
||||
6. Проверить: `agent_status` через `sdlc-jira`, `status` через `sdlc-wiki`, веб `http://127.0.0.1:8790/app/` без
|
||||
повторного входа.
|
||||
|
||||
Откат: остановить `ru.aepif.sdlc-hub`, перенести каталоги обратно, вернуть plist `ru.aepif.jira-hub` из README
|
||||
предыдущей версии, `git checkout` прежнего коммита, вернуть записи MCP — порядок в README.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Сессии Claude с открытым MCP** держат старый путь к пакету: после переноса checkout их серверы падают до
|
||||
reconnect. Принято: переключение — разовое событие владельца.
|
||||
- **codegraph-индексы** в `.codegraph/` checkout и worktree содержат абсолютные пути: после переноса — `codegraph init`
|
||||
заново (шаг в сводке скрипта).
|
||||
- **Гонка с #98**: массовый перенос `src/jira_hub` → `src/sdlc_hub` конфликтует с любой веткой, трогающей Python;
|
||||
#98 — только `web/`, поэтому конфликт ограничен брендом и ключами хранилища (решение 11).
|
||||
|
||||
## Decisions made without discussion
|
||||
|
||||
Владелец 24.10.2026 ответил «делай» на вопросы brainstorming — все решения ниже приняты агентом.
|
||||
|
||||
| # | Дата | Решение | Обоснование | Цена ошибки |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 24.10.2026 | Бренд «SDLC Hub» | имя из решения владельца, латиница как в имени репозитория | правка строки в вебе |
|
||||
| 2 | 24.10.2026 | Без псевдонима `jira-hub` | явный отказ лучше тихой работы старых записей | вернуть точку входа в `pyproject.toml` |
|
||||
| 3 | 24.10.2026 | MCP `sdlc-jira`/`sdlc-wiki`, модуль обязателен | симметрия модулей, ядро без «jira по умолчанию» | переименовать серверы ещё раз во всех `.mcp.json` |
|
||||
| 4 | 24.10.2026 | Каталог данных переносит скрипт, код только отказывает | гонка открытых баз при переносе на лету | ручной `mv` при остановленном хабе |
|
||||
| 5 | 24.10.2026 | Скрипт `scripts/cutover.sh` с сухим прогоном | много шагов в чужих конфигурациях, порядок важен | шаги руками по README |
|
||||
| 6 | 24.10.2026 | Gitea переименовывает владелец | действие на сервере, вне репозитория | — |
|
||||
| 7 | 24.10.2026 | Workspace `jira-bridge` не переименовывается | от пути зависят память Claude, реле, агенты | отдельный change позже |
|
||||
| 8 | 24.10.2026 | Перенос ключей `localStorage` | не просить токен заново | повторный вход в веб |
|
||||
| 9 | 24.10.2026 | История (планы, архив) не меняется | это запись прошлого | — |
|
||||
| 10 | 24.10.2026 | Агенты без релиза, документация — мелкими PR | протокол не меняется | строки документации в `agent.py` со старым именем до следующего релиза |
|
||||
| 11 | 24.10.2026 | Сначала переименование, потом #98 | #98 перебазируется на новые пути | перебазирование веток |
|
||||
42
openspec/changes/sdlc-rename/proposal.md
Normal file
42
openspec/changes/sdlc-rename/proposal.md
Normal file
@ -0,0 +1,42 @@
|
||||
## Why
|
||||
|
||||
Хаб перестал быть только мостом Jira: с hub-modules (#87) он состоит из ядра и модулей, с wiki-module (#91) — второй
|
||||
модуль (EVA Wiki) со своим агентом и своим MCP. Имя `jira-hub` теперь вводит в заблуждение: в вебе «Хаб Jira», MCP вики
|
||||
называется `jira-hub-wiki`, данные вики лежат в `~/.local/share/jira-hub/`. Решение владельца 23.10.2026 — хаб
|
||||
становится **sdlc-hub**; переименование было отложено до готовности модулей. 24.10.2026 владелец решил делать его сейчас,
|
||||
вместе с пересборкой веба под модули (jira-hub#98), «раз мы причёсываем визуал».
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Пакет и CLI.** Пакет `jira_hub` → `sdlc_hub`, дистрибутив `jira-hub` → `sdlc-hub`, команда `jira-hub` → `sdlc-hub`.
|
||||
Псевдонима `jira-hub` нет.
|
||||
- **MCP по модулю без модуля по умолчанию.** `sdlc-hub mcp <модуль> [--project <метка>]`; модуль обязателен. Серверы
|
||||
называются `sdlc-<модуль>`: `sdlc-jira`, `sdlc-wiki`. Фрагмент `.mcp.json` из `project_create` — с новыми именами.
|
||||
- **Каталог данных.** `~/.local/share/sdlc-hub/`, переменная `SDLC_HUB_HOME`. Если нового каталога нет, а прежний
|
||||
`~/.local/share/jira-hub/` есть, любая команда хаба, открывающая базу, отказывает с текстом, как перенести каталог;
|
||||
сама ничего не переносит.
|
||||
- **Веб.** Бренд «SDLC Hub» (заголовок вкладки, боковая и верхняя панели), ключи `localStorage` `sdlc-hub-*` с разовым
|
||||
переносом значений из `jira-hub-*`, подсказка входа `uv run sdlc-hub tokens`, `npm run dev` — `SDLC_HUB_URL`.
|
||||
- **Переключение живого хаба.** Скрипт `scripts/cutover.sh` (сухой прогон по умолчанию, `--apply` — выполнить) для
|
||||
владельца: останавливает launchd `ru.aepif.jira-hub`, переносит каталог данных, ставит `ru.aepif.sdlc-hub` с логом
|
||||
`~/Library/Logs/sdlc-hub.log`, переписывает записи MCP в `.mcp.json` известных проектов и в user scope, переносит
|
||||
checkout `jira-bridge/jira-hub` → `jira-bridge/sdlc-hub` и чинит worktree. Репозиторий в Gitea `aep/jira-hub` →
|
||||
`aep/sdlc-hub` владелец переименовывает в интерфейсе Gitea, скрипт затем меняет `origin`.
|
||||
- **Документация.** README, CLAUDE.md, ARCHITECTURE.md, текущие спеки (`openspec/specs/`) — на новые имена; workspace
|
||||
`CLAUDE.md` и упоминания в jira-agent/eva-agent (README, CLAUDE.md, ARCHITECTURE.md, `agent.example.json`, e2e-скрипт
|
||||
eva-agent) — отдельными мелкими PR в своих репозиториях.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Пересборка веба под модули — jira-hub#98, отдельный change.
|
||||
- Переименование workspace `jira-bridge`, общей папки реле (`relay_dir`), репозиториев агентов, launchd агентов на
|
||||
виртуалке.
|
||||
- Правка `src/agent.py` и `src/run.py` агентов: строки документации в них меняются со следующим релизом агента.
|
||||
- Правка истории: `docs/superpowers/plans/`, `openspec/changes/archive/`.
|
||||
- Изменения протокола агента, HTTP API (кроме текстов), схемы базы, токенов.
|
||||
|
||||
## Anti-Goals
|
||||
|
||||
- Живой хаб не должен оказаться в состоянии «код новый, данные/launchd старые» без явной ошибки: молчаливо создать пустой
|
||||
`~/.local/share/sdlc-hub/` рядом с живыми данными — провал, даже если тесты зелёные.
|
||||
- Агенты на виртуалке не требуют ни перенастройки, ни нового релиза.
|
||||
41
openspec/changes/sdlc-rename/specs/data-model/spec.md
Normal file
41
openspec/changes/sdlc-rename/specs/data-model/spec.md
Normal file
@ -0,0 +1,41 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Настройки
|
||||
Таблица `setting` SHALL хранить парами ключ–JSON: `project_key`, `field_ids`, `issue_types`, `priority_map`, `link_types`,
|
||||
`transitions_map`, `pacing`, `tokens` (агентский и клиентский), `settings_version` (увеличивается при любой
|
||||
правке), `backup_keep`, `auto_refresh_minutes`, `relay_dir` (абсолютный путь папки реле или `null`, по умолчанию `null`;
|
||||
относительный путь и не-строка при записи — 422; агенту не передаётся), `closed_statuses` (список имён статусов Jira, считающихся закрытыми, по умолчанию
|
||||
`["Готово", "Снято"]`; не-список при записи — 422; агенту не передаётся; на существующей базе появляется со значением
|
||||
по умолчанию при открытии хаба). Базовая JQL вычисляется из проектов, доска — свойство проекта (требование
|
||||
«Проекты»); на новой базе ключей `base_jql` и `board_id` нет. Оставшиеся от v3 ключи хаб SHALL NOT использовать, кроме
|
||||
передачи `board_id` агенту как запасной доски для команд без доски в параметрах (поставленных до миграции);
|
||||
`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` берёт их из ключей конфига
|
||||
`issue_type_<тип>` и `issue_type_<тип>_id`; `bug` попадает в `issue_types`, только если в конфиге есть хотя бы
|
||||
один из ключей `issue_type_bug`, `issue_type_bug_id` (`task` и `epic` — как раньше, всегда).
|
||||
|
||||
#### Scenario: Правка настроек поднимает версию
|
||||
- **WHEN** через API изменён `pacing`
|
||||
- **THEN** `settings_version` увеличен, следующий `claim` отдаёт агенту новую версию
|
||||
|
||||
#### Scenario: Конфиг без ключей ошибки
|
||||
- **WHEN** `sdlc-hub init --config` с `issue_type_task`, `issue_type_epic`, без `issue_type_bug`
|
||||
- **THEN** `issue_types` содержит `task` и `epic`, `bug` нет; постановка `ensure_issue` с `issue_type = bug` — 422
|
||||
|
||||
#### Scenario: Неверный relay_dir
|
||||
- **WHEN** `PUT /settings` с `relay_dir = "rshb/jira-bridge"`
|
||||
- **THEN** 422, значение не записано, `settings_version` не изменился
|
||||
|
||||
#### Scenario: Закрытые статусы по умолчанию
|
||||
- **WHEN** хаб открыл базу, в которой нет ключа `closed_statuses`
|
||||
- **THEN** `GET /settings` отдаёт `closed_statuses = ["Готово", "Снято"]`; `claim` агенту этого ключа не передаёт
|
||||
|
||||
#### Scenario: Запись базовой JQL
|
||||
- **WHEN** `PUT /settings {base_jql: "project = AURORA"}`
|
||||
- **THEN** 422 «базовая JQL собирается из проектов»; `settings_version` не изменился
|
||||
|
||||
#### Scenario: Создание проекта не трогает версию настроек
|
||||
- **WHEN** создан проект `STOCKS_DRRK`
|
||||
- **THEN** `settings_version` прежний; `GET /settings` отдаёт `base_jql` с обеими метками
|
||||
236
openspec/changes/sdlc-rename/specs/hub-api/spec.md
Normal file
236
openspec/changes/sdlc-rename/specs/hub-api/spec.md
Normal file
@ -0,0 +1,236 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Процесс и доступ
|
||||
`sdlc-hub serve` SHALL запускать один процесс: HTTP на `0.0.0.0:<port>` (по умолчанию 8790), база и настройки в
|
||||
`~/.local/share/sdlc-hub/` (переопределяется `SDLC_HUB_HOME`). Все маршруты API SHALL требовать
|
||||
`Authorization: Bearer <token>`: токен агента модуля открывает только `/agent/*` (маршруты ядра — от имени этого агента,
|
||||
маршруты модуля со scope `agent` — только агенту своего модуля, см. `modules`, «Агенты модулей»), клиентский — всё
|
||||
остальное. `sdlc-hub tokens` SHALL печатать клиентский токен и токены агентов с именами модулей. Исключение —
|
||||
раздача веб-приложения (`GET /app`, `GET /app/…`) и перенаправления `GET /`, `GET /links`: это статические файлы без
|
||||
данных, они отдаются без токена. Ответы API — JSON, ошибки — `{error, detail}` с кодами 400 (неверное тело или
|
||||
`Content-Length`), 401, 404, 409, 413 (тело больше предела), 422, 500 (внутренняя ошибка); реле общей папки
|
||||
дополнительно отвечает 502. Методы, кроме GET, POST, PUT и PATCH, отвечают 501 стандартной страницей `http.server`, не
|
||||
JSON.
|
||||
|
||||
#### Scenario: Чужой токен
|
||||
- **WHEN** токеном агента `jira` вызван `GET /issues`
|
||||
- **THEN** 401
|
||||
|
||||
#### Scenario: Неподдерживаемый метод
|
||||
- **WHEN** `DELETE /issues/AURORA-1`
|
||||
- **THEN** 501, тело не JSON; база не меняется
|
||||
|
||||
#### Scenario: Страница без токена
|
||||
- **WHEN** `GET /app/board` без заголовка `Authorization`, сборка приложения есть
|
||||
- **THEN** 200, `Content-Type: text/html; charset=utf-8`, тело — `index.html` сборки; данных зеркала в нём нет
|
||||
|
||||
#### Scenario: Токены после миграции
|
||||
- **WHEN** база v6 с агентским токеном T мигрировала в v7, запущен `sdlc-hub tokens`
|
||||
- **THEN** вывод содержит клиентский токен, `jira: T` и новый токен `wiki`
|
||||
|
||||
### Requirement: Реле через общую папку
|
||||
`sdlc-hub serve` SHALL запускать поток реле вместе с HTTP-сервером. Пока настройка `relay_dir` пуста (`null`), реле
|
||||
бездействует; настройку оно SHALL перечитывать не реже раза в 5 с, включение и выключение не требуют перезапуска. Если
|
||||
каталог `relay_dir` существует, реле SHALL создать в нём подкаталоги `req/`, `resp/`, `work/`; если не существует —
|
||||
записать одно предупреждение в лог и проверять снова.
|
||||
|
||||
Обмен:
|
||||
- запрос агента — файл `req/<id>.json`: `{id, method, path, body, authorization, deadline}`, где `deadline` — момент UTC
|
||||
(ISO 8601), после которого ответ не нужен; файлы, имя которых начинается с точки, реле MUST NOT читать;
|
||||
- реле SHALL брать запрос переименованием в `work/<id>.json` и обрабатывать каждый взятый запрос в отдельном потоке;
|
||||
- запрос с истёкшим `deadline` SHALL выбрасываться без ответа; `POST /agent/claim` SHALL пересылаться, только если до
|
||||
`deadline` осталось не меньше 35 с, иначе выбрасываться без ответа;
|
||||
- остальные запросы реле SHALL пересылать в свой HTTP на `<host>:<port>` процесса `serve` (`127.0.0.1`, если `serve`
|
||||
слушает `0.0.0.0`) тем же методом, путём и телом, с заголовком `Authorization` из файла, и записывать ответ
|
||||
`resp/<id>.json` = `{id, status, body}` (тело — строка ответа HTTP) через временный файл с именем на точку и
|
||||
переименование; затем удалять `work/<id>.json`;
|
||||
- если соединение со своим HTTP не удалось или ответ нарушает протокол HTTP, ответ SHALL иметь `status = 502` и текст
|
||||
ошибки в `body`;
|
||||
- нечитаемый JSON запроса SHALL перечитываться до 2 с, затем запрос выбрасывается с записью в лог.
|
||||
|
||||
При первой подготовке каждой папки после старта реле (в том числе новой папки после смены `relay_dir`) реле SHALL
|
||||
удалить файлы из `work/` с записью в лог. Повторная подготовка той же папки (пропал подкаталог, не было доступа) SHALL
|
||||
только создавать недостающие подкаталоги, не трогая `work/`. Раз в минуту
|
||||
реле SHALL удалять ответы в `resp/` старше 10 мин, запросы в `req/` с `deadline` старше 60 с и файлы с именем на точку
|
||||
старше 10 мин. На каждый обработанный запрос реле SHALL писать в лог хаба строку `relay <METHOD> <path> <status>
|
||||
<секунды>`. HTTP API, обработчики и проверка токенов при этом не меняются.
|
||||
|
||||
#### Scenario: Запрос через папку
|
||||
- **WHEN** в `req/` появился `a1.json` с `GET /agent/settings`, агентским токеном и `deadline` через 60 с
|
||||
- **THEN** в `resp/` появляется `a1.json` со `status = 200` и телом настроек; `work/a1.json` удалён
|
||||
|
||||
#### Scenario: Чужой токен через папку
|
||||
- **WHEN** в файле запроса `GET /issues` с агентским токеном
|
||||
- **THEN** ответ со `status = 401`, как по HTTP
|
||||
|
||||
#### Scenario: Просроченный claim
|
||||
- **WHEN** реле взяло `POST /agent/claim`, до `deadline` которого осталось 20 с
|
||||
- **THEN** запрос выброшен без ответа и без обращения к хабу; команда в очереди осталась `queued`
|
||||
|
||||
#### Scenario: Долгий claim не задерживает другие запросы
|
||||
- **WHEN** реле пересылает `claim`, который ждёт команду 30 с, и в это время в `req/` появляется `GET /agent/settings`
|
||||
- **THEN** ответ на `settings` пишется, не дожидаясь `claim`
|
||||
|
||||
#### Scenario: Папка не подключена
|
||||
- **WHEN** `relay_dir` задан, но каталога нет; через минуту каталог появился
|
||||
- **THEN** в логе одно предупреждение; после появления каталога реле создаёт подкаталоги и обрабатывает запросы
|
||||
|
||||
#### Scenario: Сбой протокола своего HTTP
|
||||
- **WHEN** при пересылке запроса свой HTTP оборвал ответ (`http.client.HTTPException`)
|
||||
- **THEN** в `resp/` ответ со `status = 502` и текстом ошибки; реле продолжает работу
|
||||
|
||||
#### Scenario: Повторная подготовка не трогает взятые запросы
|
||||
- **WHEN** реле работает, в `work/` лежит взятый запрос, и пропал подкаталог `resp/`
|
||||
- **THEN** реле создаёт `resp/` заново; файл в `work/` не удалён, в логе нет «удалён незавершённый запрос»
|
||||
|
||||
### Requirement: Публикация версии агента
|
||||
`sdlc-hub agent publish [--module <модуль>] PATH` SHALL публиковать файл агента модуля (по умолчанию `jira`;
|
||||
неизвестный модуль или модуль без команд — код 2 и список модулей с агентами в stderr): текст в UTF-8 не больше 1 МБ,
|
||||
ровно одна строка `AGENT_VERSION = <целое>`, версия больше опубликованной для этого модуля. Файл SHALL записываться в
|
||||
`<SDLC_HUB_HOME>/release/<модуль>/agent.py` (каталоги 0700), затем `manifest.json` `{version, sha256, size,
|
||||
published_at}`; оба — через временный файл с `fsync` и переименование, манифест последним. Прежний `agent.py` на время
|
||||
публикации SHALL переименовываться в `agent.py.prev` и удаляться после записи манифеста; если запись нового файла или
|
||||
манифеста не удалась (`OSError`), хаб SHALL вернуть `agent.py.prev` на место `agent.py`, удалить временные файлы и
|
||||
завершить команду сообщением «не удалось записать публикацию: <ошибка>» и кодом 1 — опубликованной остаётся прежняя
|
||||
версия. Команда SHALL работать без запущенного `serve` и печатать «опубликована v<N>, sha256 <первые 12 символов>…,
|
||||
<size> байт»; при ошибке — сообщение и код 1, опубликованное не меняется. При старте хаб SHALL один раз перенести
|
||||
прежнюю публикацию `release/agent.py` и `release/manifest.json` в `release/jira/`, если `release/jira/` ещё нет; ошибка
|
||||
переноса — предупреждение в лог, хаб стартует. Хаб SHALL читать манифест модуля агента при каждом `claim`,
|
||||
`GET /agent/release` и `GET /agent`; манифест годен, только если это объект, `version` — целое число (не логическое) не
|
||||
меньше 1, `sha256` — строка из 64 символов `[0-9a-f]`; негодный или нечитаемый манифест означает «публикации нет» и
|
||||
предупреждение в лог — одно на каждый новый текст ошибки. Файл `agent.py`, не декодируемый как UTF-8 при совпавшем
|
||||
sha256, означает «публикации нет» (`GET /agent/release` — 404 `no_release`). `GET /agent?module=<модуль>` (без
|
||||
параметра — `jira`) и каждый элемент `GET /agents` SHALL дополняться полями `release` (`{version, sha256,
|
||||
published_at}` или `null`), `update_state`, `launcher_version`, `agent_reported` (был ли `claim` этого агента с момента
|
||||
запуска хаба) и `update_hint`: «агент запущен без run.py — обновится только вручную» (опубликована версия выше версии
|
||||
агента, агент онлайн, `agent_reported = true`, `launcher_version = null`), «откат с v<to> на v<from>: <error>»
|
||||
(`rolled_back`), «v<to> отвергнута: <error>» (`rejected`), иначе `null`.
|
||||
|
||||
#### Scenario: Публикация новой версии
|
||||
- **WHEN** опубликована v5, `sdlc-hub agent publish jira-agent/src/agent.py` с `AGENT_VERSION = 6`
|
||||
- **THEN** в `release/jira/` — новый `agent.py` и манифест с `version = 6`, `agent.py.prev` нет; вывод «опубликована v6,
|
||||
sha256 …»; код 0
|
||||
|
||||
#### Scenario: Публикация eva-agent
|
||||
- **WHEN** `sdlc-hub agent publish --module wiki eva-agent/src/agent.py` с `AGENT_VERSION = 1`, опубликована jira v14
|
||||
- **THEN** в `release/wiki/` — v1; `claim` агента `wiki` получает `agent_release {version: 1, …}`, `claim` агента `jira` —
|
||||
`{version: 14, …}`
|
||||
|
||||
#### Scenario: Неизвестный модуль
|
||||
- **WHEN** `sdlc-hub agent publish --module nope agent.py`
|
||||
- **THEN** код 2, в stderr «неизвестный модуль nope» и список модулей с агентами; `release/` не изменён
|
||||
|
||||
#### Scenario: Версия не выше опубликованной
|
||||
- **WHEN** опубликована v6, публикуется файл с `AGENT_VERSION = 6`
|
||||
- **THEN** «опубликована v6, новая должна быть больше», код 1; `release/` не изменён
|
||||
|
||||
#### Scenario: Нет строки версии
|
||||
- **WHEN** публикуется файл без строки `AGENT_VERSION = <целое>` или с двумя такими строками
|
||||
- **THEN** сообщение об ошибке, код 1; `release/` не изменён
|
||||
|
||||
#### Scenario: Диск полон при записи манифеста
|
||||
- **WHEN** опубликована v8, публикуется v9, запись `manifest.json` завершилась `OSError`
|
||||
- **THEN** «не удалось записать публикацию: …», код 1, без трейсбека; `agent.py` и манифест — прежние v8,
|
||||
`GET /agent/release` отдаёт v8; временных файлов и `agent.py.prev` в `release/jira/` нет
|
||||
|
||||
#### Scenario: Негодный манифест
|
||||
- **WHEN** в `manifest.json` `version = 6.9` (или `true`, или `sha256` короче 64 символов), хаб получает 100 `claim`
|
||||
- **THEN** в `claim` `agent_release = null`, `GET /agent/release` — 404; в логе одно предупреждение о манифесте
|
||||
|
||||
#### Scenario: Агент без запускателя
|
||||
- **WHEN** опубликована v6, агент v5 онлайн, в последнем `claim` `launcher_version = null`
|
||||
- **THEN** `GET /agent` → `agent_reported = true`, `update_hint` = «агент запущен без run.py — обновится только вручную»
|
||||
|
||||
#### Scenario: Хаб перезапущен, агент ещё не выходил на связь
|
||||
- **WHEN** опубликована v10, агент v9 под запускателем, хаб перезапущен, `claim` после запуска ещё не было
|
||||
- **THEN** `GET /agent` → `agent_reported = false`, `launcher_version = null`, `update_hint = null`; после первого
|
||||
`claim` — `agent_reported = true`, `launcher_version` из `claim`
|
||||
|
||||
#### Scenario: Перенос прежней публикации
|
||||
- **WHEN** хаб с `release/agent.py` v14 и манифестом без `release/jira/` запущен на новом коде
|
||||
- **THEN** публикация v14 лежит в `release/jira/`, `claim` агента `jira` получает `agent_release {version: 14, …}`
|
||||
|
||||
### Requirement: Раздача веб-приложения
|
||||
Хаб SHALL отдавать собранное веб-приложение (спека `web-app`) из каталога пакета `src/sdlc_hub/web/app/`, читая файлы
|
||||
с диска на каждый запрос:
|
||||
- `GET /app/assets/<путь>` — файл из `assets/` сборки; путь, который после разрешения выходит за `assets/`, не
|
||||
существует или содержит нулевой байт, — 404; `Content-Type` по расширению (`.js` — `text/javascript; charset=utf-8`, `.css` —
|
||||
`text/css; charset=utf-8`, `.svg` — `image/svg+xml`, `.png` — `image/png`, `.ico` — `image/x-icon`, `.woff2` —
|
||||
`font/woff2`, `.map` — `application/json`, прочее — `application/octet-stream`);
|
||||
`Cache-Control: public, max-age=31536000, immutable`;
|
||||
- любой другой `GET /app` или `GET /app/…` — `index.html` сборки, `text/html; charset=utf-8`, `Cache-Control: no-cache`;
|
||||
- если `index.html` сборки нет — на любой `GET /app…` 503 `text/html; charset=utf-8` с текстом, что приложение не
|
||||
собрано и нужно выполнить `make web` в каталоге sdlc-hub;
|
||||
- `GET /` и `GET /links` — 302 с `Location: /app/board`.
|
||||
Сборка приложения не хранится в git; её создаёт `make web`.
|
||||
|
||||
#### Scenario: Файл сборки
|
||||
- **WHEN** в сборке есть `assets/index-abc123.js`, запрошен `GET /app/assets/index-abc123.js`
|
||||
- **THEN** 200, `Content-Type: text/javascript; charset=utf-8`, `Cache-Control` с `immutable`, тело — содержимое файла
|
||||
|
||||
#### Scenario: Путь приложения
|
||||
- **WHEN** запрошен `GET /app/issues/AURORA-1620`
|
||||
- **THEN** 200, тело — `index.html` сборки, `Cache-Control: no-cache`
|
||||
|
||||
#### Scenario: Выход за каталог сборки
|
||||
- **WHEN** запрошен `GET /app/assets/../../api.py` (в том числе в виде `%2e%2e`)
|
||||
- **THEN** 404, содержимое файлов вне `assets/` не отдаётся
|
||||
|
||||
#### Scenario: Приложение не собрано
|
||||
- **WHEN** каталога сборки нет, запрошен `GET /app/board`
|
||||
- **THEN** 503, тело — HTML с подсказкой `make web`
|
||||
|
||||
#### Scenario: Старый адрес доски
|
||||
- **WHEN** запрошен `GET /links` или `GET /`
|
||||
- **THEN** 302, `Location: /app/board`
|
||||
|
||||
#### Scenario: Нулевой байт в пути
|
||||
- **WHEN** запрошен `GET /app/assets/%00` или `GET /app/assets/a%00.js`
|
||||
- **THEN** 404 `not_found`, а не 500
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Каталог данных прежнего имени
|
||||
Если `SDLC_HUB_HOME` не задан, `~/.local/share/sdlc-hub/` не существует, а `~/.local/share/jira-hub/` существует, любая
|
||||
команда `sdlc-hub`, которой нужен каталог данных (`serve`, `mcp`, `tokens`, `init`, `import`, `agent publish`), SHALL
|
||||
завершиться с кодом 2 и текстом в stderr: каталог данных лежит под прежним именем, перенести его —
|
||||
`scripts/cutover.sh` (или `mv ~/.local/share/jira-hub ~/.local/share/sdlc-hub` при остановленном хабе). Команда MUST NOT
|
||||
создавать новый каталог, переносить или менять прежний. Если существуют оба каталога, используется новый, в stderr —
|
||||
предупреждение, что прежний можно удалить после проверки.
|
||||
|
||||
#### Scenario: Код обновлён, каталог не перенесён
|
||||
- **WHEN** есть только `~/.local/share/jira-hub/`, запущен `sdlc-hub serve`
|
||||
- **THEN** код выхода 2, в stderr — подсказка о переносе; `~/.local/share/sdlc-hub/` не создан, прежний каталог не тронут
|
||||
|
||||
#### Scenario: Задан SDLC_HUB_HOME
|
||||
- **WHEN** `SDLC_HUB_HOME=/tmp/x`, есть `~/.local/share/jira-hub/`
|
||||
- **THEN** команда работает с `/tmp/x`, проверка прежнего каталога не выполняется
|
||||
|
||||
### Requirement: Переключение живого хаба
|
||||
`scripts/cutover.sh` SHALL переводить живую установку Mac mini со старого имени на новое. Без `--apply` скрипт SHALL
|
||||
только печатать шаги и найденные проблемы, ничего не меняя. С `--apply` — по порядку, с остановкой на первой ошибке:
|
||||
1. проверки: основной checkout `jira-bridge/jira-hub` чистый и на `main`; в Gitea доступен `aep/sdlc-hub` (владелец
|
||||
переименовал репозиторий); `~/.local/share/sdlc-hub/` и `jira-bridge/sdlc-hub` не существуют;
|
||||
2. остановить launchd `ru.aepif.jira-hub` и удалить его plist;
|
||||
3. перенести каталог данных `~/.local/share/jira-hub` → `~/.local/share/sdlc-hub`;
|
||||
4. перенести checkout `jira-bridge/jira-hub` → `jira-bridge/sdlc-hub`, `git worktree repair`, `origin` →
|
||||
`aep/sdlc-hub`, `git pull --ff-only`, пересоздать `.venv` (`uv sync`), `make web`;
|
||||
5. поставить и запустить launchd `ru.aepif.sdlc-hub` (`uv run --project <checkout> sdlc-hub serve`, лог
|
||||
`~/Library/Logs/sdlc-hub.log`) и дождаться ответа хаба на порту из настроек;
|
||||
6. в проектах из списка (по умолчанию `~/projects/awp-ca`, `~/projects/stocks_drrk`): в `.mcp.json` заменить записи
|
||||
`jira-hub` → `sdlc-jira` (`sdlc-hub mcp jira`) и `jira-hub-wiki` → `sdlc-wiki` (`sdlc-hub mcp wiki`) с новым путём; в
|
||||
`.claude/settings*.json` — имена серверов (`enabledMcpjsonServers` и т. п.) и разрешения `mcp__jira-hub__` →
|
||||
`mcp__sdlc-jira__`, `mcp__jira-hub-wiki__` → `mcp__sdlc-wiki__`; в `CLAUDE.md` и `.claude/skills/**` — те же имена
|
||||
серверов и инструментов. Прочие файлы проектов (заметки, отчёты) не трогаются. Перед правкой файл копируется рядом
|
||||
с суффиксом `.pre-sdlc`; коммиты скрипт не делает;
|
||||
7. в user scope Claude Code заменить `jira-hub` на `sdlc-jira` (`claude mcp remove`/`add`).
|
||||
Каждый выполненный шаг SHALL печататься; в конце — сводка, что осталось владельцу (`/mcp` reconnect в открытых сессиях,
|
||||
`codegraph init` в checkout и worktree, удалить копии `.pre-sdlc` после проверки). Шаги откатываются в обратном порядке по инструкции из README.
|
||||
|
||||
#### Scenario: Сухой прогон
|
||||
- **WHEN** `bash scripts/cutover.sh` без аргументов
|
||||
- **THEN** напечатаны шаги и проверки, файловая система, launchd и конфигурации Claude не изменены
|
||||
|
||||
#### Scenario: Репозиторий в Gitea не переименован
|
||||
- **WHEN** `bash scripts/cutover.sh --apply`, `aep/sdlc-hub` в Gitea не найден
|
||||
- **THEN** скрипт останавливается на шаге 1 с подсказкой, launchd и каталоги не тронуты
|
||||
243
openspec/changes/sdlc-rename/specs/mcp/spec.md
Normal file
243
openspec/changes/sdlc-rename/specs/mcp/spec.md
Normal file
@ -0,0 +1,243 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Сервер и регистрация
|
||||
`sdlc-hub mcp <модуль> [--project <метка>]` SHALL запускать MCP-сервер модуля хаба по stdio (пакет `mcp`), ходящий в
|
||||
HTTP API хаба клиентским токеном из настроек; сервер называется `sdlc-<модуль>` (`sdlc-jira`, `sdlc-wiki`). Модуль
|
||||
обязателен: без него — код выхода 2 и список модулей в stderr, сервер не запускается; неизвестный модуль — так же.
|
||||
Общая регистрация — в user scope Claude Code как `sdlc-jira` (`uv run --project <путь> sdlc-hub mcp jira`), без
|
||||
проекта. Репозиторий проекта SHALL регистрировать серверы модулей в своём `.mcp.json` (project scope) с
|
||||
`--project <метка>`; эта запись перекрывает общую. Если хаб недоступен, каждый инструмент SHALL вернуть текст «хаб не
|
||||
запущен: sdlc-hub serve», а не исключение.
|
||||
|
||||
#### Scenario: Хаб выключен
|
||||
- **WHEN** вызван `issues` при остановленном хабе
|
||||
- **THEN** ответ инструмента — одна строка с подсказкой, сервер MCP продолжает работать
|
||||
|
||||
#### Scenario: Сервер модуля jira
|
||||
- **WHEN** запущен `sdlc-hub mcp jira --project AWPCA`
|
||||
- **THEN** сервер называется `sdlc-jira`, список инструментов и их описания совпадают с прежним сервером `jira-hub`,
|
||||
сессия привязана к `AWPCA`
|
||||
|
||||
#### Scenario: Модуль по умолчанию
|
||||
- **WHEN** запущен `sdlc-hub mcp --project AWPCA`
|
||||
- **THEN** модуля по умолчанию больше нет: код выхода 2, в stderr — подсказка указать модуль и список модулей (`jira`, `wiki`)
|
||||
|
||||
#### Scenario: Неизвестный модуль
|
||||
- **WHEN** запущен `sdlc-hub mcp nope`
|
||||
- **THEN** код выхода 2, в stderr — «неизвестный модуль nope» и список модулей (`jira`, `wiki`)
|
||||
|
||||
### Requirement: Инструменты
|
||||
| Инструмент | Параметры | Результат |
|
||||
|---|---|---|
|
||||
| `projects` | — | таблица: метка, название, доска, тип доски («неизвестен», пока агент не определил), число своих задач; в привязанной сессии текущий проект отмечен |
|
||||
| `project_create` | `label`, `name`, `board_id` (необязательно) | `POST /projects` с автором `claude`: проект, id пакета обновления и фрагмент `.mcp.json` для репозитория проекта (см. ниже); 409/422 — текст ошибки |
|
||||
| `issues` | фильтры как у `GET /issues` (кроме `project` — его задаёт привязка), `limit` (по умолчанию 50) | таблица: ключ, тема (до 80 символов), статус, срок, исполнитель, спринт, статус по факту |
|
||||
| `issue` | `key` | поля, связи (обе стороны, с типом и направлением), заметки, последние команды, комментарии: «комментарии (N):» и 5 последних (номер «#<comment_id>», автор, время, текст до 300 символов с «…»), «ещё M», если их больше 5; без комментариев — «комментариев нет»; у комментария со ссылками на вложения — строка «файлы: #<id> <имя>, …»; затем вложения (см. «Вложения в MCP») |
|
||||
| `comment` | `key`, `comment_id` (номер из ответа `issue`, «#» в начале допускается) | один комментарий целиком по `GET /issues/{key}`: строка «<key> #<id> <автор>, создан <created>» (и «, изменён <updated>», если `updated` есть и отличается от `created`), под ней полный текст с переносами строк; нет такого номера — «у <key> нет комментария #<id>»; есть ссылки на вложения — после текста строка «вложения:» и по строке на каждое: «#<id> <имя> (<размер>)» |
|
||||
| `attachment` | `key`, `attachment` (номер вложения «#<id>» или «<id>», иначе — точное имя файла), `wait_seconds` (по умолчанию 120, не больше 600) | скачивает вложение и отдаёт путь к локальной копии (см. «Вложения в MCP») |
|
||||
| `notes_set` | `key`, подмножество полей заметок | обновлённые заметки |
|
||||
| `batch_create` | `title`, `commands` (список объектов по контракту `commands`; у `ensure_attachment` вместо `file` можно `path`) | `id`, `estimate_seconds`, число команд; 422 — текст ошибки валидации |
|
||||
| `batch_status` | `id` | статусы команд, ошибки, оценка остатка |
|
||||
| `batch_wait` | `id`, `timeout_minutes` (≤ 60) | как `batch_status`, возвращается по завершении или по таймауту |
|
||||
| `batches` | `limit` | последние пакеты со статусом |
|
||||
| `command_retry` | `id` | новый пакет |
|
||||
| `batch_cancel` | `id` | вид пакета; для команд пакета, уже взятых агентом, — строка «команда #N уже у агента и будет выполнена; остановить — `POST /commands/N/cancel` с `force: true`» |
|
||||
| `refresh` | `keys` или `full`, `wait` (bool) | без `wait` — строка с id поставленного пакета; с `wait` — итог пакета, как у `batch_wait` |
|
||||
| `agent_status` | — | онлайн/офлайн, версия, текущая команда, довыданные команды (id, тип, число выдач), 429 за сутки, версия настроек; публикация агента («публикация: v<N> от <дата>» или «публикации нет»), состояние обновления («обновление: <state> v<from> → v<to>»), запускатель («запускатель: v<N>», «без запускателя» или, пока `agent_reported = false`, «запускатель: неизвестно (агент ещё не выходил на связь после перезапуска хаба)»), `update_hint`, если он есть |
|
||||
| `sync_log` | `limit` | последние `sync_run` |
|
||||
| `settings_get`, `settings_set` | ключ, значение | текущее значение / новая `settings_version` |
|
||||
|
||||
Разрез по проекту и поведение без привязки — требование «Привязка сессии к проекту».
|
||||
|
||||
Описание инструмента `batch_create` SHALL перечислять параметры каждого типа команды (обязательные, необязательные в
|
||||
`[ ]`, формат значения) и собираться из описания параметров в коде хаба (см. `commands`), а не вручную.
|
||||
|
||||
`ensure_attachment` в `batch_create` SHALL принимать вместо `file` параметр `path` — абсолютный путь к локальному файлу на
|
||||
машине MCP-сервера. До постановки пакета MCP SHALL прочитать файл, загрузить его `POST /files` и заменить `path` на
|
||||
`file`; `filename` по умолчанию — имя файла из `path`. Нет файла, нет прав, не файл, больше 10 МБ — ответ инструмента
|
||||
с причиной и путём, пакет не ставится. Передан и `path`, и `file` — ответ «у ensure_attachment либо path, либо file».
|
||||
|
||||
Описание инструмента `batch_create` SHALL сообщать, что метку проекта сессии хаб дописывает в `ensure_issue` сам, метки
|
||||
других проектов запрещены, а команды над задачами вне проекта отклоняются; `board_id` для `ensure_sprint` выставляет хаб.
|
||||
|
||||
Ответ `project_create` SHALL содержать фрагмент `.mcp.json`
|
||||
`{"mcpServers": {"sdlc-jira": {"command": "uv", "args": ["run", "--project", "<корень хаба>", "sdlc-hub", "mcp", "jira",
|
||||
"--project", "<метка>"]}}}`, где корень хаба — корень репозитория, из которого загружен пакет текущего процесса MCP
|
||||
(вычисляется от расположения пакета, без зашитого пути), и указание: положить файл в корень репозитория проекта (слить
|
||||
с имеющимся `.mcp.json`) и переподключить MCP (`/mcp` → reconnect, одобрить сервер проекта).
|
||||
|
||||
Автор команд из MCP SHALL быть `claude`; из API без автора — `api`.
|
||||
|
||||
#### Scenario: Пакет из чата
|
||||
- **WHEN** `batch_create {title: "1538 в спринт 2", commands: [ensure_fields{…}, ensure_link{…}, ensure_sprint{…}]}`
|
||||
- **THEN** пакет создан с четырьмя командами (три + `refresh`), ответ содержит оценку в минутах
|
||||
|
||||
#### Scenario: Проект из чата
|
||||
- **WHEN** сессия без привязки, `project_create {label: "STOCKS_DRRK", name: "Запасы ДРРК", board_id: 5120}`
|
||||
- **THEN** проект создан, ответ содержит id пакета обновления и фрагмент `.mcp.json` с `"--project", "STOCKS_DRRK"`
|
||||
|
||||
#### Scenario: Ошибка с файлом из чата
|
||||
- **WHEN** `batch_create {title: "Ошибка ОСВ 60", commands: [ensure_issue{temp_key: new:1, issue_type: bug, …},
|
||||
ensure_attachment{key: new:1, path: "/Users/…/ОСВ сч. 60 за 2 кв. 2026.xlsx"}]}`
|
||||
- **THEN** файл загружен в хаб; пакет создан с `ensure_attachment {key: new:1, file: <sha>, filename: "ОСВ сч. 60 за 2 кв.
|
||||
2026.xlsx"}` и хвостовым `refresh`
|
||||
|
||||
#### Scenario: Файла нет
|
||||
- **WHEN** тот же вызов, файла по `path` нет
|
||||
- **THEN** ответ «файл не найден: <path>», пакет не создан, в хаб ничего не загружено
|
||||
|
||||
#### Scenario: Ожидание результата
|
||||
- **WHEN** `batch_wait {id, timeout_minutes: 10}` и пакет завершился через 6 минут
|
||||
- **THEN** ответ через 6 минут со статусами всех команд
|
||||
|
||||
#### Scenario: Статус агента с довыдачей
|
||||
- **WHEN** агент на связи, команда 40 выдана дважды и ещё без отчёта
|
||||
- **THEN** ответ `agent_status` содержит «агент онлайн» и строку о команде 40 с числом выдач 2
|
||||
|
||||
#### Scenario: Отмена пакета с командой у агента
|
||||
- **WHEN** `batch_cancel {id: 9}`, команда 30 пакета в `claimed`
|
||||
- **THEN** ответ содержит вид пакета и строку «команда #30 уже у агента и будет выполнена; остановить — …»
|
||||
|
||||
#### Scenario: Статус агента после отката
|
||||
- **WHEN** опубликована v7, агент v6 под запускателем v1 прислал `update_state = rolled_back` для v7
|
||||
- **THEN** ответ `agent_status` содержит «публикация: v7 от …», «обновление: rolled_back v6 → v7», «запускатель: v1» и
|
||||
«откат с v7 на v6: …»
|
||||
|
||||
#### Scenario: Статус агента сразу после перезапуска хаба
|
||||
- **WHEN** хаб перезапущен, агент ещё не прислал `claim`
|
||||
- **THEN** ответ `agent_status` содержит «запускатель: неизвестно (агент ещё не выходил на связь после перезапуска
|
||||
хаба)» и не содержит «без запускателя» и «агент запущен без run.py»
|
||||
|
||||
#### Scenario: Задача с семью комментариями
|
||||
- **WHEN** `issue {key: "AURORA-9001"}`, у задачи 7 комментариев, у самого нового текст 500 символов
|
||||
- **THEN** ответ содержит «комментарии (7):», 5 последних комментариев с номерами «#<comment_id>» (самый новый — первым, обрезан до 300 символов с «…») и «ещё 2»
|
||||
|
||||
#### Scenario: Комментарий целиком
|
||||
- **WHEN** у задачи `AURORA-9001` комментарий `#101` длиной 800 символов с переносами строк, вызван `comment {key: "AURORA-9001", comment_id: "101"}`
|
||||
- **THEN** ответ — «AURORA-9001 #101 <автор>, создан <created>» и полный текст комментария без обрезки, переносы строк сохранены
|
||||
|
||||
#### Scenario: Нет такого комментария
|
||||
- **WHEN** `comment {key: "AURORA-9001", comment_id: "999"}`, у задачи нет комментария 999
|
||||
- **THEN** ответ «у AURORA-9001 нет комментария #999»
|
||||
|
||||
### Requirement: Привязка сессии к проекту
|
||||
`sdlc-hub mcp <модуль> --project <метка>` SHALL привязывать сервер MCP к проекту. Привязка SHALL проверяться при каждом вызове
|
||||
инструмента по `GET /projects`: метки нет — ответ «проект <метка> не заведён в хабе: project_create или исправьте
|
||||
--project в .mcp.json» (кроме `projects`, `project_create`).
|
||||
|
||||
В привязанной к проекту P сессии:
|
||||
- `issues` — `GET /issues?project=P` с остальными фильтрами;
|
||||
- `issue`, `comment` — любая задача по ключу;
|
||||
- `notes_set` — только своя задача P (метка P; соседи — нет) или временный ключ `new:<n>` (его задача создаётся пакетом
|
||||
проекта), иначе ответ «<key> не в проекте P»;
|
||||
- `batch_create`, `command_retry` — с `project = P` (проверки пакета проекта, см. `commands`; повтор команды другого
|
||||
проекта или пакета без проекта — отказ хаба);
|
||||
- `batches` — `GET /batches?project=P`; `batch_status`, `batch_wait`, `batch_cancel` — по id, без ограничения;
|
||||
- `refresh` — `full` — общий полный прогон всех проектов, `keys` — как раньше.
|
||||
|
||||
Без привязки инструменты `issues`, `issue`, `comment`, `notes_set`, `batch_create`, `command_retry`, `refresh` SHALL отвечать
|
||||
«сессия не привязана к проекту: запустите MCP с --project <метка> (список — projects)» и в хаб не ходить; `projects`,
|
||||
`project_create`, `agent_status`, `sync_log`, `settings_get`, `settings_set`, `batches`, `batch_status`, `batch_wait`,
|
||||
`batch_cancel` работают.
|
||||
|
||||
#### Scenario: Сессия без проекта
|
||||
- **WHEN** MCP запущен без `--project`, вызван `issues`
|
||||
- **THEN** ответ «сессия не привязана к проекту: …», запросов к `GET /issues` нет
|
||||
|
||||
#### Scenario: Задачи своего проекта
|
||||
- **WHEN** MCP запущен с `--project STOCKS_DRRK`, вызван `issues`
|
||||
- **THEN** в таблице только свои задачи `STOCKS_DRRK` и их соседи
|
||||
|
||||
#### Scenario: Создание задачи без метки
|
||||
- **WHEN** сессия `STOCKS_DRRK`, `batch_create {commands: [ensure_issue {temp_key: new:1, summary: "…"}]}` без `labels`
|
||||
- **THEN** пакет поставлен с `project = STOCKS_DRRK`, у `ensure_issue` `labels = ["STOCKS_DRRK"]`
|
||||
|
||||
#### Scenario: Опечатка в проекте
|
||||
- **WHEN** MCP запущен с `--project STOKS`, такого проекта нет; вызван `issues`
|
||||
- **THEN** ответ «проект STOKS не заведён в хабе: …»
|
||||
|
||||
### Requirement: Вложения в MCP
|
||||
`issue` SHALL после комментариев выводить «вложения (N):» и по строке на каждое вложение (новые сверху): «#<id> <имя>
|
||||
(<размер>) — <автор>, <created>», дальше через запятую: «в описании», если на вложение ссылается описание, «в #<comment_id>»
|
||||
для каждого ссылающегося комментария, «скачано», если `state = ready`, «больше 10 МБ — только в Jira» для таких файлов;
|
||||
без вложений — «вложений нет». Размер — в байтах, КБ или МБ с одним знаком после запятой («512 Б», «234,4 КБ», «2,3 МБ»);
|
||||
неизвестный — «размер неизвестен».
|
||||
|
||||
`attachment` SHALL:
|
||||
1. найти вложение по `GET /issues/{key}`: по номеру, иначе по точному имени (несколько с одним именем — самое новое, в
|
||||
ответ строка «у <key> N вложений «<имя>», взято самое новое #<id>»); не найдено — «у <key> нет вложения «<значение>»;
|
||||
список — issue <key>», в хаб больше не ходить;
|
||||
2. `POST /attachments/{id}/fetch` с автором `claude`; 413 — «файл больше 10 МБ (<размер>) — скачайте в Jira:
|
||||
задача <key>, вложение «<имя>»»; 409 и прочие ошибки хаба — их текст;
|
||||
3. пока `state = fetching` — опрашивать `GET /attachments/{id}` раз в 2 с, не дольше `wait_seconds`; не дождался —
|
||||
«скачивание ещё идёт (команда #<command_id>, агент <онлайн/офлайн>); вызовите attachment ещё раз — вторая команда не
|
||||
поставится»; `failed` — «не удалось скачать: <error>»;
|
||||
4. при `ready` — `GET /attachments/{id}/content` и записать файл в `<каталог данных хаба>/downloads/<id>/<имя>`, где имя —
|
||||
`filename` с заменой `/`, `\`, `:` и управляющих символов на `_` (пустое — `attachment-<id>`); каталог с правами
|
||||
`0700`; запись через временный файл и переименование; если там уже лежит файл того же размера — не перезаписывать;
|
||||
5. ответить «<абсолютный путь>» отдельной строкой и под ним «<имя> (<размер>), <mime_type>, задача <key>».
|
||||
|
||||
#### Scenario: Скриншот из комментария
|
||||
- **WHEN** `comment {key: "AURORA-1650", comment_id: "101"}` показал «вложения: #31337 скрин 1.png (234,4 КБ)»; вызван
|
||||
`attachment {key: "AURORA-1650", attachment: "#31337"}`, агент онлайн, файла у хаба нет
|
||||
- **THEN** поставлена `fetch_attachment`; через несколько секунд ответ — путь
|
||||
`…/sdlc-hub/downloads/31337/скрин 1.png` и строка «скрин 1.png (234,4 КБ), image/png, задача AURORA-1650»; файл по пути
|
||||
читается
|
||||
|
||||
#### Scenario: Повторный вызов
|
||||
- **WHEN** тот же вызов второй раз, файл у хаба (`ready`) и в `downloads/31337/`
|
||||
- **THEN** ответ сразу, команд в очереди не прибавилось
|
||||
|
||||
#### Scenario: Агент не успел
|
||||
- **WHEN** `attachment {…, wait_seconds: 30}`, агент занят долгим `refresh`
|
||||
- **THEN** через 30 с ответ «скачивание ещё идёт (команда #N, агент онлайн); вызовите attachment ещё раз — вторая
|
||||
команда не поставится»
|
||||
|
||||
#### Scenario: По имени
|
||||
- **WHEN** `attachment {key: "AURORA-1650", attachment: "ОСВ 60.xlsx"}`, вложение с таким именем одно
|
||||
- **THEN** скачивается оно; путь оканчивается на `/ОСВ 60.xlsx`
|
||||
|
||||
#### Scenario: Вложения в карточке
|
||||
- **WHEN** `issue {key: "AURORA-1650"}`, у задачи вложения #31337 «скрин 1.png» (на него ссылается комментарий 101,
|
||||
скачано) и #31338 «запись.mp4» на 25 МБ
|
||||
- **THEN** в ответе «вложения (2):», строка #31338 с «больше 10 МБ — только в Jira», строка #31337 с «в #101, скачано»; у
|
||||
комментария 101 строка «файлы: #31337 скрин 1.png»
|
||||
|
||||
### Requirement: MCP вики
|
||||
`sdlc-hub mcp wiki [--project <метка>]` SHALL запускать по stdio MCP-сервер модуля `wiki` с именем `sdlc-wiki`,
|
||||
ходящий в HTTP API хаба клиентским токеном; хаб недоступен — каждый инструмент отвечает «хаб не запущен: sdlc-hub serve».
|
||||
Привязка к проекту проверяется так же, как у сервера jira («Привязка сессии к проекту»). Инструменты:
|
||||
|
||||
| Инструмент | Параметры | Ответ |
|
||||
|---|---|---|
|
||||
| `projects` | — | таблица: метка, название, корень вики, страниц, последняя синхронизация; текущий проект отмечен |
|
||||
| `project_create` | `label`, `name`, `wiki_root` (необязательно) | `POST /projects` с автором `claude`; проект и фрагмент `.mcp.json` с записью `sdlc-wiki` |
|
||||
| `root_set` | `root` | `PATCH /projects/<текущий> {wiki_root}`; новый корень и id пакета синхронизации |
|
||||
| `tree` | `code` (необязательно), `depth` (по умолчанию 2) | дерево от корня или от `code`: отступ, код, название, размер |
|
||||
| `page` | `code` или `title`, `offset` (по умолчанию 0) | шапка (код, путь в дереве, адрес в Eva, дата синхронизации, вложения с `id`) и Markdown с `offset` до 20 000 символов, в конце — «продолжение: offset=<N>», если текст длиннее; `title` ищется точно, иначе — до 10 кандидатов с кодами |
|
||||
| `search` | `query`, `limit` (по умолчанию 10) | страницы: код, название, путь, фрагмент |
|
||||
| `attachment` | `id` | ставит скачивание, ждёт до 60 с; готово — путь к копии `downloads/wiki-<id>/<имя>`; иначе состояние и подсказка повторить |
|
||||
| `sync` | `wait` (по умолчанию `false`) | ставит синхронизацию (или сообщает о текущей) и id пакета; с `wait` — ждёт до 300 с и отдаёт итог |
|
||||
| `status` | — | корень, последняя синхронизация, ошибка, пакет в работе, агент `wiki`: онлайн/офлайн, версия, публикация, обновление, запускатель (как `agent_status` сервера jira) |
|
||||
|
||||
Инструменты, кроме `projects` и `project_create`, SHALL требовать привязки к проекту. В привязанной сессии без корня
|
||||
(кроме `root_set`, `status`) ответ SHALL быть «у проекта <метка> нет корня вики: root_set DOC-…»; с корнем, но без
|
||||
синхронизаций — «вики проекта ещё не синхронизирована: sync». Набор инструментов и их описания у сервера `sdlc-jira`
|
||||
(модуль jira) SHALL остаться прежним.
|
||||
|
||||
#### Scenario: Поиск из сессии awp-ca
|
||||
- **WHEN** сервер запущен с `--project AWPCA`, копия синхронизирована, вызван `search {query: "статус согласован"}`
|
||||
- **THEN** ответ — страницы с кодами и фрагментами; запрос в хаб — `GET /wiki/search?project=AWPCA&q=…`
|
||||
|
||||
#### Scenario: Длинная страница
|
||||
- **WHEN** `page {code: DOC-3}`, Markdown 45 000 символов
|
||||
- **THEN** первые 20 000 символов и «продолжение: offset=20000»; `page {code: DOC-3, offset: 40000}` — остаток без строки
|
||||
продолжения
|
||||
|
||||
#### Scenario: Корня нет
|
||||
- **WHEN** сессия `sfm_kedr` без корня, вызван `tree`
|
||||
- **THEN** ответ «у проекта sfm_kedr нет корня вики: root_set DOC-…», запроса `GET /wiki/tree` нет
|
||||
|
||||
#### Scenario: Сервер jira не изменился
|
||||
- **WHEN** запущен `sdlc-hub mcp jira --project AWPCA`
|
||||
- **THEN** имя сервера `sdlc-jira`, список инструментов и описания совпадают с эталоном до change
|
||||
39
openspec/changes/sdlc-rename/specs/migration/spec.md
Normal file
39
openspec/changes/sdlc-rename/specs/migration/spec.md
Normal file
@ -0,0 +1,39 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Инициализация настроек
|
||||
`sdlc-hub init --config <admin/jira-sync/config.example.json>` SHALL перенести в `setting`: `project_key`, типы задач,
|
||||
`epic_name_field_id`, `fields`, `epic_link`, `priority_map`, `labels`, `status_labels`, `kind_labels`, `comment_marker`,
|
||||
`pacing` (в новую форму по типам команд), `transitions.map`, `link_types`; сгенерировать два токена; завести проект
|
||||
`AWPCA` «АРМ КА» с `board_id` из `sprints.board_id` конфига (если проекта `AWPCA` ещё нет). Поля `auth`, `base_url`,
|
||||
`verify_ssl`, `ca_bundle` MUST NOT переноситься (они у агента).
|
||||
|
||||
#### Scenario: Повторный init
|
||||
- **WHEN** `init` вызван на уже инициализированной базе
|
||||
- **THEN** настройки не перезаписываются без `--force`, токены сохраняются; проект `AWPCA` не дублируется
|
||||
|
||||
#### Scenario: Первый init
|
||||
- **WHEN** `init --config` на пустой базе, в конфиге `sprints.board_id = 4216`
|
||||
- **THEN** есть проект `AWPCA` «АРМ КА» с `board_id = 4216`; в `setting` нет `base_jql` и `board_id`
|
||||
|
||||
### Requirement: Импорт CSV-пакета
|
||||
`sdlc-hub import <папка admin/jira-sync>` SHALL прочитать `tasks.csv`, `new_tasks.csv`, `epics.csv` и заполнить
|
||||
`issue_note` по ключу Jira: статус по факту, вид работы, что сделано, что осталось, внутренние колонки. Строки
|
||||
`new_tasks.csv` без ключа SHALL получать временный ключ `new:<номер строки>`; строки `tasks.csv` и `epics.csv` без ключа
|
||||
SHALL пропускаться. `links.csv` SHALL импортироваться как сверочный набор в отчёт импорта (ожидаемые связи против зеркала
|
||||
после первого `refresh`), не как команды. Связь, которой в зеркале нет как `source→target` того же типа, но есть как
|
||||
`target→source` того же типа, SHALL попадать в `reversed_links`, а не в `missing_links`. Импорт идемпотентен: повтор
|
||||
обновляет заметки, не дублирует.
|
||||
|
||||
#### Scenario: Импорт и первая синхронизация
|
||||
- **WHEN** выполнены `init`, `import`, запущены хаб и агент, `refresh full` завершён
|
||||
- **THEN** число задач зеркала ≥ числа строк `tasks.csv` + созданных из `new_tasks.csv`; отчёт импорта перечисляет ключи
|
||||
из CSV, которых нет в зеркале, связи из `links.csv`, которых нет в `issue_link`, и связи, которые есть в обратную сторону
|
||||
|
||||
#### Scenario: Связь в обратную сторону
|
||||
- **WHEN** в `links.csv` строка «AURORA-1445 → AURORA-1446, связанные», а в зеркале есть только «ASUPR_Связанные»
|
||||
1446→1445
|
||||
- **THEN** связь в `reversed_links`, в `missing_links` её нет
|
||||
|
||||
#### Scenario: Эпик без ключа
|
||||
- **WHEN** в `epics.csv` строка без значения в колонке «Ключ»
|
||||
- **THEN** строка пропущена, заметка `new:*` для неё не создаётся
|
||||
169
openspec/changes/sdlc-rename/specs/web-app/spec.md
Normal file
169
openspec/changes/sdlc-rename/specs/web-app/spec.md
Normal file
@ -0,0 +1,169 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Вход по токену
|
||||
Приложение SHALL работать с API хаба того же хоста под клиентским токеном: хранить его в `localStorage` под ключом
|
||||
`sdlc-hub-token` (и в памяти, если хранилище закрыто) и передавать в заголовке `Authorization: Bearer` каждого запроса.
|
||||
Если токена нет или любой запрос ответил 401, приложение SHALL показывать экран входа вместо страниц: поле токена с
|
||||
подсказкой `uv run sdlc-hub tokens`, без панелей страниц и без фоновых запросов, которые могли бы изменить поле ввода.
|
||||
Введённый токен SHALL проверяться запросом `GET /settings` и сохраняться только при успехе. Действие «Сменить токен»
|
||||
SHALL очищать токен и показывать экран входа. Ответ 401 SHALL выводить на экран входа, только если запрос ушёл с текущим токеном: запоздавший 401 на
|
||||
запрос с прежним токеном после входа с новым не учитывается. Если проверка токена получила ответ 4xx, кроме 401,
|
||||
экран входа SHALL показывать текст ошибки хаба, а не «хаб не отвечает».
|
||||
|
||||
Ключи `localStorage` приложения с префиксом `sdlc-hub-` (`sdlc-hub-token`, `sdlc-hub-sidebar`, `sdlc-hub-legend`) SHALL
|
||||
при первом чтении, если ключа нет, брать значение одноимённого ключа с префиксом `jira-hub-`, записывать его под новым
|
||||
ключом и удалять прежний.
|
||||
|
||||
#### Scenario: Первый вход
|
||||
- **WHEN** приложение открыто впервые, токена нет
|
||||
- **THEN** показан экран входа; после ввода верного токена открывается запрошенная страница, токен сохранён
|
||||
|
||||
#### Scenario: Устаревший токен
|
||||
- **WHEN** сохранённый токен не подходит и запрос ответил 401
|
||||
- **THEN** показан экран входа с пояснением, что токен не подошёл; панели доски и задач не видны
|
||||
|
||||
#### Scenario: Токен от прежней доски
|
||||
- **WHEN** в `localStorage` нет `sdlc-hub-token`, но есть `jira-hub-token` (сохранён до переименования хаба)
|
||||
- **THEN** приложение входит с ним без запроса токена; значение перенесено в `sdlc-hub-token`, `jira-hub-token` удалён
|
||||
|
||||
#### Scenario: Запоздавший 401 от прежнего токена
|
||||
- **WHEN** запрос ушёл с прежним токеном, пользователь вошёл с новым, и после этого запрос ответил 401
|
||||
- **THEN** приложение остаётся на странице, новый токен сохранён
|
||||
|
||||
#### Scenario: Хаб отверг проверку токена
|
||||
- **WHEN** проверка `GET /settings` ответила 400 с `detail`
|
||||
- **THEN** экран входа показывает текст `detail`, а не «хаб не отвечает»
|
||||
|
||||
### Requirement: Оболочка и адреса
|
||||
Приложение SHALL открываться под префиксом `/app/` и иметь страницы `/app/board` (доска связей), `/app/issues` (список
|
||||
задач), `/app/feed` (лента изменений, требование «Лента изменений») и `/app/issues/{key}` (карточка задачи — окно поверх страницы, требование «Карточка задачи»); `/app` и `/app/` SHALL вести на `/app/board`, неизвестный путь —
|
||||
на страницу «не найдено» со ссылками на разделы. Боковая панель SHALL быть закреплена у левого края окна на всю его
|
||||
высоту и MUST NOT прокручиваться вместе со страницей; страница занимает остальную ширину. Сверху вниз панель SHALL
|
||||
содержать: название «SDLC Hub» и кнопку «Свернуть панель»; выпадающий список проектов («<метка> — <название>», текущий
|
||||
отмечен, в конце «+ Новый проект…»; требование «Проект в приложении»); разделы «Доска связей», «Задачи» и «Лента» — вертикальным
|
||||
списком ссылок со значками (текущий отмечен `aria-current="page"`); внизу — блок данных Jira с действием «Обновить из
|
||||
Jira» (требование «Обновление из Jira»), переключатель темы (требование «Тема») и меню с действием «Сменить токен».
|
||||
Свёрнутая панель SHALL быть узкой полосой с теми же действиями значками: «Развернуть панель», метка текущего проекта,
|
||||
разделы, «Обновить из Jira» с точкой состояния агента, тема, меню; названия — в подсказках и `aria-label`. Свёртка
|
||||
SHALL храниться в `localStorage` под ключом `sdlc-hub-sidebar` (`collapsed`; без записи — развёрнута). На экранах уже
|
||||
640 px панели в потоке нет: сверху SHALL быть закреплена строка с кнопкой «Меню», названием и кнопкой «Обновить из
|
||||
Jira»; «Меню» открывает развёрнутую панель поверх страницы, панель закрывается выбором раздела, клавишей Escape или
|
||||
щелчком мимо неё. Горизонтальной прокрутки страницы на любой ширине нет.
|
||||
Неожиданная ошибка отрисовки страницы SHALL показываться сообщением с кнопкой «Перезагрузить», не пустым экраном.
|
||||
|
||||
#### Scenario: Переход между разделами
|
||||
- **WHEN** на доске в боковой панели выбран раздел «Задачи»
|
||||
- **THEN** адрес — `/app/issues` с текущим проектом, показан список задач; «назад» возвращает на доску
|
||||
|
||||
#### Scenario: Раздел «Лента»
|
||||
- **WHEN** на доске в боковой панели выбран раздел «Лента»
|
||||
- **THEN** адрес — `/app/feed` с текущим проектом, показана лента изменений за сегодня; раздел отмечен в панели
|
||||
|
||||
#### Scenario: Сменить токен
|
||||
- **WHEN** в меню боковой панели выбрано «Сменить токен»
|
||||
- **THEN** токен очищен, показан экран входа
|
||||
|
||||
#### Scenario: Панель на месте при прокрутке
|
||||
- **WHEN** доска прокручена до самого низа
|
||||
- **THEN** боковая панель видна целиком на прежнем месте: разделы и «Обновить из Jira» доступны без прокрутки вверх
|
||||
|
||||
#### Scenario: Свёрнутая панель
|
||||
- **WHEN** нажата «Свернуть панель», страница перезагружена
|
||||
- **THEN** панель свёрнута в полосу значков, доска шире; значок «Задачи» открывает список задач; «Развернуть панель»
|
||||
возвращает полную панель
|
||||
|
||||
#### Scenario: Узкий экран
|
||||
- **WHEN** ширина окна 390 px, нажата «Меню», в панели выбран раздел «Задачи»
|
||||
- **THEN** панель открылась поверх страницы, после выбора закрыта, показан список задач; горизонтальной прокрутки нет
|
||||
|
||||
### Requirement: Доска связей
|
||||
Страница `/app/board` SHALL воспроизводить поведение доски связей хаба (`links.html` ревизии 3931a88) без изменений,
|
||||
кроме перечисленных в этом требовании и в требовании «Источник данных доски»: построение модели из `GET /board`, CSV
|
||||
фильтра Jira и JSON доски (`arm-ka-links/1`); дорожки по эпикам со свёрткой и перестановкой, служебные дорожки внизу;
|
||||
карточки с перетаскиванием, растяжкой, размером по содержимому и привязкой к сетке; стрелки связей с выравниванием;
|
||||
фильтры по спринтам и исполнителям; легенда со скрытием видов работ и типов связей; «соседи по связям», «только
|
||||
передачи», «скрыть без связей», «критический путь», «по датам» с колонками дат и их растяжкой; ящик «Проблемы»; таблица
|
||||
связей; экспорт и импорт JSON; «Сбросить раскладку». Раскладка, дорожки и фильтры SHALL храниться в
|
||||
`localStorage` под теми же ключами `graph-<имя>…`, что у `links.html`, с той же миграцией старых ключей.
|
||||
Результат построения модели, видимой выборки, критического пути и проблем SHALL совпадать с результатом `links.html`
|
||||
на одних и тех же данных и фильтрах.
|
||||
|
||||
Шапка доски (закреплена при прокрутке) SHALL содержать заголовок «Доска связей», счётчики (задачи из всех, соседи по
|
||||
связям, связи из всех, скрытые без связей), пометку источника, если на доске снимок из файла (с действием вернуться к
|
||||
данным хаба), событие доски, кнопку «Проблемы» со счётчиком и меню с действиями «Перечитать», «Загрузить файл…»,
|
||||
«Экспорт JSON», «Таблица связей», «Привязка к сетке» и «Сбросить раскладку» (с подтверждением). Под шапкой — панель
|
||||
управления: мультиселекты «Спринты» и «Исполнители», переключатели группы «Показать» («Соседи по связям», «Только
|
||||
передачи», «Скрыть без связей») и группы «Режим» («Критический путь», «По датам»), каждый с `aria-pressed` и прежней
|
||||
подсказкой. Под панелью — легенда, которая сворачивается кнопкой «Легенда»; свёрнутая показывает число скрытых видов
|
||||
работ и типов связей; состояние свёртки хранится в `localStorage` под ключом `sdlc-hub-legend`. «Обновить из Jira»,
|
||||
тема и «Сменить токен» — в боковой панели приложения, на доске их нет.
|
||||
|
||||
У карточки задачи (не у заглушки) SHALL быть значок «Открыть карточку»,
|
||||
открывающий `/app/issues/{key}` окном поверх доски; ключ карточки по-прежнему открывает задачу в Jira. Пока окно открыто
|
||||
и после его закрытия доска SHALL сохранять прокрутку, раскладку, фильтры и строку состояния. Ещё два отличия от `links.html` (R-10, R-18): двойной щелчок по уголку
|
||||
карточки меряет высоту по содержимому при новой ширине, а не при прежней; указатель при перетаскивании и растяжке
|
||||
захватывается на нажатом элементе, а не на карточке, поэтому двойной щелчок по уголку сбрасывает размер. Загрузка файла CSV/JSON доступна только после входа, с экрана входа её нет.
|
||||
Карточка задачи, статус Jira которой входит в `closed_statuses`, SHALL быть приглушена (полупрозрачна), под
|
||||
указателем и с фокусом клавиатуры внутри карточки — в полную яркость; стрелка связи, у которой хотя бы один конец — такая задача, SHALL быть приглушена так же.
|
||||
Приглушение — только вид: модель, видимая выборка, критический путь, проблемы, раскладка и экспорт JSON от него не
|
||||
зависят; переключателя нет; правило одно для данных хаба, CSV и JSON.
|
||||
При возврате на `/app/board` с другой страницы приложения в той же вкладке доска SHALL восстанавливать прежнюю
|
||||
прокрутку; после перезагрузки страницы доска открывается сверху.
|
||||
|
||||
Дорожки SHALL разделяться линией; фон развёрнутых дорожек один, без чередования, чтобы подсветка колонок «по датам»
|
||||
(прошедшие даты, сегодня) была видна на всех дорожках; свёрнутая дорожка выделяется фоном. У карточки задачи, статус
|
||||
Jira которой не входит в `closed_statuses`, с плановой датой окончания раньше сегодняшней дата «до …» SHALL быть цветом
|
||||
ошибки, с подсказкой и текстом для экранного чтения «просрочено»; это только вид, модель и раскладка от него не зависят.
|
||||
|
||||
#### Scenario: Раскладка переживает переход
|
||||
- **WHEN** раскладка карточек сохранена страницей `links.html`, открыта `/app/board` с теми же данными
|
||||
- **THEN** карточки стоят на тех же местах, дорожки в том же порядке и свёртке, фильтры те же
|
||||
|
||||
#### Scenario: Совпадение со старой доской
|
||||
- **WHEN** на одной фикстуре `GET /board` и одном наборе фильтров построены модель, видимая выборка, критический путь
|
||||
и проблемы старой страницей и приложением
|
||||
- **THEN** результаты совпадают
|
||||
|
||||
#### Scenario: Открыть карточку
|
||||
- **WHEN** на карточке AURORA-1620 нажат значок «Открыть карточку»
|
||||
- **THEN** адрес `/app/issues/AURORA-1620`, карточка задачи открыта окном, под ним доска
|
||||
|
||||
#### Scenario: Возврат с карточки
|
||||
- **WHEN** доска прокручена вниз, нажат значок «Открыть карточку», затем окно закрыто
|
||||
- **THEN** адрес `/app/board`, доска показана с той же прокруткой и раскладкой, без повторной загрузки `GET /board`
|
||||
|
||||
#### Scenario: Возврат из другого раздела
|
||||
- **WHEN** доска прокручена вниз, выбран раздел «Задачи», затем раздел «Доска связей»
|
||||
- **THEN** доска показана с той же прокруткой
|
||||
|
||||
#### Scenario: Закрытые на доске приглушены
|
||||
- **WHEN** на доске AURORA-1620 «В работе», AURORA-1621 «Готово», AURORA-1622 «Снято», связь AURORA-1620 → AURORA-1621
|
||||
- **THEN** карточки AURORA-1621 и AURORA-1622 и стрелка AURORA-1620 → AURORA-1621 приглушены, AURORA-1620 — нет;
|
||||
под указателем AURORA-1621 — в полную яркость; критический путь и ящик «Проблемы» те же, что без приглушения
|
||||
|
||||
#### Scenario: Свёрнутая легенда
|
||||
- **WHEN** в легенде скрыт вид работы «тестирование», легенда свёрнута, страница перезагружена
|
||||
- **THEN** легенда свёрнута, рядом с кнопкой «Легенда» — «скрыто: 1»; карточек вида «тестирование» на доске нет
|
||||
|
||||
#### Scenario: Просрочка на доске
|
||||
- **WHEN** сегодня 22.10, у AURORA-1620 статус «В работе» и срок 20.10, у AURORA-1621 «Готово» и срок 20.10, включено
|
||||
«По датам»
|
||||
- **THEN** дата AURORA-1620 — цветом ошибки с пометкой «просрочено», у AURORA-1621 — обычная; подсветка колонки 20.10
|
||||
видна на всех дорожках, дорожки одного фона
|
||||
|
||||
### Requirement: Сборка и разработка
|
||||
Приложение SHALL быть npm-проектом в `web/` (React, TypeScript, Vite) с точными версиями зависимостей и
|
||||
`package-lock.json`. `make web` SHALL устанавливать зависимости (`npm ci`) и собирать приложение в
|
||||
`src/sdlc_hub/web/app/`; каталог сборки не хранится в git. Шрифты (Mulish — текст интерфейса, IBM Plex Mono — ключи
|
||||
задач и исходники), иконки и прочие ресурсы страницы SHALL входить в сборку: приложение не обращается к внешним
|
||||
адресам. `make check` SHALL запускать `uv run pytest`, проверку типов и линтер фронта и тесты фронта; зелёным считается
|
||||
только весь набор. `npm run dev` SHALL поднимать сервер разработки с прокси маршрутов API на хаб из `SDLC_HUB_URL` (по
|
||||
умолчанию тестовый `http://127.0.0.1:8791`).
|
||||
|
||||
#### Scenario: Сборка с нуля
|
||||
- **WHEN** в чистом checkout выполнен `make web`
|
||||
- **THEN** в `src/sdlc_hub/web/app/` есть `index.html` и `assets/`; `git status` не показывает файлов сборки
|
||||
|
||||
#### Scenario: Без интернета
|
||||
- **WHEN** приложение открыто на машине без доступа в интернет
|
||||
- **THEN** страница не запрашивает ресурсов вне хаба, текст набран Mulish, ключи задач — IBM Plex Mono
|
||||
33
openspec/changes/sdlc-rename/specs/wiki/spec.md
Normal file
33
openspec/changes/sdlc-rename/specs/wiki/spec.md
Normal file
@ -0,0 +1,33 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Markdown-копия вики
|
||||
Копия проекта SHALL лежать в `<SDLC_HUB_HOME>/wiki/<метка>/` (каталоги 0700): у каждой страницы — каталог
|
||||
`NN_<название>` внутри каталога родителя (`NN` — двузначная позиция среди соседей по `orderno`, затем по коду; у корня
|
||||
номера нет; название без символов `<>:"/\|?*` и управляющих, обрезано до 80 символов; совпадение у соседей —
|
||||
суффикс `_2`, `_3`), в нём `index.md`: frontmatter `code`, `title`, `url`, `synced_at`, строка `# <title>` и текст. В
|
||||
корне копии — `_tree.json`, список `{code, title, depth, path}` в порядке обхода. Копия SHALL собираться рядом
|
||||
(`wiki/.<метка>.new/`) и подменять прежнюю переименованием; незавершённую подмену хаб SHALL доводить при старте.
|
||||
|
||||
HTML страницы SHALL переводиться в Markdown (заголовки `#`); ссылка или картинка на файл Eva (по списку `attachments` агента: адрес на хосте Eva
|
||||
с путём `/files/…`, относительный адрес — тоже Eva; миниатюра `<адрес>.meta/thumbnail.jpg` означает файл `<адрес>`, а
|
||||
ссылки на `static/…` — значки интерфейса — из текста удаляются) SHALL заменяться на
|
||||
`[📎 <имя>](attachment:<id>)`, где `id` — строка вложения; ссылка на документ Eva с кодом `DOC-<цифры>` (в Eva — относительный
|
||||
`project/Document/DOC-…`) — на `[<текст>](page:<код>)`; прочие ссылки остаются. Сырой HTML в копию не попадает.
|
||||
|
||||
#### Scenario: Раскладка
|
||||
- **WHEN** корень DOC-1 «АРМ КА», дети DOC-3 «Статусы» (`orderno` 1) и DOC-2 «Роли/права» (`orderno` 2)
|
||||
- **THEN** `wiki/AWPCA/АРМ КА/index.md`, `…/01_Статусы/index.md`, `…/02_Роли_права/index.md`; `_tree.json` в порядке
|
||||
DOC-1, DOC-3, DOC-2
|
||||
|
||||
#### Scenario: Картинка на странице
|
||||
- **WHEN** в HTML DOC-3 `<img src="/files/obj/CmfDocument/CmfDocument%3Aa08/CmfDocument%3A<uuid>/scheme.png?version=1">`
|
||||
- **THEN** в `index.md` — `[📎 scheme.png](attachment:<id>)`, у вложения `<id>` `state = none`, файл не скачан
|
||||
|
||||
#### Scenario: Прикреплённый файл с миниатюрой
|
||||
- **WHEN** на странице миниатюра `…/<uuid>/model.xlsx.meta/thumbnail.jpg?version=1`, значок `static/attach-file.png` и
|
||||
ссылка на `…/<uuid>/model.xlsx`
|
||||
- **THEN** одно вложение `model.xlsx` с адресом файла; в тексте одна ссылка `[📎 model.xlsx](attachment:<id>)`, значка нет
|
||||
|
||||
#### Scenario: Ссылка на другую страницу
|
||||
- **WHEN** в HTML `<a href="project/Document/DOC-019834">Модель данных</a>`
|
||||
- **THEN** в `index.md` — `[Модель данных](page:DOC-019834)`
|
||||
Loading…
x
Reference in New Issue
Block a user