2026-10-19 21:47:46 +04:00

52 KiB
Raw Blame History

Context

main eb485d7. Устройство, на которое опираются правки:

  • Настройки — src/jira_hub/settings.py: DEFAULTS содержит base_jql = "project = AURORA AND labels = AWPCA", project_key = "AURORA", board_id = None; AGENT_KEYS отдаёт агенту base_jql и board_id. На живом хабе board_id = 4216, settings_version = 10, schema_version = 3. Ключи labels, comment_marker хаб и агент не читают (наследие jira-sync), здесь не трогаются.
  • Полный прогон — api (POST /refresh {full: true}) и queue.maybe_auto_refresh ставят refresh {jql: base_jql}; queue.is_full_refresh (по params.jql == base_jql) решает, считать ли загрузку полной (authoritative в mirror.apply_state → пересчёт neighbor). mirror.board(conn, base_jql) ищет last_sync по sync_run.query = base_jql.
  • Снимок доски и список — mirror.board отдаёт всё зеркало; mirror.list_issues фильтрует по label через json_each(i.labels); признак neighbor — колонка зеркала (вне базовой JQL).
  • Выдача команд — queue.deliver_params дописывает параметры при выдаче (сейчас size у ensure_attachment), в базе params без них.
  • Агент (jira-agent/src/agent.py, v11): sprints() читает активные и будущие спринты доски settings.board_id; sprint_names(keys) в refresh собирает имена спринтов задач по этой доске; do_ensure_sprint без board_id — failed. Канбан-доска на GET /board/{id}/sprint отвечает Jira ошибкой (спринтов у неё нет).
  • MCP — mcp_server.py, user scope, uv run --project ~/projects/jira-bridge/jira-hub jira-hub mcp; ходит только через client. Описание batch_create требует метку AWPCA в ensure_issue.
  • Веб-приложение — shell/Layout.tsx (шапка «Хаб Jira», вкладки «Доска», «Задачи»), доска — board/useBoardSource.ts (GET /board), список — issues/IssuesPage.tsx (GET /issues, варианты фильтров из снимка доски). Раскладка доски — board/storage.ts, ключи localStorage graph-<имя>… от имени источника (у хаба — «Хаб»). fromHub.ts ставит hasSprint: true всегда.

Goals / Non-Goals

Goals: несколько проектов-меток в одном мосте; у каждого своя доска Jira (scrum или kanban); сессия Claude и страница веб-приложения работают в разрезе одного проекта; AWPCA после выкатки работает как раньше.

Non-Goals и Anti-Goals: см. proposal.md.

