2026-10-19 20:31:19 +04:00

259 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 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
«<key> не в проекте 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`, нестрока — 422 `bad_project`.** Обоснование: как у
остальных маршрутов (`body.get("author") or "api"`); спека требует 422 для не-строки. Цена ошибки: пустая строка
пишется автором `api`, а не отвергается.
23. 2026-10-19 — **`CommandError` из постановки refresh в `POST /projects` — 422 `bad_batch`, а не `bad_project`.**
Обоснование: ошибка относится к пакету, а не к полям проекта; спека называет только `project_exists` и
`bad_project`. Цена ошибки: код ошибки в ответе.