Ctrl K
ComunidadGuías…

CHANGELOG.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Changelog

Все заметные изменения Полки. Формат — [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/), версии — [SemVer](https://semver.org/lang/ru/). До `1.0.0` API, схема БД и интерфейс могут меняться между prerelease-версиями.

## [Unreleased]

### Added

- **Стенд «Контур»** ([deploy/contour-stand](deploy/contour-stand/README.md)) — окружение для сторонних агентов на локальной ВМ: профиль задачи от ИБ, диспетчер (Keycloak → ключ LiteLLM с бюджетом → токен задачи Полки → правила Squid → песочница gVisor), подстановка git-токена прокси, Gitea с защищённым `main`, журнал по `task_id` с расходом и сводкой сети, отзыв задачи. `checks.sh` проверяет границы изнутри песочницы и ищет секреты стенда в её окружении.
- **Рой на стенде «Контур»** ([deploy/contour-stand](deploy/contour-stand/README.md#рой-компания-из-агентов)) — процессы компании как пакеты в каталоге, а не отдельные внедрения. В составе: маркет навыков с допуском по digest, шина NATS JetStream, политика OPA со ступенями доверия, проверки в песочнице без сети, приёмка версии на Полке, шлюз действий с идемпотентностью и компенсацией, системы учёта только с API, подпись только принятой версии. Новый процесс появляется из описания словами и теневого прогона на прошлых данных. `swarm-demo.sh` показывает весь путь, `swarm.mjs selftest` добавляет проверки в `checks.sh`.

- **Финансовый стенд, этап M1** ([deploy/finance-stand](deploy/finance-stand/README.md)) — внешний мир синтетической фирмы для первой версии финансового продукта «хвосты квартала». Детерминированный генератор квартала с 40 заложенными хвостами и эталоном бухгалтера. Банк A отдаёт выписку в схеме T-API, банк B — в формате «Клиент-банк → 1С» (windows-1251). Есть ЭДО, учёт по образцу OData 1С (писать можно только черновиком) и почта; симулятор контрагентов отвечает по их профилям. Адаптеры банков: токен банка держит координатор, а не песочница. `fin.sh contract` проверяет адаптер на публичной песочнице Т-Бизнеса.
- **Финансовый стенд, этап M2** — журнал обязательств на событиях. Обязательство закрыто только по обеим линиям: деньги и документы. Повтор оплаты, расхождение учёта с ЭДО и смена реквизитов — исключения, их закрывает человек. Писать контрагенту можно не чаще раза в 3 дня. Сверка `fin.match` идёт через рой: навык `fin-match` в песочнице без сети, проверки, политика, отчёт на Полке, хук пишет события в журнал. `fin.sh audit` делает ту же сверку на выгрузках компании («Клиент-банк → 1С», T-API, реестр ЭДО, счета из учёта) в отдельном журнале.
- **Финансовый стенд, этап M3** — работа с хвостами: запросы первички поставщикам (ЭДО или письмо), напоминания должникам, разбор ответов в ящике бухгалтерии, черновики актов сверки. Каждый шаг — пакет со своим навыком и проверками, письма уходят после приёмки на Полке. Слова письма не закрывают оплату, «новые реквизиты» решает человек, исправленный УПД сам снимает исключение по НДС. Акт сверки подписывает только роль `signer`. `finance-demo.sh` показывает две недели работы на синтетической фирме.
- **Финансовый стенд, этап M4** — границы стенда в `checks.sh`. Изнутри песочницы агента: ЭДО только читается, банк без токена закрыт, до API банка не достучаться. Снаружи (`fin.mjs selftest`): платёжных действий нет, а политика запрещает `bank.payment`; реквизиты из письма уходят человеку; повтор запроса не создаёт второй документ; журнал отказывает второму письму раньше трёх дней; картина пересобирается из журнала; навыки сверены с допуском.
- **Финансовый стенд, этап M5** — аудит на выгрузках компании в том виде, в каком их отдаёт бухгалтер. Читаются списки 1С в Excel и CSV (счета, поступления, реализации, контрагенты), реестры Диадока и СБИС, «Клиент-банк» в Windows и DOS с несколькими счетами. Перед сверкой печатается паспорт выгрузки: что взято из каждого файла, сходятся ли выписки с итогами банка, чего не хватает (`fin.sh audit --check`). Навык `fin-match` 1.2.0:
  - находит контрагента по наименованию, если в списке нет ИНН;
  - различает одинаковые номера счетов у разных поставщиков;
  - сопоставляет УПД без основания по контрагенту, сумме и дате;
  - раскладывает платёж за несколько счетов;
  - принимает законные ставки НДС (22, 10, 7, 5, 0).

  `fin.sh review` и `fin.sh verdicts` дают точность и полноту по разметке бухгалтера: это проверка допущения 2 на настоящих данных.
- **Финансовый стенд, этап M6** — документы из почты в аудите. Читаются выгрузки ящика (`.mbox`, `.eml`) и вложения папкой. УПД в XML по формату ФНС (5.01 и 5.03) берётся без модели. PDF рядом с XML считается печатной формой. PDF или скан без XML паспорт называет с письмом, а счёт остаётся хвостом. УПД, пришедший и через ЭДО, и письмом, считается один раз.
- **Финансовый стенд, этап M7** — прямые подключения компании: банк через T-API, 1С через стандартный OData, отдельный ящик для документов через IMAP. Все подключения только читают. Коннектор кладёт новое с прошлого прогона в папку компании в том же виде, что и ручные выгрузки, поэтому аудит не меняется. Доступы хранятся в OpenBao (`fin.sh secret`, из stdin) и идут только по TLS, кроме loopback. Если источник не ответил, остальные работают дальше, а отчёт прогона в паспорте называет, на какую дату его данные. `fin.sh connect <компания> --audit`, ежедневный таймер. В ВМ есть компания для показа с настоящим IMAP-сервером: через подключения она даёт те же 41 хвост, что и ручные выгрузки.
- **Финансовый стенд: расписание шагов** — пакеты говорят, когда идут сами (`trigger.timer`): сверка `fin.match` каждый день в 09:00, запросы первички в 09:30, напоминания должникам по понедельникам в 10:00, разбор писем на каждом проходе таймера. `fin.mjs due` запускает то, что пора, в порядке рабочего дня; пропущенный за неделю слот идёт один раз, упавший шаг — снова на следующем проходе. Таймер `finance-schedule.timer` (каждые 5 минут) включается `fin.sh schedule on`, по умолчанию выключен. Акты сверки по-прежнему по запросу.
- **Финансовый стенд: решения по исключениям на Полке** — каждое открытое исключение (смена реквизитов, двойная оплата, учёт ≠ ЭДО, ошибка НДС, сумма УПД) один раз уходит на Полку работой пакета `fin.exceptions`. На странице: что случилось, основание и безопасное решение. Приёмка версии записывает решение в журнал обязательств через шлюз (`fin.decision`, запись хука пакета), повтор не пишет второе. В границах ИБ `human_only`: решает только человек на любой ступени. В рое маршрут действия может быть записью хука пакета (`"hook": true`), в политике — флаг `human_only`. Подпись актов сверки остаётся за ролью `signer`.
- **Финансовый стенд, ревью M7.** Исправлено:
  - токен Т-Банка тоже идёт только по TLS, кроме loopback, а свой корень компании действует и для банка;
  - одно огромное письмо больше не останавливает ящик;
  - два одновременных прогона и прогон, упавший посередине, не задваивают письма;
  - удалённый файл подключения берётся с начала периода;
  - ручные выгрузки рядом с файлами подключений не считаются дважды;
  - запрос к 1С кодирует пробелы как `%20`;
  - счета контрагента, помеченного на удаление, не пропадают;
  - самопроверка не проходит, если не прочитала секреты.

### Changed

- **Журнал обязательств принимает любую законную ставку НДС 2026 года** (22, 10, 7, 5, 0) и реестр без НДС. Раньше УПД поставщика на УСН без НДС становился исключением «ошибка НДС».
- **`fin.sh sync` перезапускает службы стенда.** Рой загружает хуки стенда один раз, поэтому после копирования новых файлов он продолжал работать на старом коде.
- **Рой стенда «Контур» передаёт параметры цели и даёт пакету право вето.** `swarm.mjs goal` принимает `--param`. Хук пакета может отказать в действии до шлюза (`beforeEffect`) и получает контекст: публикацию на шину и запись в ленту цели.
- **Рой стенда «Контур» опрашивает события Полки раз в 8 секунд, а не раз в 4.** Опрос раз в 4 секунды давал 150 запросов за 10 минут при лимите Полки 120 на токен. После нескольких минут работы демона `reader-bot` получал 429, и рой переставал видеть приёмку.
- **Рой стенда «Контур» подключает другие стенды** через `/etc/contour/extensions.json`: свои каталоги навыков, пакеты процессов и маршруты действий шлюза; вместо цепочки условий шлюз берёт маршруты из таблицы. Свои границы стенд кладёт в `policy/ext/<стенд>/`, а хук пакета (`work.hook`) видит результат и выполненные действия. В Squid и диспетчере новое назначение `fin` — только чтение финансовых систем.

## [0.19.0] — 2026-10-17

Обновление: без миграции. Журнал агентов и новые права по умолчанию работают только с `TEAM_SHELVES=on` (и `SERVICE_ACCOUNTS=on` для сервисных доступов).

### Added

- **Стенд «агент под ограничениями»** ([deploy/agent-stand](deploy/agent-stand/README.md)) — показ для службы ИБ на локальной ВМ: Codex в песочнице `srt` с токеном задачи сервисного доступа, обходы прав из bash, журнал агентов, приёмка, замер отзыва.
- **Проверка песочницы агента** ([AGENT_SANDBOX](docs/specs/AGENT_SANDBOX.md)). `scripts/polka-sandbox-check.mjs` (Node 22, без зависимостей) проверяет на конкретной машине, что песочница агента не даёт прочитать приманки (`cat`, `bash`, `python3`, `node`), не пропускает секрет из окружения, запись вне рабочей папки, сеть и DNS, Docker. Каждая проверка идёт в паре с контролем без песочницы, каждый инструмент сначала читает разрешённый файл, поэтому сломанная песочница даёт «не проверено», а не «заблокировано». Отчёт — таблица и JSON.
- **Журнал агентов полки отдела** ([TEAM_SHELVES](docs/specs/TEAM_SHELVES.md#журнал-агентов-16102026)). `GET /api/shelves/:id/agent-audit` для администратора полки: действия под токенами агентов, события сервисных доступов и приёмка — с подключением, сервисным доступом, номером задачи и ответственным. Чтение содержимого версии сервисным доступом теперь пишется в журнал (`revision.read`, одна строка на задачу и версию); чтения людей остаются дневным счётчиком. Без миграции.

### Changed

- **Пустая полка не обещает загрузку папки.** Карточка «Загрузить файл» говорила, что с компьютера можно загрузить папку проекта, а выбора папки в вебе нет. Теперь: «Папку проекта сохраняет агент».
- **Узкие права агента на полке отдела.** На странице согласия OAuth для полки отдела по умолчанию отмечены только «Сведения и статус», чтение, исходники и «Сохранять новые работы»; «Создавать версии» и «Управлять…» отмечаются вручную ([MCP_CONNECTOR](docs/MCP_CONNECTOR.md)). На своей полке по-прежнему отмечено всё. Галочки помнятся для каждой полки отдельно. В ответе `GET /oauth/authorize/details` появилось поле `teamDefaultScopes`.

### Fixed

- **Ссылка после правки.** Если агент после `polka_revise` просил новую ссылку, а у работы уже была открытая ссылка на прошлую версию, Полка отвечала 409 и не называла эту ссылку, так что перенести её (`polka_share` с `moveShareId`) агент не мог. Теперь отказ содержит `shareId` и `revisionId` открытой ссылки.
- **«Вставить текст» сохраняет формат.** Вставленный текст всегда сохранялся как `.txt`, а новая версия страницы из вставки тихо становилась текстом. Теперь новая версия сохраняет формат своей работы, а новая работа становится страницей, если вставлен HTML, и документом Markdown, если у текста есть заголовок и другая разметка Markdown; под полем видно, как он сохранится.
- **Подсказки о форматах.** Блок «Подключите агента», пустая полка и главная перечисляли только HTML, текст и изображения; теперь называют и Markdown, CSV, JSON, PDF (блок агента берёт тот же список, что окно загрузки).

## [0.18.0] — 2026-10-15

Обновление: миграция 077 (сделайте дамп); рецепты прав `runtime-grants.sql`, `purge-worker-grants.sql` и `restore-worker-grants.sql` теперь ждут ровно 001–077.

### Added

- **Markdown, CSV и JSON — работы из одного файла** ([SINGLE_FILE_FORMATS](docs/specs/SINGLE_FILE_FORMATS.md), часть #46). Файлы `.md`, `.csv`, `.json` сохраняются как есть: с компьютера, через `polka_publish` и `POST /api/v1/publish` (`kind: "file"` с `name` и `text` или `base64`) и через `polka-publish.mjs`, который раньше заворачивал Markdown в HTML. Полка показывает Markdown свёрстанным документом, CSV — таблицей (первые 2 000 строк), JSON — кодом; страницу рисует сама, без скриптов, и отдаёт только с домена просмотра по правилам статичного просмотра, для владельца и по ссылке. Без домена просмотра показывается текст файла. Сервер проверяет UTF-8 и разбор JSON; поиск, фильтр содержимого, модель-ревьюер, обложка, контекст агента (`source.md`, `source.csv`, `source.json`) и перенос полки знают новые форматы. В пакетах ничего не меняется.
- **PDF — работа из одного файла** ([SINGLE_FILE_FORMATS](docs/specs/SINGLE_FILE_FORMATS.md#pdf), часть 2 #46). PDF до 5 МБ сохраняется с компьютера, через `polka_publish` и `POST /api/v1/publish` (`kind: "file"`, байты в `base64`) и через `polka-publish.mjs`. Сохранение проверяет, что pdf.js открывает файл: повреждённый файл или PDF с паролем — 422. На домене просмотра PDF рисует pdf.js, который отдаёт сам домен просмотра (без CDN), страницы — по мере прокрутки, по ширине экрана; JavaScript и формы PDF не выполняются, CSP страницы не разрешает ни eval, ни worker, ни сеть. Без домена просмотра владелец видит карточку «Скачать PDF», ссылка не выпускается; получатель оригинал не скачивает. Текст PDF (pdf.js в пуле воркеров, с дедлайном) идёт в поиск, фильтр содержимого, модели-ревьюеру и обложку «Документ»; ссылки PDF проверяются на фишинг как ссылки страницы; агент получает `source.pdf` и `source.txt` с текстом. Новая зависимость — `pdfjs-dist` 6.3.289 (Apache-2.0).

- **Кто сохранил работу.** В списке работ и в карточке работы есть `savedBy`: кто сохранил последнюю версию — человек или подключение агента с его именем (из журнала `revision.saved`, одним запросом на страницу, частичный индекс `audit_outbox_saved`). На карточке — «Сохранил Codex», в «О работе» — «Сохранил Codex» или «Сохранили вы» ([ONBOARDING_V2](docs/specs/ONBOARDING_V2.md#решения-владельца-15102026)).
- **Интерактивный пример.** Где интерактивный просмотр работает на отдельном домене (`liveMode=production`, polochka.page), «Сохранить пример» на `/start` кладёт на полку страницу с переключателем недель (профиль `limited`); на остальных установках — прежний статичный пример.
- **Подключение видно сразу.** После «Разрешить» страница согласия сообщает об этом другим вкладкам Полки (`BroadcastChannel('polka:agent-connected')`) и показывает «Вернуться на полку»; открытая полка перечитывает подключения сразу и ещё дважды за 5 секунд (агент завершает вход через мгновение после «Разрешить»). Опрос раз в 8 секунд остаётся запасным.
- **Проверка восстановления на минуту без остановки** (`deploy/hosted/pitr-drill.sh`, [hosted](deploy/hosted/README.md#восстановление-на-момент-времени)): восстанавливает базовый бэкап и WAL во временный том, поднимает PostgreSQL без сети и сверяет копию с работающей базой — в копии должны быть все версии работ, сохранённые до цели. Ключ чтения бэкапов, цель по умолчанию — 30 минут назад; том удаляется при выходе.

### Changed

- **«Подключите агента» только новым полкам.** Блок наверху полки видят полки младше 30 дней или с меньше чем 3 работами; остальные — одну строку с «Подключить агента», которая открывает шаги по просьбе.
- **«Скрыть» хранится в аккаунте** (`accounts.onboarding_hidden_at`, `POST /api/account/onboarding/hide` и `…/unhide`, поле `onboardingHiddenAt` в `/api/session`) и действует во всех браузерах, включая приватные окна. Скрытие, сделанное до обновления в этом браузере, при следующем визите переносится в аккаунт.

## [0.17.1] — 2026-10-14

Обновление без миграций. С профилем `pitr` образ бэкапа пересобирается, `wal-archive` перезапускается и начинает писать `wal/heartbeat`; `objects-mirror` тоже перезапускается.

### Added

- **Статус оператора видит архив журнала.** С профилем `pitr` `/api/ops/status` получает проверку `walArchive`: `wal-archive` раз в 5 минут обновляет `BACKUP_BUCKET/wal/heartbeat`, и проверка краснеет, если отметка старше 15 минут или самый новый базовый бэкап в `base/` старше `BASE_BACKUP_INTERVAL_SECONDS` плюс 2 часа. Раньше о сбое архива говорил только healthcheck контейнера, а алерты мониторинга его не видели. Профили приложение берёт из `COMPOSE_PROFILES` (`OPS_COMPOSE_PROFILES` в compose); новых прав ключу приложения не нужно — только листинг ([hosted](deploy/hosted/README.md#мониторинг)).
- **Точки расширения для объединения аккаунтов** ([EXTENSIONS](docs/specs/EXTENSIONS.md#точки-расширения)): `policies.accountMerge` может запретить объединение (на `/claim` — 409, у оператора — «Отказ»), например для аккаунта, которым управляет каталог компании; `accountMerged` переносит записи расширения в той же транзакции, и его ошибка отменяет объединение. Команда оператора `account merge` теперь загружает расширения из `POLKA_EXTENSIONS`.

### Changed

- **Зависимости:** 13 npm-пакетов в пределах минорных версий (среди них lucide-react 1.55.0 в библиотеках сборки страниц), playwright-core и образ рендерера 1.64.1 (Chromium прежний), образ бэкапа на свежем postgres:16-alpine, setup-node 7.1.0 в CI.
- **Восстановление на новую машину:** событие `restore.failed` команды `restore-target` называет класс ошибки, её код и статус хранилища (без текста ошибки, в нём бывают строки подключения): по нему видно, что сломалось — база или хранилище.

### Fixed

- **Сохранение проекта под нагрузкой.** После сохранения сервер проверял сборку каждой страницы с React по очереди, и каждая могла ждать свободный сборщик до 10 секунд: проект из многих страниц держал ответ агенту минутами, а «сервер занят» попадал в `pagesNotBuilt` как страница, которая не собирается. Теперь занятый сборщик заканчивает проверку сразу, вся проверка — не дольше 20 секунд, а непроверенные страницы собираются при первом открытии ([PROJECTS](docs/specs/PROJECTS.md#страницы-с-react)).
- **Код выхода при остановке.** Если сторож пула БД останавливал процесс во время остановки по SIGTERM, процесс завершался с кодом 0; теперь — с наибольшим запрошенным (1).

## [0.17.0] — 2026-10-14

Обновление: миграции 075 и 076 (сделайте дамп), `postgres` один раз перезапускается с новыми настройками сервера, роли получают таймауты из `role-settings.sql`, Caddy перезапускается ради лимитов тел и сжатия ([hosted → Обновление](deploy/hosted/README.md#обновление)). Восстановление на минуту и копия работ включаются отдельно: профили `pitr` и `mirror`.

### Added

- **Восстановление базы на любую минуту** (профиль `pitr`, [hosted](deploy/hosted/README.md#восстановление-на-момент-времени)): служба `wal-archive` принимает журнал предзаписи по репликационному соединению (`pg_receivewal`, слот `polka_wal`) и раз в минуту кладёт его в `BACKUP_BUCKET/wal/`, раз в сутки делает базовый бэкап в `base/`; `polka-pitr-restore` в образе бэкапа готовит том базы к восстановлению на заданное время UTC или до последнего байта. RPO — около минуты вместо часа. Новые настройки `POLKA_REPLICA_PASSWORD`, `WAL_UPLOAD_SECONDS`, `BASE_BACKUP_INTERVAL_SECONDS`; роль `polka_replica` создают `init-roles.sh` и `worker-roles.sh`; PostgreSQL держит для остановленного архива не больше 2 ГБ WAL (`max_slot_wal_keep_size`, перезапуск `postgres` при обновлении). Ключу записи бэкапов нужны `wal/*` и `base/*` в политике бакета.
- **Копия работ под object lock** (профиль `mirror`, [hosted](deploy/hosted/README.md#копия-работ)): служба `objects-mirror` каждые 15 минут копирует `S3_BUCKET` в `MIRROR_BUCKET` внутри хранилища своим ключом, которому нельзя удалять версии; бакет копии версионирован с блокировкой версий на 14 дней, прежние версии удаляются через 15 дней после удаления у источника. Массовое исчезновение объектов (больше `MIRROR_MAX_DELETES`) в копию не переносится и пишется как `mirror ALERT`; объект, который не удалось скопировать, назван в логе, остальное копируется, служба не перезапускается. `objects-restore` (профиль `ops`) возвращает пропавшие версии из копии и переводит на них строки базы.
- **Метрики для Prometheus:** `GET /api/ops/metrics/prometheus` с токеном `OPS_STATUS_TOKEN` — запросы и гистограмма времени ответа по методу, шаблону маршрута (не адресу) и коду, пул БД (`total`, `idle`, `waiting`), задержка цикла событий, память, очередь воркеров разбора страниц, очередь вебхуков и накопительные счётчики ([hosted](deploy/hosted/README.md#метрики-для-prometheus)).
- **Строка лога на запрос:** `{"event":"request","method":…,"route":…,"status":…,"ms":…,"reqId":…}` — шаблон маршрута, никогда адрес с токенами; клиент получает тот же id в `x-request-id`, он же в `request.failed`. Проверки здоровья — на уровне `debug`; `ACCESS_LOG=info|debug|off`.
- **Самовосстановление приложения.** Docker не перезапускает «unhealthy» контейнер, поэтому процесс завершается сам: цикл событий заблокирован дольше `WATCHDOG_EVENT_LOOP_SECONDS` (30) — отдельный поток пишет `watchdog.event_loop_blocked` и убивает процесс; пул БД полон, запросы ждут и ничего не движется `WATCHDOG_POOL_STALL_SECONDS` (120) — `watchdog.pool_stalled` и остановка с кодом 1. `restart: unless-stopped` поднимает его заново. У `app` в `deploy/hosted/compose.yml` и `deploy/compose.base.yml` теперь `init: true`: процесс с PID 1 не может убить себя сам.

### Security

- **Сессии агентов: пароль в аргументах инструмента.** `polka-sessions.mjs` проверял аргументы инструментов уже как JSON: `PASSWORD="Secr3t123!" npm run task` превращался в `PASSWORD=\"Secr3t123!\"`, экранированная кавычка останавливала правило присваивания, и пароль уходил в расшифровку. То же с heredoc (строки склеивались через `\n`) и с JSON, переданным строкой внутри аргумента. Теперь каждая строка аргументов проверяется раскодированной (и JSON внутри строки — тоже), затем весь JSON ещё раз — для значения под именем-ключом (`{"PASSWORD": "…"}`). Метки и отпечатки прежние. Плагин нужно обновить; уже отправленные расшифровки не переписаны — их заменит повторная отправка (`sync --since …`).
- **Сессии агентов: отказ в загрузке расшифровки удалял сохранённую.** Повторная отправка тех же байтов токеном той же полки без права «Сессии агентов» (или отзыв токена во время загрузки) удаляла из хранилища уже сохранённую версию, хотя строка сессии на неё указывала. Теперь токен, полка и сессия проверяются до записи в хранилище, а при неудаче удаляется только версия, которую положила эта попытка и на которую строка не указывает.

### Fixed

- **Тело запроса без токена больше не читается.** `/mcp`, HTTP API (`/api/v1/…`, включая расшифровки сессий до 64 МиБ) проверяли токен уже после того, как сервер принял тело целиком: два десятка анонимных запросов по 64 МиБ занимали 1,8 ГБ памяти и роняли контейнер на 1,5 ГиБ. Теперь токен проверяется по заголовкам, без него ответ 401 приходит сразу, соединение закрывается, тело не читается (на замере: +1854 МБ → +1 МБ). Новая настройка `BODY_BYTES_IN_FLIGHT_MB` (по умолчанию 256): сколько байт тел запросов сервер держит в памяти одновременно; сверх неё новый запрос с телом получает `503` с `code: "busy"` и `Retry-After: 5`, не прочитав тело. Один запрос больше бюджета по-прежнему принимается; видео проекта идёт потоком и в бюджет не входит.
- **Markdown проектов:** нарисованные страницы хранились до 300 штук без счёта байт (текстовая копия огромного документа занимала мегабайты, на долгом прогоне 36 страниц — 155 МБ). Теперь не больше 64 МиБ, а текстовая копия документа больше 512 КБ не хранится. Подпись ссылок `/away` в хранимой странице истекала через 12 часов, а страница хранилась дольше: внешние ссылки старого документа переставали открываться. Теперь подпись ставится при каждой отдаче.
- **Просмотр проекта после объединения аккаунтов** брал файлы по старым ключам хранилища, пока сервер не перезапустят: карта файлов версии хранилась только по её id. Теперь в ключе и ключ объекта версии.
- **Импорт по URL:** `<base>`, скрипты и другие убираемые элементы вынимались из дерева по одному с фильтром всех соседей (100 тысяч элементов — около 10¹⁰ шагов на основном потоке); теперь за один проход. Страницу больше 16 тысяч знаков сначала читает воркер с дедлайном: не прочитанную за 2 с импорт отклоняет (`too_complex`) и не разбирает на основном потоке.
- **Вход по ссылке от агента после закрепления или отзыва.** Вход читал ссылку как живую, затем ждал блокировку полки; если за это время полку закрепили (закрепление удаляет слабые сессии) или отозвали подключение агента, вход всё равно выдавал слабую сессию по устаревшему решению. Теперь ссылка и подключение перечитываются под блокировкой полки, и такой вход получает «Ссылка устарела».
- **Обслуживание оставляло временную полку недоудалённой.** Без конвейера удаления (`ACCOUNT_DELETION_ENABLED=false`) обслуживание закрывало простаивающую временную полку и затем удаляло её объекты и строки; сбой S3, конец времени прогона или остановка между этими шагами оставляли полку закрытой с объектами навсегда. Теперь закрытие записывает задание (`provisional_erasures`, миграция 076), и следующий прогон доводит удаление до конца; рецепты прав обновлены.
- **Импорт полки и снимок на дату.** Импорт ставил работе время ухода в корзину, но не писал это событие в журнал; после восстановления работы на новой установке снимок полки за время между исходным удалением в корзину и восстановлением показывал её на полке. Теперь уход в корзину пишется в журнал с исходным временем, как отметка принятия.
- **Рендерер не зависает на оборванном сжатом ответе.** Если сайт закрывал соединение посреди тела в gzip, deflate или br, запрос `/fetch` (и `robots.txt`) не завершался никогда, даже по таймауту, и держал место в очереди рендерера. Теперь такой ответ сразу заканчивается ошибкой сети.
- **Лимит соединений egress-прокси рендерера учитывает ожидание DNS.** Место занимается до разрешения имени и освобождается при любом исходе, поэтому зависшие DNS-запросы больше не позволяют открыть больше `maxConnections` соединений.
- **Объединение полок теряло сессии агентов.** Работы и подключения переносились, а сессии (строки, расшифровки, место, ключ отпечатков) оставались у источника и пропали бы при его очистке. Теперь они переносятся: расшифровки копируются под префикс получателя, сессия, отправленная на обе полки, остаётся одна (отправленная позже), место пересчитывается, ключ отпечатков получатель без своего ключа берёт у источника ([AGENT_SESSIONS](docs/specs/AGENT_SESSIONS.md#объединение-полок)). Заодно переносятся ответственный за работы (`owner_account_id`), автор перенесённых ссылок и предложений в «Ленту». Список `MERGE_INVENTORY` и тест сверяют со схемой каждую колонку, указывающую на аккаунт или полку: новая таблица без решения для объединения не пройдёт тесты.
- **Страница сессии с большой расшифровкой держала сервер.** Чтобы показать 50 событий, сервер скачивал расшифровку целиком, распаковывал до 256 МиБ в память и делил на строки: на синтетической расшифровке в 250 МБ пик памяти ~1 000 МБ и 0,3 с остановки цикла событий. Теперь она читается из хранилища потоком и распаковывается по кускам, в памяти — только запрошенные строки: пик ~210 МБ (база процесса ~170 МБ), остановка не дольше 2 мс. Индекс сессии распаковывается вне главного потока, предел распакованного индекса — 16 МиБ вместо 96 (самый полный индекс скрипта ~10 МиБ): худший разбор держит цикл событий ~80 мс вместо ~700.
- **`hosted:setup` портил секреты с `$`, `#`, пробелами и кавычками.** Значения писались в `hosted.env` без кавычек, и Docker Compose подставлял вместо `$NAME` переменную окружения, а всё после ` #` отбрасывал: пароль S3 или почты, заданный так, не совпадал с настоящим. Теперь такие значения пишутся в двойных кавычках с экранированием и читаются буквально; простые значения и строки, оставшиеся от прежнего файла, не меняются. Проверено через `docker compose config`.
- **Медленные анонимные отправители больше не занимают бюджет тел запросов.** Запрос резервировал свой `Content-Length` (без него — предел маршрута) ещё до проверки, кто его прислал, и до конца ответа: полсотни запросов с `Content-Length: 5 MiB` на любой адрес, даже несуществующий, присылающих по нескольку байт, занимали все 256 МиБ, и настоящие загрузки получали `503 busy`. Теперь объявленный размер резервирует только запрос, чей токен проверен до тела (`/api/v1/…`, `/mcp`); любой другой считается по пришедшим байтам, такие вместе занимают не больше четверти `BODY_BYTES_IN_FLIGHT_MB`, а с одного адреса — не больше шестнадцатой; сверх — `503 busy` и закрытое соединение.
- **Поиск из одних стоп-слов находил всю полку.** Запрос вроде «что», «что и» или «the» превращался в пустой `tsquery`, и после перехода на индекс по полкам от него оставалась одна лексема полки: на полке и в `polka_list` / `GET /api/v1/works` находилась каждая работа с текстом. Теперь такой запрос по тексту ничего не находит, по названию ищется как набран.
- **Счётчик чтений агентов не теряет пачку при коротком сбое базы.** Пачка, которую база не приняла (нет соединения, перезапуск, таймаут, взаимоблокировка), возвращается в память и пишется со следующей; в памяти не больше 10 000 счётчиков (полка × день × тип токена).
- **Одна неудаляемая временная полка больше не останавливает обслуживание.** Сбой удаления одной полки (объекты или строки) прерывал весь прогон, и следующие шаги не выполнялись ни в этот раз, ни в следующие. Теперь такая полка считается (`provisionalErasuresFailed` в `maintenance.completed`) и пробуется снова следующим прогоном, а остальные шаги идут дальше. Перед удалением строк аккаунт перепроверяется под блокировкой: если полку тем временем закрепили, строки остаются.

### Changed

- **Меньше блокировок и запросов на частых путях.** Открытие ссылки получателем трижды проверяло, не в «Ленте» ли она, запросом из восьми таблиц, у которого планирование занимало 4 мс; теперь сначала одна выборка по уникальному `share_id` (0,6 мс вместо 11 мс на вызов под нагрузкой), полный запрос — только для ссылок из каталога. Новая ссылка закрывала прежние перебором всех ссылок установки; теперь по индексу открытой ссылки (`AND NOT revoked`), так же корзина. Агент блокировал полку, аккаунт и участие дважды в каждой транзакции сохранения; повторная блокировка в той же транзакции теперь одна проверка без `FOR UPDATE` (90 → 82 запроса и 37 → 25 блокирующих выборок на одно сохранение). Список участников полки отдела больше не ждёт сохранений на ней ([TEAM_SHELVES](docs/specs/TEAM_SHELVES.md#проверки-прав)).
- **Поиск и список полки быстрее на большой установке.** Текст искался по всей установке, а полка отбиралась потом: частое слово читало строки всех полок (на тестовой базе в 50 000 работ — 650 мс, «Сначала подходящие» — до 4,5 с), а счётчики по типам заново проверяли каждую работу. Теперь индекс текста знает полку, вектор названия хранится, тип последней версии лежит на работе, а страница и счётчики берутся из одного запроса: первая страница полки отдела с частым словом — 25–35 мс вместо 570, `GET /api/v1/works` — 120 мс вместо 380. Порядок, фразы и «Сначала подходящие» не изменились. Миграция 075 (`artifact_search.tenant_id`, `artifacts.title_document`, `artifacts.latest_mime`, индексы вызовов сессий для «Расхода»); рецепты прав обновлены, расширений не нужно ([CONTENT_SEARCH](docs/specs/CONTENT_SEARCH.md#индекс)).
- **Вызов агента дешевле для базы.** Каждый `/api/v1/*` и `/mcp` делал шесть запросов к базе, из них три записи: отметку «когда подключение работало», лимит частоты и счётчик чтений на одной горячей строке полки за день; подключение к тому же читалось дважды. Теперь `last_seen_at` пишется не чаще раза в минуту на подключение (в каждом процессе), счётчик чтений копится в памяти и пишется раз в 10 секунд одним запросом (при аварийной остановке теряется не больше 10 секунд счёта; при штатной его сбрасывает `flushAgentReads`), первая перепроверка подключения в запросе берёт результат проверки токена, а следующие читают базу, как раньше. Лимит частоты не изменился и по-прежнему общий для всех процессов. JSON-схемы инструментов MCP строятся один раз, а не на каждый `tools/list` и вызов. На замере `GET /api/v1/works`: 3 запроса к базе вместо 6, 756 ответов в секунду вместо 547 при 10 параллельных и 1046 вместо 531 при 50; `polka_list` через MCP — 881 вместо 578, `tools/list` — 1332 вместо 591.
- **Вызов агента дешевле для базы.** Каждый `/api/v1/*` и `/mcp` делал шесть запросов к базе, из них три записи: отметку «когда подключение работало», лимит частоты и счётчик чтений на одной горячей строке полки за день. Теперь `last_seen_at` пишется не чаще раза в минуту на подключение (в каждом процессе) и по id, без повторной проверки подключения; счётчик чтений копится в памяти и пишется раз в 10 секунд одним запросом (при аварийной остановке теряется не больше 10 секунд счёта; при штатной его записывает остановка сервера). Проверка доступа не кэшируется: каждая перепроверка подключения читает базу, поэтому отзыв токена и удаление аккаунта действуют сразу. Лимит частоты не изменился и по-прежнему общий для всех процессов. JSON-схемы инструментов MCP строятся один раз, а не на каждый `tools/list` и вызов. На замере `GET /api/v1/works`: 4 запроса к базе вместо 6; `tools/list` — 2 вместо 3 и 1387 ответов в секунду вместо 591.
- **Пул воркеров для проверки страниц.** Классификация и признаки фишинга больших страниц (больше 16 тысяч знаков), текстов и скриптов, факты обложки и Markdown проектов шли в новом воркере на каждую страницу: его запуск занимал 180–230 мс при 10 мс работы. Теперь `HTML_WORKERS` воркеров (по умолчанию 2, на одном CPU — 1) загружены заранее и берут задачи по очереди; задача дольше дедлайна по-прежнему получает «не прочитано», а её воркер заменяется новым. В очереди ждут не больше `HTML_WORKER_QUEUE` задач (по умолчанию 64), сверх — 503 `busy` с `Retry-After: 5`. На замере: публикация страницы 85 КБ при 10 параллельных запросах — 67,6 запроса/с вместо 10,5; четыре страницы по 4,5 МБ сразу — +156 МБ памяти вместо +619 МБ; проект из 100 страниц по 20 КБ — `finalize` за 1,1 с вместо 26,3 с.
- **Сборка страниц проекта читает файлы только после допуска:** одновременно не больше сборок, чем воркеров сборщика, и не больше 64 МиБ входных файлов на всех; раньше каждый просмотр сначала читал все файлы страницы в память и только потом ждал сборщика.
- **Первый экран грузится быстрее.** Общий первый файл скрипта, который скачивает любая страница (лендинг, ссылка получателя, лента), уменьшился с 551 до 247 КБ (с 167 до 78 КБ в gzip): рабочая полка, её окна и настройки, участники, вебхуки, сервисные аккаунты, сравнение версий, просмотр работы и комментарии загружаются отдельно, когда они нужны, а zod — только со страницами, которым он нужен. Общие стили — 49 КБ вместо 121: стили страниц приходят вместе со страницами. Гостю на `/` лендинг начинает загружаться сразу, не дожидаясь ответа о входе; иконки собраны в один файл вместо десятков мелких. Полужирный шрифт заголовков загружается заранее. `/api/capabilities` и `/api/account/identities` запрашиваются один раз на страницу; первый список полки — без задержки поиска. `tests/web-entry-chunk.test.ts` собирает приложение и не пускает zod и рабочую полку обратно в первый файл.
- **Мягкая остановка.** По SIGTERM приложение снимает готовность, останавливает таймеры модерации, писем о проверке и вебхуков, закрывает порт, дожидается запросов в работе и работы после коммита (письма, удаление объектов, доставки), затем закрывает пул БД — всё в пределах `SHUTDOWN_GRACE_SECONDS` (25). Раньше работа после коммита обрывалась вместе с пулом. В `deploy/hosted/compose.yml` у `app` `stop_grace_period: 30s`, у `maintenance` — `65s`, как в `compose.base.yml`.
- **PostgreSQL своей установки настроен под нагрузку** (`deploy/hosted/compose.yml`): `shared_buffers=256MB` (было 128MB по умолчанию), `pg_stat_statements` для статистики запросов, в журнал попадает запрос дольше 500 мс — текстом, без значений параметров. У ролей появились таймауты по умолчанию (`deploy/hosted/role-settings.sql`): `polka_runtime` — 15 с на запрос и 60 с простоя в транзакции, `polka_purge` и `polka_restore` — 60 с и 5 минут, `polka_schema` — 15 минут простоя в транзакции; бэкап и миграции работают как раньше. **При обновлении** `up -d` один раз перезапускает `postgres` (несколько секунд без базы), затем один раз нужен `role-settings.sql` и `restart caddy` — команды в [README](deploy/hosted/README.md#обновление).
- **Caddy своей установки** сжимает ответы приложения zstd или gzip (раньше только gzip) и обрывает слишком большое тело запроса ответом 413 до приложения: видео проекта — больше 201 МиБ, расшифровка сессии — больше 65 МиБ, всё остальное — больше 9 МиБ, чуть выше лимитов самого приложения. Пути `/api/ext/*` расширения ограничивают сами.
- **Задержка цикла событий в `/api/ops/status`** (`runtime.eventLoopMs`) включала период замера: простаивающий процесс показывал около 20 мс вместо долей миллисекунды. Теперь период (10 мс) вычитается; пороги тревоги, подобранные под старые числа, стоит снизить на 20 мс.
- **Страница не прыгает при загрузке:** строка с названием у получателя ссылки, строка «Вы вошли как…» в настройках, подвал на `/sessions` и в материале ленты держат место, пока данные в пути.
- **Доступность (WCAG 2.1 AA, axe):** переключатель комментариев у получателя ссылается на панель, только когда она открыта; контраст `--muted` (`#5d687d`), числа на выбранном фильтре и подписей обложек ленты не ниже 4,5:1; ссылка «перенесите полку» подчёркнута; блоки с командами на лендинге и в настройках доступны с клавиатуры; у `/trash` есть заголовок h1, у материала ленты — один banner и один contentinfo; «Перейти к содержимому» ведёт на `<main id="main">` на каждой странице; порядок заголовков в «Перенести полку», ленте и корзине без пропусков.

## [0.16.0] — 2026-10-13

### Added

- **Гайд «Полка в компании: запуск за один день»** ([docs/company-launch.md](docs/company-launch.md)): облако, своя установка или коммерческая редакция; что подготовить (VM, два домена, S3, почта, IdP, документы); установка `install.sh`, первый администратор `account bootstrap`, проверка `ops doctor`; настройки для одной компании и подключение сотрудников фразой `Подключи Полку: https://<ваш домен>/connect`.

- **`ACCOUNT_TRUST=company` для установки одной компании.** Правила для новых аккаунтов (не больше 5 ссылок, срок до 7 дней, проверка ссылок нового автора, повтор содержимого у разных аккаунтов как спам) защищают открытое облако и мешают внутри компании. С `company` любой вошедший аккаунт доверенный с первого дня; приостановленная ссылка, блокировка и непросмотренная жалоба по-прежнему снимают доверие. По умолчанию `public`, как на polochka.app. Ставить, только если вход закрыт для чужих ([ABUSE_PROTECTION](docs/specs/ABUSE_PROTECTION.md#своя-установка-компании)).
- **Первый администратор без почты:** `npm run admin -- account bootstrap <почта>` (на VM — `scripts/admin.ts account bootstrap` в контейнере `app`) находит или создаёт аккаунт с этой почтой (она считается подтверждённой), делает его администратором компании (`--org` на общем облаке, `--no-admin` — только вход) и печатает одноразовую ссылку `/welcome#…` на час. На странице видно, чья полка, вход — по кнопке; дальше код на почту или SSO с тем же адресом открывают этот же аккаунт. Новая ссылка гасит прежнюю. Миграция 074 (`operator_sign_in_links`, только SHA-256 токена); рецепты прав обновлены.

### Security

- **Расширение действует только для людей своей организации:** `context.accounts.update`, `deprovision`, `reactivate` и `startSession` проверяют, что текущая подтверждённая почта аккаунта на домене организации, от имени которой идёт вызов (иначе 403, `organisation_member`). Раньше SSO или каталог одной организации могли сменить почту человеку другой и войти под ним, отключить его или снять отключение, сделанное другой организацией. Отключение помнит организацию (`accounts.deprovisioned_organisation_id`, миграция 073), и снять его может только она. `reactivate` и `startSession` теперь принимают `{ organisationId }` (у `startSession` — вместе с `previous`); вызов без организации отклоняется (400). Коммерческой редакции нужна совместимая версия.
- **Ссылка-приглашение не возвращает снятых:** кого сняли с полки отдела после того, как ссылку создали (администратор полки, уход сотрудника у администратора компании, каталог SCIM), той же многоразовой ссылкой вернуться не может (403, `removed_after_invitation`).

### Fixed

- **Вход по ссылке от агента после закрепления или отзыва.** Вход читал ссылку как живую, затем ждал блокировку полки; если за это время полку закрепили (закрепление удаляет слабые сессии) или отозвали подключение агента, вход всё равно выдавал слабую сессию по устаревшему решению. Теперь ссылка и подключение перечитываются под блокировкой полки, и такой вход получает «Ссылка устарела».
- **Обслуживание оставляло временную полку недоудалённой.** Без конвейера удаления (`ACCOUNT_DELETION_ENABLED=false`) обслуживание закрывало простаивающую временную полку и затем удаляло её объекты и строки; сбой S3, конец времени прогона или остановка между этими шагами оставляли полку закрытой с объектами навсегда. Теперь закрытие записывает задание (`provisional_erasures`, миграция 076), и следующий прогон доводит удаление до конца; рецепты прав обновлены.
- **Импорт полки и снимок на дату.** Импорт ставил работе время ухода в корзину, но не писал это событие в журнал; после восстановления работы на новой установке снимок полки за время между исходным удалением в корзину и восстановлением показывал её на полке. Теперь уход в корзину пишется в журнал с исходным временем, как отметка принятия.
- **Своя установка подключала агентов к облаку.** `/connect`, `/llms.txt`, страница «Агенты», блок «Подключите агента» и первые шаги предлагали плагин `polka@polka` из `artkruglov/polka-plugin`, а он подключает `https://polochka.app/mcp`: сотрудники компании попадали в публичное облако. Теперь на любом `APP_ORIGIN`, кроме polochka.app, первыми идут прямые команды с адресом установки (`claude mcp add --transport http --scope user polka <адрес>/mcp`, `codex mcp add polka --url <адрес>/mcp`), скиллы ставятся отдельно с самой установки (`npx skills add <адрес>`), а `/connect` просит агента плагин не ставить. На polochka.app всё как раньше.
- **Своя установка показывала документы облака.** `/privacy` и `/terms` на любой установке показывали политику и соглашение polochka.app с именем его оператора. Теперь на другой установке это короткая страница: «Эта установка Полки принадлежит <`OPERATOR_NAME` или «организации, которая её развернула»>. Политику обработки персональных данных и условия использования публикует она» и контакт из `OPERATOR_CONTACT`. С `PRIVACY_URL` и `TERMS_URL` страницы переходят на документы владельца, туда же ведут ссылки в подвале, под формой входа и в заявке на `/enterprise`. Где страница описывает само облако (тарифы, «Для компаний»), ссылка ведёт на документы polochka.app. Настройки — в `compose.yml` и `hosted.env.example`.
- **Сессии агентов: пароль в шаблоне команды.** Шаблон команды (`argv0` и флаги, его видно в «Сессиях» и его читают правила) строился из исходной команды, а не скрытой, и оставлял любое слово с `-` как есть: `mysql -u root -pS3cret` уходил на сервер с паролем, так же `curl -uadmin:пароль` и `sshpass -p пароль` (слово из строчных букв считалось подкомандой). Теперь шаблон строится из скрытой команды и оставляет только подкоманды до первого флага и флаги без значений: `mysql -u <arg> <arg>`, `git push --force <arg>`. Сервер так же чистит шаблоны от прежних версий `polka-sessions.mjs`. Шаблоны, уже сохранённые на сервере, не переписаны. Плагин нужно обновить.
- **Сессии агентов: пароль в шаблоне команды.** Шаблон команды (`argv0` и флаги, его видно в «Сессиях» и его читают правила) строился из исходной команды, а не скрытой, и оставлял любое слово с `-` как есть: `mysql -u root -pS3cret` уходил на сервер с паролем, так же `curl -uadmin:пароль` и `sshpass -p пароль` (слово из строчных букв считалось подкомандой). Теперь шаблон строится из скрытой команды и оставляет только подкоманды до первого флага и флаги без значений: `mysql -u <arg> <arg>`, `git push --force <arg>`. Сервер так же чистит шаблоны от прежних версий `polka-sessions.mjs`. Шаблоны, уже сохранённые на сервере, переписывает команда оператора `npm run admin -- sessions scrub-templates [--dry-run]` (на VM — `docker compose --env-file hosted.env run --rm --no-deps app node --import tsx scripts/admin.ts sessions scrub-templates`): пачками, повторный запуск ничего не меняет, печатает только числа. Плагин нужно обновить.
- **Сессии агентов: пропущенные именованные секреты.** `DB_PASSWORD=Pa$$w0rd123!` (знак `$`), `SECRET=correcthorsebatterystaple` (одни буквы) и `AWS_SECRET_ACCESS_KEY=/wJalr…` (начало с `/`) принимались за код или путь и не скрывались. Значение имени со словом SECRET, PASSWORD, PASSWD, TOKEN, API_KEY или PRIVATE_KEY длиной от 8 знаков теперь скрывается всегда, кроме явного кода (вызов, имя через точку, `$VAR`, camelCase и snake_case) и пути под именем `*_FILE`/`*_PATH`/`*_DIR`. Новый тип `cli-password` — пароль клиента в командной строке (`mysql -p…`, `sshpass -p …`, `curl -u user:…`) и в расшифровке. Формат меток `[REDACTED:<тип>:<отпечаток>]` и отпечатки прежние; `context.redact` на сервере использует те же правила.
- **Вебхуки:** запись результата доставки блокирует адрес раньше доставки, в том же порядке, что изменение адреса и отмена очереди, — без взаимной блокировки в базе.
- **Список сессий:** неверный курсор страницы (`before` с несуществующей датой или кривым id) — 400, а не ошибка сервера.
- **`/connect` в браузере:** человек, который нажал ссылку «Подключи Полку: …/connect» в чате или посте, видел сырой текст инструкции для агента. Теперь браузеру (`Accept: text/html`) отдаётся страница с фразой и кнопкой «Скопировать», тремя шагами и ссылками на ручное подключение Claude.ai и ChatGPT; полный текст для агента — под «Текст для агента». Агенты, которые просят текст или markdown (и `curl`), получают прежний текст. `/connect/` перенаправляет на `/connect`.
- **Вебхуки: секрет нового вебхука был за краем экрана.** После «Добавить вебхук» уведомление с секретом появлялось выше формы, а форма просто очищалась. Теперь страница прокручивается к секрету и фокус переходит на него, а под формой (и для экранного диктора) — «Вебхук добавлен. Секрет показан выше — сохраните его сейчас.» После удаления фокус — на сообщении под списком, после «Новый секрет» — на новом секрете, после «Сохранить» и «Отмена» — на карточке вебхука, после остальных действий — на той же кнопке; раньше он падал на страницу и Tab уходил из окна «Участники».
- **Окна (Dialog) держат Tab внутри:** Tab и Shift+Tab ходят по кругу внутри открытого окна, в том числе когда кнопка с фокусом стала неактивной или исчезла.
- **«Полки компании» после «Убрать»:** список полок отделов обновляется — число участников, администраторы, «без администратора» и «Стать администратором», открытые списки участников. Пустой список участников — «На полке пока никого нет.» Для одной полки — «больше не на полке «X»», для нескольких — «на полках».
- **Настройки → агенты на телефоне:** форма токена и команды больше не растягивают страницу шире экрана (колонка страницы сжимается до ширины экрана).
- **Слепок полки:** «Войти» возвращает на `/snapshot` той же полки отдела (`?shelf=` больше не теряется).
- **Тексты без рода:** «Коллеге нужно хотя бы раз войти», «Взять ответственность и разморозить», «если у страницы включён доступ по ссылке», «Ссылка размещена на странице», «Заметок от автора пока нет», «Вход выполнен — открыть»; журнал библиотеки шаблонов — «Выпуск опубликован · <имя>» вместо «<имя> опубликовал выпуск». Флажок в заявке на `/enterprise` — «Согласие с Политикой обработки персональных данных» вместо «Я прочитал(а) Политику…».
- **`/enterprise`:** на макете полки метка «Работа» вместо «Диск».
- **Восстановление из дампа ([порядок](deploy/hosted/README.md#восстановление-из-дампа)):** шаг 1 останавливал `app`, `maintenance` и `backup`, но не службу очистки `account-purge` (профиль `purge`): она раз в минуту ходила в восстановленную, ещё не мигрированную базу и в журнал стираний, и `restore-reconcile` мог отказать. Теперь она останавливается вместе с ними и запускается в конце (`--profile purge up -d`; без службы очистки команда та же, что раньше). Шаг 5б закрывает и новые пути доступа: активные приглашения на полки отделов отзываются, вебхуки выключаются (как выключенные владельцем, неотправленные доставки не уйдут). То же делает `scripts/restore-drill.ts`.
- **Курсоры ленты после восстановления:** курсор `GET /api/v1/events` и `context.auditFeed` расширений, сохранённый до восстановления из дампа, указывал на номер транзакции, которого новый кластер ещё не выдал, и лента стояла на нём неделями. Теперь такой курсор продолжается с той же записи: приходит всё, что записано после восстановления, и новый курсор — как у рассылки вебхуков.
- **`hosted:setup --force`** заново генерировал пароли БД и `LINK_KEY` в существующем `hosted.env`: приложение теряло вход в базу, а все ссылки и секреты вебхуков переставали работать. Теперь `--force` оставляет всё, что в файле уже есть (секреты, ключи S3, настройки не из примера), и меняет только переданное; новые секреты — только с `--rotate`. Домен просмотра на том же registrable domain, что и приложение (`view.company.ru` при `polka.company.ru`), команда отклоняет.
- **Статус оператора:** проверка `backup` краснела только через 26 часов без дампа, даже при бэкапе раз в час. Порог теперь — `BACKUP_INTERVAL_SECONDS` плюс 2 часа (`compose.yml` передаёт интервал приложению), в ответе — `limitHours`.
- **`company admin` с опечаткой в ключе** (`--revok`) молча выдавал роль администратора компании. Неизвестные ключи и лишние аргументы теперь отклоняются, ничего не меняя.
- **Письмо о блокировке** называло несуществующую команду `npm run moderation:hold`; теперь — `npm run admin -- moderation legal-hold <id> on --authority …`.
- Заголовки и `\echo` рецептов прав БД называли миграцию 071, хотя проверяют 072.
### Changed

- **Своя установка без сборки ([«Быстрый путь»](deploy/hosted/README.md#быстрый-путь)):**
  - на тег `v*` workflow `release-images` публикует `ghcr.io/artkruglov/polka:<версия>` и `ghcr.io/artkruglov/polka-backup:<версия>` (и `latest` для релиза без суффикса) для amd64 и arm64, с метками OCI, provenance и SBOM. Веб-сборка в образе делается один раз на платформе сборки;
  - `compose.yml` берёт образ бэкапа из `POLKA_BACKUP_IMAGE`; пусто — собирает его на VM, как раньше;
  - в образе есть файлы `deploy/hosted` своей версии, рецепты прав и `scripts/hosted-setup.mjs`: `hosted.env` можно создать на VM без Node.js;
  - `deploy/hosted/install.sh` (POSIX sh): проверяет Docker и compose, скачивает образы и закрепляет версию, раскладывает файлы версии в `/opt/polka/deploy` (изменённые сохраняет как `*.previous`), сверяет DNS обоих доменов с IP машины, пишет `hosted.env` (600) через `hosted-setup.mjs` в контейнере без сети и запускает `up -d`. Без `--force` существующий `hosted.env` не меняет; ключи — из окружения или с вводом без эха;
  - `hosted:setup` берёт из окружения ещё `SMTP_PASS` и `OIDC_CLIENT_SECRET`, новый ключ `--backup-image`;
  - `npm run admin -- ops doctor`: настройки, DNS и TLS обоих доменов, отдельный registrable domain просмотра, база и версия схемы, бакет (версионирование, условная запись — без новых объектов), возраст последнего дампа, вход в SMTP (письмо — только с `--send-test`), OIDC discovery, есть ли способ входа и администратор компании, сводка настроек. ✓/✗ с подсказками по-русски, код выхода 1 при ✗, секреты не печатаются;
  - CI проверяет `install.sh` через `sh -n` и shellcheck.
- **Шаблон своей установки (`deploy/hosted`) — для компании, а не для polochka.app:**
  - модели фильтра содержимого в `hosted.env.example` выключены (`CONTENT_MODEL_PROVIDER=off`): раньше там был Yandex AI Studio с заглушкой модели и пустым ключом; как включить — в комментарии;
  - `OPERATOR_CONTACT` пуст вместо `privacy@polochka.app`, и `hosted:setup` перечисляет его как незаполненный;
  - `TEAM_SHELVES_SCOPE=installation` в примере и по умолчанию в `compose.yml` (было `organisation`): своя установка — одна компания, как и по умолчанию в коде. На polochka.app `organisation` задан в `hosted.env` явно и не меняется;
  - в примере появился блок `OIDC_*` (свой IdP компании), так что `hosted:setup --set OIDC_…` работает.
- **README хостинга:** раздел «Что нужно» (VM от 4 ГБ, Docker с compose, git, Node.js 22 для `hosted:setup`, два домена и зачем, S3 с версионированием и условной записью, три ключа, открытые 80/443); ключ бэкапа назван обязательным, как в `compose.yml`; `TEAM_SHELVES` и `TEAM_SHELVES_SCOPE` описаны как в шаблоне; команды `company …` даны и в форме для VM (`docker compose … run --rm --no-deps app node --import tsx scripts/admin.ts company …`); у `account-email` добавлен `--no-deps`; сказано, откуда берётся `$PASSWORD`; обновление ждёт `backup ok`, прежде чем `up -d`.
- **SAML и SCIM на сайте и в README — как есть:** «Для компаний», тарифы, README, README_EN и COMMERCIAL.md больше не говорят, что SAML и SCIM «пока нет»: они в коммерческой редакции с 0.9.0, на `/enterprise` — отдельная готовая карточка «Вход через SSO и каталог сотрудников». Вебхуки названы среди возможностей открытого ядра и облака.

## [0.15.1] — 2026-10-13

### Fixed

- **Вход по ссылке от агента после закрепления или отзыва.** Вход читал ссылку как живую, затем ждал блокировку полки; если за это время полку закрепили (закрепление удаляет слабые сессии) или отозвали подключение агента, вход всё равно выдавал слабую сессию по устаревшему решению. Теперь ссылка и подключение перечитываются под блокировкой полки, и такой вход получает «Ссылка устарела».
- **Обслуживание оставляло временную полку недоудалённой.** Без конвейера удаления (`ACCOUNT_DELETION_ENABLED=false`) обслуживание закрывало простаивающую временную полку и затем удаляло её объекты и строки; сбой S3, конец времени прогона или остановка между этими шагами оставляли полку закрытой с объектами навсегда. Теперь закрытие записывает задание (`provisional_erasures`, миграция 076), и следующий прогон доводит удаление до конца; рецепты прав обновлены.
- **Импорт полки и снимок на дату.** Импорт ставил работе время ухода в корзину, но не писал это событие в журнал; после восстановления работы на новой установке снимок полки за время между исходным удалением в корзину и восстановлением показывал её на полке. Теперь уход в корзину пишется в журнал с исходным временем, как отметка принятия.
- **Сервер падал на доставке вебхука** по адресу, до которого у машины нет маршрута (IPv6 на VM без IPv6, EHOSTUNREACH): ответ закреплённого DNS приходил сразу, подключение падало внутри `request()`, и ошибка сокета без обработчика завершала процесс. Ответ теперь приходит на следующем тике, как у настоящего DNS, а `request()` обёрнут. То же исправлено в загрузке по ссылке (`url-import/public-fetch.ts`). На polochka.app вебхуки были выключены, пока исправление не выложено.
- **Восстановление из дампа:** номера транзакций в журнале событий оставались от старого кластера, и в новом кластере такие записи были «из будущего». Лента `GET /api/v1/events` их не показывала, пока счётчик транзакций их не догонит, а рассылка вебхуков каждые 5 секунд писала `webhooks.cursor_skipped_stale`. В [порядок восстановления](deploy/hosted/README.md#восстановление-из-дампа) добавлен шаг 3a, который снимает эти номера. На polochka.app 59 записей после переезда 11.10.2026 исправлены вручную.
- **Вход по коду при `MAIL_MODE=local` не подтверждал почту:** `accounts.email_verified_at` ставился только при `MAIL_MODE=smtp`, поэтому на локальной установке нельзя было проверить правила организации (прямое добавление на полку отдела, поиск сотрудника администратором компании). Теперь верный код подтверждает адрес в любом режиме почты — при входе, при регистрации и при закреплении временной полки. В рабочих режимах (SMTP, в том числе Postbox на polochka.app) поведение не менялось: там адрес и раньше подтверждался, переносить данные не нужно. Адрес, привязанный командой `npm run admin -- account email`, по-прежнему подтверждается первым входом по коду.
- **Настройки на телефоне** прокручивались вбок (страница ~628 px при ширине 390): длинная команда curl в «Перенести полку» растягивала колонку. Теперь она прокручивается внутри своего блока.
- **Вебхуки:** у только что созданного вебхука уведомление о секрете говорило «Прежний секрет больше не подписывает запросы» — теперь только после «Новый секрет». «Новый секрет» спрашивает подтверждение (получатель перестаёт принимать запросы сразу), «Удалить» подтверждается красной кнопкой. Ответы «Проверить», «Выключить», «События» и ошибки показываются под нужным вебхуком или под формой, а не внизу раздела; пока меняются события, кнопки вебхука скрыты. Журнал доставок называет события по-русски. Список событий и права сервисного доступа — в стиле приложения, без серой рамки браузера; идентификатор события — на отдельной строке. Отказы сервера переписаны: «Вебхуки отправляются только по https.», внутренние имена и адреса — одним текстом «Адрес во внутренней сети…».
- **Участники полки отдела:** карточка полки, сервисные доступы и вебхуки — после списка участников, добавления и приглашения. Ошибка прямого добавления — под полем и пропадает после созданной ссылки. В облаке текст приглашения больше не говорит «для тех, кто ещё ни разу не входил»: ссылка нужна всем, кого нельзя добавить сразу. Роль в приглашении — на всю ширину, не обрезается.
- **«Полки компании»** для не-администратора: без надписи «Администратор компании» и без запроса `/api/company/shelves` (404 в консоли). Текст об уходе сотрудника зависит от установки: «вход закрывается в SSO компании» — только когда SSO подключено; в облаке — что аккаунт и личная полка остаются у человека; на своей установке без SSO — что вход закрывает оператор.
- **Тексты без рода:** «<имя> теперь на полке», «Попросите коллегу один раз войти», «автор ссылки: …» и другие фразы о третьих лицах больше не предполагают пол.

### Changed

- Лендинг: «Одна полка для любого агента» вместо «Один диск…». На `/enterprise` — строка о вебхуках (включены на polochka.app).

## [0.15.0] — 2026-10-12

### Added

- **Вебхуки** ([WEBHOOKS](docs/specs/WEBHOOKS.md), миграция 071, `WEBHOOKS=off` по умолчанию, на polochka.app включены). Владелец личной полки в «Настройках» или администратор полки отдела добавляет HTTPS-адреса и выбирает события: те же, что в ленте `GET /api/v1/events`, плюс открытие и закрытие ссылки и новый комментарий. Полка отправляет POST с событием ленты (только id и время, без содержимого) и подписью `Polka-Signature: t=…,v1=…` (HMAC-SHA256 секретом вебхука; секрет показывается один раз, хранится зашифрованным, меняется кнопкой). Повторы через 1 минуту … 24 часа, 8 попыток; не больше 2 попыток одновременно на адрес из общего пула на 10, пауза адреса после 3 ошибок подряд, очередь адреса до 1000 событий; журнал доставок с кодом ответа и первыми 200 знаками; после 3 суток одних ошибок вебхук выключается и автор получает письмо. Защита от SSRF: только https на 443, имя проверяется в DNS (напрямую, со сроком 3 секунды) перед каждой попыткой и соединение идёт на проверенный публичный адрес, перенаправления не выполняются. Вебхуки удаляются вместе с аккаунтом (и снова — при восстановлении старой копии); на полке отдела вебхук выключается, когда добавивший перестал быть администратором. Курсор рассылки сам восстанавливается после переноса базы в новый кластер и после долгого выключения. Ядро пишет в журнал полки `comment.created` (без текста). Решение владельца от 12.10.2026 отменяет «вебхуки — только по запросу пилота».
- **Точки расширения для входа и учёта сотрудников** ([EXTENSIONS](docs/specs/EXTENSIONS.md#вход-и-учёт-сотрудников-sso-scim), миграция 072). Расширение может:
  - принять ответ сервиса входа компании (`signInPaths`: POST без `Origin` Полки, без входа по cookie);
  - показать свою кнопку на `/signup` и `/login` (`signInMethods`, в `/api/capabilities` — `extensionSignIn`);
  - запретить код, пароль, поставщиков, привязку поставщика и `/claim` для адресов компании (`policies.signIn` с адресом самого аккаунта, 403 с `reason: "sign_in_policy"`, у поставщика — `idp_error=policy`);
  - заводить людей и менять им почту только на доменах организации ядра; смена почты закрывает сеансы и коды;
  - отключать и возвращать людей своей причиной и начинать им сеанс ядра (`context.accounts`);
  - вести состав полок отделов своей организации — только её людьми с подтверждённой почтой (`context.teamShelves`).

  Отключение расширением (`deprovision`) заканчивает сеансы, отзывает агентов и снимает с полок отделов организации. Оно и отключение оператором или модерацией хранятся отдельно (`accounts.deprovisioned_at`, `operator_disabled_at`): аккаунт отключён, пока держится хоть одна причина, и ни одна сторона не снимает чужую. Уже отключённые аккаунты миграция записывает отключёнными оператором, кроме удаляемых: их по-прежнему отключает запрос на удаление. `machinePaths` принимает `…/*` для адресов с id (SCIM). Протоколов SAML и SCIM в ядре нет.

- **Горячая перезагрузка интерфейса при разработке** ([local-development](docs/local-development.md#интерфейс-с-горячей-перезагрузкой), #14). `npm run dev:web` запускает Vite на http://127.0.0.1:4392 рядом с `npm run dev`: правка компонента в `apps/web/src` видна на странице без `npm run build` и без перезагрузки. Страницы приложения отдаёт Vite, API, MCP, OAuth и остальные адреса сервера он передаёт серверу на `PORT`. Вход и сохранение работ через него работают, проверка `Origin` на сервере не ослаблена.

### Changed

- **Бэкапы polochka.app каждый час** вместо раза в сутки (#32, [deploy/hosted](deploy/hosted/README.md#бэкапы-и-секреты)): после отключения питания в зоне `ru-central1-a` 11.10.2026 восстановление из суточного дампа потеряло данные почти за сутки. Настройка `BACKUP_INTERVAL_SECONDS=3600`, код не менялся.
- **Обложки-снимки** ([SHELF_COVERS](docs/specs/SHELF_COVERS.md), #31): у визуальных работ на полке — картинка первого экрана. На polochka.app включались 10.10.2026; после переезда в зону `ru-central1-d` 11.10.2026 временно выключены, пока рендерер не вернётся (#33).
- **Полки отделов и сервисные доступы в облаке — по организациям** ([TEAM_SHELVES](docs/specs/TEAM_SHELVES.md#организации-в-облаке-миграция-070), миграция 070). На polochka.app с 12.10.2026 включены `TEAM_SHELVES` и `SERVICE_ACCOUNTS`. Новая настройка `TEAM_SHELVES_SCOPE=installation|organisation`: в режиме `organisation` (облако, по умолчанию в `deploy/hosted/compose.yml`) у администратора компании есть организация, и страница «Полки компании», поиск и уход сотрудника, «Стать администратором» касаются только её полок. Сразу добавить коллегу на полку по почте или логину можно, только если у него подтверждённая почта на домене организации полки; остальных, в том числе вступивших по ссылке на другую её полку, — ссылкой-приглашением, а отказ не выдаёт, есть ли такой аккаунт. Почту участников видно только у адресов на доменах организации, и по скрытой почте никого не найти. Команды оператора: `npm run admin -- company org "<название>" [--domain …] [--shelf … [--move]]` и `npm run admin -- company admin <логин|почта> --org "<название>"` (организация должна уже быть). Расширения получают `companyAdmin(accountId)` с организацией администратора. Стирание аккаунта снимает с него роль администратора компании. На своей установке (`installation`, по умолчанию) всё как раньше. Как получить полку отдела в облаке, сказано на `/settings/company`: написать на hello@polochka.app.
- **Проверка слоёв фронтенда читается** (#12). `scripts/check-frontend-layers.mjs` переписан на TypeScript (`scripts/check-frontend-layers.ts`, запускается через `tsx`): понятные имена, короткие функции, правила перечислены в заголовке файла. Поведение не менялось: те же 162 проверенных модуля, те же сообщения и коды выхода.

### Fixed

- **Вход по ссылке от агента после закрепления или отзыва.** Вход читал ссылку как живую, затем ждал блокировку полки; если за это время полку закрепили (закрепление удаляет слабые сессии) или отозвали подключение агента, вход всё равно выдавал слабую сессию по устаревшему решению. Теперь ссылка и подключение перечитываются под блокировкой полки, и такой вход получает «Ссылка устарела».
- **Обслуживание оставляло временную полку недоудалённой.** Без конвейера удаления (`ACCOUNT_DELETION_ENABLED=false`) обслуживание закрывало простаивающую временную полку и затем удаляло её объекты и строки; сбой S3, конец времени прогона или остановка между этими шагами оставляли полку закрытой с объектами навсегда. Теперь закрытие записывает задание (`provisional_erasures`, миграция 076), и следующий прогон доводит удаление до конца; рецепты прав обновлены.
- **Импорт полки и снимок на дату.** Импорт ставил работе время ухода в корзину, но не писал это событие в журнал; после восстановления работы на новой установке снимок полки за время между исходным удалением в корзину и восстановлением показывал её на полке. Теперь уход в корзину пишется в журнал с исходным временем, как отметка принятия.
- **Рендерер не запускался** в 0.14.0: Dependabot (#49) поднял образ Playwright до 1.64, а `playwright-core` остался 1.63, и Chromium нужной сборки в образе не было — контейнер перезапускался по кругу. Обе версии теперь 1.64.0, а тест `tests/renderer-playwright-version.test.ts` не пропустит такое расхождение снова.
- Рендерер на той же VM: в `deploy/hosted/compose.yml` передаётся `RENDERER_CHROMIUM_SANDBOX`, а в [deploy/hosted](deploy/hosted/README.md#рендерер-ссылок) описано, что песочнице Chromium нужны непривилегированные user namespaces на хосте; на Ubuntu 24.04 в Yandex Cloud их нет.
- **Песочница Chromium в контейнере рендерера:** стандартный профиль seccomp Docker закрывал вызовы, без которых она не запускается, даже на хосте, где user namespaces разрешены. Compose обоих вариантов подключает [`deploy/renderer/seccomp-chromium.json`](deploy/renderer/seccomp-chromium.json) — стандартный профиль, в котором разрешены `clone`, `unshare`, `setns` и `chroot`, без новых capabilities. В [deploy/renderer](deploy/renderer/README.md#yandex-cloud) исправлены правила `DOCKER-USER` (они отрезали DNS облака и ответы на внутренние запросы), добавлены DNS для Docker, работа по IP (`default_sni`) и формат `RENDERER_CA`.
- `npm run dev:web` с пустым `WEB_DEV_PORT=`, `PORT=` или `APP_ORIGIN=` в `.env` брал порт 0 или 2 и не подменял `Origin`, так что вход и сохранение через него не проходили; пустое значение теперь значит «по умолчанию».

## [0.14.0] — 2026-10-10

### Added

- **Страницы проекта с React** ([PROJECTS](docs/specs/PROJECTS.md#страницы-с-react), #20). HTML-страница проекта может грузить React, ReactDOM, Babel или Tailwind с CDN или подключать `<script type="module">` и `text/babel`. Просмотрщик отдаёт её собранной тем же сборщиком, что одиночные страницы, без сети и с копиями библиотек Полки. Стили, картинки, обычные скрипты и ссылки остаются файлами проекта. Раньше такие скрипты блокировались и страница оставалась пустой. Страницы, которые не собрались, перечислены в `pagesNotBuilt` в ответе на сохранение проекта (REST и MCP), CLI печатает их.
- **Контекст для агента у сохранённой ссылки** ([AGENT_CONTEXT_TEMPLATES](docs/specs/AGENT_CONTEXT_TEMPLATES.md#обычные-исходники--серверный-пакет-реализован), #46): «Скопировать для агента», «Подробный контекст» и `polka_read_source` передают адрес и заметку ссылки (`link.json`). Раньше на ссылке они отвечали 422. Отказ для формата без контекста теперь перечисляет форматы, у которых он есть.
- **Приёмка управления работами настоящим агентом** ([AGENT_MANAGE_ACCEPTANCE](docs/dev/AGENT_MANAGE_ACCEPTANCE.md), #47): `scripts/agent-manage-acceptance.ts` даёт Claude Code или Codex с правами `context`, `read`, `manage` переименовать работу, перенести, убрать в корзину и вернуть без веба, затем проверяет результат и журнал. Прогон 09.10.2026: оба агента прошли. В `connect-agents.md` добавлено, что Codex в `exec` останавливается перед `polka_trash` и `polka_restore`, пока их не одобрить.

### Changed

- **Одна команда оператора** (#38). Вместо 33 npm-скриптов `moderation:*`, `account:*`, дозаполнений, метрик, переноса полки и проверки «Ленты» — `npm run admin -- <группа> <команда>` (`scripts/admin.ts`; без аргументов печатает список). Скрипты работают как раньше и запускаются по-прежнему напрямую. Подсказки в письмах модерации, сообщения скриптов и документация называют новые команды, например `npm run admin -- moderation unblock <id>`.
- **Журнал сервера на pino** (#36). Все события пишет один логгер (`apps/server/log.ts`). Каждая строка — JSON `{"level":…,"time":…,"event":…}`; токены, пароли, секреты, cookie и `authorization` вырезаются, даже если попали в поля. Fastify получает этот логгер, обработчики ошибок пишут через `req.log` (с `reqId`). Строки на каждый запрос выключены, потому что в адресах бывают токены. Уровень задаёт `LOG_LEVEL` (по умолчанию `info`). Рецепт чтения журнала в `deploy/hosted/README.md` обновлён: строки теперь начинаются с `"level"`, а не с `"event"`.
- **Маршруты сервера по модулям** (#37). `createApp()` в `apps/server/app.ts` сократился примерно с 1440 до 240 строк. Около 70 маршрутов разнесены по `site-routes.ts`, `account-routes.ts`, `shelf-routes.ts`, `work-routes.ts`, `view-routes.ts` и `service-account-routes.ts`, общие помощники — в `route-helpers.ts`. Поведение не менялось.

### Fixed

- **Вход по ссылке от агента после закрепления или отзыва.** Вход читал ссылку как живую, затем ждал блокировку полки; если за это время полку закрепили (закрепление удаляет слабые сессии) или отозвали подключение агента, вход всё равно выдавал слабую сессию по устаревшему решению. Теперь ссылка и подключение перечитываются под блокировкой полки, и такой вход получает «Ссылка устарела».
- **Обслуживание оставляло временную полку недоудалённой.** Без конвейера удаления (`ACCOUNT_DELETION_ENABLED=false`) обслуживание закрывало простаивающую временную полку и затем удаляло её объекты и строки; сбой S3, конец времени прогона или остановка между этими шагами оставляли полку закрытой с объектами навсегда. Теперь закрытие записывает задание (`provisional_erasures`, миграция 076), и следующий прогон доводит удаление до конца; рецепты прав обновлены.
- **Импорт полки и снимок на дату.** Импорт ставил работе время ухода в корзину, но не писал это событие в журнал; после восстановления работы на новой установке снимок полки за время между исходным удалением в корзину и восстановлением показывал её на полке. Теперь уход в корзину пишется в журнал с исходным временем, как отметка принятия.
- Апостроф в названиях, заметках и письмах теперь экранируется во всём HTML, который строит сервер: три из восьми копий `escapeHtml` его пропускали (#10). Одна функция и один шаблон UUID живут в `packages/contracts` (#11).
- Нестабильные тесты модерации, адаптеров здоровья и ленты событий агента ждут явного сигнала вместо пауз; в последнем найдена и исправлена гонка в самом тесте (#13).

## [0.13.0] — 2026-10-09

### Added

- **Приглашение ссылкой на полку отдела** ([TEAM_SHELVES](docs/specs/TEAM_SHELVES.md#приглашение-ссылкой-миграция-067)), миграция 067: администратор или куратор полки создаёт ссылку с ролью, сроком (до недели) и числом мест (1–50); коллега, который ещё ни разу не входил, открывает её, входит и оказывается на полке. Ссылку можно отозвать; куратор приглашает только читателей и авторов, администратора ссылкой не назначить. Всё пишется в журнал полки.
- **«Предложить в Ленту» с полки отдела** ([DISCOVER_V2](docs/specs/DISCOVER_V2.md), раздел 5а), миграция 068: куратор предлагает версию работы с заголовком и строкой описания, решает и публикует оператор (`npm run feed:proposals`). Предложение можно отозвать; всё пишется в журнал полки.
- **«Полка на дату»** ([SHELF_SNAPSHOT](docs/specs/SHELF_SNAPSHOT.md)): экран `/snapshot` (ссылка в боковой панели полки под «Корзиной») показывает полку на выбранный момент — какая версия каждой работы была последней, какая принята тогда и что изменилось с тех пор. Только чтение; на полке отдела — любому участнику. MCP-инструмент `polka_snapshot` (scope `read`) отдаёт ровно ответ `GET /api/v1/snapshot` (`at`, `limit`, `cursor`).
- **Поиск по фразе** ([CONTENT_SEARCH](docs/specs/CONTENT_SEARCH.md#поиск)): слова в кавычках — `"годовой отчёт"` или «годовой отчёт» — ищутся рядом и в этом порядке, на полке и в `polka_list`. Слово в кавычках ищется целиком, а не как начало слова.
- **«Сначала подходящие»:** пока в поиске есть запрос, полка ставит выше работы, где запрос есть в названии, затем те, где найденные слова в тексте стоят близко. `polka_list` ранжирует так же.
- **Для расширений** ([EXTENSIONS](docs/specs/EXTENSIONS.md)):
  - маршруты для машин — `machinePaths` и `context.agent(req, reply, scope)`: запросы агентов и сборщиков телеметрии по токену агента с нужным разрешением, без проверки `Origin`; вход по cookie на этих адресах не принимается;
  - `context.redact(text)` скрывает секреты в тексте, пришедшем нескрытым (телеметрия по OTLP), теми же правилами и метками, что `polka-sessions.mjs`; при `AGENT_SESSION_FINGERPRINTS=installation` отпечаток совпадает с отпечатком того же секрета в сессиях;
  - `session.saved` в журнале несёт сработавшие правила (`alerts`), а стирание аккаунта оставляет строку `account.erased` без личных данных (миграция 069): тревогам в SIEM и индексу сессий больше не нужно читать сессию или сверяться с таблицей аккаунтов.
- **Уведомления о заявках в Telegram:** `OPERATOR_TELEGRAM_BOT_TOKEN` и `OPERATOR_TELEGRAM_CHAT_ID` (оба или ни одного) — оператор узнаёт о заявке с /enterprise сразу, письмо уходит как раньше.
- **`npm run hosted:setup`** пишет `hosted.env` для своей установки с новыми секретами (права 600; `--force`, `--set`, `--live`), ключи S3 берёт из окружения ([deploy/hosted](deploy/hosted/README.md), шаг 4).
- **Документация для участников на английском:** CONTRIBUTING.md (русский — CONTRIBUTING.ru.md), [docs/en](docs/en/README.md) — устройство и локальная разработка.

### Changed

- Работа находится по названию, даже если слова запроса стоят в нём в другом порядке или в другой форме: «отчёт годовой» находит «Годовой отчёт».
- **CI на GitHub Actions снова работает** на каждый pull request и push в `main`. MinIO для разработки и CI собирается из исходников (`deploy/minio/Dockerfile`; первый `infra:up` — около 10 минут): готовые образы MinIO больше не публикуются.

### Fixed

- **Вход по ссылке от агента после закрепления или отзыва.** Вход читал ссылку как живую, затем ждал блокировку полки; если за это время полку закрепили (закрепление удаляет слабые сессии) или отозвали подключение агента, вход всё равно выдавал слабую сессию по устаревшему решению. Теперь ссылка и подключение перечитываются под блокировкой полки, и такой вход получает «Ссылка устарела».
- **Обслуживание оставляло временную полку недоудалённой.** Без конвейера удаления (`ACCOUNT_DELETION_ENABLED=false`) обслуживание закрывало простаивающую временную полку и затем удаляло её объекты и строки; сбой S3, конец времени прогона или остановка между этими шагами оставляли полку закрытой с объектами навсегда. Теперь закрытие записывает задание (`provisional_erasures`, миграция 076), и следующий прогон доводит удаление до конца; рецепты прав обновлены.
- **Импорт полки и снимок на дату.** Импорт ставил работе время ухода в корзину, но не писал это событие в журнал; после восстановления работы на новой установке снимок полки за время между исходным удалением в корзину и восстановлением показывал её на полке. Теперь уход в корзину пишется в журнал с исходным временем, как отметка принятия.
- **Хук сессий агентов: `/clear`, закрытый терминал, `disableAllHooks`** ([AGENT_SESSIONS](docs/specs/AGENT_SESSIONS.md#когда-хук-не-срабатывает)):
  - `sync` берёт не только `--since`, но и всё, что изменилось после начала последнего прошедшего `sync`. Сессия, закрытая в пятницу вместе с терминалом (без `SessionEnd`), к понедельнику выпадала из `--since 2d` и не доходила совсем;
  - одна сессия, которой сервер отказал (слишком большая), больше не останавливает `sync`: остальные уходят, её пробуем в следующий раз, код выхода 1;
  - отправка из хука отмечает сессию, и досылка по расписанию не отправляет её второй раз;
  - во фрагменте `managed-settings` у хука `timeout: 10`: хуки `SessionEnd` получают на всех 1,5 с. В установке пилота подхватится при повторном запуске установщика;
  - `/clear` до первого запроса: хук пишет в журнал «nothing to send», а не ошибку отправки;
  - проверено и записано: после `/clear` уходят обе части разговора; начатую отправку закрытие терминала не останавливает; `disableAllHooks` у человека управляемый хук не выключает, а хук плагина выключает.
- Список сессий листается по паре «начало, id»: сессии, начатые в одну и ту же миллисекунду, больше не пропадают на границе страниц. Курсор старого вида (просто время) по-прежнему работает.
- Курсор страницы поиска, выданный до нового ранжирования, отклоняется, а не пропускает работы.
- Нестабильные тесты: журнал расширений, закладка и Chrome, срок проверки страницы — на загруженной машине CI падали без ошибки в коде.

## [0.12.1] — 2026-10-08

### Fixed

- **Вход по ссылке от агента после закрепления или отзыва.** Вход читал ссылку как живую, затем ждал блокировку полки; если за это время полку закрепили (закрепление удаляет слабые сессии) или отозвали подключение агента, вход всё равно выдавал слабую сессию по устаревшему решению. Теперь ссылка и подключение перечитываются под блокировкой полки, и такой вход получает «Ссылка устарела».
- **Обслуживание оставляло временную полку недоудалённой.** Без конвейера удаления (`ACCOUNT_DELETION_ENABLED=false`) обслуживание закрывало простаивающую временную полку и затем удаляло её объекты и строки; сбой S3, конец времени прогона или остановка между этими шагами оставляли полку закрытой с объектами навсегда. Теперь закрытие записывает задание (`provisional_erasures`, миграция 076), и следующий прогон доводит удаление до конца; рецепты прав обновлены.
- **Импорт полки и снимок на дату.** Импорт ставил работе время ухода в корзину, но не писал это событие в журнал; после восстановления работы на новой установке снимок полки за время между исходным удалением в корзину и восстановлением показывал её на полке. Теперь уход в корзину пишется в журнал с исходным временем, как отметка принятия.
- **Хук сессий агентов, по проверке перед пилотом:** что ответила фоновая отправка, теперь пишется в `~/.polka/sessions-hook.log` (до 1 МБ) — раньше ошибка пропадала без следа. Папки сессий берутся из `CLAUDE_CONFIG_DIR` и `CODEX_HOME`, если они заданы: у человека с перенесённой папкой Claude Code хук молча ничего не отправлял.
- Токен Telegram-бота в адресе Bot API (`https://api.telegram.org/bot<токен>/…`) не скрывался: перед ним нет границы слова. Теперь скрывается.

## [0.12.0] — 2026-10-08

### Added

- **Сессии агентов в компании** ([AGENT_SESSIONS](docs/specs/AGENT_SESSIONS.md#в-компании)) — основа центра управления агентами коммерческой редакции:
  - `AGENT_SESSION_FINGERPRINTS=installation` — один ключ отпечатков на установку, один секрет совпадает у разных людей;
  - `AGENT_SESSION_NOTICE` — что человек читает при `login` и на странице «Сессии» до отправки;
  - точки расширения `context.sessions` (сессии всех людей установки) и `policies.sessionDelete` ([EXTENSIONS](docs/specs/EXTENSIONS.md));
  - `polka-sessions.mjs managed-settings` печатает хук для управляемых настроек Claude Code: компания раскладывает его через MDM, и человек его не отключит.

### Fixed

- **Вход по ссылке от агента после закрепления или отзыва.** Вход читал ссылку как живую, затем ждал блокировку полки; если за это время полку закрепили (закрепление удаляет слабые сессии) или отозвали подключение агента, вход всё равно выдавал слабую сессию по устаревшему решению. Теперь ссылка и подключение перечитываются под блокировкой полки, и такой вход получает «Ссылка устарела».
- **Обслуживание оставляло временную полку недоудалённой.** Без конвейера удаления (`ACCOUNT_DELETION_ENABLED=false`) обслуживание закрывало простаивающую временную полку и затем удаляло её объекты и строки; сбой S3, конец времени прогона или остановка между этими шагами оставляли полку закрытой с объектами навсегда. Теперь закрытие записывает задание (`provisional_erasures`, миграция 076), и следующий прогон доводит удаление до конца; рецепты прав обновлены.
- **Импорт полки и снимок на дату.** Импорт ставил работе время ухода в корзину, но не писал это событие в журнал; после восстановления работы на новой установке снимок полки за время между исходным удалением в корзину и восстановлением показывал её на полке. Теперь уход в корзину пишется в журнал с исходным временем, как отметка принятия.
- `deploy/hosted/compose.yml` и `deploy/compose.base.yml` не передавали приложению `AGENT_SESSION_QUOTA_BYTES` и `AGENT_MODEL_PRICES`: квота и цены из файла настроек установки не действовали.
- «Расход» в сессиях агентов: строки короткой карточки (MCP-серверы) больше не растягиваются на высоту соседней.

## [0.11.1] — 2026-10-07

### Fixed

- **Вход по ссылке от агента после закрепления или отзыва.** Вход читал ссылку как живую, затем ждал блокировку полки; если за это время полку закрепили (закрепление удаляет слабые сессии) или отозвали подключение агента, вход всё равно выдавал слабую сессию по устаревшему решению. Теперь ссылка и подключение перечитываются под блокировкой полки, и такой вход получает «Ссылка устарела».
- **Обслуживание оставляло временную полку недоудалённой.** Без конвейера удаления (`ACCOUNT_DELETION_ENABLED=false`) обслуживание закрывало простаивающую временную полку и затем удаляло её объекты и строки; сбой S3, конец времени прогона или остановка между этими шагами оставляли полку закрытой с объектами навсегда. Теперь закрытие записывает задание (`provisional_erasures`, миграция 076), и следующий прогон доводит удаление до конца; рецепты прав обновлены.
- **Импорт полки и снимок на дату.** Импорт ставил работе время ухода в корзину, но не писал это событие в журнал; после восстановления работы на новой установке снимок полки за время между исходным удалением в корзину и восстановлением показывал её на полке. Теперь уход в корзину пишется в журнал с исходным временем, как отметка принятия.
- **Сессии агентов, по приёмке на реальных сессиях:** шаблон команды берётся из первого настоящего шага цепочки — `cd "<папка>"; npm test` теперь `npm test`, а не `cd <arg>` (служебные `cd`, `export`, `source` пропускаются, слова в кавычках не дробятся). Нулевая стоимость, которую Claude Code пишет при подписке, показывается как «нет данных», а не «$0.00».
- Инструкция «Отправьте первые сессии» читается предложениями: ссылки и код больше не разрывают строку.

## [0.11.0] — 2026-10-07

### Added

- **Сессии агентов** ([AGENT_SESSIONS](docs/specs/AGENT_SESSIONS.md)): ваши сессии Claude Code и Codex на Полке — что делал агент, какие команды запускал и куда обращался, где видел секреты и куда их передал, сколько ушло токенов и денег, какие работы и пулл-реквесты из этого вышли. Страницы «Сессии» (список, сессия с расшифровкой), «Секреты» (отпечатки по всем сессиям), «Расход»; на странице работы — «Сделано в сессии».
- **`polka-sessions.mjs`** (без зависимостей, `GET /api/v1/cli/polka-sessions.mjs`): `login`, `list`, `preview`, `upload`, `sync`, `hook`. Секреты заменяются метками с отпечатком ещё на компьютере; рассуждения модели не отправляются без `--thinking`. Хук `SessionEnd` в плагине Полки — только при `POLKA_SESSIONS=on`.
- Право токена **«Сессии агентов»** (`sessions`), только для личной полки; MCP-инструменты `polka_sessions` и `polka_session_stats`.
- Правила v1 на сервере: секрет ушёл наружу, разрушительная команда, скрипт из сети в оболочку, режим без подтверждений.
- Своя квота сессий: `AGENT_SESSION_QUOTA_BYTES` для всей установки или по полке; 0 — выключено. Оценка расхода по ценам `AGENT_MODEL_PRICES`, если агент не записал стоимость сам.
- Миграция 065: таблицы `agent_sessions*`, квота и ключ отпечатков у полки, право `sessions` в проверках подключений, стирание сессий вместе с аккаунтом.
- Исследование этапа 0 ([agent-telemetry-storage](docs/research/agent-telemetry-storage.md)): что лежит в сессиях, Postgres против ClickHouse на 24,7 млн вызовов, форма OTLP-телеметрии Claude Code и Codex, стирание и сроки хранения в ClickHouse.

## [0.10.0] — 2026-10-07

### Added

- **Чтение версий для расширений** (`context.content`, [EXTENSIONS](docs/specs/EXTENSIONS.md)): версия любой полки установки с работой, полкой и файлами, каждый файл потоком с проверкой размера и SHA-256 (видео тоже). Версию, которую модератор изолировал или стёр, ядро называет, но не отдаёт. Нужно коммерческой редакции для выгрузки принятых версий.
- **Журнал действий для расширений** (`context.auditFeed`): чтение `audit_outbox` всей установки по своему курсору в порядке фиксации, без пропусков за поздно зафиксированной транзакцией.
- Миграция 064: индекс `audit_outbox` для чтения журнала по всей установке.
- **Печать в PDF** у рендерера (`POST /pdf`): вся страница на A4 в той же песочнице без сети, что и обложки; для расширений — `context.content.pdf`, где рендерер настроен.

### Fixed

- **Вход по ссылке от агента после закрепления или отзыва.** Вход читал ссылку как живую, затем ждал блокировку полки; если за это время полку закрепили (закрепление удаляет слабые сессии) или отозвали подключение агента, вход всё равно выдавал слабую сессию по устаревшему решению. Теперь ссылка и подключение перечитываются под блокировкой полки, и такой вход получает «Ссылка устарела».
- **Обслуживание оставляло временную полку недоудалённой.** Без конвейера удаления (`ACCOUNT_DELETION_ENABLED=false`) обслуживание закрывало простаивающую временную полку и затем удаляло её объекты и строки; сбой S3, конец времени прогона или остановка между этими шагами оставляли полку закрытой с объектами навсегда. Теперь закрытие записывает задание (`provisional_erasures`, миграция 076), и следующий прогон доводит удаление до конца; рецепты прав обновлены.
- **Импорт полки и снимок на дату.** Импорт ставил работе время ухода в корзину, но не писал это событие в журнал; после восстановления работы на новой установке снимок полки за время между исходным удалением в корзину и восстановлением показывал её на полке. Теперь уход в корзину пишется в журнал с исходным временем, как отметка принятия.
- `docs/specs/EXTENSIONS.md` обещал доставку `onEvent` «как минимум один раз»; на деле событие не хранится и теряется при перезапуске. Описание исправлено, для надёжной работы — `context.auditFeed`.

## [0.9.1] — 2026-10-06

### Fixed

- **Вход по ссылке от агента после закрепления или отзыва.** Вход читал ссылку как живую, затем ждал блокировку полки; если за это время полку закрепили (закрепление удаляет слабые сессии) или отозвали подключение агента, вход всё равно выдавал слабую сессию по устаревшему решению. Теперь ссылка и подключение перечитываются под блокировкой полки, и такой вход получает «Ссылка устарела».
- **Обслуживание оставляло временную полку недоудалённой.** Без конвейера удаления (`ACCOUNT_DELETION_ENABLED=false`) обслуживание закрывало простаивающую временную полку и затем удаляло её объекты и строки; сбой S3, конец времени прогона или остановка между этими шагами оставляли полку закрытой с объектами навсегда. Теперь закрытие записывает задание (`provisional_erasures`, миграция 076), и следующий прогон доводит удаление до конца; рецепты прав обновлены.
- **Импорт полки и снимок на дату.** Импорт ставил работе время ухода в корзину, но не писал это событие в журнал; после восстановления работы на новой установке снимок полки за время между исходным удалением в корзину и восстановлением показывал её на полке. Теперь уход в корзину пишется в журнал с исходным временем, как отметка принятия.
- **Отчёт `shelf:import`** называет версии, которые фильтр содержимого новой установки изолировал при сохранении («заблокировано модерацией этой установки: v2»), а не показывает их просто перенесёнными. В `--json` — поле `blocked` у каждой работы.
- **Кнопка «Отозвать доступ»** в окне подтверждения: красный текст на синем фоне заменён обычной кнопкой опасного действия.

## [0.9.0] — 2026-10-03

### Added

- **Перенос полки в свою установку** ([SHELF_TRANSFER](docs/specs/SHELF_TRANSFER.md)). `GET /api/v1/export` отдаёт опись всей личной полки (все версии с датами и файлами, папки, карточка, корзина), `GET /api/v1/export/revisions/:id/files/:index` — файлы, со своим лимитом. `polka-export.mjs` (без зависимостей, `GET /api/v1/cli/polka-export.mjs`) пишет их в папку, каждый файл один раз, и продолжает с места. На другой установке `npm run shelf:import -- --dir … --account …` сохраняет их на личную полку обычным путём сохранения и возвращает номера и даты версий, принятую версию, ответственного, карточку и корзину; `--dry-run` проверяет каждый файл, `--raise-quota` поднимает лимит. Ссылки и комментарии не переносятся. Права — существующие `read` и `source:read`, миграций нет.
- Раздел «Перенести полку» в «Настройках»; «Удалить полку» сначала отправляет к нему.

## [0.8.0] — 2026-10-02

### Added

- **«Удалить аккаунт» кнопкой** («Настройки» → «Удалить полку»): план с числом работ, версий и сроками, подтверждение словом «УДАЛИТЬ», страница статуса `/account-deleted`. Выключена по умолчанию (`ACCOUNT_DELETION_ENABLED`); отказывает, если модерация хранит заблокированное содержимое или вы единственный администратор полки отдела с участниками.
- **Служба очистки** `account-purge` (`scripts/account-purge-loop.ts`, профиль compose `purge`): проход раз в минуту стирает данные подтверждённых заявок. `ACCOUNT_DELETION_PURGE_WORKER=true` разрешает включать кнопку вне loopback, только вместе со службой ([hosted](deploy/hosted/README.md#удаление-аккаунта)).

## [0.7.4] — 2026-10-02

### Changed

- **Вход и регистрация** на той же шапке и светлом фоне, что главная, «Для компаний» и тарифы.

## [0.7.3] — 2026-10-02

### Changed

- **Новый вид сайта.** Главная переделана: отдельная шапка вместо бокового меню приложения, макет продукта в первом экране (папка, версии, реплика агента, ссылка), «три шага», крупные карточки возможностей, тёмный блок про свою установку, FAQ в две колонки. Пустая полка после входа показывает три карточки «с чего начать». «Для компаний» и тарифы переведены на ту же шапку и светлый фон.

## [0.7.2] — 2026-10-02

### Fixed

- **Вход по ссылке от агента после закрепления или отзыва.** Вход читал ссылку как живую, затем ждал блокировку полки; если за это время полку закрепили (закрепление удаляет слабые сессии) или отозвали подключение агента, вход всё равно выдавал слабую сессию по устаревшему решению. Теперь ссылка и подключение перечитываются под блокировкой полки, и такой вход получает «Ссылка устарела».
- **Обслуживание оставляло временную полку недоудалённой.** Без конвейера удаления (`ACCOUNT_DELETION_ENABLED=false`) обслуживание закрывало простаивающую временную полку и затем удаляло её объекты и строки; сбой S3, конец времени прогона или остановка между этими шагами оставляли полку закрытой с объектами навсегда. Теперь закрытие записывает задание (`provisional_erasures`, миграция 076), и следующий прогон доводит удаление до конца; рецепты прав обновлены.
- **Импорт полки и снимок на дату.** Импорт ставил работе время ухода в корзину, но не писал это событие в журнал; после восстановления работы на новой установке снимок полки за время между исходным удалением в корзину и восстановлением показывал её на полке. Теперь уход в корзину пишется в журнал с исходным временем, как отметка принятия.
- **Ротация токена и смена ответственного у сервисного доступа** выдают новый секрет на той же строке подключения, а не на новой: расширение, хранящее ограничение по подключению (агент только к папке в коммерческой редакции), больше не теряет его, и доступ не расширяется молча. Совместимость с коммерческой редакцией проверена ([реализация](docs/dev/AGENT_ACCESS_IMPLEMENTATION.md#совместимость-с-коммерческой-редакцией-проверено-02102026)).

## [0.7.1] — 2026-10-02

### Changed

- **Позиционирование «рабочее место агента»** на главной (подпись, подзаголовок, карточки, вопрос «Чем это отличается от обычного диска?»), в README, описании плагина и материалах ([POSITIONING](docs/specs/POSITIONING.md), docs/marketing). «Принятое» остаётся отметкой в интерфейсе.

### Added

- Тест: чат-агент сам создаёт работу-папку через `polka_capture` с манифестом `project-v1` и дальше работает с ней.

## [0.7.0] — 2026-10-02

### Added

- **Работа как папка для агента** ([AGENT_WORKSPACE](docs/specs/AGENT_WORKSPACE.md)). Для работы-проекта (`project-v1`) без выхода в сеть: `polka_list_files` (файлы версии без байтов), `polka_read_file` (один файл по пути, текст UTF-8 или base64, до 256 КиБ), `polka_change_files` (следующая версия с добавленными, заменёнными и удалёнными файлами, остальные копирует сервер; ссылка не двигается). По HTTP: `GET /api/v1/works/:id/file?path=` и `POST /api/v1/works/:id/changes`. Тест паритета MCP и REST обновлён.

### Changed

- Позиционирование: «рабочее место агента — папка с версиями, ссылкой для людей и чтением для других агентов» ([DECISIONS](docs/specs/DECISIONS.md#02102026-полка--рабочее-место-агента-с-версиями)).

### Fixed

- **Вход по ссылке от агента после закрепления или отзыва.** Вход читал ссылку как живую, затем ждал блокировку полки; если за это время полку закрепили (закрепление удаляет слабые сессии) или отозвали подключение агента, вход всё равно выдавал слабую сессию по устаревшему решению. Теперь ссылка и подключение перечитываются под блокировкой полки, и такой вход получает «Ссылка устарела».
- **Обслуживание оставляло временную полку недоудалённой.** Без конвейера удаления (`ACCOUNT_DELETION_ENABLED=false`) обслуживание закрывало простаивающую временную полку и затем удаляло её объекты и строки; сбой S3, конец времени прогона или остановка между этими шагами оставляли полку закрытой с объектами навсегда. Теперь закрытие записывает задание (`provisional_erasures`, миграция 076), и следующий прогон доводит удаление до конца; рецепты прав обновлены.
- **Импорт полки и снимок на дату.** Импорт ставил работе время ухода в корзину, но не писал это событие в журнал; после восстановления работы на новой установке снимок полки за время между исходным удалением в корзину и восстановлением показывал её на полке. Теперь уход в корзину пишется в журнал с исходным временем, как отметка принятия.
- Тесты: HTTP-маршрут снимка полки и API карточки полки; `status.md`, `DECISIONS.md`, `PRODUCT.md` и `roadmap.md` говорят, что работает в облаке.

## [0.6.0] — 2026-10-02

### Added

- **Снимок полки на дату** ([SHELF_SNAPSHOT](docs/specs/SHELF_SNAPSHOT.md), `GET /api/v1/snapshot?at=`, миграция 063). Работы, которые были на полке и не в корзине на тот момент, с версией, бывшей последней тогда, и версией, принятой тогда; корзина и принятие читаются из журнала действий (возврат из корзины учтён). Название и папка текущие; момент в последней минуте может ещё измениться. Для агентов и отчётов «как было»; экрана нет.

## [0.5.3] — 2026-10-02

### Added

- **Принятая версия видна на полке:** отметка «принята» в выборе версии, списке версий, сведениях о работе и на карточке работы; фильтр «Принятые» на полке (`GET /api/artifacts?accepted=1`; числа на фишках следуют фильтру; пустой результат объясняет, что принятых работ нет, и предлагает «Показать все»; карточка пишет «принята другая версия», если принята не последняя).

### Fixed

- **Вход по ссылке от агента после закрепления или отзыва.** Вход читал ссылку как живую, затем ждал блокировку полки; если за это время полку закрепили (закрепление удаляет слабые сессии) или отозвали подключение агента, вход всё равно выдавал слабую сессию по устаревшему решению. Теперь ссылка и подключение перечитываются под блокировкой полки, и такой вход получает «Ссылка устарела».
- **Обслуживание оставляло временную полку недоудалённой.** Без конвейера удаления (`ACCOUNT_DELETION_ENABLED=false`) обслуживание закрывало простаивающую временную полку и затем удаляло её объекты и строки; сбой S3, конец времени прогона или остановка между этими шагами оставляли полку закрытой с объектами навсегда. Теперь закрытие записывает задание (`provisional_erasures`, миграция 076), и следующий прогон доводит удаление до конца; рецепты прав обновлены.
- **Импорт полки и снимок на дату.** Импорт ставил работе время ухода в корзину, но не писал это событие в журнал; после восстановления работы на новой установке снимок полки за время между исходным удалением в корзину и восстановлением показывал её на полке. Теперь уход в корзину пишется в журнал с исходным временем, как отметка принятия.
- Отступы блока «Искать также на полках отделов» в форме токена.

## [0.5.2] — 2026-10-02

### Fixed

- **Вход по ссылке от агента после закрепления или отзыва.** Вход читал ссылку как живую, затем ждал блокировку полки; если за это время полку закрепили (закрепление удаляет слабые сессии) или отозвали подключение агента, вход всё равно выдавал слабую сессию по устаревшему решению. Теперь ссылка и подключение перечитываются под блокировкой полки, и такой вход получает «Ссылка устарела».
- **Обслуживание оставляло временную полку недоудалённой.** Без конвейера удаления (`ACCOUNT_DELETION_ENABLED=false`) обслуживание закрывало простаивающую временную полку и затем удаляло её объекты и строки; сбой S3, конец времени прогона или остановка между этими шагами оставляли полку закрытой с объектами навсегда. Теперь закрытие записывает задание (`provisional_erasures`, миграция 076), и следующий прогон доводит удаление до конца; рецепты прав обновлены.
- **Импорт полки и снимок на дату.** Импорт ставил работе время ухода в корзину, но не писал это событие в журнал; после восстановления работы на новой установке снимок полки за время между исходным удалением в корзину и восстановлением показывал её на полке. Теперь уход в корзину пишется в журнал с исходным временем, как отметка принятия.
- **Счётчик чтений** считает только удавшийся ответ с данными (не отказ, не 404, не пустой опрос ленты); при отсутствии таблицы или прав пишет предупреждение в журнал. Миграция 062: при стирании аккаунта счётчики его личной полки удаляются.
- **Тексты:** главная, README и материалы больше не обещают, что отметку «принято» видит получатель ссылки или коллеги в облаке, где полок отделов нет.

## [0.5.1] — 2026-10-02

### Added

- **Счётчик чтений агентов** (миграция 061, `agent_read_days`). Список, одна работа (HTTP API и MCP) и лента изменений (только HTTP; пустой опрос не считается) считаются по полке, дню и виду токена (человека или сервисного доступа): никаких имён, работ и содержимого. Нужен для метрики «доля машинных чтений»: `npm run metrics:agent-access` показывает чтения агентов за 30 дней, открытия ссылок получателями и долю.

## [0.5.0] — 2026-10-02

### Added

- **Полка хранит принятое** ([POSITIONING](docs/specs/POSITIONING.md), [AGENT_ACCESS_AND_MEMORY](docs/specs/AGENT_ACCESS_AND_MEMORY.md), миграции 054–060). Принятая версия и ответственный у работы (меню работы «Принятая версия», события `revision.accepted` и `owner.changed`); режим ссылки `pinned`/`follows` (сервисный доступ не двигает закреплённую ссылку, пока куратор не включил «следует»; агент с токеном человека работает как раньше); карточка полки «как у нас принято» в `polka_context` (`GET`/`PUT /api/shelf/card`, поле на странице «Агенты» и в диалоге «Участники полки»).
- **Агенты читают полку** ([PUBLISH_API](docs/PUBLISH_API.md#чтение-полки-get-apiv1works-get-apiv1worksartifactid-get-apiv1events)). `GET /api/v1/works` (`query`, `since`, `cursor`, `shelfIds`), `GET /api/v1/works/:id` с версиями, лента `GET /api/v1/events?after=` над журналом, `since` и `revisions` в MCP-инструментах; поиск ранжирует совпадение в названии выше совпадения в тексте. Отчёт оператора: `npm run metrics:agent-access`.
- **Сервисные доступы и токены задач** (`SERVICE_ACCOUNTS=on`, по умолчанию выключено; миграции 058–060). Агент для cron и CI с названным ответственным человеком; при его уходе или стирании аккаунта доступ замораживается; `POST /api/v1/task-token` — короткий токен на задачу (5–60 минут). Экран «Сервисные доступы» в диалоге «Участники полки».
- **Поиск по нескольким полкам отделов** (миграция 059): владелец выбирает полки при выдаче токена; `shelfIds` в `GET /works` и `polka_list`, `searchableShelves` в `polka_context`.
- **Позиционирование:** «Принятое хранится. Читают люди и агенты.» на главной, в README и описании плагина; материалы — docs/marketing.

- **Лента: десять подробных разборов выпусков Lenny’s Podcast** (дизайн в эпоху ИИ, продукт и исследования в Anthropic, управление командой Claude Code и Cowork, команда Codex, прогнозы Dan Shipper, агентность и «податливое» ПО, ИИ как «1997 год», разговор с создателем iPod, компании как «петли», системное мышление в Netflix). Каждый разбор сделан по полной расшифровке, с метками ▶ на нужную минуту в YouTube.
- **Лента: разборы выпусков подкастов и школьный проект.** В каталог добавлены два разбора Lenny’s Podcast (Tara Seshan, OpenAI; Cat Wu, Claude Code), пересказанные своими словами по описанию и конспекту выпуска, и проект «Золотое кольцо: Хранители времени». Восемь демо-инструментов («Доли без зубрёжки», «Матрица решений» и др.) сняты с Ленты, их исходники остаются в `content/editorial/`.
- **`/sitemap.xml`** со всеми публичными страницами и материалами Ленты; ссылка на него в `robots.txt`. Общие работы (`/s#…`), полки и API по-прежнему закрыты от индексации.
- **Автор узнаёт о ссылке на проверке** ([LINK_REVIEW_LETTERS](docs/specs/LINK_REVIEW_LETTERS.md)). Письмо, что ссылку проверяет человек, и письмо, когда её одобрили (по одному на круг, без причины проверки, не чаще 6 в сутки на адрес); на работе видно, с какого времени ссылка ждёт. Экран получателя объясняет, что это не ошибка, и сам открывает работу, когда проверка закончится. Миграция 053.
- **Счётчик открытий ссылки** ([LINK_OPENS](docs/specs/LINK_OPENS.md)). В панели ссылки автор видит, сколько раз её открыли и когда в последний раз («Открывали 3 раза, последний раз 30 сентября»). Считается число и время по дням, читателя нигде не видно; свои открытия и ссылки Ленты не считаются. Миграция 052.
- **Удаление работы навсегда** ([WORK_DELETION](docs/specs/WORK_DELETION.md)). В корзине появилась кнопка «Удалить навсегда» с подтверждением: из хранилища стираются все версии объектов работы, из поиска и с полки уходят её текст и обложка, ссылки закрываются, место возвращается. Если процесс прервался, часовой обход заканчивает стирание. На работу с жалобой или решением модерации, а также опубликованную в Ленте или библиотеке шаблонов кнопка отвечает отказом с объяснением. Миграция 051 (`artifacts.purged_at`).
- **Видео в проектах** ([PROJECT_VIDEO](docs/specs/PROJECT_VIDEO.md)). Проект принимает MP4 и WebM: до 200 МБ на файл и 400 МБ сверху к 48 МБ страниц и картинок. Видео уходит потоком в S3 по частям и не читается в память целиком (`PUT /api/v1/projects/:uploadId/media/:index`), просмотрщик отдаёт его с `Range`, так что перемотка работает. `polka-publish-project.mjs` и `polka-pull.mjs` передают его потоком. Полка не сканирует видео, поэтому сохранять его можно только на полке с флагом `tenants.video_enabled` (миграция 050, выключен по умолчанию); оператор включает его вместе с квотой.
- **Удаление аккаунта оператором** (`account-erase`, [deploy/hosted](deploy/hosted/README.md#удаление-аккаунта)). По обращению владельца команда закрывает аккаунт сразу: вход, агентов, ссылки и публикации. Затем готовый конвейер очистки удаляет все версии объектов полки, пишет отзыв и итог во внешний журнал стираний и стирает метаданные защищённой функцией. Сначала идёт пробный прогон, номер обращения попадает в журнал модерации, прерванную очистку продолжает `--resume`. Команда откажет, если на полке есть заблокированное содержимое или аккаунт — единственный администратор полки отдела. Обработчик очистки (`scripts/account-purge*.ts`) теперь работает с удалённым хранилищем по HTTPS; база по-прежнему только локальная. Самостоятельного удаления в интерфейсе пока нет.
- **Восстановление из дампа применяет журнал стираний** ([deploy/hosted](deploy/hosted/README.md#восстановление-из-дампа), шаг 5). `restore-authorities` читает журнал ключом только на чтение, пишет описание бэкапа и печатает значения, которые проверяет `restore-reconcile`. Тот снова стирает аккаунты, удалённые после момента дампа, и пишет квитанцию. Роль `polka_restore` получает свои функции на шаге `grants`, если она есть. Адаптеры восстановления и очистки принимают регион подписи (`S3_REGION`, `ERASURE_LEDGER_REGION`).

## [0.4.0] — 2026-09-27

### Added

- **Место для расширений в окне «Поделиться»** (`share-dialog`, [EXTENSIONS](docs/specs/EXTENSIONS.md)). Раздел расширения стоит над примечанием внизу окна и получает работу, полку отдела и состояние ссылки. Так, например, коммерческая редакция заранее показывает правила ссылок с полки. Запрещает выпуск по-прежнему `policies.linkIssue` на сервере. Страницы с местами для расширений загружают их модули одним общим вызовом (`ensureExtensions`).

- **Алерты внешней проверки** (`npm run ops:uptime`, [hosted](deploy/hosted/README.md#мониторинг)). `scripts/ci/uptime.mjs` пишет в Telegram (`TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`) или на webhook (`ALERT_WEBHOOK_URL`), когда набор упавших проверок меняется, и когда всё снова проходит. Пока ничего не изменилось, повторов нет; недоставленное сообщение отправится при следующем запуске. Прошлый результат — в `UPTIME_STATE_FILE`. Сертификат проверяется только у адресов `https:`.

- **Правка одного файла проекта** (`polka_revise` с `edits` и `path`, `POST /api/v1/works/:id/edits`; [PROJECTS](docs/specs/PROJECTS.md#правка-одного-файла)). Раньше проект правился только новой загрузкой целиком. Теперь сервер читает один текстовый файл (Markdown, HTML, CSS, JavaScript, JSON, SVG, текст), применяет правки, а остальные файлы хранилище копирует в новую версию само (`CopyObject`), поэтому размер проекта не важен. У каждой версии по-прежнему свои объекты. При сохранении все файлы читаются обратно, сверяются с манифестом, проверяются и индексируются как при обычной загрузке. Ключ идемпотентности, проверка базовой версии и права подключения — те же.
- **`polka pull` и `push`** ([PUBLISH_API](docs/PUBLISH_API.md#polka-pull-и-push)). `polka-pull.mjs` скачивает версию работы в папку: `GET /api/v1/works/:id/files` и `…/revisions/:rev/files/:index`, scope `source:read`, каждый файл сверяется с SHA-256. Заодно он записывает `.polka.json`. `polka-publish-project.mjs` такой папки сохраняет следующую версию того же проекта. Изменённые файлы он отправляет, а остальные хранилище копирует само (`POST /api/v1/projects/:uploadId/reuse`). Токен из `polka_project_upload` теперь получает и `source:read`, если оно есть у подключения, и тогда в ответе есть `pullCommand`. Синхронизации нет: если на Полке уже есть более новая версия, сохранение отказывает.
- **Письмо с приглашением в библиотеку шаблонов.** Раньше администратор получал ссылку и пересылал её сам. Теперь Полка отправляет приглашённому письмо: название библиотеки, логин пригласившего, роль, срок и ссылку. Текст фиксированный, своего сообщения к нему не добавить: приглашать может любой закреплённый аккаунт. Не больше 20 писем в сутки от одного администратора и 3 на один адрес. Окно говорит, ушло ли письмо, а ссылку по-прежнему можно скопировать. Ответ `POST /api/template-libraries/:id/invitations` содержит `mail`: `sent`, `disabled`, `limited` или `failed`. В [Политике](docs/legal/privacy.md) раздел «Библиотеки шаблонов» говорит об этом письме.

### Fixed

- **Вход по ссылке от агента после закрепления или отзыва.** Вход читал ссылку как живую, затем ждал блокировку полки; если за это время полку закрепили (закрепление удаляет слабые сессии) или отозвали подключение агента, вход всё равно выдавал слабую сессию по устаревшему решению. Теперь ссылка и подключение перечитываются под блокировкой полки, и такой вход получает «Ссылка устарела».
- **Обслуживание оставляло временную полку недоудалённой.** Без конвейера удаления (`ACCOUNT_DELETION_ENABLED=false`) обслуживание закрывало простаивающую временную полку и затем удаляло её объекты и строки; сбой S3, конец времени прогона или остановка между этими шагами оставляли полку закрытой с объектами навсегда. Теперь закрытие записывает задание (`provisional_erasures`, миграция 076), и следующий прогон доводит удаление до конца; рецепты прав обновлены.
- **Импорт полки и снимок на дату.** Импорт ставил работе время ухода в корзину, но не писал это событие в журнал; после восстановления работы на новой установке снимок полки за время между исходным удалением в корзину и восстановлением показывал её на полке. Теперь уход в корзину пишется в журнал с исходным временем, как отметка принятия.
- **Загрузка больше не держит полку, пока работает хранилище.** Раньше запись файла в S3, а при сохранении — чтение всех файлов обратно и проверка страниц шли внутри транзакции под блокировкой полки. Медленное хранилище останавливало другие сохранения на той же полке и занимало соединения с базой (известное ограничение из 0.3.1). Теперь байты пишутся и проверяются до транзакции, а под блокировкой остаются только сверка версии объекта и запись строк. Если файл за это время прислали заново, сохраняется записанная версия. Так работают загрузка из браузера, агенты (`polka_publish`, `polka_capture`, `polka_revise`) и проекты из CLI. Внутри транзакции остались две короткие записи: документ «Сохранить как ссылку» и собранная интерактивная страница (под блокировкой работы, а не полки). Тест — `tests/upload-outside-lock.test.ts`.

## [0.3.1] — 2026-09-27

### Security

Исправления по итогам проверки безопасности перед запуском.

- **Страницы проекта без WebRTC.** Страница проекта выполняет свои скрипты, а CSP не закрывает WebRTC, поэтому через `RTCPeerConnection` она могла отправить наружу то, что вводит читатель. Теперь каждый HTML-ответ просмотрщика проектов начинается с того же скрипта, что и интерактивный просмотр: он убирает конструкторы RTC раньше любого скрипта страницы, даже стоящего до `<head>`. Проверено в Chrome.
- **Просмотр проекта не зависает на подобранной странице.** Место для скриптов Полки искалось регулярным выражением по `<head>`, и страница из множества незакрытых `<head` на 256 КБ обрабатывалась 4 секунды (на 5 МБ — около 25 минут). Теперь скрипты ставятся линейным проходом в начало документа, после doctype.
- **Заголовок страницы по ссылке не блокирует сервер.** «Сохранить как ссылку» и рендерер искали `<title>` и `og:title` регулярными выражениями по всей странице (до 512 КБ); подобранная страница на 32 КБ держала сервер 7 секунд. Теперь заголовки читаются только из первых 32 КБ линейным проходом.
- **Скрипты пакетов и проектов проверяются вне основного потока.** Проверка на фишинг читала `.js`-файлы при сохранении прямо в потоке запросов: 48 МБ подобранных строк останавливали сервер примерно на 25 секунд. Теперь скрипты читаются в воркере с дедлайном (2 секунды и ещё секунда на мегабайт); не успевшая проверка добавляет сигнал `scan:incomplete`, и интерактивная ссылка ждёт проверки, как для страниц. Из каждого файла в текстовые сигналы попадает не больше 256 тысяч символов строк. Так же теперь работает `moderation.ts recheck --fraud`.
- **Воркеров проверки не больше, чем ядер.** Каждая проверка страницы запускала отдельный воркер с кучей до 256 МБ без ограничения их числа, и одновременные сохранения могли исчерпать память. Теперь одновременно работает не больше воркеров, чем ядер (`os.availableParallelism()`), остальные ждут в очереди. Ожидание в очереди не входит в дедлайн проверки.
- **Сессия по ссылке от агента не объединяет полки.** С такой сессией можно было войти в другую полку и нажать «Объединить»: работы и агенты временной полки уходили туда. Держатель ссылки мог так забрать чужие работы. Теперь `POST /api/account/claim/merge` требует обычного входа, а сессии по ссылке доступно только «Открыть ту полку без объединения»; работы остаются на месте. Экран `/claim` пока показывает «Объединить» и для такой сессии, сервер ответит отказом.
- **Похожие на чужие имена клиентов OAuth.** «CIaude» (заглавная I), «Clauԁe» (кириллическая ԁ) и «ΟpenAI» (греческая Ο) показывались на странице согласия без пометки «(имя не подтверждено)». Теперь имя сравнивается после сведения похожих латинских, кириллических и греческих букв, а также I/l/1 и O/0, к одному написанию. Имена «Полка», «Polka» и «Полочка» подтверждаются только для адресов возврата на домене самой установки.
- **HSTS с `includeSubDomains`** на обоих хостах в `deploy/hosted/Caddyfile`.
- **Полка сортирует и фильтрует все работы, а не загруженную страницу.** `GET /api/artifacts` принимает `sort` (`new`, `old`, `title`) и `kind` (`pages`, `documents`, `images`, `other`), первая страница отдаёт `counts` по всей полке. Любая HTML-работа теперь «Страница» везде: в чипах, на карточке и в просмотре. Снимки — docs/assets/launch-fixes-2026-09-27.
- **Удаление.** В корзине сказано, сколько там лежат работы (пока их не восстановят) и где удалить их навсегда. В настройках (`/settings`, пункт «Настройки» в меню аккаунта) появился раздел «Удалить полку» с адресом оператора: `/api/capabilities` отдаёт его как `privacyContact` (`OPERATOR_CONTACT`, иначе `OPERATOR_EMAIL`).
- **Быстрее первая загрузка.** Шрифты IBM Plex Sans — подмножества WOFF2 (латиница, кириллица, пунктуация; 35 и 37 КБ вместо 218 КБ каждый), обычное начертание предзагружается. Zod ушёл из основного бандла (500 → 413 КБ). `/assets/*` кешируются навсегда (`immutable`), `/fonts/*` — на 30 дней, оболочка `index.html` — `no-cache`.

### Added

- **`npm run local:setup` включает локальный интерактивный просмотр** (`HTML_LIVE_MODE=local`, только loopback) и записывает в `.env` порты и имя compose-проекта (`POLKA_LOCAL_PROJECT`, `POLKA_LOCAL_PG_PORT`, `POLKA_LOCAL_S3_PORT`, `PORT`, `VIEWER_PORT`; по умолчанию прежние). Второй клон поднимается рядом с первым, если при setup задать другие значения. В конце setup печатает всю оставшуюся последовательность команд.

### Changed

- **Плагин ставится из лёгкого репозитория [artkruglov/polka-plugin](https://github.com/artkruglov/polka-plugin)** (84 КБ вместо клона всего ядра, который обрывался на медленной сети). Его собирает `node scripts/plugin-repo.mjs <папка>` из `.claude-plugin/`, `.codex-plugin/`, `.agents/plugins/`, `.mcp.json` и `skills/` этого репозитория. Команды: `claude plugin marketplace add artkruglov/polka-plugin && claude plugin install polka@polka`, `codex plugin marketplace add artkruglov/polka-plugin && codex plugin add polka@polka`, скилл — `npx skills add artkruglov/polka-plugin`. Команды с `artkruglov/polka` из записи 0.3.0 по-прежнему работают (манифесты остались в этом репозитории), но клонируют всё ядро; документация, `/connect` и `/llms.txt` дают `artkruglov/polka-plugin`.
- **Письмо с кодом входа** ([apps/server/mail-templates/login-code.ts](apps/server/mail-templates/login-code.ts); снимки — в истории git, `docs/design/2026-09-26-code-email` до коммита c9efc10). Кроме текстовой части теперь есть HTML: крупный моноширинный код «1234 5678», срок действия 10 минут, «Если вы не запрашивали вход — просто проигнорируйте письмо», подвал с адресом установки. Вёрстка на таблицах с инлайновыми стилями, ширина до 560 px, светлая и тёмная темы. Без картинок, внешних шрифтов и счётчиков, единственная ссылка ведёт на адрес установки. Код теперь стоит в теме письма («1234 5678 — код для входа в Полку»), так что его видно прямо в уведомлении. Текстовая часть по-прежнему содержит письмо целиком.
- **Экран ввода кода на `/signup`** (через него же входят со страниц входа, `/signin` и `/claim`). Для любого адреса добавлена строка «Письмо не пришло за минуту? Проверьте «Спам» и «Промоакции»», а «Другая почта» переименована в «Изменить адрес». Если на установке задан список доменов (`EMAIL_SIGNUP_DOMAINS`) и адрес в него не входит, после «Получить код» остаётся предупреждение, что без существующей полки код не придёт. Рядом стоят кнопки Яндекс ID, VK ID и Google, которые открывают полку, и «Изменить адрес». Сервер по-прежнему отвечает одинаково для любого адреса. Повторная отправка работает как раньше: через 60 секунд, не больше трёх раз на адрес.
- **Регистрация на polochka.app открыта для любого адреса** (с 26.09.2026, `EMAIL_SIGNUP_DOMAINS=any`; изменение настройки, не кода). Шаблон `deploy/hosted` по-прежнему по умолчанию `ru-only`.

### Fixed

- **Вход по ссылке от агента после закрепления или отзыва.** Вход читал ссылку как живую, затем ждал блокировку полки; если за это время полку закрепили (закрепление удаляет слабые сессии) или отозвали подключение агента, вход всё равно выдавал слабую сессию по устаревшему решению. Теперь ссылка и подключение перечитываются под блокировкой полки, и такой вход получает «Ссылка устарела».
- **Обслуживание оставляло временную полку недоудалённой.** Без конвейера удаления (`ACCOUNT_DELETION_ENABLED=false`) обслуживание закрывало простаивающую временную полку и затем удаляло её объекты и строки; сбой S3, конец времени прогона или остановка между этими шагами оставляли полку закрытой с объектами навсегда. Теперь закрытие записывает задание (`provisional_erasures`, миграция 076), и следующий прогон доводит удаление до конца; рецепты прав обновлены.
- **Импорт полки и снимок на дату.** Импорт ставил работе время ухода в корзину, но не писал это событие в журнал; после восстановления работы на новой установке снимок полки за время между исходным удалением в корзину и восстановлением показывал её на полке. Теперь уход в корзину пишется в журнал с исходным временем, как отметка принятия.
- **Исправления сервера перед запуском** (миграция 049).
  - Пул соединений с базой: `DATABASE_POOL_MAX` (по умолчанию 20 вместо 8), у соединений приложения `statement_timeout` 15 с и `idle_in_transaction_session_timeout` 60 с. Загрузка пока держит блокировку полки на время записи в S3; это исправим после запуска (описано в `apps/server/db.ts`).
  - Напоминание оператору о завтрашнем удалении заблокированного отмечается только после того, как письмо ушло. Если отправка не удалась, следующий обход повторит его.
  - Письма о комментариях отправляются каждому адресату отдельно: адрес, который отклонил почтовый сервер, не мешает остальным. В журнал пишется, сколько писем ушло и сколько нет.
  - Повтор загрузки агентом, подключённым к папке, с адресом источника больше не получает ложный 409.
  - Все инструменты MCP отвечают на сбой структурированной ошибкой `{code, message}`. На неожиданный сбой приходит `internal` с текстом по-русски, без текста базы данных, а в журнал пишется `mcp.tool.failed`.
  - Комментарии на странице работы загружаются для всех ссылок разом: один набор блокировок и несколько запросов вместо примерно десяти на ссылку. Если комментариев больше 5000 или групп реакций больше 1000, показываются самые новые комментарии и флаг `truncated`. Раньше без предупреждения отбрасывались самые новые.
  - Куратор, которого убрали с полки отдела, больше не считается её стороной в обсуждениях своих ссылок и не получает писем о них.
  - Срок загрузки и срок ссылки сверяются по часам базы. Оставшиеся дни ссылки при переносе на новую версию округляются вниз.
  - Сообщение сборщика, у которого тип не `result`, считается сбоем сборки, а не пустым результатом.
  - Страницы входа по ссылке считают гостем только при ответе 401 или 403. Прочие сбои становятся ошибкой запроса, а не входом в другую полку.
  - Журнал: ошибки работы после коммита пишутся с name, code и message; у ошибки 500 указан маршрут, id запроса, тип ошибки и первая строка стека. `unhandledRejection` записывается в журнал, процесс продолжает работу; после `uncaughtException` процесс записывает ошибку и завершается.
  - Миграция 049 добавляет индексы `shares(revision_id)` и `artifacts(tenant_id, folder_id)` для работ не в корзине. `npm run db:migrate` ждёт чужую блокировку не дольше 5 с (`lock_timeout`).
  - Тесты ждут фоновые письма через `settled()` вместо паузы в 300 мс. Пределы времени в тестах скорости увеличены в десять раз. При сбое подготовки `npm test` печатает настоящую причину.

- **Документация по безопасности приведена к коду.** Объединение временной полки требует сильной сессии: из слабой (открытой ссылкой от агента) можно только перейти в свою полку ([SIGN_IN_PROVIDERS](docs/specs/SIGN_IN_PROVIDERS.md)). В просмотрщике проектов `nav.js` с защитой от WebRTC стоит в самом начале каждой страницы, страницы проекта получают `VIEWER_GUARD` ([PROJECTS](docs/specs/PROJECTS.md)).
- **Нестабильные тесты.** `tests/comment-overlay-dom.test.ts` ждёт событий Chrome (загрузка страницы, контекст выполнения фрейма) вместо фиксированных пауз и находит фрейм и в отдельном процессе; падал примерно в каждом третьем прогоне. Импорт по ссылке не начинает DNS-запрос, если импорт уже отменён, и не оставляет запрос без обработчика (`unhandled ENOTFOUND` в `tests/url-import-fetch.test.ts`). Live-набор берёт для viewer свободный порт (или `TEST_VIEWER_PORT`), а не 4391, занятый запущенным `npm run dev`.
- **Поправка к 0.3.0: право `sign_in` по умолчанию включено.** В записи 0.3.0 сказано, что `sign_in` («Давать ссылку для входа») «по умолчанию выключено». Это не так: с 25.09.2026 (решение владельца) на странице согласия OAuth по умолчанию отмечены все права, включая `share`, `manage` и `sign_in` (`OAUTH_DEFAULT_SCOPES` в `apps/server/oauth.ts`); любое можно снять. Существующим подключениям `sign_in` сам не добавляется. Запись 0.3.0 оставлена как была.

- Неизвестный адрес (`/nope`, `/works/abc`) открывает страницу «Страница не найдена» с кодом 404 вместо JSON; `/api/…`, `/mcp`, `/oauth/…` и `/.well-known/…` по-прежнему отвечают JSON. Список страниц один для сервера и клиента ([packages/contracts/app-routes.ts](packages/contracts/app-routes.ts)). Несуществующая работа показывает «Работа не найдена» с кнопкой «На полку».
- Ошибки сети везде по-русски («Нет связи с Полкой»), а не «Failed to fetch».
- Бледно-серый текст (`--muted-2`) проходит AA по контрасту. Шапка полки не прыгает при загрузке. «В корзину» из меню карточки спрашивает подтверждение на месте. Поиск без сети помечает старые результаты.
- Мелочи: «Поделился пользователь Полки», ссылка по умолчанию на 7 дней и в диалоге, и на «Сохранить», карточка регистрации у получателя открывается только по нажатию, на `127.0.0.1` не предлагается подключать claude.ai и ChatGPT, «Перейти к содержимому» для клавиатуры, сессия по ссылке от агента на `/claim` видит только «Перейти в ту полку».

### Removed

- **Снимки дизайна и протоколы ревью** (`docs/design/**`, `docs/reviews/**`, около 50 МБ) удалены из репозитория; они остаются в истории git до коммита c9efc10. Оставлены протоколы, на которые ссылаются манифесты «Ленты» (`docs/reviews/2026-09-2*-editorial-*`); исходники карточек превью ссылок переехали в `docs/assets/og/`.

## [0.3.0] — 2026-09-26

### Added

- **Обложки на полке** ([SHELF_COVERS](docs/specs/SHELF_COVERS.md); миграция 040). Карточка больше не показывает уменьшенную страницу в iframe.
  - Отчёты, заметки и документы получают текстовую обложку: собственный заголовок документа, начало лида, метку вида и цвет страницы. Дашборды, приложения и изображения получают снимок первого экрана, который рендерер рисует один раз на версию без доступа к сети (`COVER_SNAPSHOTS_ENABLED`, по умолчанию выключено).
  - Под обложкой: название (не больше двух строк), одна строка «вид · версия · дата» со значком доступа и меню «…». Вся карточка открывает работу; «Открыть ↗» ушло в меню (для ссылок — «Открыть оригинал»).
  - Работы с общим началом названия («Y360 Radar · …») получают значок серии. В списке появились миниатюры.
  - Для уже сохранённых работ: `npm run covers:backfill -- --dry-run`.
- **«Войти через Google»** (миграция 039, [SIGN_IN_PROVIDERS](docs/specs/SIGN_IN_PROVIDERS.md), «Google»; настройка — [google-oauth-setup](docs/ops/google-oauth-setup.md)).
  - Обычный OpenID Connect с accounts.google.com: discovery, PKCE S256, state, nonce, scopes только `openid email profile`, выбор аккаунта при каждом входе. `id_token` проверяется полностью: подпись по JWKS Google, `iss`, `aud`, `azp`, `exp`, `nonce`. Почта учитывается только при `email_verified: true`. Домен Google Workspace (`hd`) сохраняется в `account_identities.hosted_domain`, если он есть; аватар не хранится.
  - `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`: без них кнопки нет. `GOOGLE_SIGNUP=on` (по умолчанию) — Google открывает полки, как Яндекс ID. `link-only` (по умолчанию в `deploy/hosted`) — Google только входит в полку, к которой владелец привязал его в «Способах входа». Он не открывает новую полку, не привязывается по почте и не закрепляет временную полку (ч. 10 ст. 8 149-ФЗ).
  - Кнопка по правилам Google Identity: светлая тема, стандартный «G», текст «Войти через Google». Привязка и отвязка в «Способах входа», последний способ входа отвязать нельзя. Удаление аккаунта стирает привязку Google вместе с остальными.
  - В политике обработки данных — пункт «Вход через Google» (редакция от 25 сентября 2026).
- **Агент раскладывает полку по папкам** (миграция 038, [connect-agents](docs/connect-agents.md#инструменты-mcp)).
  - Скилл `polka-organize` (`skills/polka-organize/SKILL.md`, `/.well-known/agent-skills/polka-organize/SKILL.md`, в индексе Agent Skills): на «разложи полку», «наведи порядок в папках», «структурируй работы» агент читает всю полку, предлагает 3–8 папок по проектам и темам (серии — в одну папку), показывает план таблицей и только после согласия создаёт папки и переносит работы. Папки владельца остаются, работы не удаляются и не переименовываются. Скилл `polka` теперь сохраняет новую работу в подходящую папку.
  - Инструменты MCP (scope `manage`, с ключом идемпотентности и записью в журнал): `polka_create_folder`, `polka_rename_folder`, `polka_delete_folder` (только пустую папку; работы в корзине при этом остаются «без папки»), `polka_move` — до 100 работ в папку или «без папки» одной транзакцией, всё или ничего, без изменения порядка полки.
  - `polka_list` (и `GET /api/v1/status/{id}`) отдаёт вид работы (`kind`), название папки, дату создания; `polka_list` — до 100 работ за вызов; `polka_list_folders` — число работ в папке.
  - У каждого инструмента MCP есть `title` и подсказка `readOnlyHint` или `destructiveHint`; тест следит за этим.
- **Плагин Полки для Claude Code и Codex** ([connect-agents](docs/connect-agents.md#плагин-полки-для-claude-code-и-codex)). Репозиторий — маркетплейс `polka` с одним плагином `polka`: `.claude-plugin/{marketplace,plugin}.json` для Claude Code, `.agents/plugins/marketplace.json` и `.codex-plugin/plugin.json` для Codex, общий `.mcp.json` (удалённый HTTP-сервер `https://polochka.app/mcp`, вход OAuth при первом подключении) и все скиллы из `skills/*` — каталог, а не список, так что новый скилл приходит с обновлением плагина. Установка одной командой:
  - Claude Code: `claude plugin marketplace add artkruglov/polka && claude plugin install polka@polka`, затем `/mcp` → `plugin:polka:polka` → Authenticate;
  - Codex: `codex plugin marketplace add artkruglov/polka && codex plugin add polka@polka`, затем `codex mcp login polka`.

  Проверено установкой из локальной копии в пустые профили Claude Code 2.1.282 и Codex 0.153.4. `/connect`, страница «Агенты», подсказки на `/start` и лендинг дают эти команды; `claude mcp add` и `codex mcp add` остались вариантом без плагина.

- **Одна полка на человека** ([SIGN_IN_PROVIDERS](docs/specs/SIGN_IN_PROVIDERS.md), § 1, 8–10; миграция 036).
  - **«Похоже, у вас уже есть полка».** Браузер после входа помнит только имя полки, способ и время входа — без id и почты, в `localStorage`. Если Яндекс ID, VK ID или код на новый адрес откроют новую полку, Полка сначала спрашивает на `/signup/choose`: войти в существующую (новый способ входа привяжется к ней) или создать новую. Данные поставщика 10 минут хранятся в памяти сервера и в URL не попадают.
  - **Страница подключения агента** показывает, куда пойдут работы («полка «…» (почта, Яндекс ID)»), и позволяет выйти и войти в другую.
  - **Без регистрации.** «Начать без регистрации» открывает временную полку этого браузера: сессия 30 дней, продлевается при посещении. Второй агент попадает в ту же полку. Ссылки и публикации — только после закрепления через Яндекс ID, VK ID или почту на российском домене (ч. 10 ст. 8 149-ФЗ). До того агенты и HTTP API получают `claimUrl`, а в интерфейсе вместо ссылки — «Закрепить, чтобы поделиться». Закрепление привязывает способ входа к этой же полке. Если способ уже открывает другую полку — «Объединить». Простаивающую 30 дней полку удаляет обслуживание.
  - **`npm run account:merge -- --from … --into … [--dry-run]`** — объединение двух полок оператором:
    - работы, версии, объекты хранилища, папки, ссылки (старые адреса открываются), обсуждения, подключения агентов с ключами продления (продолжают работать), привязки, библиотеки, заметки, статистика;
    - источник удаляется, в журнал пишется `account.merged` с номером обращения (`--proof`).
  - **«Открой мою Полку»** (`polka_open_shelf`, `POST /api/v1/sign-in-link`):
    - закреплённой полке агент даёт страницу входа без секрета;
    - одноразовую ссылку `…/enter#…` он даёт только временной полке и только с отдельным правом `sign_in` («Давать ссылку для входа», по умолчанию выключено; существующим подключениям не включено). Ссылка работает 5 минут, один раз, после клика, и никогда молча не заменяет текущий вход;
    - сессия по такой ссылке слабая: она не может закреплять и объединять полки, менять способы входа, управлять агентами и удалять аккаунт.
  - Объединение оператором требует `--proof <номер обращения>`, а источник удаляется. При закреплении каждый переезжающий агент отмечается вручную. Комментарии, реакции, библиотеки и приглашения тоже требуют закреплённой полки. У временной полки квота 20 МБ, одна полка на запрос подключения и свой суточный бюджет (20 с адреса, 2000 на установку), отдельный от регистраций. Слабая сессия живёт сутки, не выдаёт ссылок и удаляется при закреплении полки.
- **Импорт по ссылке: одна таблица источников** (`packages/contracts/link-providers.ts`). Её читают сервер, веб и рендерер. Маршруты: `server-api`, `server-fetch`, `server-try`, `server-render`, `extension`, `html` ([URL_IMPORT_SUPPORT](docs/specs/URL_IMPORT_SUPPORT.md)).
- **GitHub Gist** через официальный API (`api.github.com/gists/<id>`): HTML — страница, несколько файлов — пакет, код — статичная страница. Лимит GitHub сообщается как `rate_limited`, без повтора; `GITHUB_TOKEN` необязателен.
- **Рендерер** `apps/renderer`: отдельный сервис на образе Playwright, закреплённом по digest. `POST /render` делает снимок DOM SPA-сайтов (Lovable, bolt.host, Replit, GitHub Pages, Gemini share) и одну попытку для артефакта Claude. `POST /fetch` — один GET без браузера для общих ссылок ChatGPT. Egress только через прокси с закреплённым DNS, без приватных адресов и metadata. HMAC-подпись с окном 60 с. robots.txt для `PolkaRenderer` читает сам рендерер; проверки на бота → `source_blocked`, без повторов. Развёртывание: профиль compose, Yandex Cloud или Fly.io (`deploy/renderer`). Включается `RENDERED_IMPORT_ENABLED` (по умолчанию выключен), состояние задания `rendering` — миграция 037.
- **ChatGPT share и canvas** разбираются из данных React Router: canvas становится страницей или компонентом, HTML/React/SVG из последнего ответа — работой, иначе сохраняется сам разговор. Сервер Полки к chatgpt.com не обращается.
- **«Сохранить как ссылку»** ([SAVED_LINKS](docs/specs/SAVED_LINKS.md)): работа-закладка с адресом, названием и заметкой, без миграции. `POST /api/links`, MCP `polka_save_link`. На полке, на странице работы и у получателя — карточка со значком сервиса и кнопкой «Открыть ↗». Фильтр содержимого читает адрес (список доменов) и текст.
- **Страница `/bot`** для владельцев сайтов: что делает PolkaRenderer, что он работает только по действию пользователя и как запретить его в robots.txt.

### Changed

- **Исследования с сотнями источников не попадают на проверку.** Фильтр задержал страницу доверенного автора с 287 ссылками на 122 сайта: `9to5google.com` считался «похожим на Google» (цифра перед брендом больше не граница слова; хвостовая, как в `google1-login.ru`, — граница), справочные страницы с `/support/`, `/security/oauth`, `/id/` — «страницей входа» (теперь только `login`, `signin`, `signon` в пути). Правило «у доверенного автора находки одних правил, которые модель не подтвердила, — уведомление, а не задержка» распространено с мошенничества на спам. Блог Google (`googleblog.com`, `blog.google`) и `google.dev` — официальные домены.
- **Фишинг — только когда страница уводит читателя** ([CONTENT_FILTER, «Фишинг»](docs/specs/CONTENT_FILTER.md#фишинг); [ABUSE_PROTECTION, п. 6](docs/specs/ABUSE_PROTECTION.md#6-признаки-фишинга); без миграции).
  - Экран входа с паролем, названия брендов (Яндекс, Apple, Telegram) и «подтвердите» больше не задерживают ссылку: на Полке введённое никуда не уходит (CSP `connect-src 'none'`, `form-action 'none'`, песочница, `/away`). Так задерживались прототипы B2B-продуктов, например прототип «Алиса AI в Яндекс 360» 25–26.09. Поля по-прежнему отмечаются, и получатель видит «Страница … просит пароль, код или данные карты. Не вводите их здесь.».
  - Нужен канал вне страницы. Это просьба передать код, пароль или данные карты («продиктуйте код», «отправьте код из SMS в Telegram @…»), перевести деньги на карту или кошелёк, или похожий домен бренда (`yandex-360-login.ru`, `apple-id-verify.com`, IDN, цифры вместо букв). Telegram, телефон, почта и ссылка на чужую страницу входа только добавляют к счёту. Бренд без канала весит ноль.
  - Фишинг, найденный только правилами у доверенного автора, — письмо оператору, ссылка работает, если модель прочитала работу и не назвала её фишингом (или ещё прочитает: её ответ решает ссылку заново). Подтверждённый моделью фишинг задерживается, как раньше.
  - Одобрение оператора переходит на следующие версии работы: версия с теми же или меньшими признаками не задерживается снова, новый признак задерживает (`revisions.content_filter.approved`, `details.approvedSignals` у `share.approved`).
  - `moderation.ts recheck --fraud [--dry-run]` перечитывает версии задержанных за фишинг ссылок по новым правилам и открывает те, что больше не подходят (журнал: `revision.rescanned`, `share.released`).
- **Все разрешения агента отмечены по умолчанию.** На странице согласия («Что сможет приложение») сразу стоят все галочки, включая «Управлять названиями, папками и корзиной» и «Давать ссылку для входа»: агент сразу умеет всё, владелец может снять любую. Действия за ними обратимы (корзина, удаление только пустых папок, новые версии не стирают старые); ссылка для входа открывает только временную полку. Уже выданные подключения не меняются — чтобы добавить разрешения, подключите агента заново.
- **Главная полка начинается с агента.** Вместо поля «Вставьте ссылку на артефакт» — один блок «Подключите агента — он сам сохранит работу на полку» с переключателем Claude (claude.ai и Desktop) / Claude Code / Codex / Другой MCP-клиент и одним шагом для каждого: адрес коннектора с кнопкой копирования, команда плагина или адрес MCP и `npx skills add artkruglov/polka`. Полная версия — «Агенты» (`/settings/agents?client=…`), «Загрузить файл» рядом. Когда агент подключён (или блок скрыт), остаётся одна строка: «Подключено: …», фраза «Сохрани это на Полку», «Агенты», «Загрузить файл». Чек-лист первых шагов на полке больше не показывается, он остался на `/start` (`features/agent-hero`).
- **«Сохранить» — файл или код.** Страница `/bring` называется «Сохранить работу» и предлагает загрузку файла и «Вставить код»; `?url=` из старых ссылок игнорируется.

- **Предупреждение получателю — только когда есть повод** ([CONTENT_FILTER, «Поля для секретов»](docs/specs/CONTENT_FILTER.md#поля-для-секретов)). При сохранении Полка отмечает, есть ли на странице поля для пароля, карты, одноразового кода или seed-фразы: в разметке и в скриптах, которые строят форму (`content_filter.sensitiveInput`, без миграции). Отчёт или дашборд без таких полей подписан тихо: «Опубликовал пользователь Полки». Страница с такими полями и версия, сохранённая раньше, показывают предупреждение без неточного «не проверена». В «Подробнее» сказано, проверила ли Полка текст и код автоматически. Старые версии размечает `npm run moderation:backfill-sensitive-input -- [--dry-run]`.

- **Страница работы — режим чтения.** Над работой одна тонкая полоса: «←», название, «v1 ▾», вкладки «Работа · Версии», ⓘ с подробностями (вид, доступ, версия, файл, пояснение о просмотре), «На весь экран», «Поделиться» и «…» со всеми остальными действиями. Работа занимает весь оставшийся экран и прокручивается внутри; на 1440×900 она начинается в 52 px от верха вместо ~450. Вкладка сохраняется в адресе (`?tab=versions`). Боковое меню на этой странице свёрнуто до иконок, переключатель запоминается в браузере. На телефоне — одна полоса и строка вкладок ([FRONTEND_COMPONENT_SYSTEM](docs/FRONTEND_COMPONENT_SYSTEM.md)).
- **Карточка ссылки AI-сервиса.** Способы идут по порядку: «Попросить Claude» (или фраза для своего агента), «Сохранить как ссылку», файл прямо в карточке. Расширение перенесено за ссылку «Сохранять в один клик». Неудавшийся импорт предлагает сохранить ссылку.

- **«На Полку» на странице `claude.ai/artifact/<id>`.** У неё нет Copy и вкладки Code, поэтому исходник берётся через меню названия → Export → Download: на время одного сохранения в мире страницы перехватываются `URL.createObjectURL`, клики и `dispatchEvent` по `a[download]` и `window.open`, Blob читается, файл в «Загрузки» не попадает (скачивание с сервера тоже останавливается — тогда берётся фрейм), всё восстанавливается, меню закрывается Escape. Меню — Base UI: открывается фокусом + ArrowDown (синтетические клики и Enter его не открывают), подменю — ArrowRight, пункты ищутся по `data-title-menu`/`data-download-submenu`/`data-download-item`, затем по тексту; закрывается Escape на пункте в фокусе, иначе на кнопке, иначе нажатием вне меню. «Copy as Markdown» не нажимается. Фрейм — `*.frame.claudeusercontent.com`, скрытый фрейм 1×1 пропускается; кнопка «На Полку» стоит рядом с Share. Фикстура `claude-artifact-page.html` повторяет эту структуру.

### Removed

- **Импорт по ссылке из веб-интерфейса** ([FAQ](docs/faq.md#почему-нельзя-вставить-ссылку-на-артефакт-claude-или-chatgpt)). Ссылки на артефакты Claude закрыты Cloudflare, а серверная загрузка на polochka.app выключена, поэтому сохранять работу теперь предлагается только агентом или файлом. Убраны поле ссылки на полке, на лендинге и на `/start`, карточка ссылки AI-сервиса на «Сохранить» (`features/import-url` удалён целиком), «Сохранить по ссылке» на пустой полке, подсказки «Сохранять в один клик» про расширение и закладку. `/bookmarklet` и `/bring/receive` работают по прямому адресу, но ни откуда не связаны. Сервер (`apps/server/url-import`), рендерер и MCP `polka_import_url` / `polka_save_link` не менялись; [URL_IMPORT_SUPPORT](docs/specs/URL_IMPORT_SUPPORT.md) помечен как эксперимент.

### Added

- **Закладка «На Полку»** — альтернатива расширению для тех, кто не ставит расширения: обычная закладка с `javascript:`-адресом, страница установки `/bookmarklet` (кнопка для перетаскивания, код для телефона, исходник на GitHub). По нажатию на странице Claude, ChatGPT или другого AI-чата из списка читает артефакт теми же модулями, что и расширение (Export → Download, кнопка Copy, вкладка Code; на других чатах — снимок страницы без скриптов), открывает вкладку `/bring/receive#nonce=…` и передаёт данные только ей через `postMessage` (протокол — `packages/contracts/bookmarklet.ts`). Вкладка принимает одно сообщение: от `window.opener`, с https-адреса AI-чата из списка, с её nonce, до 5 МБ; показывает карточку с названием, источником и размером, гостю — вход с возвратом (данные ждут в `sessionStorage` этой вкладки), сохраняет обычной загрузкой файла и открывает работу. Адрес страницы попадает в provenance: у загрузки HTML появилось необязательное поле `sourceUrl` (https, без query и фрагмента) → `provenance.kind = "url"`. Сборка — `extensions/bookmarklet` (esbuild, ~24 КБ), адрес Полки подставляет сама страница установки. Компонент `BookmarkletHint` для карточки ссылок Claude/ChatGPT.
- **Расширение «На Полку» для Chrome** (`extensions/chrome`, Manifest V3, `npm run ext:build` → `dist/` и zip). Сохраняет артефакт Claude (и, экспериментально, код из ChatGPT) из браузера пользователя: значок на панели, кнопка «На Полку» рядом с артефактом, а ссылка `claude.ai/artifact/…`, вставленная в «Сохранить», передаётся расширению через `postMessage` с nonce (только то же окно и origin Полки; протокол — `packages/contracts/extension-bridge.ts`), оно открывает её в фоновой вкладке и возвращает ссылку. Без расширения карточка Claude/ChatGPT показывает, как его установить. Код берётся по нажатию: кнопка Copy артефакта с перехватом `navigator.clipboard` в мире страницы, документ во фрейме `*.claudeusercontent.com` без рантайма просмотрщика, вкладка Code. Вход — OAuth 2.1 с PKCE через `chrome.identity.launchWebAuthFlow` и DCR; сохранение — `POST /api/v1/publish`. Аналитики нет, данные уходят только на Полку. В магазин не опубликовано.
- **Сервер для расширений браузера.** Экран согласия называет клиента с redirect `https://<id>.chromiumapp.org/…` «расширением браузера» с его ID, а ID из новой переменной `BROWSER_EXTENSION_IDS` — «Расширение браузера «На Полку»»; имя «На Полку» у остальных помечается «(имя не подтверждено)». `POST /api/v1/publish` принимает `Origin: chrome-extension://<id>`. В метриках клиент такого подключения — `browser-extension`.

### Changed

- **Страница «Агенты» — для нетехнического человека.** Один вопрос «Где вы работаете с ИИ?» с карточками ChatGPT, Claude.ai, Claude Code, Codex и «Другое» (Codex подписан как «агент OpenAI в терминале, не сайт ChatGPT»); выбор открывает одну панель пронумерованных шагов с кнопками «Скопировать адрес / фразу / команду» — для ChatGPT и Claude.ai это ручное добавление коннектора (`…/mcp`, OAuth), для Codex и Claude Code сначала фраза «Подключи Полку: …/connect», команда — «или выполните сами». Выбор запоминается в `localStorage`, deep link `/settings/agents?client=chatgpt|claude-ai|claude-code|codex|other`. Сверху — живой статус: «Подключено: <клиент> · последний раз …» или «Пока ничего не подключено»; пока панель шагов открыта и вкладка видна, список опрашивается раз в 5 с, и новое подключение показывает «Готово! Попросите агента: «Сохрани это на Полку»». Строка аккаунта «Вы вошли как … через Яндекс ID / почту / по логину», а для полки младше суток — подсказка, как объединить её со старой. Токены, HTTP API и CLI свёрнуты в «Для разработчиков» с объяснением, кому нужен токен (ChatGPT, Claude, Codex и Claude Code — не нужен). Список «Подключения» описан словами: вид, состояние, разрешения, даты, «Отозвать». Главная, `/connect`, `llms.txt` и скилл говорят прямо: ChatGPT и Claude.ai в браузере коннектор добавляют вручную, агент должен пересказать шаги, а не пытаться что-то загрузить.

## [0.2.0] — 2026-09-24

### Added

- **Открытый код — на первом экране.** Кнопка «GitHub» с логотипом в навигации (гостям — на каждой странице, всем — на главной); число звёзд показывается от 10. Число получает сервер: `GET /api/source/stars` раз в час спрашивает `api.github.com` о репозитории из `SOURCE_URL` (только `https://github.com/<owner>/<repo>`; иначе и при любой ошибке — `{"stars":null}`), браузер к GitHub не обращается (CSP `connect-src 'self'`). Главная: кнопки «Подключить агента» и «Развернуть у себя» (инструкция для одной VM на GitHub), строка «Открытый код · AGPL-3.0 · ★ N на GitHub», ссылка «Для компаний» и внизу раздел «Разверните у себя в компании» — три шага, команды первого запуска с копированием, «Инструкция на GitHub» и «Нужна помощь» (→ `/enterprise`). `scripts/render-readme-assets.mjs shot`: `SHOT_WIDTH` и `SHOT_JS`.
- **Метрики продукта без сторонней аналитики.** Сервер сам пишет обезличенные события в PostgreSQL (миграция 034: `analytics_events`, `analytics_daily`, `analytics_active_days`, `analytics_optouts`): `page_view` публичных страниц (путь, очищенный `?ref=`, домен реферера; боты, предпросмотры и вошедшие не считаются), `signup_completed` со способом и источником, `agent_connected` с типом клиента, `work_saved` и `share_created` (`via`, `first`), `share_opened` (раз в сутки на ссылку, без получателя), `note_added`, `enterprise_request` и дни активности для удержания. Без cookies, IP-адресов и сторонних скриптов; аккаунт хранится только как HMAC. Сырые события — 13 месяцев, счётчики по дням остаются. Отчёт оператора: страница `/ops/metrics` и `GET /api/ops/metrics` по `OPS_STATUS_TOKEN` (воронка по неделям регистрации с конверсиями, источники → регистрации, клиенты агентов, удержание D1/D7/D30, активность), `npm run metrics`. Возражение — `npm run metrics -- forget <аккаунт>`; при удалении аккаунта события удаляются. В политике строка «нет аналитики» заменена целью «Статистика использования» (законный интерес, п. 7 ч. 1 ст. 6 152-ФЗ).
- **Два исследования в «Ленте»** (тема «Исследования»): «Почерк и грамотность младшеклассника: что работает по исследованиям» и «Как устроен редактор презентаций изнутри». Из исследований владельца с Claude; источники перечислены в каждом материале и проверены по реестрам Минюста и списку ФСБ. Статичные снимки — в `static-candidates.json`.
- **`editorial-seed-hosted.ts --withdraw <slugs>`** снимает публикации редакционного аккаунта по slug и отзывает их share одной транзакцией, ничего не публикуя; `absent` при повторе, `blocked` для чужого slug.

### Fixed

- **Вход по ссылке от агента после закрепления или отзыва.** Вход читал ссылку как живую, затем ждал блокировку полки; если за это время полку закрепили (закрепление удаляет слабые сессии) или отозвали подключение агента, вход всё равно выдавал слабую сессию по устаревшему решению. Теперь ссылка и подключение перечитываются под блокировкой полки, и такой вход получает «Ссылка устарела».
- **Обслуживание оставляло временную полку недоудалённой.** Без конвейера удаления (`ACCOUNT_DELETION_ENABLED=false`) обслуживание закрывало простаивающую временную полку и затем удаляло её объекты и строки; сбой S3, конец времени прогона или остановка между этими шагами оставляли полку закрытой с объектами навсегда. Теперь закрытие записывает задание (`provisional_erasures`, миграция 076), и следующий прогон доводит удаление до конца; рецепты прав обновлены.
- **Импорт полки и снимок на дату.** Импорт ставил работе время ухода в корзину, но не писал это событие в журнал; после восстановления работы на новой установке снимок полки за время между исходным удалением в корзину и восстановлением показывал её на полке. Теперь уход в корзину пишется в журнал с исходным временем, как отметка принятия.
- **Изображения новых аккаунтов больше не ждут человека.** При `SHARE_MODERATION=auto` ссылка на версию с изображениями от аккаунта моложе `NEW_ACCOUNT_DAYS` ждала (`image-unchecked`), пока модель не посмотрит, — и после чистого ответа модели так и оставалась на проверке. Теперь ответ модели решает такие ссылки заново: ничего не нашлось — ссылка открывается (`share.released` в журнале, без письма), нашлось — причина меняется на найденное или версия блокируется. Так же при `auto` открываются ссылки, задержанные раньше в режиме `new-accounts`. Другие причины задержки модель не снимает; без модели изображения ждут, как раньше. Уже задержанные ссылки разбирает `moderation.ts recheck [--dry-run]`.
- **Диагностика входа через поставщика.** Каждый неудачный возврат пишет в лог `{"event":"idp.callback_failed","provider","code"}` — только коды. `/login` ведёт на `/signup` (безопасный `next` сохраняется) вместо JSON 404.

### Changed

- **«Интересное» теперь называется «Лента».** Пункт меню, заголовок каталога, подзаголовок «Исследования, разборы и инструменты, сделанные с Claude», тексты публикации и документация. Адрес `/discover` прежний. План развития раздела — [DISCOVER_V2](docs/specs/DISCOVER_V2.md).
- **Проверка кода моделью без ложных тревог** (`PROMPTS_VERSION` 2026-09-24.3): промпт перечисляет обычное поведение страниц, которое не нарушение (формы обратной связи, запросы к публичным API, экспорт по кнопке, Workers), а «наружу» считается вредом, только если скрытно уходит введённое посетителем или данные устройства. Ревьюер кода на polochka.app — qwen3.6: на 55 честных страницах ноль ложных срабатываний (gpt-oss-120b — 4).
- **Промпт модели модерации v2** (`PROMPTS_VERSION` 2026-09-24.2): словарь жаргона и «групп смерти», правило «побеждает самая тяжёлая категория», граница мошенничества и спама. На синтетическом наборе qwen3.6 — ноль пропусков и ложных срабатываний (оценка оптимистична: промпт уточняли на этом наборе).

### Removed

- **Три истории «Как сделана Полка» больше не в «Ленте»** (`why-no-artifact-link`, `one-regex-froze-server`, `31-sandbox-escapes`): убраны из `candidates.json`, исходники остались в `content/editorial`.

## [0.1.0] — 2026-09-24

### Added

- **Вход через Яндекс ID, VK ID и свой OIDC** ([SIGN_IN_PROVIDERS](docs/specs/SIGN_IN_PROVIDERS.md); миграция 032). Требование ч. 10 ст. 8 149-ФЗ: пользователей из России авторизует российская система.
  - **Как устроен вход.** Полка — клиент OAuth 2.0 / OIDC: код обменивается на сервере, PKCE S256 всегда, state и (для OIDC) nonce лежат в зашифрованной cookie `polka_idp`. Redirect URI фиксирован: `<APP_ORIGIN>/api/auth/idp/<provider>/callback`. У OIDC проверяются подпись `id_token` (RS256/PS256/ES256 по JWKS), `iss`, `aud`, срок и nonce. Токены поставщика не хранятся и не пишутся в журналы.
  - **Аккаунты.** Аккаунт находится по `(provider, subject)`. Подтверждённая поставщиком почта открывает существующую полку с этим адресом, неподтверждённая — никогда. Привязать и отвязать поставщика можно в «Способах входа» (`/settings/agents`). Удаление аккаунта сразу стирает привязки: это делает триггер, функция purge не переопределяется.
  - **Настройки:**
    - `YANDEX_CLIENT_ID`/`YANDEX_CLIENT_SECRET`, `VK_CLIENT_ID`;
    - `OIDC_DISCOVERY_URL`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, `OIDC_ALLOWED_DOMAINS`, `OIDC_ORG_CLAIM`/`OIDC_ORG_VALUE`.
  - **Кнопки** «Войти с Яндекс ID» и «Войти с VK ID» — на `/signup` и на странице входа. Подключение агента (`/oauth/consent`) продолжается после входа через поставщика.
- **Доступ компании.** `ORG_DOMAINS=company.ru=<библиотека>[:reader|curator]` и `OIDC_ORG_LIBRARY`: при входе через поставщика с подтверждённой почтой домена человек становится участником библиотеки шаблонов. Публичные почтовые домены запрещены, роль admin не выдаётся, исключённый участник не возвращается. В журнале это событие `template_library.domain_joined`.
- **Заметки владельца: `COMMENTS_MODE=on|owner-notes|off`.**
  - **`owner-notes`** (по умолчанию в `deploy/hosted`):
    - к работе пишет только владелец, сам или через агента (новый инструмент `polka_note`);
    - получатели ссылки читают заметки только для чтения;
    - реакций и писем нет;
    - комментарии получателей, оставленные раньше, скрыты, но не удалены.
  - **`off`** убирает обсуждения и оверлей.
  - Режим видят интерфейс («Заметки», «Заметки автора»), `/api/capabilities`, `/llms.txt`, описание MCP и скилл.

- **Страница «Полка для компаний»** (`/enterprise`) с формой заявки. Что получает компания (данные под контролем, библиотеки шаблонов, агенты сотрудников через MCP/OAuth и HTTP API, модерация, открытый код и коммерческая лицензия; вход через SSO и доступ по домену, автоматическая проверка содержимого помечены «в разработке»), варианты развёртывания, 7 вопросов и форма: имя, компания, рабочая почта, размер команды, что интересует, комментарий до 2000 символов, подтверждение «прочитал Политику». `POST /api/enterprise-requests`: zod, проверка Origin, 5 заявок в час с IP (в базе только хеш ключа счётчика) и 200 в сутки всего, honeypot, идемпотентность по ключу формы. Заявка хранится в `enterprise_requests` (миграция 033) и уходит письмом на `OPERATOR_EMAIL` — только текст, `Reply-To` — почта заявителя; через год служебная очистка её удаляет. Ссылки «Для компаний» в подвале и на главной ведут на `/enterprise`, со страницы вариантов (`/pricing`) — кнопка «Оставить заявку» у коммерческой лицензии и блок «Полка для команды». В политике — цель «Заявки компаний» (основание — п. 5 ч. 1 ст. 6 152-ФЗ, действия до заключения договора по запросу человека; срок — год).
- **Фильтр запрещённого содержимого и автоматическая модерация** ([CONTENT_FILTER](docs/specs/CONTENT_FILTER.md); миграция 031).
  - **Правила.** 14 категорий по закону РФ (CSAM, экстремизм и терроризм, наркотики, оружие и взрывчатка, списки персональных данных, порнография, суицид, азартные игры, пиратство, VPN, запрещённые домены, фишинг, спам, вредоносный код). Списки с весами, основами, фразами и парами «слово + намерение» в `apps/server/content-filter/lists` (как обновлять — `lists/README.md`). Нормализация: NFKC, ё/й, невидимые символы, латиница вместо кириллицы, буквы через пробел. Читаются текст, атрибуты, meta, строки скриптов, ссылки и CSS в том же ограниченном проходе, что и классификатор; тесты на честных медицинских, исторических, новостных и профилактических текстах и на линейное время.
  - **Модели** Yandex AI Studio после сохранения, не задерживая его: основная (текст и изображения), второе мнение другого семейства, ревьюер кода. Скрывает только согласие двух моделей; одна модель — письмо оператору; CSAM модели — скрыть до проверки. Имена моделей, параметры и цены — настройки; промпты — `content-filter/prompts.ts`. Бюджет в сутки, повтор непроверенного.
  - **Действия:** `CONTENT_FILTER_MODE` (`balanced`/`strict`), `CONTENT_FILTER_AUTOBLOCK`, `SHARE_MODERATION=auto` — никакого ручного одобрения авторов, доверие автоматическое. Ссылка `blocked`: получатель видит «Ссылка недоступна», владелец — «Заблокировано модератором» и адрес для обжалования. Изоляция и удаление всех версий объектов по сроку категории (`MODERATION_RETENTION`), напоминание за сутки, legal hold, «передано в полицию». Стоп-лист SHA-256: заблокированный файл нельзя сохранить снова.
  - **Журнал модерации** `moderation_events`: кто, что, когда, почему, sha256 и размеры, без содержимого; для runtime-роли только добавление, хранится 3 года.
  - **Оператору:** кнопка «Заблокировать» в письме (с legal hold), письмо о CSAM без содержимого, скрипты `moderation:takedown` (квитанция), `block`, `unblock`, `legal-hold`, `handed-over`, `purge-artifact`, `events`, `sweep`, `editorial:content-scan`; порядок на 24 часа — в [hosted](deploy/hosted/README.md). Чек-лист каталога — [EDITORIAL_CHECKLIST](docs/EDITORIAL_CHECKLIST.md).
  - **Антиспам:** одноразовые почты при регистрации, лимиты новых полок на сеть и домен, одинаковое содержимое с разных аккаунтов, ссылочные фермы и скрытый текст, спам в комментариях, лимит ссылок нового аккаунта в сутки, «теневая» проверка.
  - **Жалобы** «сексуальное с участием детей», «интимное без согласия», «угроза жизни» ставят ссылку на паузу с первой.

- **Три истории в «Интересном» о том, как Полка делалась с Claude** (тема «Как сделана Полка»): «Почему сервер не может забрать артефакт по ссылке», «Одна регулярка, которая могла остановить сервер», «31 попытка выбраться из песочницы». Автономный HTML без сети, читается и без скриптов; статичные снимки — `editorial-static-snapshots.ts`. Опубликовать только новые материалы можно флагом `--only` у `editorial-seed-hosted.ts`.
- **Ссылка на исходный код.** Главная говорит, что Полка — открытый код (AGPL-3.0), и ведёт на GitHub; ссылка «Открытый код на GitHub» есть в подвале каждой страницы и в подробностях у получателя.
- **Страница «Для компаний»** (`/pricing`): облако polochka.app (бесплатно на время пилота), своя установка (бесплатно по AGPL-3.0) и коммерческая лицензия с кнопкой «Написать». Ссылки — в подвале и в блоке для компаний на главной.
- **Комментарии и реакции на тексте работы** (этап 3 [COMMENTS](docs/specs/COMMENTS.md); миграция 030).
  - **Обсуждение принадлежит ссылке.** Получатель выделяет фрагмент и пишет замечание или ставит реакцию (👍 👎 🎉 🤔 ❤️ 👀 ✅, повторное нажатие снимает); ответы в один уровень, «Решено», «Удалить». Читать может любой, у кого открыта ссылка, писать — только вошедший; вход возвращает на ту же ссылку, токен не попадает в `?next=`. Получатели одной ссылки не видят обсуждение другой; отзыв, истечение и проверка модератором закрывают обсуждение для получателей, у владельца оно остаётся. Владелец видит обсуждения всех ссылок работы на её странице, с числом новых.
  - **Привязка к тексту** — цитата `{exact, prefix, suffix}`; в новой версии она ищется заново (точно, затем без учёта пробелов, среди повторов — по контексту), не найденная или неоднозначная становится комментарием ко всей работе с пометкой «к прежней версии».
  - **Оверлей в просмотрщике** добавляется только к гранту, выданному оболочкой с `{comments: true}`; скачивания и прочие просмотры остаются побайтно исходными. В интерактивном просмотре он идёт после защиты от WebRTC; в статичном — `sandbox allow-scripts` и CSP `script-src 'nonce-…'` со случайным nonce на ответ, поэтому скрипты, обработчики и `javascript:` самой страницы по-прежнему не выполняются, а сеть закрыта и для оверлея. Подсветка — CSS Custom Highlight API, DOM страницы не меняется. Сообщения принимаются только от `parent` с `APP_ORIGIN`. Проверено в настоящем Chrome (`tests/comment-overlay-dom.test.ts`).
  - **Интерфейс:** лента справа от документа с карточками на высоте фрагментов, на телефоне — выезжающая панель; плавающая кнопка «Комментировать» и реакции у выделения; счётчик 💬 в верхней полосе. Текст комментариев — только текст, адреса не кликабельны; автор показан выбранным им именем (его просят выбрать перед первым комментарием), не почтой.
  - **Письма:** владельцу о каждом новом комментарии, участникам ветки — об ответах, пока ссылка открыта; не больше 30 в сутки на адрес в своём счётчике (вход не страдает), до 300 символов, адреса в тексте обезврежены (`example[.]com`), чужих почт нет; отключаются подписанной ссылкой `/mail-off#…` или в панели.
  - **Агентам:** `polka_comments` (`read`), `polka_resolve_comment` (`revise`), `polka_revise` с `edits: [{oldText, newText}]` (точное совпадение, затем нормализованное — NFKC, кавычки, тире, пробелы в конце строк; меняются только найденные места; 422 с номером правки, 409 с `currentRevisionId`), `polka_share` с `moveShareId` — перенести ссылку и её обсуждение на новую версию. HTTP: `POST /api/v1/works/:id/edits` (с `moveLink`). Цикл описан в `/llms.txt`, скилле и [connect-agents](docs/connect-agents.md).
  - **Модерация:** признаки фишинга в комментариях (как у страниц, плюс адреса); подозрительный комментарий нового автора виден только автору и владельцу, пока оператор не решит, оператор получает письмо. Жалоба на комментарий (не ставит ссылку на паузу). Скрипты `moderation:comments`, `moderation:delete-comment`, `moderation:release-comment`; `disable` и запрос удаления аккаунта скрывают комментарии и реакции автора, окончательное удаление аккаунта стирает их. Лимиты: 2000 символов, 200 комментариев на ссылку в сутки, 60 комментариев и реакций автора в час.

- **Иконка Полки во вкладке браузера и при установке.** `favicon.ico`/`favicon.svg`, иконки для iPhone и Android (в том числе maskable) и `manifest.webmanifest`: Полку можно установить как приложение или добавить на главный экран. PNG и ICO собираются из `apps/web/icons/*.svg` скриптом `scripts/gen-icons.mjs`.
- **Сравнение версий.** Во вкладке «Версии» владелец выбирает «Было» и «Стало» и видит построчный diff исходного кода: добавленные строки зелёным, удалённые красным, неизменённые участки свёрнуты («31 строка без изменений»). Байты читаются теми же маршрутами владельца, что «Скачать оригинал» (`/api/revisions/:id/bytes`, у пакета — `/export`: сравнивается главная страница, изменённые файлы перечислены), нового серверного маршрута нет. Diff — алгоритм Майерса в `shared/lib/line-diff.ts`, без зависимостей, в Web Worker; пределы названы в интерфейсе: 50 000 строк на сторону, после 2 500 отличий — замена целиком, показ до 2 000 изменённых строк («показано не всё»). Для изображений и двоичных файлов сравнение недоступно, и это сказано.
- **Превью ссылок в мессенджерах.** Оболочка приложения получает Open Graph и Twitter-теги с абсолютными адресами из `APP_ORIGIN`. Для `/s` — общая карточка «Полка — вам отправили страницу»: токен ссылки в `#` и до сервера не доходит, поэтому название работы в превью не попадает (тест сверяет, что страница одинакова для любого запроса и не содержит названия, токена и id). Для главной и остальных страниц — карточка Полки. Картинки 1200×630 собираются из SVG в `docs/assets/og/` скриптом `scripts/render-og-images.mjs` (headless Chrome).
- **Полку находит агент без подсказок** (этап 2 [COMMENTS](docs/specs/COMMENTS.md#этапы)), как у justhtml.sh. `GET /llms.txt`: что такое Полка, подключение (фраза `/connect`, одна команда для Codex и Claude Code, коннектор для Claude.ai и ChatGPT, токен для остальных; человек входит в браузере), инструменты MCP со scope — список строится из зарегистрированных инструментов и не расходится с ними, HTTP API с curl, лимиты, модерация и как показать ссылку. `GET /openapi.json` — OpenAPI 3.1 HTTP API из zod-схем маршрутов; схемы ответов тесты сверяют с настоящими ответами. `GET /.well-known/agent-skills/index.json` и скилл `polka` в `skills/polka/SKILL.md`: `npx skills add artkruglov/polka`. Все адреса — из `APP_ORIGIN`, ответы публичные, без cookies, кэш 5 минут. `/connect` ссылается на `/llms.txt`.
- **Политика обработки персональных данных и пользовательское соглашение** на `/privacy` и `/terms`, ссылки в подвале и на странице входа. Оператор — Круглов Артем Игоревич; тексты описывают, как Полка устроена по коду, и ещё не проверены юристом (это сказано на странице). Обещанные сроки выполняются на деле: жалобы удаляются обслуживанием через год, дампы базы — правилом жизненного цикла хранилища через 30 дней.

### Changed

- **Новые полки по коду из письма — только на разрешённых доменах.** `EMAIL_SIGNUP_DOMAINS=any|ru-only|<список>`. В `deploy/hosted` по умолчанию `ru-only`: Яндекс, Mail.ru, Рамблер, VK и домен установки.
  - Существующие аккаунты на других доменах входят по коду, как раньше. `EMAIL_LOGIN_DOMAINS=signup` отключит и это.
  - Форма отвечает одинаково, есть ли полка на адресе. О правиле интерфейс предупреждает заранее и предлагает войти с Яндекс ID или VK ID.
- **Политика и соглашение (редакция 24.09.2026).** Добавлены вход через Яндекс ID и VK ID: какие данные передаются, основание, срок, cookie `polka_idp`. Описаны заметки владельца на polochka.app.

- **Почта оператора — на домене Полки.** Вместо artkruglov@gmail.com: privacy@polochka.app (данные, жалобы, правообладатели) и hello@polochka.app (коммерческая лицензия, модерация). Ящик в Яндекс 360, данные в России: из политики ушла передача писем в Google (США).
- **Лицензия: AGPL-3.0 вместо Apache-2.0, плюс коммерческая** ([LICENSE](LICENSE), [COMMERCIAL.md](COMMERCIAL.md)). Все коммиты в репозитории сделал один автор, поэтому автор вправе сменить лицензию. Зачем: Apache-2.0 разрешает взять Полку, доработать и запустить закрытым сервисом, ничего не вернув. AGPL-3.0 требует предложить исходный код изменённой версии всем, кто пользуется ею по сети (§ 13), а кому это не подходит, может взять коммерческую лицензию. Пользоваться Полкой без изменений или дорабатывать её открыто по-прежнему можно бесплатно. **Версии до v0.1.0-rc.5 включительно и все коммиты до этого изменения остаются под Apache-2.0**: выданную лицензию нельзя отозвать. Что изменилось в коде и документах:
  - `LICENSE` — официальный текст GNU AGPL-3.0 без изменений (тест сверяет SHA-256), `package.json` — `AGPL-3.0-only`, `NOTICE` — новая лицензия и оговорка про прежние версии; `/openapi.json` называет `AGPL-3.0-only`. Лицензии production-зависимостей (MIT, ISC, Apache-2.0, BSD, BlueOak, 0BSD, MIT-0, Unlicense) с AGPL-3.0 совместимы; проверка `npm run verify` не менялась.
  - Новая настройка `SOURCE_URL` (https, по умолчанию `https://github.com/artkruglov/polka`): куда ведёт ссылка на исходный код. Её отдаёт `GET /api/capabilities` (`sourceUrl`), интерфейс берёт её вместо зашитого адреса; она же в `/llms.txt` и `/connect`. Кто запускает изменённую Полку, обязан указать здесь свой код ([deploy/hosted](deploy/hosted/README.md#исходный-код-изменённой-версии)). Подвал со ссылкой теперь есть и на страницах `/away` и `/mail-off`.
  - Pull requests принимаются по [соглашению участника](CLA.md) (на основе Apache ICLA 2.2): автор сохраняет права, мейнтейнер может распространять вклад и по коммерческой лицензии. Принять — строкой «Я принимаю CLA (CLA.md)» или галочкой в шаблоне pull request.
  - Политика и соглашение называют новую лицензию; соглашение добавляет, что для пользования сервисом лицензия на код не нужна.
- **Политика и соглашение вычитаны по российскому праву** (протокол — `docs/reviews/2026-09-23-legal`, в истории git до коммита c9efc10). Политика разложена по целям: данные, основание по ст. 6 152-ФЗ и срок для каждой; добавлены получатели ссылок, отпечаток жалобщика, автоматическая проверка на фишинг, localStorage, почта оператора в Gmail и комментарии как готовящийся пункт; удаление аккаунта — доступ закрывается за 10 рабочих дней, данные удаляются за 30 дней; ответ на запрос — 10 рабочих дней с продлением на 5. Соглашение — публичная оферта с акцептом первым входом, простая лицензия с территорией и закрытым перечнем способов, порядок заявлений правообладателей, изменения с уведомлением за 10 дней, необязательный досудебный порядок и сохранённая подсудность по месту жительства. Строка под формой входа больше не «принимает» политику, а подтверждает, что её прочитали.
- **Панель интерактивного просмотра у получателя на телефоне — одна строка.** «Развернуть», «Остановить» и «Обновить доступ» становятся кнопками-значками 40×40 с `aria-label` (слова видны с 761px); поведение, фокус при разворачивании и Escape не менялись. Снимки: `docs/design/2026-09-23-history-og/`.
- Письма модерации больше не содержат почту автора, только логин аккаунта: письма идут через почтовый сервис оператора, в том числе зарубежный. Адрес по-прежнему виден оператору на сервере (`/moderation`, `scripts/moderation.ts`).

### Added

- **Защита от злоупотреблений при открытой регистрации** (`docs/specs/ABUSE_PROTECTION.md`, пункты 1–7; миграция 029).
  - **Доверие к аккаунту.** Доверенный — созданный оператором (вход по паролю), одобренный оператором или старше `NEW_ACCOUNT_DAYS` (7) без нерассмотренных жалоб; аккаунт с приостановленной ссылкой доверие теряет до решения оператора. Все аккаунты, существовавшие до миграции, отмечены доверенными.
  - **Лимиты новых аккаунтов:** ссылка не дольше 7 дней и не больше `NEW_ACCOUNT_MAX_LINKS` (5) открытых ссылок; отказ объясняет причину и что сделать. `polka_publish` и publish API выдают новому аккаунту ссылку на 7 дней вместо 30 и сообщают это в `expiresNote`.
  - **Состояние ссылки** `none` / `held` / `paused`. `SHARE_MODERATION`: `off`, `flagged` (по умолчанию: подозрительная страница недоверенного автора), `new-accounts` (любая ссылка недоверенного автора), `all` (любая ссылка аккаунта, зарегистрированного по почте). Получатель ожидающей или приостановленной ссылки видит экран «Ссылка на проверке у модератора Полки» без названия и содержимого; владелец видит работу и состояние «на проверке». Новая версия, опубликованная по одобренной ссылке недоверенного автора, снова ждёт проверки.
  - **Агентам:** `polka_publish`, `polka_share` и `POST /api/v1/publish` возвращают `moderation: "held"` и `moderationMessage`, пока ссылка ждёт; описания инструментов велят не выдавать такую ссылку за готовую. CLI печатает это в stderr.
  - **Жалобы:** каждая — письмо оператору; `MODERATION_AUTOPAUSE_REPORTS` (3) разных жалобщиков за 7 дней ставят ссылку на паузу. Жалобщики различаются по `HMAC(LINK_KEY, ip|share)`, адреса не хранятся. Ссылки «Интересного» автоматически не приостанавливаются.
  - **Письма оператору с действиями в один клик** (`OPERATOR_EMAIL`): «Посмотреть» (тот же песочный просмотр, грант на 60 секунд), «Одобрить ссылку», «Одобрить и доверять автору», «Снять паузу», «Закрыть ссылку», «Закрыть и отключить автора». Кнопка — `/moderation#<токен>`: HMAC от ключа, выведенного из `LINK_KEY` с отдельным назначением, срок 7 дней. Открытие страницы ничего не меняет, действие выполняет только POST; повтор безопасен. Отправка писем вынесена в `apps/server/mailer.ts` (вход по коду и модерация); в `MAIL_MODE=local` письма оператору пишутся в `.local/mail/operator/`.
  - **Признаки фишинга** считаются при сохранении в том же линейном проходе parse5, что и классификатор, и по строкам скриптов (включая JSX-компоненты): поля паролей, карт и кодов из SMS вместе с брендом банка или сервиса либо срочностью. Список признаков — один файл `apps/server/phishing-signals.ts`.
  - **Скрипты:** `npm run moderation:queue`, `moderation:approve -- <shareId> [--trust]`, `moderation:unpause`, `moderation:trust`.

### Changed

- **Получатель видит предупреждение над страницей, а не мелким шрифтом под ней:** «Эту страницу опубликовал пользователь Полки. Полка её не проверяла. Не вводите здесь пароли, коды из SMS и данные карт.», рядом «Пожаловаться»; у автора младше 7 дней — «Автор недавно на Полке». Имя автора не показывается. Материалы «Интересного» подписаны «Редакция Полки» без предупреждения.
- `moderation:revoke-share` отмечает жалобы на закрытую ссылку рассмотренными.

### Security

- **Глубоко вложенная страница больше не останавливает сервер.** Разбор HTML в parse5 квадратичен по глубине вложенности (200 КБ вложенных `<div>` — около 4 с, 5 МБ — около 40 мин), а обход дерева в классификаторе был рекурсивным и падал с переполнением стека. Запись rc.4 о линейном времени была верна только для «плоских» страниц. Теперь страницы больше 16 КБ классифицируются в отдельном потоке с дедлайном 2 с (не успел — ссылка не выдаётся, владелец видит и скачивает работу), обход дерева — без рекурсии.
- **С доменом просмотра домен приложения больше не отдаёт пользовательский HTML.** Статичный (без скриптов) просмотр переехал на домен просмотра (`VIEWER_ORIGIN/static/:token`, грант на 60 секунд: у владельца — по сессии, у получателя — от гранта ссылки) с CSP статичного просмотра и `frame-ancestors <APP_ORIGIN>`; `/api/revisions/:id/document` и `/api/view/:grant/document` на таком сервере отвечают 404, а приложение встраивает фреймы только с домена просмотра (`frame-src` без `'self'`). Установка на одном домене работает как раньше. Спецификация: `docs/specs/ABUSE_PROTECTION.md`, п. 8.
- **Внешние ссылки статичного просмотра ведут через «Вы уходите с Полки».** При отдаче страницы абсолютные `http(s)`-ссылки в `<a>` и `<area>` заменяются на `APP_ORIGIN/away#<токен>`; токен подписан ключом, выведенным из `LINK_KEY`, и живёт 12 часов. Страница `/away` называет адрес и ждёт нажатия; без действующей подписи она ссылку не открывает и сама никуда не перенаправляет. Скачивание отдаёт исходные байты. Спецификация: п. 9.

## [0.1.0-rc.5] — 2026-09-23

Подготовка к открытому запуску: вход в продукт одной фразой агенту, открытая регистрация с потолком, онбординг, модерация и закрытый выход страниц через WebRTC.

### Added

- **Статус оператора** `GET /api/ops/status` (по токену `OPS_STATUS_TOKEN`, без него маршрута нет): БД, работает ли обслуживание, возраст свежего бэкапа, свободный диск; 503, если проверка не прошла. Скрипт `scripts/ci/uptime.mjs` проверяет снаружи доступность, TLS-сертификаты и этот статус — для запуска по расписанию вне VM.
- `npm run check:links` проверяет относительные ссылки и якоря во всех Markdown-файлах.
- **Вход по почте только по приглашению** (`EMAIL_SIGNUP=invite`, по умолчанию в hosted и в SMTP-оверлее base): код получают аккаунты с привязанным адресом (`npm run account:email -- <логин> <почта>`) и адреса или домены из `EMAIL_SIGNUP_ALLOW`; остальным форма отвечает так же, но письмо не уходит. `EMAIL_SIGNUP=open` оставляет открытую регистрацию.
- Hosted compose передаёт приложению `SMTP_*` и `MAIL_FROM`; описана отправка через Yandex Cloud Postbox.
- **Подключение одной фразой.** Агенту достаточно сказать «Подключи Полку: <APP_ORIGIN>/connect»: по этому адресу лежит инструкция для агентов (текст), агент выполняет одну команду, человек входит или создаёт полку по почте и разрешает доступ. Страница входа в этом сценарии объясняет, что агент просит доступ.
- **Суточный потолок новых полок** при регистрации по почте: `EMAIL_SIGNUP_DAILY_LIMIT` на установку (по умолчанию 50) и `EMAIL_SIGNUP_DAILY_PER_IP` (3). О заполненном дне человек узнаёт при запросе кода; существующие аккаунты входят как обычно.
- **Черновики политики обработки персональных данных и пользовательского соглашения** — страницы `/privacy` и `/terms`, ссылки внизу каждой страницы и под формой входа по почте. Текст — в `docs/legal/`, помечен как черновик для проверки юристом; поля оператора (имя, почта, сроки) — заглушки в квадратных скобках.
- **Codex и Claude Code подключаются одной командой, без токена** — через OAuth, как Claude.ai: `codex mcp add polka --url <APP_ORIGIN>/mcp` или `claude mcp add --transport http polka <APP_ORIGIN>/mcp`. Команды — на странице «Агенты» и в `docs/connect-agents.md`; подключение по токену осталось для остальных клиентов.
- **Модерация для оператора** — скрипты без веб-интерфейса, от runtime-роли БД: `npm run moderation:reports` показывает жалобы на ссылки (владелец, открыта ли ссылка, сколько жалоб на ссылку и владельца), `moderation:revoke-share` закрывает одну ссылку, `moderation:disable` блокирует аккаунт одной транзакцией (сессии, подключения агентов вместе с OAuth, все ссылки; данные не удаляются), `moderation:enable` снимает блокировку. Команды для hosted — в `deploy/hosted/README.md`.

- **Первые шаги на полке и `/start`.** Чек-лист из трёх шагов — агент подключён, первая работа сохранена, ссылка создана — выводится из существующих ответов API (`/api/agent-connections`, `/api/artifacts`), без новых таблиц. У каждого шага одно действие: скопировать фразу «Подключи Полку: …/connect» (с командами для Codex, Claude Code, Claude.ai и ChatGPT), «Сохранить без агента», «Поделиться». Блок «Попробовать за 10 секунд» сохраняет встроенную страницу-пример обычной загрузкой. Живая область объявляет выполненные шаги; «Скрыть» запоминается в этом браузере. Спецификация: `docs/specs/ONBOARDING_V2.md`.

### Changed

- Пустая полка объясняет, что здесь появится, вместо набора одинаковых кнопок; после «Разрешить» страница согласия говорит вернуться к агенту.

### Security

- **Страница в интерактивном просмотре больше не может выйти в сеть через WebRTC.** CSP его не закрывает (Chrome игнорирует `webrtc 'block'`), и проверка на проде показала, что страница достаёт внешний STUN-сервер: могла бы отправить IP читателя и введённый текст. Просмотрщик теперь первым скриптом каждой страницы убирает конструкторы RTC и отдаёт `X-DNS-Prefetch-Control: off`. Отчёт — `docs/reviews/2026-09-23-viewer-egress`.
- **Посторонний больше не может заблокировать вход по почте.** Суточный лимит неудачных попыток на адрес убран: его мог исчерпать любой, кто знает адрес. Вместо него код стал восьмизначным, поэтому перебор при прежних ограничениях отправки и попыток бесполезен. Коды из уже отправленных писем (шестизначные) после обновления не принимаются — запросите новый.

## [0.1.0-rc.4] — 2026-09-22

Правки по итогам второго полного ревью перед открытым релизом (сервер, безопасность, веб, эксплуатация, документация).

### Security

- **Классификатор страниц работает за линейное время.** Регулярные выражения перезапускались с каждого `<`, поэтому время росло квадратично и занимало поток запросов: страница в 480 КБ останавливала сервер примерно на минуту, а разрешённые 5 МБ — на часы. Для второй половины атаки хватало ссылки на такую страницу. Теперь документ разбирается один раз (parse5), 5 МБ — меньше полсекунды.
- **Открытый редирект после входа закрыт.** `next=/.//evil.example` после нормализации превращался в `//evil.example` и уводил уже вошедшего пользователя с Полки без клика.
- **Токен ссылки не уходит в `?next=`.** «Войти» на странице получателя переносил `/s#<токен>` в query-строку, которую видят сервер и прокси.
- **Сохранённая страница не открывается как самостоятельная страница Полки.** Браузер, заявивший навигацию верхнего уровня (`Sec-Fetch-Dest: document`), получает 404; во фрейме всё как раньше.
- **Сборка не оставляет процессов-зомби.** Каждая runtime-сборка оставляла esbuild зомби-процессом сервера; воркер теперь дожидается его выхода.
- **Упавшую сборку нельзя перезапускать в цикле.** После таймаута, нехватки памяти или сбоя сборщика тот же исходник не пересобирается 30 секунд, поэтому один владелец больше не занимает слоты сборки всех остальных.

### Fixed

- **Вход по ссылке от агента после закрепления или отзыва.** Вход читал ссылку как живую, затем ждал блокировку полки; если за это время полку закрепили (закрепление удаляет слабые сессии) или отозвали подключение агента, вход всё равно выдавал слабую сессию по устаревшему решению. Теперь ссылка и подключение перечитываются под блокировкой полки, и такой вход получает «Ссылка устарела».
- **Обслуживание оставляло временную полку недоудалённой.** Без конвейера удаления (`ACCOUNT_DELETION_ENABLED=false`) обслуживание закрывало простаивающую временную полку и затем удаляло её объекты и строки; сбой S3, конец времени прогона или остановка между этими шагами оставляли полку закрытой с объектами навсегда. Теперь закрытие записывает задание (`provisional_erasures`, миграция 076), и следующий прогон доводит удаление до конца; рецепты прав обновлены.
- **Импорт полки и снимок на дату.** Импорт ставил работе время ухода в корзину, но не писал это событие в журнал; после восстановления работы на новой установке снимок полки за время между исходным удалением в корзину и восстановлением показывал её на полке. Теперь уход в корзину пишется в журнал с исходным временем, как отметка принятия.
- Один лимит названия — 160 символов: MCP-захват и импорт по URL принимали 200 и затем падали с «internal».
- Запрос, дождавшийся занятой строки (медленный S3 под блокировкой владельца), получает повторяемый 503 вместо 500; S3-клиент делает не больше двух попыток.
- Интерактивные просмотры по ссылкам одного владельца больше не выстраиваются в очередь друг за другом.
- Неудачный `ROLLBACK` больше не подменяет исходную ошибку.
- Страница входа не говорит, что регистрации нет, если на установке включён вход по почте; приглашение в библиотеку не мигает ошибкой; повтор жалобы не создаёт дубль.
- Healthcheck PostgreSQL проверяет TCP: при первом старте временный сервер отвечал только на сокет, и `migrate` падал на медленной VM.
- Документированное восстановление hosted-дампа не работало (`pg_restore --clean`); новая процедура проверена на реальном дампе схемы 028.

### Changed

- Образ содержит `NOTICE` и `THIRD_PARTY_NOTICES.md`.
- Документация сверена с кодом: OAuth `resource` необязателен, коннектор проверен с Claude.ai (ChatGPT — ещё нет), приёмка изоляции viewer на hosted пока не записана, runbook первого запуска hosted дополнен.

### Known issues

- Посторонний, знающий адрес почты, может на сутки заблокировать вход по коду для этого адреса: лимит неудачных попыток считается на адрес. Касается только установок с `MAIL_MODE=smtp`, на polochka.app вход по почте выключен.

## [0.1.0-rc.3] — 2026-09-22

### Security

- Классификатор страниц читает атрибуты так же, как браузер: `<meta http-equiv="&#x72;efresh">` и `href="&#x6a;avascript:"` больше не проходят как обычная страница и не получают ссылку.
- `<base target="_blank">` вставляется в настоящий `<head>`, а не в закомментированный, поэтому ссылки в статичном просмотре всегда открываются в новой вкладке.

### Fixed

- **Вход по ссылке от агента после закрепления или отзыва.** Вход читал ссылку как живую, затем ждал блокировку полки; если за это время полку закрепили (закрепление удаляет слабые сессии) или отозвали подключение агента, вход всё равно выдавал слабую сессию по устаревшему решению. Теперь ссылка и подключение перечитываются под блокировкой полки, и такой вход получает «Ссылка устарела».
- **Обслуживание оставляло временную полку недоудалённой.** Без конвейера удаления (`ACCOUNT_DELETION_ENABLED=false`) обслуживание закрывало простаивающую временную полку и затем удаляло её объекты и строки; сбой S3, конец времени прогона или остановка между этими шагами оставляли полку закрытой с объектами навсегда. Теперь закрытие записывает задание (`provisional_erasures`, миграция 076), и следующий прогон доводит удаление до конца; рецепты прав обновлены.
- **Импорт полки и снимок на дату.** Импорт ставил работе время ухода в корзину, но не писал это событие в журнал; после восстановления работы на новой установке снимок полки за время между исходным удалением в корзину и восстановлением показывал её на полке. Теперь уход в корзину пишется в журнал с исходным временем, как отметка принятия.
- В базовом `compose.base.yml` вернулся проброс `127.0.0.1:${APP_PORT}` — без него обратный прокси не мог достучаться до приложения.

## [0.1.0-rc.2] — 2026-09-22

### Added

- **Коннектор для Claude.ai и ChatGPT.** Удалённый MCP-сервер со входом через OAuth 2.1: PKCE S256, Dynamic Client Registration, protected-resource и authorization-server metadata, ротация refresh-токена с обнаружением повтора, страница согласия. Миграция 028. См. [docs/MCP_CONNECTOR.md](docs/MCP_CONNECTOR.md).
- **Инструмент `polka_publish`.** Одна HTML-страница или исходник React-компонента на входе, приватное сохранение и ссылка на выходе. Если на установке включён интерактивный просмотр, сразу собирается интерактивная версия.
- **Интерактивный viewer на отдельном домене.** Режим `HTML_LIVE_MODE=production`, второй registrable domain (polochka.page), Caddy vhost. Получатели и карточки «Интересного» сразу открывают интерактивную версию.
- **React runtime `react-runtime-v1`.** JSX/TS артефактов из чата компилируется при сборке производной. Доступны React, lucide-react, recharts, lodash, d3, three, papaparse, mathjs, chart.js и Tailwind v4. Библиотеки берутся из копий Полки, сети нет, неизвестный import отклоняет сборку.
- **Сборщик `bundle-inline-v6`** (через v4 и v5): принимает обычные артефакты из чата, в том числе data:-шрифты; пропускает внешние шрифты и картинки с предупреждением; даёт общую среду для `text/babel` скриптов. Готовые производные v3 остаются действительными.
- **HTTP API публикации** `POST /api/v1/publish` и `GET /api/v1/status/:id` для агентов без MCP, а также CLI без зависимостей `scripts/polka-publish.mjs` (установка отдаёт его по `/api/v1/cli/polka-publish.mjs`). См. [docs/PUBLISH_API.md](docs/PUBLISH_API.md).
- **«Вставить код»** на странице «Сохранить» и карточка HTTP API на странице «Агенты».
- **Интерактивный каталог «Интересное».** 12 материалов Редакции Полки, статичные снимки для установок без viewer, скрипт засева hosted-каталога.
- **Ссылки Claude/ChatGPT.** Полка узнаёт такую ссылку, объясняет, почему не может забрать артефакт сама, и принимает скачанный файл прямо в карточке.
- Ссылки внутри сохранённых страниц открываются в новой вкладке по клику получателя.
- Hosted-пилот на одной VM: роли PostgreSQL, Caddy с TLS, ежесуточные бэкапы.

### Changed

- **Полный рестайлинг по концептам.** Одна система токенов, общие компоненты, старый CSS удалён. Точки входа в интерфейсе зависят от возможностей установки.
- Обложки «Интересного» подписаны «Редакция».
- `polka_publish` сразу отказывает для страницы больше 5 МБ и называет лимит. Лимит 5 МиБ сохранён ([решение](docs/specs/DECISIONS.md#22092026-лимит-страницы-остаётся-5-миб)).
- Документация переписана: новые README (RU/EN), [архитектура](docs/architecture.md), [подключение агентов](docs/connect-agents.md), [FAQ](docs/faq.md), [состояние](docs/status.md), [дорожная карта](docs/roadmap.md). Спецификации перенесены в `docs/specs/`, внутренние рабочие материалы убраны из репозитория.

### Security

- Коннектор OAuth усилен по итогам ревью: неподтверждённые имена клиентов помечаются, ошибки запроса не пересылаются на адрес клиента, запрос привязан к браузеру, `/oauth/token` и `/oauth/revoke` принимают только form-urlencoded.
- Получатель интерактивной ссылки никогда не получает и не запускает исходную однофайловую загрузку, только собранную производную.
- Runtime-сборщик не читает файлы сервера, а память esbuild ограничена. Закрыты обходы проверки импортов через `require` и sloppy-mode.
- Capabilities viewer не попадают в журнал ошибок Caddy.

## [0.1.0-rc.1] — 2026-09-21

Первый открытый prerelease.

### Added

- Аккаунты: вход по паролю (аккаунт создаёт оператор) и по коду из письма (при настроенном SMTP).
- Полка: папки, поиск, переименование и перенос. Неизменяемые версии с SHA-256 и CAS. Корзина и восстановление.
- Загрузка HTML, текста и PNG/JPEG/WebP до 5 МБ. Multi-file bundle через MCP (до 64 файлов).
- Ссылки: срок, отзыв, привязка к версии, жалобы получателя. Статичный просмотр HTML в песочнице без скриптов и сети.
- MCP-сервер (Streamable HTTP) с токенами подключения и узкими scopes: `polka_context`, `polka_capture`, `polka_status`, `polka_revise`, `polka_share`, `polka_revoke_share`, `polka_list`, `polka_prepare_preview`, инструменты управления и шаблонов. Страница «Агенты» для выдачи и отзыва токенов.
- Контекст для агента и библиотеки шаблонов: роли reader/curator/admin, приглашения, журнал, закреплённые версии.
- Экспериментальный интерактивный просмотр на отдельном loopback origin.
- Импорт standalone HTML по URL (выключен по умолчанию).
- Удаление аккаунта: отзыв доступа в коде (выключено по умолчанию); очистка данных — отдельный purge worker, не принят и не подключён к установке. Синтетический restore drill, штатный restore CLI.
- Self-host: Docker-образ с закреплённым base digest, readiness, maintenance-воркер, рецепты прав ролей БД.
- CI: тесты на временных PostgreSQL и MinIO, проверка лицензий, поиск секретов, smoke-тест образа.

[Unreleased]: https://github.com/artkruglov/polka/compare/v0.19.0...HEAD
[0.19.0]: https://github.com/artkruglov/polka/compare/v0.18.0...v0.19.0
[0.18.0]: https://github.com/artkruglov/polka/compare/v0.17.1...v0.18.0
[0.17.1]: https://github.com/artkruglov/polka/compare/v0.17.0...v0.17.1
[0.17.0]: https://github.com/artkruglov/polka/compare/v0.16.0...v0.17.0
[0.16.0]: https://github.com/artkruglov/polka/compare/v0.15.1...v0.16.0
[0.15.1]: https://github.com/artkruglov/polka/compare/v0.15.0...v0.15.1
[0.15.0]: https://github.com/artkruglov/polka/compare/v0.14.0...v0.15.0
[0.14.0]: https://github.com/artkruglov/polka/compare/v0.13.0...v0.14.0
[0.13.0]: https://github.com/artkruglov/polka/compare/v0.12.1...v0.13.0
[0.12.1]: https://github.com/artkruglov/polka/compare/v0.12.0...v0.12.1
[0.12.0]: https://github.com/artkruglov/polka/compare/v0.11.1...v0.12.0
[0.11.1]: https://github.com/artkruglov/polka/compare/v0.11.0...v0.11.1
[0.11.0]: https://github.com/artkruglov/polka/compare/v0.10.0...v0.11.0
[0.10.0]: https://github.com/artkruglov/polka/compare/v0.9.1...v0.10.0
[0.9.1]: https://github.com/artkruglov/polka/compare/v0.9.0...v0.9.1
[0.9.0]: https://github.com/artkruglov/polka/compare/v0.8.0...v0.9.0
[0.8.0]: https://github.com/artkruglov/polka/compare/v0.7.4...v0.8.0
[0.7.4]: https://github.com/artkruglov/polka/compare/v0.7.3...v0.7.4
[0.7.3]: https://github.com/artkruglov/polka/compare/v0.7.2...v0.7.3
[0.7.2]: https://github.com/artkruglov/polka/compare/v0.7.1...v0.7.2
[0.7.1]: https://github.com/artkruglov/polka/compare/v0.7.0...v0.7.1
[0.7.0]: https://github.com/artkruglov/polka/compare/v0.6.0...v0.7.0
[0.6.0]: https://github.com/artkruglov/polka/compare/v0.5.3...v0.6.0
[0.5.3]: https://github.com/artkruglov/polka/compare/v0.5.2...v0.5.3
[0.5.2]: https://github.com/artkruglov/polka/compare/v0.5.1...v0.5.2
[0.5.1]: https://github.com/artkruglov/polka/compare/v0.5.0...v0.5.1
[0.5.0]: https://github.com/artkruglov/polka/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/artkruglov/polka/compare/v0.3.1...v0.4.0
[0.3.1]: https://github.com/artkruglov/polka/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/artkruglov/polka/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/artkruglov/polka/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/artkruglov/polka/compare/v0.1.0-rc.5...v0.1.0
[0.1.0-rc.5]: https://github.com/artkruglov/polka/releases/tag/v0.1.0-rc.5
[0.1.0-rc.4]: https://github.com/artkruglov/polka/releases/tag/v0.1.0-rc.4
[0.1.0-rc.3]: https://github.com/artkruglov/polka/releases/tag/v0.1.0-rc.3
[0.1.0-rc.2]: https://github.com/artkruglov/polka/releases/tag/v0.1.0-rc.2
[0.1.0-rc.1]: https://github.com/artkruglov/polka/releases/tag/v0.1.0-rc.1