Decisions

  1. Проект — таблица project, схема v4. Поля: label (TEXT PK — метка Jira, она же ключ проекта), name (TEXT, непустое), board_id (INTEGER или NULL), board_type (scrum | kanban | NULL — ещё не известен), created_at. Отдельная таблица, а не ключ настроек: проект — сущность со своим жизненным циклом, а настройки уходят агенту целиком и поднимают settings_version при каждой правке. Создание проекта settings_version не поднимает: агенту проекты не нужны, доски он получает в параметрах команд (решение 7).
  2. Метка проекта — непустая строка без пробельных символов, запятых, кавычек и скобок (метка Jira не может содержать пробел; остальное исключено, чтобы метку можно было без экранирования вставить в JQL labels in (…)), до 100 символов. Регистр сохраняется как введён; совпадение с существующей без учёта регистра — 409 (Jira сравнивает метки без учёта регистра в JQL). Название — непустое после обрезки пробелов, до 100 символов. board_id — целое > 0 или отсутствует.
  3. Базовая JQL собирается, а не хранится. base_jql = "project = <project_key> AND labels in (<метки по created_at, затем label>)"; функция в settings (или новом модуле projects), её используют POST /refresh {full}, автообновление, is_full_refresh, mirror.board и GET /settings (поле base_jql в ответе — вычисленное, только для чтения; PUT /settings с base_jql или board_id — 422 с указанием, что JQL собирается из проектов, а доска — свойство проекта). Без проектов base_jql = null: POST /refresh {full} — 422 «нет проектов», автообновление не ставится. Решение владельца: одна JQL на все проекты.
  4. Миграция v3 → v4 (в одной транзакции с копией базы, как прежние): создать project и колонку batch.project; если base_jql в настройках имеет вид … labels = <M> или … labels in (<M1>, …) — завести проекты с этими метками; название проекта AWPCA — «АРМ КА», остальных — сама метка; board_id из настроек достаётся проекту, если проект один, иначе никому (с предупреждением в лог); board_type = NULL (определит первый refresh). Строкам sync_run с query = старая base_jql записать query = новая вычисленная — иначе доска до первого полного прогона показывала бы «полного прогона не было»; так же переписать params.jql команд refresh в queued и claimed, равный старой base_jql, — иначе такой прогон не признается полным. Ключи base_jql и board_id в setting миграция не удаляет: базу открывает любой процесс новой версии (HubClient.from_home в MCP, CLI), и это может случиться после git pull, но до перезапуска старого хаба, а старый хаб эти ключи читает. Новый код base_jql из setting не читает (вычисляет из проектов), а board_id остаётся в AGENT_KEYS как запасной для агента: по нему агент v12 выполнит ensure_sprint, поставленные до миграции без board_id. Ключи убираются отдельной задачей после выкатки (follow-up). В DEFAULTS новой версии ключей base_jql и board_id нет — на новой базе их не появляется. Если base_jql не разобрана — проектов нет, в лог — предупреждение с прежним значением. jira-hub init --config заводит проект AWPCA «АРМ КА» с board_id из конфига вместо записи base_jql/board_id.
  5. Разрез по проекту — одна функция зеркала для доски и списка. Для проекта P: свои — задачи зеркала с меткой P; соседи — задачи без метки P, связанные со своими связью (в любую сторону), и эпики своих (epic_key), если они есть в зеркале. GET /board?project=P отдаёт своих и соседей, neighbor = false у своих и true у соседей; links — связи, у которых хотя бы один конец свой. GET /issues?project=P — та же выборка, фильтр neighbor относится к признаку относительно P; остальные фильтры — поверх. Без project — прежнее поведение (всё зеркало, neighbor из колонки). Неизвестный проект — 404. last_sync — общий (прогон один на все проекты). Доска признак neighbor не читает (fromHub.ts): соседи по связям из других проектов рисуются обычными карточками, как сейчас рисуются соседи зеркала; «+ соседи по связям» доска считает сама по связям. Признак neighbor относительно проекта виден в колонке «Сосед» списка задач. Владелец связей между проектами не ожидает, поэтому отдельной отрисовки соседей-из-другого-проекта нет.
  6. Пакет проекта — проверки в хабе, не в MCP. POST /batches {project: P, …}: неизвестный P — 422. Для каждой команды:
    • ensure_issue — если в labels нет P, хаб дописывает P в конец (параметры хранятся уже с ней); метка другого проекта в labels — 422;
    • ensure_fields — labels_remove с P или labels_add с меткой другого проекта — 422;
    • «метка другого проекта» и «P в labels_remove» сравниваются без учёта регистра (Jira в JQL сравнивает метки так же): awpca в пакете STOCKS_DRRK — тоже чужая метка;
    • команды над существующей задачей (ensure_fields, ensure_comment, ensure_status, ensure_sprint, ensure_attachment) — key должен быть временным ключом этого пакета или задачей зеркала с меткой P, иначе 422 « не в проекте P»;
    • ensure_link — хотя бы один из source, target свой (временный ключ пакета или задача с меткой P);
    • refresh — без ограничений. Пакет хранит batch.project = P; GET /batches?project=P — только пакеты проекта. POST /commands/{id}/retry принимает необязательный project: передан и у исходного пакета другой проект (в том числе NULL) — 422 «команда #N не из проекта P»; передан и совпадает — новый пакет с P и проверками; не передан — новый пакет получает проект исходного. MCP в привязанной сессии всегда передаёт свой P, поэтому сессия не может повторить чужую команду или команду пакета без проекта. Без project — прежнее поведение, проверок нет, batch.project = NULL (автообновление, веб-приложение, старые клиенты). Проверки в хабе, а не в MCP: MCP по границам ходит только через client, а правило о метке проекта должно действовать для любого клиента, который назвал проект.
  7. Доски — в параметрах команд, настройки board_id нет. Агенту не нужен список проектов, ему нужны доски:
    • ensure_sprint — при постановке хаб определяет проект задачи (метки задачи в зеркале ∩ проекты; для временного ключа — метки её ensure_issue в пакете) и дописывает в параметры board_id его доски. 422 при постановке: задача ни в одном проекте; в нескольких проектах с разными досками; у проекта нет доски; доска kanban («у проекта P канбан-доска, спринтов нет»); тип доски ещё не известен («дождитесь обновления из Jira»). sprint: "backlog" на scrum-доске — как раньше. board_id от клиента в ensure_sprint — 422 (его выставляет хаб). board_id хранится в params (видно в истории команд, переживает довыдачу); повтор команды (retry) снимает board_id с параметров до проверки и вычисляет доску заново.
    • refresh — при выдаче (deliver_params, в базе не хранится) хаб дописывает boards: [{id, type}] — все доски проектов без повторов, type — известный тип или null. Агент для досок с type = null запрашивает GET /rest/agile/1.0/board/{id} и возвращает в результате команды boards: {"<id>": "scrum" | "kanban" | null} (null — доска не найдена или нет доступа; причина — в лог агента); хаб записывает известные типы в project. Имена спринтов агент собирает со всех scrum-досок (известных и только что определённых); канбан-доски пропускает. Одна задача в спринтах двух досок (или двух спринтов одной) — побеждает последнее найденное имя, как в v11 (sprint_names перезаписывает). Агент v12 без boards/board_id в параметрах (старый хаб или команда, поставленная до миграции) берёт settings.board_id, как v11 (ключ остаётся до follow-up, решение 4).
  8. MCP: привязка к проекту флагом запуска. jira-hub mcp --project <метка>. Привязка проверяется при каждом вызове (хаб может быть не запущен при старте MCP): метки нет в GET /projects — инструмент отвечает «проект <метка> не заведён в хабе: project_create или исправьте --project в .mcp.json». В привязанной сессии:
    • issues — GET /issues?project=P; issue — любая задача по ключу (явный ключ — осознанный запрос, а карточка соседа нужна при работе со связями);
    • notes_set — только своя задача проекта (метка P; соседи — нет), иначе отказ;
    • batch_create, command_retry — с project = P (проверки решения 6);
    • batches — GET /batches?project=P; batch_status, batch_wait, batch_cancel — по id, без ограничения;
    • refresh — full — общий полный прогон, keys — как раньше. Без привязки инструменты issues, issue, notes_set, batch_create, command_retry, refresh отвечают «сессия не привязана к проекту: …» с подсказкой про --project и projects; остальные (projects, project_create, agent_status, sync_log, settings_get, settings_set, batches, batch_status, batch_wait, batch_cancel) работают. Решение владельца: «только свои»; безопасное поведение без привязки — моё (см. ниже).
  9. project_create в MCP — POST /projects {label, name, board_id?, author: "claude"}; ответ — проект, id пакета обновления и готовый фрагмент .mcp.json: {"mcpServers": {"jira-hub": {"command": "uv", "args": ["run", "--project", "<корень хаба>", "jira-hub", "mcp", "--project", "<метка>"]}}} с инструкцией: положить в корень репозитория проекта (слить с имеющимся .mcp.json) и сделать /mcp → reconnect (одобрить project-scope сервер). Запись jira-hub в project scope перекрывает одноимённую из user scope. Корень хаба MCP вычисляет от расположения своего пакета (src/jira_hub/ → корень репозитория), не зашивая путь; путь к .venv в подсказку не попадает (uv run восстановить из sys.argv нельзя). Если MCP запущен из worktree, в подсказке окажется путь worktree — это видно в ответе, рабочий MCP запускается из основного checkout. projects — таблица: метка, название, доска, тип доски («неизвестен», пока агент не определил), число своих задач; в привязанной сессии отмечает текущий проект.
  10. POST /projects — {label, name, board_id?, author?} → 201 {project, batch_id}; проект и пакет «Новый проект <метка>» из одного полного refresh ставятся в одной транзакции; автор пакета — author (по умолчанию api). Ошибки: 422 — неверные поля (решение 2), 409 — метка уже есть. GET /projects → список {label, name, board_id, board_type, created_at, issues_count} по created_at, затем label; issues_count — задачи зеркала с меткой проекта.
  11. Веб-приложение — проект в адресе и в памяти браузера. Выбор проекта — выпадающий список рядом с «Хаб Jira» (вариант A): «<метка> — <название>», текущий отмечен; в конце — «+ Новый проект…». Текущий проект — параметр project в адресе /app/board и /app/issues; без него — последний выбранный (localStorage), иначе первый из GET /projects; выбранный записывается в адрес (replace). Неизвестный проект в адресе — сообщение «проект <метка> не найден» со списком проектов. Нет ни одного проекта — доска и список показывают приглашение создать проект. Переходы «Доска» / «Задачи» сохраняют проект. Карточка задачи (/app/issues/{key}) от проекта не зависит. Форма «Новый проект» — окно поверх страницы: метка, название, номер доски (необязательно); ошибка хаба (detail) — рядом с кнопкой «Создать»; после создания — переключение на новый проект. Пакет создания ставится с автором viewer, поэтому доска подхватывает его как свой refresh (правило «Источник данных доски») и показывает ход.
  12. Доска в разрезе проекта. GET /board?project=P; «Обновить из Jira» — как раньше POST /refresh {full: true} — обновляет все проекты (решение владельца). Состояние доски в localStorage — отдельно на проект. Сейчас в web/src/board/storage.ts ключи от имени источника не зависят: fileKey(n) = graph-<n>:board (pos, posdate, lanes, fold, sprint, who, colw) и pref(n) = graph-<n> (флаги neighbors, onlylinked, crit, bydate, hidekind, hideedge, snap, tbl, prob). На проект делятся оба набора: у проекта AWPCA — ровно нынешние ключи, у остальных — те же ключи с суффиксом @<метка> (graph-pos:board@STOCKS_DRRK, graph-crit@STOCKS_DRRK); общие для всего приложения настройки вне доски (если есть) не делятся. Загрузка CSV/JSON и applyJsonSettings пишут в ключи текущего проекта. Так раскладка AWPCA переживает выкатку, а проекты не смешиваются. Фильтр спринтов: hasSprint в модели из хаба — есть ли хотя бы одна своя задача (neighbor = false) со спринтом (даже если канбан-проект связан с задачами scrum-проекта); у канбан-проекта фильтра спринтов нет («колонки «Спринт» нет — показаны все задачи», как у CSV без спринтов).
  13. Список задач в разрезе проекта. GET /issues?project=P&…; варианты фильтров — из снимка GET /board?project=P; параметр project в адресе — рядом с фильтрами; фильтр «Метка» остаётся.
  14. Порядок выкатки. (1) Агент v12 опубликован и agent_status показывает v12 — он понимает и boards/board_id в параметрах, и прежний settings.board_id. (2) Слияние хаба, make web, перезапуск хаба (владелец) — миграция v4. (3) min_agent_version = 12. (4) В awp-ca положить .mcp.json с --project AWPCA и переподключить MCP — до этого сессии awp-ca с задачами работать не могут (решение 8). Хаб новее агента v11 до шага (1) не выкатывать: v11 не знает boards и board_id в параметрах и читает спринты только с доски из настроек — спринты задач других проектов пропадут из зеркала, а ensure_sprint для них уйдёт не на ту доску. Миграция v4 нестрашна для старого хаба, если базу откроет новый MCP/CLI раньше перезапуска (решение 4), но между git pull и перезапуском хаба лучше не открывать новые сессии MCP.
  15. Агент v12 — отдельные issue, ветка, design-only change и PR в jira-agent. Контракт (параметры boards, board_id, результат boards) — в этом change (спека agent живёт в хабе); у агента — решения реализации. Агент сливается и публикуется первым (решение 14).

