From b92d5e8582d2166d952abd87a9828846168f8e5b Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 24 Oct 2026 15:58:10 +0400 Subject: [PATCH 1/2] =?UTF-8?q?chore(openspec):=20=D0=B0=D1=80=D1=85=D0=B8?= =?UTF-8?q?=D0=B2=20sdlc-rename,=20Purpose=20=D1=81=D0=BF=D0=B5=D0=BA=20hu?= =?UTF-8?q?b-api,=20mcp,=20web-app=20=D0=BF=D0=BE=D0=B4=20sdlc-hub=20(#97)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../2026-10-24-sdlc-rename}/.openspec.yaml | 0 .../2026-10-24-sdlc-rename}/design.md | 0 .../2026-10-24-sdlc-rename}/proposal.md | 0 .../specs/data-model/spec.md | 0 .../specs/hub-api/spec.md | 0 .../2026-10-24-sdlc-rename}/specs/mcp/spec.md | 0 .../specs/migration/spec.md | 0 .../specs/web-app/spec.md | 0 .../specs/wiki/spec.md | 0 openspec/specs/data-model/spec.md | 4 +- openspec/specs/hub-api/spec.md | 75 +++++++++++++++---- openspec/specs/mcp/spec.md | 47 ++++++------ openspec/specs/migration/spec.md | 4 +- openspec/specs/web-app/spec.md | 26 ++++--- openspec/specs/wiki/spec.md | 2 +- 15 files changed, 108 insertions(+), 50 deletions(-) rename openspec/changes/{sdlc-rename => archive/2026-10-24-sdlc-rename}/.openspec.yaml (100%) rename openspec/changes/{sdlc-rename => archive/2026-10-24-sdlc-rename}/design.md (100%) rename openspec/changes/{sdlc-rename => archive/2026-10-24-sdlc-rename}/proposal.md (100%) rename openspec/changes/{sdlc-rename => archive/2026-10-24-sdlc-rename}/specs/data-model/spec.md (100%) rename openspec/changes/{sdlc-rename => archive/2026-10-24-sdlc-rename}/specs/hub-api/spec.md (100%) rename openspec/changes/{sdlc-rename => archive/2026-10-24-sdlc-rename}/specs/mcp/spec.md (100%) rename openspec/changes/{sdlc-rename => archive/2026-10-24-sdlc-rename}/specs/migration/spec.md (100%) rename openspec/changes/{sdlc-rename => archive/2026-10-24-sdlc-rename}/specs/web-app/spec.md (100%) rename openspec/changes/{sdlc-rename => archive/2026-10-24-sdlc-rename}/specs/wiki/spec.md (100%) diff --git a/openspec/changes/sdlc-rename/.openspec.yaml b/openspec/changes/archive/2026-10-24-sdlc-rename/.openspec.yaml similarity index 100% rename from openspec/changes/sdlc-rename/.openspec.yaml rename to openspec/changes/archive/2026-10-24-sdlc-rename/.openspec.yaml diff --git a/openspec/changes/sdlc-rename/design.md b/openspec/changes/archive/2026-10-24-sdlc-rename/design.md similarity index 100% rename from openspec/changes/sdlc-rename/design.md rename to openspec/changes/archive/2026-10-24-sdlc-rename/design.md diff --git a/openspec/changes/sdlc-rename/proposal.md b/openspec/changes/archive/2026-10-24-sdlc-rename/proposal.md similarity index 100% rename from openspec/changes/sdlc-rename/proposal.md rename to openspec/changes/archive/2026-10-24-sdlc-rename/proposal.md diff --git a/openspec/changes/sdlc-rename/specs/data-model/spec.md b/openspec/changes/archive/2026-10-24-sdlc-rename/specs/data-model/spec.md similarity index 100% rename from openspec/changes/sdlc-rename/specs/data-model/spec.md rename to openspec/changes/archive/2026-10-24-sdlc-rename/specs/data-model/spec.md diff --git a/openspec/changes/sdlc-rename/specs/hub-api/spec.md b/openspec/changes/archive/2026-10-24-sdlc-rename/specs/hub-api/spec.md similarity index 100% rename from openspec/changes/sdlc-rename/specs/hub-api/spec.md rename to openspec/changes/archive/2026-10-24-sdlc-rename/specs/hub-api/spec.md diff --git a/openspec/changes/sdlc-rename/specs/mcp/spec.md b/openspec/changes/archive/2026-10-24-sdlc-rename/specs/mcp/spec.md similarity index 100% rename from openspec/changes/sdlc-rename/specs/mcp/spec.md rename to openspec/changes/archive/2026-10-24-sdlc-rename/specs/mcp/spec.md diff --git a/openspec/changes/sdlc-rename/specs/migration/spec.md b/openspec/changes/archive/2026-10-24-sdlc-rename/specs/migration/spec.md similarity index 100% rename from openspec/changes/sdlc-rename/specs/migration/spec.md rename to openspec/changes/archive/2026-10-24-sdlc-rename/specs/migration/spec.md diff --git a/openspec/changes/sdlc-rename/specs/web-app/spec.md b/openspec/changes/archive/2026-10-24-sdlc-rename/specs/web-app/spec.md similarity index 100% rename from openspec/changes/sdlc-rename/specs/web-app/spec.md rename to openspec/changes/archive/2026-10-24-sdlc-rename/specs/web-app/spec.md diff --git a/openspec/changes/sdlc-rename/specs/wiki/spec.md b/openspec/changes/archive/2026-10-24-sdlc-rename/specs/wiki/spec.md similarity index 100% rename from openspec/changes/sdlc-rename/specs/wiki/spec.md rename to openspec/changes/archive/2026-10-24-sdlc-rename/specs/wiki/spec.md diff --git a/openspec/specs/data-model/spec.md b/openspec/specs/data-model/spec.md index ecb9b5d..d16772f 100644 --- a/openspec/specs/data-model/spec.md +++ b/openspec/specs/data-model/spec.md @@ -195,7 +195,7 @@ SHALL обнулить `issues_count` и `links_count` её `sync_run` (`started передачи `board_id` агенту как запасной доски для команд без доски в параметрах (поставленных до миграции); `GET /settings` SHALL отдавать вычисленную `base_jql` только для чтения, а запись `base_jql` или `board_id` через `PUT /settings` — 422 с пояснением. Спринты агент читает с досок проектов, отдельной настройки имён спринтов нет. Начальные значения SHALL импортироваться из `admin/jira-sync/config.example.json` -командой `jira-hub init`; пароль и логин Jira в настройках MUST NOT присутствовать. `issue_types` — объект +командой `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` — как раньше, всегда). @@ -205,7 +205,7 @@ SHALL обнулить `issues_count` и `links_count` её `sync_run` (`started - **THEN** `settings_version` увеличен, следующий `claim` отдаёт агенту новую версию #### Scenario: Конфиг без ключей ошибки -- **WHEN** `jira-hub init --config` с `issue_type_task`, `issue_type_epic`, без `issue_type_bug` +- **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 diff --git a/openspec/specs/hub-api/spec.md b/openspec/specs/hub-api/spec.md index d2a37e5..f06becc 100644 --- a/openspec/specs/hub-api/spec.md +++ b/openspec/specs/hub-api/spec.md @@ -1,16 +1,16 @@ # hub-api Specification ## Purpose -HTTP API хаба `jira-hub serve`: маршруты агента (выдача команд, отчёты, загрузка состояния), маршруты интерфейсов (зеркало, заметки, пакеты, настройки), резервные копии, автообновление зеркала и реле общей папки для агента за VPN. +HTTP API хаба `sdlc-hub serve`: маршруты агента (выдача команд, отчёты, загрузка состояния), маршруты интерфейсов (зеркало, заметки, пакеты, настройки), резервные копии, автообновление зеркала и реле общей папки для агента за VPN. ## Requirements ### Requirement: Процесс и доступ -`jira-hub serve` SHALL запускать один процесс: HTTP на `0.0.0.0:` (по умолчанию 8790), база и настройки в -`~/.local/share/jira-hub/` (переопределяется `JIRA_HUB_HOME`). Все маршруты API SHALL требовать +`sdlc-hub serve` SHALL запускать один процесс: HTTP на `0.0.0.0:` (по умолчанию 8790), база и настройки в +`~/.local/share/sdlc-hub/` (переопределяется `SDLC_HUB_HOME`). Все маршруты API SHALL требовать `Authorization: Bearer `: токен агента модуля открывает только `/agent/*` (маршруты ядра — от имени этого агента, маршруты модуля со scope `agent` — только агенту своего модуля, см. `modules`, «Агенты модулей»), клиентский — всё -остальное. `jira-hub tokens` SHALL печатать клиентский токен и токены агентов с именами модулей. Исключение — +остальное. `sdlc-hub tokens` SHALL печатать клиентский токен и токены агентов с именами модулей. Исключение — раздача веб-приложения (`GET /app`, `GET /app/…`) и перенаправления `GET /`, `GET /links`: это статические файлы без данных, они отдаются без токена. Ответы API — JSON, ошибки — `{error, detail}` с кодами 400 (неверное тело или `Content-Length`), 401, 404, 409, 413 (тело больше предела), 422, 500 (внутренняя ошибка); реле общей папки @@ -30,7 +30,7 @@ JSON. - **THEN** 200, `Content-Type: text/html; charset=utf-8`, тело — `index.html` сборки; данных зеркала в нём нет #### Scenario: Токены после миграции -- **WHEN** база v6 с агентским токеном T мигрировала в v7, запущен `jira-hub tokens` +- **WHEN** база v6 с агентским токеном T мигрировала в v7, запущен `sdlc-hub tokens` - **THEN** вывод содержит клиентский токен, `jira: T` и новый токен `wiki` ### Requirement: Маршруты агента @@ -249,7 +249,7 @@ SHALL скопировать файл базы в `backups/<дата-время> - **THEN** поставлен один `refresh` с JQL `project = AURORA AND labels in ("AWPCA", "STOCKS_DRRK")` ### Requirement: Реле через общую папку -`jira-hub serve` SHALL запускать поток реле вместе с HTTP-сервером. Пока настройка `relay_dir` пуста (`null`), реле +`sdlc-hub serve` SHALL запускать поток реле вместе с HTTP-сервером. Пока настройка `relay_dir` пуста (`null`), реле бездействует; настройку оно SHALL перечитывать не реже раза в 5 с, включение и выключение не требуют перезапуска. Если каталог `relay_dir` существует, реле SHALL создать в нём подкаталоги `req/`, `resp/`, `work/`; если не существует — записать одно предупреждение в лог и проверять снова. @@ -340,10 +340,10 @@ SHALL скопировать файл базы в `backups/<дата-время> - **THEN** 401 ### Requirement: Публикация версии агента -`jira-hub agent publish [--module <модуль>] PATH` SHALL публиковать файл агента модуля (по умолчанию `jira`; +`sdlc-hub agent publish [--module <модуль>] PATH` SHALL публиковать файл агента модуля (по умолчанию `jira`; неизвестный модуль или модуль без команд — код 2 и список модулей с агентами в stderr): текст в UTF-8 не больше 1 МБ, ровно одна строка `AGENT_VERSION = <целое>`, версия больше опубликованной для этого модуля. Файл SHALL записываться в -`/release/<модуль>/agent.py` (каталоги 0700), затем `manifest.json` `{version, sha256, size, +`/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`, удалить временные файлы и @@ -363,17 +363,17 @@ published_at}` или `null`), `update_state`, `launcher_version`, `agent_report (`rolled_back`), «v отвергнута: » (`rejected`), иначе `null`. #### Scenario: Публикация новой версии -- **WHEN** опубликована v5, `jira-hub agent publish jira-agent/src/agent.py` с `AGENT_VERSION = 6` +- **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** `jira-hub agent publish --module wiki eva-agent/src/agent.py` с `AGENT_VERSION = 1`, опубликована jira v14 +- **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** `jira-hub agent publish --module nope agent.py` +- **WHEN** `sdlc-hub agent publish --module nope agent.py` - **THEN** код 2, в stderr «неизвестный модуль nope» и список модулей с агентами; `release/` не изменён #### Scenario: Версия не выше опубликованной @@ -407,7 +407,7 @@ published_at}` или `null`), `update_state`, `launcher_version`, `agent_report - **THEN** публикация v14 лежит в `release/jira/`, `claim` агента `jira` получает `agent_release {version: 14, …}` ### Requirement: Раздача веб-приложения -Хаб SHALL отдавать собранное веб-приложение (спека `web-app`) из каталога пакета `src/jira_hub/web/app/`, читая файлы +Хаб SHALL отдавать собранное веб-приложение (спека `web-app`) из каталога пакета `src/sdlc_hub/web/app/`, читая файлы с диска на каждый запрос: - `GET /app/assets/<путь>` — файл из `assets/` сборки; путь, который после разрешения выходит за `assets/`, не существует или содержит нулевой байт, — 404; `Content-Type` по расширению (`.js` — `text/javascript; charset=utf-8`, `.css` — @@ -416,7 +416,7 @@ published_at}` или `null`), `update_state`, `launcher_version`, `agent_report `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` в каталоге jira-hub; + собрано и нужно выполнить `make web` в каталоге sdlc-hub; - `GET /` и `GET /links` — 302 с `Location: /app/board`. Сборка приложения не хранится в git; её создаёт `make web`. @@ -665,3 +665,52 @@ from_string, to, to_string}`. До записи хаб SHALL проверить #### Scenario: Два вложения с одним именем - **WHEN** у задачи два вложения «скрин.png» (#10 от 14.10, #20 от 20.10); комментарий — «!скрин.png!» - **THEN** у комментария `attachment_ids = ["20"]` + +### 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` и совпадает с `origin/main` (владелец сделал + `git pull --ff-only` прямо перед запуском: скрипт приходит этим pull; неотслеживаемая прежняя сборка `src/jira_hub/` + без tracked-файлов не считается грязью); в Gitea доступен `aep/sdlc-hub` (владелец переименовал репозиторий; адрес + выводится из `origin` заменой `/aep/jira-hub` на `/aep/sdlc-hub`, протокол сохраняется, `CUTOVER_REMOTE` + переопределяет); есть `uv`, `make`, `node`, `npm`, `claude`, `launchctl`; `~/.local/share/sdlc-hub/` и `jira-bridge/sdlc-hub` не существуют; +2. остановить launchd `ru.aepif.jira-hub`, дождаться закрытия порта и переименовать его plist в `*.pre-sdlc`; +3. перенести каталог данных `~/.local/share/jira-hub` → `~/.local/share/sdlc-hub`; +4. перенести checkout `jira-bridge/jira-hub` → `jira-bridge/sdlc-hub`, удалить неотслеживаемый `src/jira_hub`, + `git worktree repair`, `origin` → `aep/sdlc-hub`, пересоздать `.venv` (`uv sync`), `make web`; +5. поставить и запустить launchd `ru.aepif.sdlc-hub` (`uv run --project sdlc-hub serve`, лог + `~/Library/Logs/sdlc-hub.log`) и дождаться ответа хаба на порту из настроек; +6. в проектах-потребителях (по умолчанию `~/projects/awp-ca`, `~/projects/stocks_drrk` и каждый + `~/projects/*/.mcp.json`, где есть `jira-hub`, например `sfm_foxpro`; сухой прогон печатает полный список): в `.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 и каталоги не тронуты diff --git a/openspec/specs/mcp/spec.md b/openspec/specs/mcp/spec.md index 018a6d9..ba132cd 100644 --- a/openspec/specs/mcp/spec.md +++ b/openspec/specs/mcp/spec.md @@ -1,30 +1,35 @@ # mcp Specification ## Purpose -MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструменты чтения зеркала и заметок, постановки и отслеживания пакетов команд, обновления зеркала, статуса агента и настроек — поверх HTTP API хаба с клиентским токеном. +MCP-серверы модулей хаба (`sdlc-hub mcp <модуль>`, stdio) для Claude: инструменты чтения зеркала и заметок, постановки и отслеживания пакетов команд, обновления зеркала, статуса агента и настроек — поверх HTTP API хаба с клиентским токеном. ## Requirements ### Requirement: Сервер и регистрация -`jira-hub mcp [<модуль>] [--project <метка>]` SHALL запускать MCP-сервер модуля хаба по stdio (пакет `mcp`), ходящий в -HTTP API хаба клиентским токеном из настроек. Модуль по умолчанию — `jira`: `jira-hub mcp [--project <метка>]` и -`jira-hub mcp jira [--project <метка>]` SHALL поднимать один и тот же сервер с именем `jira-hub` и одним и тем же набором -инструментов. Неизвестный модуль — код выхода 2 и список модулей в stderr, сервер не запускается. Общая регистрация — в -user scope Claude Code как `jira-hub` (`uv run --project <путь> jira-hub mcp`), без проекта. Репозиторий проекта SHALL -регистрировать `jira-hub` в своём `.mcp.json` (project scope) с `--project <метка>`; эта запись перекрывает общую. Если хаб -недоступен, каждый инструмент SHALL вернуть текст «хаб не запущен: jira-hub serve», а не исключение. +`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** запущены `jira-hub mcp --project AWPCA` и `jira-hub mcp jira --project AWPCA` -- **THEN** оба сервера называются `jira-hub`, список инструментов и их описания совпадают, сессия привязана к `AWPCA` +- **WHEN** запущен `sdlc-hub mcp --project AWPCA` +- **THEN** модуля по умолчанию больше нет: код выхода 2, в stderr — подсказка указать модуль и список модулей (`jira`, `wiki`) #### Scenario: Неизвестный модуль -- **WHEN** запущен `jira-hub mcp nope` -- **THEN** код выхода 2, в stderr — «неизвестный модуль nope» и список модулей (`jira`) +- **WHEN** запущен `sdlc-hub mcp nope` +- **THEN** код выхода 2, в stderr — «неизвестный модуль nope» и список модулей (`jira`, `wiki`) ### Requirement: Инструменты | Инструмент | Параметры | Результат | @@ -61,7 +66,7 @@ user scope Claude Code как `jira-hub` (`uv run --project <путь> jira-hub других проектов запрещены, а команды над задачами вне проекта отклоняются; `board_id` для `ensure_sprint` выставляет хаб. Ответ `project_create` SHALL содержать фрагмент `.mcp.json` -`{"mcpServers": {"jira-hub": {"command": "uv", "args": ["run", "--project", "<корень хаба>", "jira-hub", "mcp", +`{"mcpServers": {"sdlc-jira": {"command": "uv", "args": ["run", "--project", "<корень хаба>", "sdlc-hub", "mcp", "jira", "--project", "<метка>"]}}}`, где корень хаба — корень репозитория, из которого загружен пакет текущего процесса MCP (вычисляется от расположения пакета, без зашитого пути), и указание: положить файл в корень репозитория проекта (слить с имеющимся `.mcp.json`) и переподключить MCP (`/mcp` → reconnect, одобрить сервер проекта). @@ -121,7 +126,7 @@ user scope Claude Code как `jira-hub` (`uv run --project <путь> jira-hub - **THEN** ответ «у AURORA-9001 нет комментария #999» ### Requirement: Привязка сессии к проекту -`jira-hub mcp --project <метка>` SHALL привязывать сервер MCP к проекту. Привязка SHALL проверяться при каждом вызове +`sdlc-hub mcp <модуль> --project <метка>` SHALL привязывать сервер MCP к проекту. Привязка SHALL проверяться при каждом вызове инструмента по `GET /projects`: метки нет — ответ «проект <метка> не заведён в хабе: project_create или исправьте --project в .mcp.json» (кроме `projects`, `project_create`). @@ -226,7 +231,7 @@ user scope Claude Code как `jira-hub` (`uv run --project <путь> jira-hub - **WHEN** `comment {key: "AURORA-1650", comment_id: "101"}` показал «вложения: #31337 скрин 1.png (234,4 КБ)»; вызван `attachment {key: "AURORA-1650", attachment: "#31337"}`, агент онлайн, файла у хаба нет - **THEN** поставлена `fetch_attachment`; через несколько секунд ответ — путь - `…/jira-hub/downloads/31337/скрин 1.png` и строка «скрин 1.png (234,4 КБ), image/png, задача AURORA-1650»; файл по пути + `…/sdlc-hub/downloads/31337/скрин 1.png` и строка «скрин 1.png (234,4 КБ), image/png, задача AURORA-1650»; файл по пути читается #### Scenario: Повторный вызов @@ -249,14 +254,14 @@ user scope Claude Code как `jira-hub` (`uv run --project <путь> jira-hub комментария 101 строка «файлы: #31337 скрин 1.png» ### Requirement: MCP вики -`jira-hub mcp wiki [--project <метка>]` SHALL запускать по stdio MCP-сервер модуля `wiki` с именем `jira-hub-wiki`, -ходящий в HTTP API хаба клиентским токеном; хаб недоступен — каждый инструмент отвечает «хаб не запущен: jira-hub serve». +`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` с записью `jira-hub-wiki` | +| `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=», если текст длиннее; `title` ищется точно, иначе — до 10 кандидатов с кодами | @@ -267,7 +272,7 @@ user scope Claude Code как `jira-hub` (`uv run --project <путь> jira-hub Инструменты, кроме `projects` и `project_create`, SHALL требовать привязки к проекту. В привязанной сессии без корня (кроме `root_set`, `status`) ответ SHALL быть «у проекта <метка> нет корня вики: root_set DOC-…»; с корнем, но без -синхронизаций — «вики проекта ещё не синхронизирована: sync». Набор инструментов и их описания у сервера `jira-hub` +синхронизаций — «вики проекта ещё не синхронизирована: sync». Набор инструментов и их описания у сервера `sdlc-jira` (модуль jira) SHALL остаться прежним. #### Scenario: Поиск из сессии awp-ca @@ -284,5 +289,5 @@ user scope Claude Code как `jira-hub` (`uv run --project <путь> jira-hub - **THEN** ответ «у проекта sfm_kedr нет корня вики: root_set DOC-…», запроса `GET /wiki/tree` нет #### Scenario: Сервер jira не изменился -- **WHEN** запущен `jira-hub mcp --project AWPCA` -- **THEN** имя сервера `jira-hub`, список инструментов и описания совпадают с эталоном до change +- **WHEN** запущен `sdlc-hub mcp jira --project AWPCA` +- **THEN** имя сервера `sdlc-jira`, список инструментов и описания совпадают с эталоном до change diff --git a/openspec/specs/migration/spec.md b/openspec/specs/migration/spec.md index 3aa7b78..b2344d2 100644 --- a/openspec/specs/migration/spec.md +++ b/openspec/specs/migration/spec.md @@ -6,7 +6,7 @@ ## Requirements ### Requirement: Инициализация настроек -`jira-hub init --config ` SHALL перенести в `setting`: `project_key`, типы задач, +`sdlc-hub init --config ` 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`, @@ -21,7 +21,7 @@ - **THEN** есть проект `AWPCA` «АРМ КА» с `board_id = 4216`; в `setting` нет `base_jql` и `board_id` ### Requirement: Импорт CSV-пакета -`jira-hub import <папка admin/jira-sync>` SHALL прочитать `tasks.csv`, `new_tasks.csv`, `epics.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 импортироваться как сверочный набор в отчёт импорта (ожидаемые связи против зеркала diff --git a/openspec/specs/web-app/spec.md b/openspec/specs/web-app/spec.md index 3e78a0b..cdb18a0 100644 --- a/openspec/specs/web-app/spec.md +++ b/openspec/specs/web-app/spec.md @@ -1,20 +1,24 @@ # web-app Specification ## Purpose -Веб-приложение хаба (`web/`, React + TypeScript + Vite) под префиксом `/app/`: вход по клиентскому токену, доска связей (перенос `links.html`) и страницы задач со служебными заметками; сборка в `src/jira_hub/web/app/`. +Веб-приложение хаба (`web/`, React + TypeScript + Vite) под префиксом `/app/`: вход по клиентскому токену, доска связей (перенос `links.html`) и страницы задач со служебными заметками; сборка в `src/sdlc_hub/web/app/`. ## Requirements ### Requirement: Вход по токену Приложение SHALL работать с API хаба того же хоста под клиентским токеном: хранить его в `localStorage` под ключом -`jira-hub-token` (и в памяти, если хранилище закрыто) и передавать в заголовке `Authorization: Bearer` каждого запроса. +`sdlc-hub-token` (и в памяти, если хранилище закрыто) и передавать в заголовке `Authorization: Bearer` каждого запроса. Если токена нет или любой запрос ответил 401, приложение SHALL показывать экран входа вместо страниц: поле токена с -подсказкой `uv run jira-hub tokens`, без панелей страниц и без фоновых запросов, которые могли бы изменить поле ввода. +подсказкой `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** показан экран входа; после ввода верного токена открывается запрошенная страница, токен сохранён @@ -24,8 +28,8 @@ SHALL очищать токен и показывать экран входа. - **THEN** показан экран входа с пояснением, что токен не подошёл; панели доски и задач не видны #### Scenario: Токен от прежней доски -- **WHEN** в `localStorage` есть `jira-hub-token`, сохранённый страницей `links.html` -- **THEN** приложение входит с ним без запроса токена +- **WHEN** в `localStorage` нет `sdlc-hub-token`, но есть `jira-hub-token` (сохранён до переименования хаба) +- **THEN** приложение входит с ним без запроса токена; значение перенесено в `sdlc-hub-token`, `jira-hub-token` удалён #### Scenario: Запоздавший 401 от прежнего токена - **WHEN** запрос ушёл с прежним токеном, пользователь вошёл с новым, и после этого запрос ответил 401 @@ -40,13 +44,13 @@ SHALL очищать токен и показывать экран входа. задач), `/app/feed` (лента изменений, требование «Лента изменений») и `/app/issues/{key}` (карточка задачи — окно поверх страницы, требование «Карточка задачи»); `/app` и `/app/` SHALL вести на `/app/board`, неизвестный путь — на страницу «не найдено» со ссылками на разделы. Боковая панель SHALL быть закреплена у левого края окна на всю его высоту и MUST NOT прокручиваться вместе со страницей; страница занимает остальную ширину. Сверху вниз панель SHALL -содержать: название «Хаб Jira» и кнопку «Свернуть панель»; выпадающий список проектов («<метка> — <название>», текущий +содержать: название «SDLC Hub» и кнопку «Свернуть панель»; выпадающий список проектов («<метка> — <название>», текущий отмечен, в конце «+ Новый проект…»; требование «Проект в приложении»); разделы «Доска связей», «Задачи» и «Лента» — вертикальным списком ссылок со значками (текущий отмечен `aria-current="page"`); внизу — блок данных Jira с действием «Обновить из Jira» (требование «Обновление из Jira»), переключатель темы (требование «Тема») и меню с действием «Сменить токен». Свёрнутая панель SHALL быть узкой полосой с теми же действиями значками: «Развернуть панель», метка текущего проекта, разделы, «Обновить из Jira» с точкой состояния агента, тема, меню; названия — в подсказках и `aria-label`. Свёртка -SHALL храниться в `localStorage` под ключом `jira-hub-sidebar` (`collapsed`; без записи — развёрнута). На экранах уже +SHALL храниться в `localStorage` под ключом `sdlc-hub-sidebar` (`collapsed`; без записи — развёрнута). На экранах уже 640 px панели в потоке нет: сверху SHALL быть закреплена строка с кнопкой «Меню», названием и кнопкой «Обновить из Jira»; «Меню» открывает развёрнутую панель поверх страницы, панель закрывается выбором раздела, клавишей Escape или щелчком мимо неё. Горизонтальной прокрутки страницы на любой ширине нет. @@ -116,7 +120,7 @@ Jira»; «Меню» открывает развёрнутую панель по управления: мультиселекты «Спринты» и «Исполнители», переключатели группы «Показать» («Соседи по связям», «Только передачи», «Скрыть без связей») и группы «Режим» («Критический путь», «По датам»), каждый с `aria-pressed` и прежней подсказкой. Под панелью — легенда, которая сворачивается кнопкой «Легенда»; свёрнутая показывает число скрытых видов -работ и типов связей; состояние свёртки хранится в `localStorage` под ключом `jira-hub-legend`. «Обновить из Jira», +работ и типов связей; состояние свёртки хранится в `localStorage` под ключом `sdlc-hub-legend`. «Обновить из Jira», тема и «Сменить токен» — в боковой панели приложения, на доске их нет. У карточки задачи (не у заглушки) SHALL быть значок «Открыть карточку», @@ -367,15 +371,15 @@ SHALL сообщать об этом с кнопкой «Закрыть»; ха ### Requirement: Сборка и разработка Приложение SHALL быть npm-проектом в `web/` (React, TypeScript, Vite) с точными версиями зависимостей и `package-lock.json`. `make web` SHALL устанавливать зависимости (`npm ci`) и собирать приложение в -`src/jira_hub/web/app/`; каталог сборки не хранится в git. Шрифты (Mulish — текст интерфейса, IBM Plex Mono — ключи +`src/sdlc_hub/web/app/`; каталог сборки не хранится в git. Шрифты (Mulish — текст интерфейса, IBM Plex Mono — ключи задач и исходники), иконки и прочие ресурсы страницы SHALL входить в сборку: приложение не обращается к внешним адресам. `make check` SHALL запускать `uv run pytest`, проверку типов и линтер фронта и тесты фронта; зелёным считается -только весь набор. `npm run dev` SHALL поднимать сервер разработки с прокси маршрутов API на хаб из `JIRA_HUB_URL` (по +только весь набор. `npm run dev` SHALL поднимать сервер разработки с прокси маршрутов API на хаб из `SDLC_HUB_URL` (по умолчанию тестовый `http://127.0.0.1:8791`). #### Scenario: Сборка с нуля - **WHEN** в чистом checkout выполнен `make web` -- **THEN** в `src/jira_hub/web/app/` есть `index.html` и `assets/`; `git status` не показывает файлов сборки +- **THEN** в `src/sdlc_hub/web/app/` есть `index.html` и `assets/`; `git status` не показывает файлов сборки #### Scenario: Без интернета - **WHEN** приложение открыто на машине без доступа в интернет diff --git a/openspec/specs/wiki/spec.md b/openspec/specs/wiki/spec.md index e43e437..a840377 100644 --- a/openspec/specs/wiki/spec.md +++ b/openspec/specs/wiki/spec.md @@ -81,7 +81,7 @@ links: [{ref, code}]}`, где `attachments` — ссылки и картинк - **THEN** 409 `sync_in_progress` с `batch_id` текущего пакета, нового пакета нет ### Requirement: Markdown-копия вики -Копия проекта SHALL лежать в `/wiki/<метка>/` (каталоги 0700): у каждой страницы — каталог +Копия проекта SHALL лежать в `/wiki/<метка>/` (каталоги 0700): у каждой страницы — каталог `NN_<название>` внутри каталога родителя (`NN` — двузначная позиция среди соседей по `orderno`, затем по коду; у корня номера нет; название без символов `<>:"/\|?*` и управляющих, обрезано до 80 символов; совпадение у соседей — суффикс `_2`, `_3`), в нём `index.md`: frontmatter `code`, `title`, `url`, `synced_at`, строка `# ` и текст. В From 3094e364a1d44a1feab8a84ca9f0382905f30fda Mon Sep 17 00:00:00 2001 From: Andrey Epifancev <andrey.epifancev@ssi-solutions.ru> Date: Sat, 24 Oct 2026 15:58:18 +0400 Subject: [PATCH 2/2] =?UTF-8?q?chore(openspec):=20=D0=B0=D1=80=D1=85=D0=B8?= =?UTF-8?q?=D0=B2=20web-modules=20(#98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../2026-10-24-web-modules}/.openspec.yaml | 0 .../2026-10-24-web-modules}/design.md | 0 .../2026-10-24-web-modules}/proposal.md | 0 .../specs/hub-api/spec.md | 0 .../specs/web-app/spec.md | 0 openspec/specs/hub-api/spec.md | 42 ++- openspec/specs/web-app/spec.md | 325 ++++++++++++------ 7 files changed, 256 insertions(+), 111 deletions(-) rename openspec/changes/{web-modules => archive/2026-10-24-web-modules}/.openspec.yaml (100%) rename openspec/changes/{web-modules => archive/2026-10-24-web-modules}/design.md (100%) rename openspec/changes/{web-modules => archive/2026-10-24-web-modules}/proposal.md (100%) rename openspec/changes/{web-modules => archive/2026-10-24-web-modules}/specs/hub-api/spec.md (100%) rename openspec/changes/{web-modules => archive/2026-10-24-web-modules}/specs/web-app/spec.md (100%) diff --git a/openspec/changes/web-modules/.openspec.yaml b/openspec/changes/archive/2026-10-24-web-modules/.openspec.yaml similarity index 100% rename from openspec/changes/web-modules/.openspec.yaml rename to openspec/changes/archive/2026-10-24-web-modules/.openspec.yaml diff --git a/openspec/changes/web-modules/design.md b/openspec/changes/archive/2026-10-24-web-modules/design.md similarity index 100% rename from openspec/changes/web-modules/design.md rename to openspec/changes/archive/2026-10-24-web-modules/design.md diff --git a/openspec/changes/web-modules/proposal.md b/openspec/changes/archive/2026-10-24-web-modules/proposal.md similarity index 100% rename from openspec/changes/web-modules/proposal.md rename to openspec/changes/archive/2026-10-24-web-modules/proposal.md diff --git a/openspec/changes/web-modules/specs/hub-api/spec.md b/openspec/changes/archive/2026-10-24-web-modules/specs/hub-api/spec.md similarity index 100% rename from openspec/changes/web-modules/specs/hub-api/spec.md rename to openspec/changes/archive/2026-10-24-web-modules/specs/hub-api/spec.md diff --git a/openspec/changes/web-modules/specs/web-app/spec.md b/openspec/changes/archive/2026-10-24-web-modules/specs/web-app/spec.md similarity index 100% rename from openspec/changes/web-modules/specs/web-app/spec.md rename to openspec/changes/archive/2026-10-24-web-modules/specs/web-app/spec.md diff --git a/openspec/specs/hub-api/spec.md b/openspec/specs/hub-api/spec.md index f06becc..cc0cdc2 100644 --- a/openspec/specs/hub-api/spec.md +++ b/openspec/specs/hub-api/spec.md @@ -417,7 +417,7 @@ published_at}` или `null`), `update_state`, `launcher_version`, `agent_report - любой другой `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`. +- `GET /` и `GET /links` — 302 с `Location: /app/` (главная страница приложения — «Обзор»). Сборка приложения не хранится в git; её создаёт `make web`. #### Scenario: Файл сборки @@ -438,7 +438,7 @@ published_at}` или `null`), `update_state`, `launcher_version`, `agent_report #### Scenario: Старый адрес доски - **WHEN** запрошен `GET /links` или `GET /` -- **THEN** 302, `Location: /app/board` +- **THEN** 302, `Location: /app/` #### Scenario: Нулевой байт в пути - **WHEN** запрошен `GET /app/assets/%00` или `GET /app/assets/a%00.js` @@ -485,15 +485,16 @@ sha256 (без расширения и имени вложения: имя за Хаб SHALL предоставлять под клиентским токеном: - `GET /projects` → список проектов по `created_at`, затем `label`; каждый — плоский объект из полей ядра (`label`, `name`, `created_at`) и полей всех модулей (`modules`, «Поля проекта от модулей»). Модуль jira даёт `board_id`, - `board_type`, `issues_count` (задачи зеркала с меткой проекта), модуль wiki — `wiki_root`, `wiki_root_title`, + `board_type`, `issues_count` (задачи зеркала с меткой проекта), `jira_synced_at` (время завершения последнего полного + обновления зеркала, общее для всех проектов; `null`, если его не было), модуль wiki — `wiki_root`, `wiki_root_title`, `wiki_pages`, `wiki_synced_at`, `wiki_sync_error`. -- `POST /projects` `{label, name, author?, <поля модулей>}` → 201 `{project, batch_id}`: в одной транзакции проект - записывается, модули записывают свои поля и ставят свои пакеты; `batch_id` — пакет первого модуля, вернувшего пакет - (у jira — «Новый проект <метка>» из одного `refresh` по базовой JQL, уже включающей новую метку). Автор пакетов — +- `POST /projects` `{label, name, author?, <поля модулей>}` → 201 `{project, batch_id, batches}`: в одной транзакции + проект записывается, модули записывают свои поля и ставят свои пакеты; `batches` — список `{module, id}` всех + поставленных пакетов в порядке модулей; `batch_id` — пакет первого модуля, вернувшего пакет (у jira — «Новый проект <метка>» из одного `refresh` по базовой JQL, уже включающей новую метку). Автор пакетов — `author` (по умолчанию `api`). Неизвестное поле, неверные поля ядра (см. `data-model`, «Проекты») или модуля — 422 с указанием поля; метка, совпадающая с существующей без учёта регистра, — 409. При ошибке ни проекта, ни пакетов нет. -- `PATCH /projects/{label}` `{<изменяемые поля модулей>, author?}` → 200 `{project, batch_ids}`: модули правят свои поля - и ставят пакеты в одной транзакции. Неизменяемое поле (метка, название, `board_id`) — 422 «<поле> после создания не +- `PATCH /projects/{label}` `{<изменяемые поля модулей>, author?}` → 200 `{project, batch_ids, batches}`: модули правят + свои поля и ставят пакеты в одной транзакции; `batches` — список `{module, id}`. Неизменяемое поле (метка, название, `board_id`) — 422 «<поле> после создания не меняется»; неизвестное поле или неверное значение — 422 с указанием поля; проекта нет — 404. #### Scenario: Новый проект @@ -508,7 +509,7 @@ sha256 (без расширения и имени вложения: имя за #### Scenario: Проект с корнем вики - **WHEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", wiki_root: "DOC-001234"}` - **THEN** 201; в очереди пакет `refresh` и пакет `wiki_sync {label: "STOCKS_DRRK", root: "DOC-001234"}`; `batch_id` — - пакет `refresh` + пакет `refresh`; `batches` — `[{module: "jira", id: <refresh>}, {module: "wiki", id: <wiki_sync>}]` #### Scenario: Повтор метки - **WHEN** `POST /projects {label: "AWPCA", name: "…"}`, проект `AWPCA` есть @@ -524,12 +525,16 @@ sha256 (без расширения и имени вложения: имя за #### Scenario: Корень вики у существующего проекта - **WHEN** `PATCH /projects/AWPCA {wiki_root: "DOC-000777"}` -- **THEN** 200, `batch_ids` — один пакет `wiki_sync`; `GET /projects` отдаёт `AWPCA` с `wiki_root = DOC-000777` +- **THEN** 200, `batch_ids` — один пакет `wiki_sync`, `batches` — `[{module: "wiki", id: <он же>}]`; `GET /projects` отдаёт `AWPCA` с `wiki_root = DOC-000777` #### Scenario: Доску не меняют - **WHEN** `PATCH /projects/AWPCA {board_id: 1}` - **THEN** 422 «board_id после создания не меняется», доска прежняя +#### Scenario: Свежесть Jira в проекте +- **WHEN** последний полный пакет `refresh` завершён в 11:07, `GET /projects` +- **THEN** у каждого проекта `jira_synced_at` — это время; до первого полного обновления — `null` + ### Requirement: Приём журнала изменений Хаб SHALL принимать от агента журнал изменений задач маршрутом `POST /agent/history` (агентский токен) с телом `{command_id, changes: [...]}`, элемент — `{history_id, seq, key, created, author, field_id, field_name, from, @@ -714,3 +719,20 @@ from_string, to, to_string}`. До записи хаб SHALL проверить #### Scenario: Репозиторий в Gitea не переименован - **WHEN** `bash scripts/cutover.sh --apply`, `aep/sdlc-hub` в Gitea не найден - **THEN** скрипт останавливается на шаге 1 с подсказкой, launchd и каталоги не тронуты + +### Requirement: Модуль пакета +`GET /batches` и `GET /batches/{id}` SHALL отдавать у пакета поле `module` — имя модуля, которому принадлежит тип первой +команды пакета (по описаниям модулей в реестре); пакет без команд или с типом, которого не знает ни один модуль, — +`module = null`. Фильтр `GET /batches?module=<имя>` SHALL отдавать только пакеты модуля; неизвестный модуль — 404. + +#### Scenario: Модуль у пакетов +- **WHEN** в очереди пакет «Автообновление» (`refresh`, автор `hub`) и пакет `wiki_sync` проекта `AWPCA`; `GET /batches` +- **THEN** у первого `module = "jira"`, у второго `module = "wiki"` + +#### Scenario: Фильтр по модулю +- **WHEN** `GET /batches?module=wiki&project=AWPCA` +- **THEN** только пакеты модуля wiki проекта `AWPCA` + +#### Scenario: Неизвестный модуль +- **WHEN** `GET /batches?module=nope` +- **THEN** 404 diff --git a/openspec/specs/web-app/spec.md b/openspec/specs/web-app/spec.md index cdb18a0..b3ad237 100644 --- a/openspec/specs/web-app/spec.md +++ b/openspec/specs/web-app/spec.md @@ -40,29 +40,43 @@ SHALL очищать токен и показывать экран входа. - **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 показываться сообщением с кнопкой «Перезагрузить», не пустым экраном. +Приложение SHALL открываться под префиксом `/app/`. Главная страница — «Обзор» ядра (`/app/?project=<метка>`, +требование «Обзор проекта»). Страницы модулей SHALL лежать под префиксом имени модуля: модуль jira — `/app/jira/board` +(доска связей), `/app/jira/issues` (список задач), `/app/jira/feed` (лента изменений, требование «Лента изменений») и +`/app/jira/issues/{key}` (карточка задачи — окно поверх страницы, требование «Карточка задачи»); модуль wiki — +`/app/wiki` (требование «Вики проекта»). Прежние адреса `/app/board`, `/app/issues`, `/app/issues/{key}`, `/app/feed` +SHALL перенаправлять на новые с сохранением строки запроса, без новой записи истории. Неизвестный путь — страница «не +найдено» со ссылками на «Обзор» и разделы модулей. + +Боковая панель SHALL быть закреплена у левого края окна на всю его высоту и MUST NOT прокручиваться вместе со +страницей; если её содержимое не помещается по высоте, прокручивается сама панель. Сверху вниз панель SHALL содержать: +название «SDLC Hub» и кнопку «Свернуть панель»; выпадающий список проектов («<метка> — <название>», текущий отмечен, в +конце «+ Новый проект…»; требование «Проект в приложении»); пункт «Обзор»; разделы модулей группами — заголовок группы +— название модуля («Jira», «Вики»), под ним ссылки разделов модуля со значками (текущий отмечен +`aria-current="page"`); внизу — блок состояния модулей (требование «Состояние и обновление модулей»), переключатель +темы (требование «Тема») и меню с действием «Сменить токен». Свёрнутая панель SHALL быть узкой полосой с теми же +действиями значками: «Развернуть панель», метка текущего проекта (полностью — в подсказке), «Обзор», разделы +(группы разделены линией), по значку состояния на модуль, тема, меню; названия — в подсказках и `aria-label`. Свёртка +SHALL храниться в `localStorage` под ключом `sdlc-hub-sidebar` (`collapsed`; без записи — развёрнута). + +На экранах уже 640 px панели в потоке нет: сверху SHALL быть закреплена строка с кнопкой «Меню», меткой текущего +проекта и сводной точкой состояния модулей (ошибка, если она есть у какого-нибудь модуля; иначе — пакет в работе; +иначе — все агенты на связи или нет), которая открывает панель. «Меню» открывает развёрнутую панель поверх страницы +модальным окном: фокус переходит в панель и остаётся в ней, страница под ней недоступна; панель закрывается выбором +раздела, клавишей Escape (если не открыто вложенное меню — тогда Escape закрывает только его) или щелчком мимо неё; фокус +возвращается на «Меню». Горизонтальной прокрутки страницы на любой ширине нет. + +Заголовок вкладки браузера SHALL быть «<название страницы> · <метка проекта> · SDLC Hub» (без проекта — без средней +части); название страницы даёт оболочка («Обзор») или модуль. Неожиданная ошибка отрисовки страницы SHALL показываться +сообщением с кнопкой «Перезагрузить», не пустым экраном. #### Scenario: Переход между разделами -- **WHEN** на доске в боковой панели выбран раздел «Задачи» -- **THEN** адрес — `/app/issues` с текущим проектом, показан список задач; «назад» возвращает на доску +- **WHEN** на доске в боковой панели выбран раздел «Задачи» группы «Jira» +- **THEN** адрес — `/app/jira/issues` с текущим проектом, показан список задач; «назад» возвращает на доску #### Scenario: Раздел «Лента» - **WHEN** на доске в боковой панели выбран раздел «Лента» -- **THEN** адрес — `/app/feed` с текущим проектом, показана лента изменений за сегодня; раздел отмечен в панели +- **THEN** адрес — `/app/jira/feed` с текущим проектом, показана лента изменений за сегодня; раздел отмечен в панели #### Scenario: Сменить токен - **WHEN** в меню боковой панели выбрано «Сменить токен» @@ -70,16 +84,25 @@ Jira»; «Меню» открывает развёрнутую панель по #### Scenario: Панель на месте при прокрутке - **WHEN** доска прокручена до самого низа -- **THEN** боковая панель видна целиком на прежнем месте: разделы и «Обновить из Jira» доступны без прокрутки вверх +- **THEN** боковая панель видна целиком на прежнем месте: разделы и блок состояния модулей доступны без прокрутки вверх #### Scenario: Свёрнутая панель - **WHEN** нажата «Свернуть панель», страница перезагружена -- **THEN** панель свёрнута в полосу значков, доска шире; значок «Задачи» открывает список задач; «Развернуть панель» - возвращает полную панель +- **THEN** панель свёрнута в полосу значков, доска шире; значок «Задачи» открывает список задач; видны два значка + состояния — Jira и Вики; «Развернуть панель» возвращает полную панель #### Scenario: Узкий экран - **WHEN** ширина окна 390 px, нажата «Меню», в панели выбран раздел «Задачи» -- **THEN** панель открылась поверх страницы, после выбора закрыта, показан список задач; горизонтальной прокрутки нет +- **THEN** панель открылась поверх страницы, фокус в панели; после выбора панель закрыта, фокус на «Меню», показан список + задач; горизонтальной прокрутки нет + +#### Scenario: Прежний адрес +- **WHEN** открыт `/app/issues/AURORA-1536?project=AWPCA` +- **THEN** адрес заменён на `/app/jira/issues/AURORA-1536?project=AWPCA`, карточка открыта поверх списка задач + +#### Scenario: Заголовок вкладки +- **WHEN** открыта страница «Вики» проекта `AWPCA` +- **THEN** заголовок вкладки — «Вики · AWPCA · SDLC Hub» ### Requirement: Ошибки хаба Приложение SHALL различать ошибки запросов к хабу: 401 — экран входа; хаб не отвечает (сеть, 5xx) — сообщение @@ -461,47 +484,51 @@ SHALL сообщать об этом с кнопкой «Закрыть»; ха 234,4 КБ»; щелчок по любой из них скачивает файл ### Requirement: Проект в приложении -Доска, список задач и вики SHALL показывать один проект — текущий. Текущий проект SHALL задаваться параметром -`project` в адресе `/app/board`, `/app/issues` и `/app/wiki`; без параметра — последний выбранный в этом браузере -(`localStorage`), если он есть в `GET /projects`, иначе первый из `GET /projects`; выбранный проект записывается в адрес -без новой записи истории. Проекта из адреса нет в `GET /projects` — страница SHALL показывать «проект <метка> не найден» -и выпадающий список проектов вместо данных. Проектов нет — доска, список и вики SHALL показывать приглашение создать -проект («+ Новый проект…»). Переходы между «Доска связей», «Задачи» и «Вики» SHALL сохранять текущий проект. Карточка -задачи (`/app/issues/{key}`) от проекта не зависит. +«Обзор», доска, список задач, лента и вики SHALL показывать один проект — текущий. Текущий проект SHALL задаваться +параметром `project` в адресе страниц; без параметра — последний выбранный в этом браузере (`localStorage`), если он +есть в `GET /projects`, иначе первый из `GET /projects`; выбранный проект записывается в адрес без новой записи истории. +Проекта из адреса нет в `GET /projects` — страница SHALL показывать «проект <метка> не найден» и подсказку выбрать +проект в списке «Проект» боковой панели (на узком экране — в «Меню») вместо данных. Проектов нет — страницы SHALL +показывать приглашение создать проект («+ Новый проект…»). Переходы между страницами SHALL сохранять текущий проект; +смена проекта на странице, которой нет у нового проекта, открывает «Обзор». Карточка задачи +(`/app/jira/issues/{key}`) от проекта не зависит. -Доска SHALL загружать `GET /board?project=<текущий>`; «Обновить из Jira» — `POST /refresh {full: true}`, обновляет все -проекты. Раскладка, дорожки и фильтры доски в `localStorage` SHALL храниться отдельно для каждого проекта; -у проекта `AWPCA` — на прежних ключах (раскладка, сделанная до проектов, сохраняется). Фильтр спринтов SHALL -показываться, только если хотя бы у одной своей задачи снимка (`neighbor = false`) есть спринт; иначе — «колонки -«Спринт» нет — показаны все задачи». +Доска SHALL загружать `GET /board?project=<текущий>`. Раскладка, дорожки и фильтры доски в `localStorage` SHALL +храниться отдельно для каждого проекта; у проекта `AWPCA` — на прежних ключах (раскладка, сделанная до проектов, +сохраняется). Фильтр спринтов SHALL показываться, только если хотя бы у одной своей задачи снимка (`neighbor = false`) +есть спринт; иначе — «колонки «Спринт» нет — показаны все задачи». -Форма «Новый проект» (пункт «+ Новый проект…» выпадающего списка) SHALL открываться окном поверх страницы: метка, -название и поля модулей — номер доски Jira (необязательно; подсказка «scrum или kanban определится сам») и корень вики -(необязательно; подсказка «DOC-… или ссылка на страницу Eva»). Пустое необязательное поле в запрос не попадает. -«Создать» — `POST /projects` с автором `viewer`; ошибка хаба 409/422 — текст `detail` рядом с кнопкой, окно остаётся -открытым; успех — окно закрывается, текущим становится новый проект, боковая панель показывает ход пакета обновления из -ответа (`batch_id`). +Форма «Новый проект» (пункт «+ Новый проект…» выпадающего списка) SHALL открываться окном поверх страницы: метка (с +подсказкой «метка задач в Jira и имя проекта в хабе») и название, затем поля каждого модуля в своей группе с +заголовком — названием модуля: «Jira» — номер доски Jira (необязательно; подсказка «scrum или kanban определится +сам»), «Вики» — корень вики (необязательно; подсказка «DOC-… или ссылка на страницу Eva»). Подсказка — текстом под +полем, не в placeholder. Пустое необязательное поле в запрос не попадает; числовое поле с нецелым значением — ошибка у +поля, запрос не уходит. «Создать» — `POST /projects` с автором `viewer`; ошибка хаба 409/422 — текст `detail` рядом с +кнопкой, окно остаётся открытым; успех — окно закрывается, текущим становится новый проект, каждый пакет из ответа +(`batches`) отслеживается своим модулем (требование «Состояние и обновление модулей»). #### Scenario: Первый вход после выкатки -- **WHEN** открыт `/app/board` без `project`, в `localStorage` выбора нет, проекты `AWPCA` и `STOCKS_DRRK` -- **THEN** адрес — `/app/board?project=AWPCA`, доска — `GET /board?project=AWPCA` с прежней раскладкой карточек +- **WHEN** открыт `/app/jira/board` без `project`, в `localStorage` выбора нет, проекты `AWPCA` и `STOCKS_DRRK` +- **THEN** адрес — `/app/jira/board?project=AWPCA`, доска — `GET /board?project=AWPCA` с прежней раскладкой карточек #### Scenario: Переключение проекта - **WHEN** на доске `AWPCA` в выпадающем списке выбран `STOCKS_DRRK`, затем открыт раздел «Задачи» -- **THEN** доска — `GET /board?project=STOCKS_DRRK` со своей раскладкой; адрес «Задач» — `/app/issues?project=STOCKS_DRRK` +- **THEN** доска — `GET /board?project=STOCKS_DRRK` со своей раскладкой; адрес «Задач» — + `/app/jira/issues?project=STOCKS_DRRK` #### Scenario: Неизвестный проект в адресе -- **WHEN** открыт `/app/issues?project=NOPE` -- **THEN** «проект NOPE не найден» и список проектов; запроса `GET /issues` нет +- **WHEN** открыт `/app/jira/issues?project=NOPE` +- **THEN** «проект NOPE не найден» и подсказка выбрать проект в боковой панели; запроса `GET /issues` нет #### Scenario: Создание проекта из формы - **WHEN** в форме введены `STOCKS_DRRK`, «Запасы ДРРК», доска и корень вики пусто; нажато «Создать» - **THEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", author: "viewer"}`; текущий проект — `STOCKS_DRRK`; - боковая панель показывает «пакет #N выполняется» + строка «Jira» блока состояния показывает «пакет #N выполняется» #### Scenario: Проект с корнем вики из формы - **WHEN** в форме введены `STOCKS_DRRK`, «Запасы ДРРК», корень `DOC-001234`; нажато «Создать» -- **THEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", wiki_root: "DOC-001234", author: "viewer"}` +- **THEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", wiki_root: "DOC-001234", author: "viewer"}`; строка + «Jira» показывает пакет обновления, строка «Вики» — пакет синхронизации #### Scenario: Метка занята - **WHEN** в форме введена метка `awpca`, хаб ответил 409 @@ -511,6 +538,10 @@ SHALL сообщать об этом с кнопкой «Закрыть»; ха - **WHEN** текущий проект — `STOCKS_DRRK`, ни у одной своей задачи снимка нет спринта (у соседа из `AWPCA` спринт есть) - **THEN** вместо фильтра спринтов — «колонки «Спринт» нет — показаны все задачи» +#### Scenario: Нецелый номер доски +- **WHEN** в поле «Номер доски Jira» введено `51.5`, нажато «Создать» +- **THEN** у поля — «нужно целое число», запроса `POST /projects` нет + ### Requirement: Дизайн-система Приложение SHALL следовать дизайн-системе AURA (handoff AURA-xlsx-tool 29.09.2026, Figma «Универсалки») в светлой и тёмной темах. Токены — цвета по семантическим ролям для обеих тем, типографика, отступы с шагом 4 px, радиусы, высоты @@ -534,50 +565,6 @@ Semibold 14/20, заголовок страницы Bold 20/28. Страницы - **THEN** канва `#181818`, поверхности `#1f1f1f`, основной текст `#f2f4f2`, основное действие `#69a93d` на всех страницах, в окнах и меню -### Requirement: Обновление из Jira -Действие «Обновить из Jira» SHALL быть в боковой панели приложения и доступно с любой страницы. Оно SHALL проверять -агента (`GET /agent`; офлайн — предупреждение, пакет всё равно ставится), ставить `POST /refresh {full: true, author: -"viewer"}` и опрашивать `GET /batches/{id}` каждые 3 с одним опросом до терминального статуса пакета. Кнопка SHALL быть -недоступна (с признаком загрузки), пока запрос `/refresh` в пути или пакет выполняется. Незавершённый пакет автора -`viewer`, первая команда которого — `refresh`, SHALL подхватываться при входе в приложение и после каждой успешной -загрузки снимка доски, кроме перечитывания снимка после завершения собственного пакета; пакеты `viewer` с другими -командами не подхватываются. Опрос SHALL прекращаться при ответе 4xx (404 — «пакета нет в хабе») и после 5 ошибок -«хаб не отвечает» подряд, с сообщением и снова доступной кнопкой. По завершении пакета приложение SHALL перечитывать -снимки досок, списки задач, открытую карточку и список проектов; доска, показывающая снимок из файла, его не заменяет. -Пакет, поставленный формой «Новый проект», SHALL опрашиваться так же сразу после создания. - -Боковая панель SHALL показывать блок данных Jira: состояние агента («агент на связи» / «агент офлайн», по `GET /agent` -при входе и раз в 60 с), время последнего полного обновления («данные на ДД.ММ ЧЧ:ММ» по `last_sync` снимка доски -текущего проекта, «полного обновления не было», если его нет) и одно текущее событие пакета: в очереди / выполняется (с -оставшимся временем, с пометкой «агент офлайн» и «хаб не отвечает, повтор»), выполнен, не выполнен с текстом ошибки, -отменён, опрос остановлен с причиной, хаб не ответил на постановку; новое событие заменяет предыдущее, «выполнен» -исчезает через 10 с. - -#### Scenario: Обновление со списка задач -- **WHEN** открыт `/app/issues`, нажато «Обновить из Jira», пакет выполнен -- **THEN** в хабе один пакет `refresh` автора `viewer`; список задач перезапрошен; боковая панель показывала «пакет #N - выполняется», затем «пакет #N выполнен» - -#### Scenario: Один прогон на всё приложение -- **WHEN** нажата «Обновить из Jira» и, пока пакет выполняется, выполнен переход между разделами и «Перечитать» на доске -- **THEN** кнопка остаётся недоступной, в хабе один пакет, его опрашивает один опрос - -#### Scenario: Опрос получил 404 -- **WHEN** опрос пакета получил 404 -- **THEN** опрос остановлен, боковая панель показывает «пакета нет в хабе», кнопка «Обновить из Jira» снова доступна - -#### Scenario: Пакет с другой вкладки -- **WHEN** пакет `viewer` с командой `refresh` поставлен с другой вкладки и не завершён, приложение открыто -- **THEN** боковая панель показывает «пакет #N выполняется» и по завершении данные перечитываются - -#### Scenario: Пакет с другими командами -- **WHEN** незавершённый пакет автора `viewer` состоит из команд `ensure_fields` -- **THEN** приложение его не опрашивает, кнопка «Обновить из Jira» доступна - -#### Scenario: Агент офлайн -- **WHEN** `GET /agent` отвечает `online: false` и нажато «Обновить из Jira» -- **THEN** пакет поставлен, событие — «агент офлайн — пакет выполнится, когда он выйдет на связь; пакет #N в очереди…» - ### Requirement: Тема Переключатель темы SHALL быть в боковой панели приложения: два положения — светлая и тёмная. Выбор SHALL храниться в `localStorage` под ключом `graph-theme` (`light` / `dark`), как у прежней доски; без сохранённого выбора или со @@ -671,19 +658,22 @@ Semibold 14/20, заголовок страницы Bold 20/28. Страницы - **THEN** под вложением этот текст и «Повторить» ### Requirement: Вики проекта -Приложение SHALL иметь страницу «Вики» (`/app/wiki?project=<метка>`, пункт боковой панели; текущий проект — как у доски -и задач, «Проект в приложении»). Страница SHALL показывать: -- дерево страниц проекта (`GET /wiki/tree`) слева, свёрнутое до первого уровня; узел раскрывается по клику; +Приложение SHALL иметь страницу «Вики» (`/app/wiki?project=<метка>`, раздел группы «Вики» боковой панели; текущий +проект — как у остальных страниц, «Проект в приложении»). Шапка страницы: заголовок «Вики», подзаголовок «<название +корня> · <N> страниц · обновлено ДД.ММ ЧЧ:ММ» и действие «Обновить вики» (вторичная кнопка; требование «Состояние и +обновление модулей»). Страница SHALL показывать: +- дерево страниц проекта (`GET /wiki/tree`) слева, свёрнутое до первого уровня; узел раскрывается по клику; узел + выбранной страницы и его предки раскрыты; - выбранную страницу справа (`GET /wiki/pages/{code}`): заголовок, дата синхронизации, ссылка «открыть в Eva», - Markdown с таблицами, без сырого HTML; ссылки `page:<код>` открывают страницу вики (код в адресе `&page=<код>`), - ссылки `attachment:<id>` скачивают вложение (запрос, опрос состояния, сохранение файла браузером), состояние - `failed` — текст ошибки рядом со ссылкой; + Markdown с таблицами, без сырого HTML; без `page` в адресе — корневая страница; ссылки `page:<код>` открывают страницу + вики (код в адресе `&page=<код>`), ссылки `attachment:<id>` скачивают вложение (запрос, опрос состояния, сохранение + файла браузером), состояние `failed` — текст ошибки рядом со ссылкой; +- на экранах уже 640 px выбранная страница — над деревом; - поиск над деревом (`GET /wiki/search`): список результатов с фрагментами вместо дерева, пока строка поиска не пуста; -- кнопку «Обновить вики» (`POST /wiki/sync`) и плашку хода синхронизации в боковой панели; ответ 409 — плашка текущего - пакета; + после очистки поиска раскрытие дерева прежнее; - проекта без корня — форму «Корень вики (DOC-… или ссылка)» с кнопкой «Привязать» (`PATCH /projects/{label}`); 422 — текст ошибки у поля; -- корень есть, синхронизаций нет — «вики ещё не синхронизирована» и кнопку «Обновить вики»; +- корень есть, синхронизаций нет — «вики ещё не синхронизирована» и действие «Обновить вики»; - последняя синхронизация с ошибкой — плашку с текстом ошибки над страницей. #### Scenario: Открыть страницу по ссылке @@ -692,8 +682,141 @@ Semibold 14/20, заголовок страницы Bold 20/28. Страницы #### Scenario: Проект без корня - **WHEN** открыта `/app/wiki?project=sfm_kedr`, корня нет; введено `DOC-001234`, нажато «Привязать» -- **THEN** `PATCH /projects/sfm_kedr {wiki_root: "DOC-001234"}`; боковая панель показывает ход пакета синхронизации +- **THEN** `PATCH /projects/sfm_kedr {wiki_root: "DOC-001234"}`; строка «Вики» блока состояния показывает ход пакета + синхронизации #### Scenario: Поиск - **WHEN** в строке поиска «согласов» - **THEN** вместо дерева — результаты `GET /wiki/search?project=AWPCA&q=согласов`; очистка строки возвращает дерево + +#### Scenario: Без выбранной страницы +- **WHEN** открыта `/app/wiki?project=AWPCA` без `page` +- **THEN** справа корневая страница проекта, в дереве она выделена + +### Requirement: Состояние и обновление модулей +Каждый веб-модуль SHALL описывать своё состояние данными, а рисовать их SHALL ядро. Описание модуля: название для +показа («Jira», «Вики»); агент (имя модуля в `GET /agents`) или его отсутствие; свежесть данных для текущего проекта — +время, охват («все проекты» или «проект») и ошибка последнего обновления; действие обновления (название, охват, +постановка пакета) или его отсутствие; правило, какие пакеты принадлежат модулю; данные, которые надо перечитать после +пакета модуля; есть ли у модуля данные для проекта (у вики без корня — нет). + +Ядро SHALL: +- опрашивать `GET /agents` при входе и раз в 60 с — один опрос на приложение; агент модуля: «на связи», «офлайн» или + «обновите агента» (есть `warning` или `update_hint`, текст — в подсказке); +- вести один трекер пакетов на приложение: на модуль — не больше одного отслеживаемого пакета; пакет подхватывается при + входе, раз в 60 с и после действия обновления, если он не завершён, принадлежит модулю (поле `module` в `GET + /batches`) и текущему проекту (для модулей с охватом «проект») — любого автора, в том числе автообновления хаба и + MCP; опрос `GET /batches/{id}` каждые 3 с до терминального статуса; 4xx (404 — «пакета нет в хабе») и 5 ошибок «хаб + не отвечает» подряд — опрос остановлен с сообщением; по завершении — перечитать данные модуля и список проектов; +- ответ 409 на постановку пакета — отслеживать уже идущий пакет модуля, без сообщения об ошибке; +- агент модуля офлайн при обновлении — пакет всё равно ставится, событие «агент офлайн — пакет выполнится, когда он + выйдет на связь; пакет #N в очереди…». + +Блок состояния в развёрнутой боковой панели — по строке на модуль, у которого есть данные для проекта: название модуля, +точка агента (цвет и текст «агент на связи» / «агент офлайн» / «обновите агента» — не только цвет), кнопка-значок +обновления с подписью действия в подсказке и `aria-label` («Обновить из Jira — все проекты», «Обновить вики — проект +AWPCA»); под ними — «данные на ДД.ММ ЧЧ:ММ» с охватом («все проекты» / «проект»), «обновления ещё не было» или ошибка +последнего обновления; под ними — одно текущее событие пакета модуля: в очереди / выполняется (с оставшимся временем, +если хаб его дал, и пометками «агент офлайн», «хаб не отвечает, повтор»), выполнен (исчезает через 10 с), не выполнен +с текстом ошибки, отменён, опрос остановлен с причиной. Кнопка обновления модуля недоступна (с признаком загрузки), пока +запрос постановки в пути или выполняется пакет действия обновления модуля (у jira — пакет с первой командой `refresh`, у +wiki — `wiki_sync`); прочие пакеты модуля показываются, но обновление не блокируют: нажатие ставит пакет обновления, и +трекер переключается на него. Свёрнутая панель — по значку модуля с точкой агента, подробности в +подсказке, нажатие — обновление. На узком экране — сводная точка в верхней строке (требование «Оболочка и адреса»), +событие пакета — строкой под верхней строкой. + +Модуль jira: агент `jira`; свежесть — `jira_synced_at` из `GET /projects`, охват «все проекты»; действие «Обновить из +Jira» — `POST /refresh {full: true, author: "viewer"}`; после пакета перечитываются снимки досок, списки задач, лента и +открытая карточка; доска, показывающая снимок из файла, его не заменяет. Модуль wiki: агент `wiki`; свежесть — +`wiki_synced_at`, `wiki_sync_error` из `GET /projects`, охват «проект»; данные для проекта есть, только если есть +`wiki_root`; действие «Обновить вики» — `POST /wiki/sync {project, author: "viewer"}`; после пакета перечитываются дерево, +страницы и поиск. Блок состояния MUST NOT загружать снимок доски или `GET /wiki/status`. + +Страница модуля с действием обновления SHALL дублировать его вторичной кнопкой в шапке страницы; основной (primary) +кнопки обновления в приложении нет. + +#### Scenario: Обновление со списка задач +- **WHEN** открыт `/app/jira/issues`, нажато «Обновить из Jira» в строке «Jira», пакет выполнен +- **THEN** в хабе один пакет `refresh` автора `viewer`; список задач перезапрошен; строка «Jira» показывала «пакет #N + выполняется», затем «пакет #N выполнен»; строка «Вики» не изменилась + +#### Scenario: Один прогон на модуль +- **WHEN** нажата «Обновить из Jira» и, пока пакет выполняется, выполнен переход между разделами и нажата «Обновить из + Jira» в шапке доски +- **THEN** кнопки «Обновить из Jira» недоступны, в хабе один пакет, его опрашивает один опрос + +#### Scenario: Опрос получил 404 +- **WHEN** опрос пакета вики получил 404 +- **THEN** опрос остановлен, строка «Вики» показывает «пакета нет в хабе», «Обновить вики» снова доступна + +#### Scenario: Автообновление хаба +- **WHEN** хаб поставил пакет «Автообновление» (автор `hub`, модуль `jira`), приложение открыто на странице «Вики» +- **THEN** не позже чем через 60 с строка «Jira» показывает «пакет #N выполняется», по завершении — новое «данные на …» + +#### Scenario: Пакет другого проекта +- **WHEN** идёт пакет `wiki_sync` проекта `STOCKS_DRRK`, текущий проект — `AWPCA` +- **THEN** строка «Вики» его не показывает, «Обновить вики» доступна + +#### Scenario: Пакет с другими командами +- **WHEN** незавершённый пакет модуля jira автора `viewer` состоит из команд `ensure_fields` +- **THEN** строка «Jira» показывает «пакет #N выполняется», кнопка «Обновить из Jira» доступна; её нажатие ставит пакет + `refresh`, и строка показывает его + +#### Scenario: Агент офлайн +- **WHEN** агент `wiki` офлайн и нажато «Обновить вики» +- **THEN** пакет поставлен; строка «Вики» — точка с текстом «агент офлайн» и событие «агент офлайн — пакет выполнится, + когда он выйдет на связь; пакет #N в очереди…» + +#### Scenario: Синхронизация уже идёт +- **WHEN** нажато «Обновить вики», хаб ответил 409 +- **THEN** строка «Вики» показывает идущий пакет, сообщения об ошибке нет + +#### Scenario: Проект без корня вики +- **WHEN** текущий проект `sfm_kedr` без корня вики +- **THEN** в блоке состояния только строка «Jira»; на свёрнутой панели — один значок состояния + +#### Scenario: Агенту нужно обновление +- **WHEN** `GET /agents` отдаёт агента `jira` с `warning` +- **THEN** строка «Jira» — точка с текстом «обновите агента», текст `warning` в подсказке + +### Requirement: Обзор проекта +Страница ядра «Обзор» (`/app/?project=<метка>`, пункт «Обзор» боковой панели) SHALL показывать шапку «Обзор» с +названием проекта и под ней карточку на каждый модуль, у которого есть данные для проекта: название модуля, агент (с +версией из `GET /agents`), свежесть с охватом, текущее событие пакета, вторичная кнопка обновления и сводка, которую +даёт модуль. Сводка jira: число своих задач проекта, из них открытых и просроченных (по снимку доски проекта), ссылки +«Доска связей», «Задачи», «Лента». Сводка wiki: число страниц, поле поиска по вики (Enter открывает «Вики» с этим +поиском), ссылка «Вики». Модуль без данных для проекта SHALL показывать карточку с объяснением и действием модуля (у +вики — «Привязать корень вики» ведёт на страницу «Вики»). Под карточками — «Последние пакеты проекта»: до 10 из +`GET /batches?project=<метка>`: номер, модуль, название, автор, статус, время создания. + +#### Scenario: Главная +- **WHEN** открыт `/app/` без проекта, последний выбранный — `AWPCA` +- **THEN** адрес `/app/?project=AWPCA`, две карточки — «Jira» и «Вики», список последних пакетов + +#### Scenario: Поиск по вики с обзора +- **WHEN** на «Обзоре» `AWPCA` в поле поиска вики введено «согласов» и нажат Enter +- **THEN** открыта «Вики» с результатами поиска «согласов» + +#### Scenario: Проект без вики +- **WHEN** «Обзор» проекта `sfm_kedr` без корня вики +- **THEN** карточка «Вики» с объяснением и ссылкой «Привязать корень вики» + +### Requirement: Граница модулей в вебе +Ядро веба (`web/src/{shell,ui,api,projects,auth,styles}`) MUST NOT импортировать модули, кроме списка модулей +`modules/index.ts`, и MUST NOT содержать типов, справочников и терминов модулей (типы задач Jira, закрытые статусы, +словари видов работ и связей, «статус по факту»). Модуль MUST NOT импортировать другой модуль; общее (форматтеры дат +«ДД.ММ», «ДД.ММ ЧЧ:ММ», «ДД.ММ.ГГГГ» для дат не текущего года, скачивание файла) — в ядре. Имена модулей и адреса +разделов (`to`) SHALL быть уникальны. Имя класса CSS, объявленное в стилях модуля, MUST NOT объявляться в стилях +другого модуля или ядра. Даты в списке задач и карточке SHALL выводиться форматтерами ядра, не строкой ISO. + +#### Scenario: Пересечение классов +- **WHEN** в `modules/jira/**.css` и `modules/wiki/**.css` объявлен класс `.wiki` +- **THEN** тесты фронта падают с именем класса и файлами + +#### Scenario: Импорт чужого модуля +- **WHEN** файл в `modules/wiki/` импортирует из `modules/jira/` +- **THEN** тесты фронта падают с именем файла + +#### Scenario: Дубль адреса раздела +- **WHEN** два модуля объявляют раздел с одинаковым `to` +- **THEN** тесты фронта падают с адресом и модулями