Risks / Trade-offs

  • Задача с метками двух проектов видна в обоих; ensure_sprint для неё при разных досках — 422. Осознанно: владелец связей между проектами не ожидает.
  • Задача проекта без метки проекта (метку сняли в Jira руками) выпадает из полной JQL и остаётся в зеркале со старыми данными, как сейчас.
  • Без .mcp.json в awp-ca сессии там после выкатки не видят задач — это намеренно (анти-цель «не та метка»), но шаг выкатки легко забыть; он в чеклисте выкатки PR.
  • refresh {keys} хвостом пакета тоже получает boards и тратит по одному вызову GET /board/{id} на доску с неизвестным типом — только до первого определения.
  • Ошибка в номере доски при создании или доска, понадобившаяся канбан-проекту позже, интерфейсом не исправляются (правки проекта нет). Решение владельца 19.10.2026: на первое время он правит базу сам — UPDATE project SET board_id = <номер>, board_type = NULL WHERE label = '<метка>'; тип доски определит следующий refresh. Агенты (Claude) базу хаба напрямую не правят. Доска, на которую агент ответил «не найдена» (null), остаётся с board_type = NULL: каждый refresh снова запрашивает её тип, ensure_sprint отвечает «тип доски ещё не известен».
  • Создание проекта меняет базовую JQL, поэтому last_sync доски становится null («полного прогона не было»), пока не завершится пакет «Новый проект» — он ставится сразу, доска показывает его ход. Переписывать sync_run.query здесь было бы неправдой: прежний прогон новую метку не покрывал.
  • Сравнение меток без учёта регистра (решение 2) при создании; в разрезе проекта (решение 5) метка сравнивается точно, как в зеркале (json_each). Jira хранит метку так, как её записали, — расхождение возможно только при ручной правке метки в Jira другим регистром.

Decisions made without discussion

  1. 2026-10-19 — проект — таблица project, а не ключ настроек. Обоснование: сущность со своим циклом; правка не должна поднимать settings_version и перечитывание настроек агентом. Цена ошибки: таблица и миграция; перенос в настройки — одна миграция назад.
  2. 2026-10-19 — сессия MCP без --project не читает и не пишет задачи. Обоснование: анти-цель — задача с меткой не того проекта; владелец согласился с этим в обсуждении («все ок»), сам режим «только служебные инструменты» — моё уточнение. Цена ошибки: awp-ca до своего .mcp.json не работает с задачами; смягчить — умолчание по проекту.
  3. 2026-10-19 — проверки метки и принадлежности задачи проекту — в хабе (POST /batches {project}), а не в MCP. Обоснование: граница «MCP только через client», правило нужно и будущим клиентам. Цена ошибки: поле пакета и проверки в queue; перенос в MCP — без изменения базы.
  4. 2026-10-19 — хаб дописывает метку проекта в ensure_issue сам (в пакете с project). Прежнее правило «хаб метки сам не добавляет» остаётся для пакетов без проекта. Обоснование: Claude не должен помнить метку — это и есть анти-цель. Цена ошибки: одна ветка в проверках постановки.
  5. 2026-10-19 — issue в MCP открывает любую задачу по ключу, issues — только проект. Обоснование: явный ключ — осознанный запрос, карточка соседа нужна при работе со связями. Цена ошибки: одна проверка в MCP.
  6. 2026-10-19 — доски передаются агенту в параметрах команд (ensure_sprint.board_id при постановке, refresh.boards при выдаче), тип доски определяет агент в refresh. Обоснование: хаб не ходит в Jira; отдельная команда «узнать доску» потребовала бы нового типа команды. Цена ошибки: два параметра контракта.
  7. 2026-10-19 — тип доски неизвестен → ensure_sprint 422, а не попытка на удачу. Обоснование: создание проекта само ставит refresh, окно короткое. Цена ошибки: одна ошибка постановки сразу после создания проекта.
  8. 2026-10-19 — разрез по проекту: свои + соседи по связям + эпики своих; neighbor относительно проекта. Обоснование: логика доски и «+ соседи» уже построена на neighbor; эпики нужны доске для имён. Цена ошибки: функция выборки и тесты к ней.
  9. 2026-10-19 — миграция называет проект AWPCA «АРМ КА», а board_id из настроек отдаёт ему. Обоснование: так называется проект в workspace; доска 4216 — его. Цена ошибки: название правится будущим «переименованием» или вручную.
  10. 2026-10-19 — раскладка доски AWPCA остаётся на прежних ключах localStorage, остальные проекты — на ключах с меткой. Обоснование: анти-цель «AWPCA потерял раскладку». Цена ошибки: особый случай в одном месте storage.ts.
  11. 2026-10-19 — project_create отвечает готовым .mcp.json с командой запуска текущего процесса MCP. Обоснование: владелец хочет, чтобы Claude сам прописал проект в репозитории; путь хаба не зашивается. Цена ошибки: формат подсказки.
  12. 2026-10-19 — метка проекта без пробелов, запятых, кавычек, скобок; уникальность без учёта регистра. Обоснование: безопасная подстановка в JQL. Цена ошибки: отказ на экзотической метке.
  13. 2026-10-19 — GET /batches?project= и batches в привязанной сессии — только пакеты проекта; batch_status/wait/cancel по id — без ограничения. Обоснование: список не засоряется чужими пакетами, а явный id — осознанный запрос. Цена ошибки: фильтр списка.
  14. 2026-10-19 — миграция v4 не удаляет base_jql и board_id из setting; board_id остаётся запасной доской агента; удаление — follow-up. Обоснование: базу мигрирует любой процесс новой версии (MCP, CLI), возможно — под работающим старым хабом (находка builder check). Цена ошибки: два мёртвых ключа до follow-up.
  15. 2026-10-19 — миграция переписывает params.jql у refresh в queued/claimed со старой базовой JQL. Обоснование: иначе прогон из очереди не признается полным. Цена ошибки: одна строка миграции.
  16. 2026-10-19 — retry с project отказывает, если проект исходного пакета другой; хаб снимает свой board_id при повторе. Обоснование: сессия проекта не должна повторять чужие команды; иначе повтор ensure_sprint падал бы на своей же проверке «board_id от клиента». Цена ошибки: поле тела retry и одна ветка.
  17. 2026-10-19 — чужая метка проекта и labels_remove P — без учёта регистра; принадлежность задачи — точно. Обоснование: Jira в JQL сравнивает метки без учёта регистра — awpca попала бы в общую JQL, но ни в чей разрез. Цена ошибки: сравнение в проверках.
  18. 2026-10-19 — notes_set в привязанной сессии — только свои задачи, без соседей. Обоснование: «только свои» владельца. Цена ошибки: одна проверка.
  19. 2026-10-19 — фрагмент .mcp.json — uv run --project <корень хаба от расположения пакета>. Обоснование: вызов uv run из sys.argv не восстановить, путь к .venv в чужом репозитории хрупок. Цена ошибки: формат подсказки.
  20. 2026-10-19 — на проект делятся и раскладка (fileKey), и флаги доски (pref); AWPCA — на нынешних ключах, остальные — с суффиксом @<метка>. Обоснование: спека требует хранить фильтры отдельно; анти-цель — потеря раскладки AWPCA. Цена ошибки: схема ключей в storage.ts.
  21. 2026-10-19 — last_sync после создания проекта — null до завершения его обновления, sync_run.query не переписывается. Обоснование: прежний прогон новую метку не покрывал. Цена ошибки: временная надпись на доске.
  22. 2026-10-19 — пустая строка и отсутствие author в POST /projects — как api; любое другое нестроковое значение (в том числе 0, false, [], {}) — 422 bad_project. Обоснование: как у остальных маршрутов (пусто — api); спека требует 422 для не-строки, и 0/false не должны молча становиться api. Цена ошибки: пустая строка пишется автором api, а не отвергается.
  23. 2026-10-19 — CommandError из постановки refresh в POST /projects — 422 bad_batch, а не bad_project. Обоснование: ошибка относится к пакету, а не к полям проекта; спека называет только project_exists и bad_project. Цена ошибки: код ошибки в ответе.
  24. 2026-10-19 — GET /batches?project= с неизвестной меткой — пустой список, а не 404; пустой project= — как без фильтра. Обоснование: спека требует 404 только для /issues и /board; у пакетов проект — произвольная сохранённая метка. Цена ошибки: одна ветка в h_batches_get.
  25. 2026-10-19 — в ensure_issue пакета проекта повторы других (не P) меток не убираются. Обоснование: спека убирает только повтор P; остальное — как прислал клиент. Цена ошибки: одна строка в _check_project.
  26. 2026-10-19 — project в retry / POST /batches не строка — 422 bad_retry / bad_batch; в retry_command сначала сверка project с проектом исходного, затем create_batch с проверками проекта. Обоснование: «команда #N не из проекта P» точнее ошибки проверок. Цена ошибки: текст 422.
  27. 2026-10-19 — типы досок из результата refresh пишутся в любом статусе отчёта (done и failed), если в результате-словаре есть ключ boards; без ключа или с мусорными значениями типы не трогаются. Обоснование: тип доски — факт о доске, а не часть выборки: проваленная выборка задач не делает присланный тип неверным; спека commands требует записать присланные типы по результату refresh. Поздний отчёт по уже терминальной команде типы не пишет (ветка терминальной команды возвращается раньше). Цена ошибки: одно условие в report_result.
  28. 2026-10-19 — метки временного ключа для ensure_sprint берутся только из его ensure_issue в пакете, без обращения к зеркалу. Обоснование: new:<n> в зеркале — не задача Jira; метки пакета (с дописанной меткой проекта) единственный надёжный источник. Цена ошибки: ветка в _assign_boards.
  29. 2026-10-19 — проверка метки сессии (GET /projects) выполняется внутри guard, поэтому при выключенном хабе привязанная сессия отвечает «хаб не запущен: jira-hub serve», а не «проект не заведён»; в async-инструментах проверка идёт через asyncio.to_thread. Обоснование: Review Focus 4 плана; scoped стоит под guard. Цена ошибки: порядок декораторов; лишний GET /projects на каждый вызов (в refresh(wait=True) — второй, из batch_wait).
  30. 2026-10-19 — projects и project_create без привязки не проверяют метку сессии; notes_set для не временного ключа проверяет принадлежность по labels из GET /issues/{key} (точное совпадение, как в хабе). Обоснование: спека исключает эти два инструмента из проверки; «только своя задача» требует метки P. Цена ошибки: одна ветка в notes_set.
  31. 2026-10-19 — ProjectGate в состоянии «проект из адреса не найден» не рисует второй выпадающий список: выбор — в списке «Проект» верхней панели, страница даёт текст «проект <метка> не найден» и подсказку. Обоснование: Task 8 брифа велел поставить ProjectPicker и в страницу, но два combobox с одним именем «Проект» двусмысленны для доступности и тестов (getByRole падает); панель с выбором присутствует всегда, спека («вместо данных» — данные не показываются) выполнена. Цена ошибки: вернуть <ProjectPicker /> в ветку unknown и getAllByRole в тесте.
  32. 2026-10-19 — useCreateProject в onSuccess возвращает промис инвалидации qk.projects, поэтому onCreated вызывается уже после перечитывания списка. Обоснование: иначе select(новая метка) выполнялся бы раньше, чем метка появится в списке, и страница мигнула бы «проект не найден». Цена ошибки: вспышка сообщения на долю секунды; ожидание — один лишний GET /projects перед закрытием окна.
  33. 2026-10-19 — служебное значение пункта «+ Новый проект…» в <select> — строка " new" (с пробелом, который метка содержать не может); выбор проекта из панели ведёт на раздел /issues, если открыт он, иначе (в том числе на «не найдено» и под карточкой с чужим разделом) — на /board. Обоснование: инвариант метки (без пробельных символов) исключает коллизию; брифом раздел для остальных страниц не задан. Цена ошибки: константа NEW и выражение section в Layout.tsx.
  34. 2026-10-19 — тесты, которые брифом не названы, поправлены под новые сигнатуры: auth/LoginScreen.test.tsx (useBoard("AWPCA"), qk.board("AWPCA")), issues/IssueModal.test.tsx (мок отдаёт /projects); в App.test.tsx ожидание «/app ведёт на доску» — через waitFor. Обоснование: без /projects оболочка не показывает страницы; findByTestId ловил заглушку «Загрузка настроек», которую страница сменяет другим элементом с тем же data-testid, и проверка падала на оторванном узле. Цена ошибки: только тесты.
  35. 2026-10-19 — в тесте storage.test.ts выражение {…} as JsonImport заменено на as unknown as JsonImport. Обоснование: tsc --noEmit (шаг make check) отвергает прямое приведение частичного объекта к JsonImport (нет model, dateColumns, collapsedLanes); в тесте важны только layout, layoutByDate, laneOrder, filters, и applyJsonSettings читает только их. Цена ошибки: тест передаёт неполный импорт; если applyJsonSettings начнёт читать другие поля, тест выдаст неверный результат, а не ошибку типа.
  36. 2026-10-19 — POST /batches с неизвестным project: сообщение «проекта {P} нет» без префикса «команда {i}». Обоснование: ошибка пакетная, а не одной команды. Цена ошибки: только текст сообщения.
  37. 2026-10-19 — GET /projects отвечает {"projects": [...]}. Обоснование: как прочие списки API ({"batches": …}), ответ расширяем полями без поломки клиентов. Цена ошибки: форма ответа; клиент client и веб читают ключ projects.
  38. 2026-10-19 — пакет «Новый проект» — с batch.project = NULL. Обоснование: его refresh по общей JQL обновляет все проекты, а не один; привязка к новой метке спрятала бы его из общих списков и от проверок других проектов. Цена ошибки: пакет виден в GET /batches без фильтра и не виден с ?project=.
  39. 2026-10-19 — доска для ensure_sprint выбирается и в пакетах без проекта (веб-приложение, старые клиенты); задача вне всех проектов — 422, хотя раньше проходила. Обоснование: без доски проекта агенту нечего передать, прежней общей доски в настройках больше нет (решение 4). Цена ошибки: постановка спринта задаче без метки проекта отказывает с «не входит ни в один проект».
  40. 2026-10-19 — board_id от клиента в ensure_sprint отвергается общей проверкой «лишние параметры board_id», без отдельного текста. Обоснование: параметр ставит только хаб; общая проверка уже называет допустимые параметры. Цена ошибки: текст отказа не объясняет, что доску определяет проект.
  41. 2026-10-19 — веб: память выбранного проекта — localStorage["hub-project"]; ручная смена проекта — новая запись истории, фильтры списка сбрасываются; автоподстановка проекта в адрес — replace и только без открытой карточки. Обоснование: «Назад» должен возвращать к прежнему проекту; подстановка не должна заменять прямой адрес карточки. Цена ошибки: один ключ localStorage и ветка в подстановке адреса.
  42. 2026-10-19 — e2e: пакет «Новый проект» (автор viewer) в projects.spec.ts разбирается agentServeRefresh(). Обоснование: иначе он остаётся в очереди и держит «Обновить из Jira» в других файлах на общем тестовом хабе. Цена ошибки: нестабильные e2e соседних файлов при смене порядка файлов.
  43. 2026-10-19 — миграция v3 → v4 — Python-шаг projects.migrate_v3, вызываемый из db.MIGRATIONS[3] с ленивым импортом; кавычки вокруг метки в старой base_jql снимаются; JQL с хвостом (ORDER BY и т. п.) не разбирается — проектов нет, предупреждение в лог. Обоснование: чистый SQL метки из JQL не вынимает; projects зависит от db, а не наоборот, поэтому импорт ленивый. Цена ошибки: при нестандартной base_jql проекты придётся завести вручную (POST /projects).
  44. 2026-10-19 — base_jql убран из AGENT_KEYS (агент его не читает: JQL приходит в params.jql команды). Обоснование: ключ в настройках агента вводил бы в заблуждение и расходился с вычисляемым значением. Цена ошибки: старый агент без ключа — проверено, он берёт JQL из команды.
  45. 2026-10-19 — метка в ?project=, POST /batches {project}, retry {project}, --project сравнивается точно (регистр значим). Обоснование: метка хранится как введена, принадлежность задачи — точное совпадение (общие правила); неточная привязка размыла бы разрез. Цена ошибки: --project awpca отвечает «проект не заведён».
  46. 2026-10-19 — веб: набор ключей доски — состояние модуля storage.ts, выставляется в useBoardPrefs при монтировании страницы (страница пересоздаётся по key={project}); миграции старых ключей — только для AWPCA. Обоснование: функции хранилища вызываются из множества мест без параметра проекта; пересоздание страницы исключает гонку ключей. Цена ошибки: глобальное состояние модуля; чтение ключей до монтирования страницы даст набор прежнего проекта.
  47. 2026-10-19 — тестовый хаб: seed-hub.py дописывает метку AWPCA задачам фикстуры без neighbor; файл фикстуры не меняется. Обоснование: в hub-basic метка есть только у двух задач из десяти, а разрез доски — по метке; golden- эталоны привязаны к фикстуре. Цена ошибки: e2e видят задачи, которых по меткам фикстуры в проекте нет.
  48. 2026-10-19 — эталон hub-empty.expected.json не перегенерируется (его пишет прежняя страница, где hasSprint всегда true): тест сравнивает с ним, подменив hasSprint на false. Обоснование: решение 12 меняет смысл hasSprint; эталон — снимок прежней страницы. Цена ошибки: эталон расходится с прежней страницей в одном поле.