Product changelog
Every Simmetric Chat release, from the newest version to the oldest.
Added
- Модель владения виджетами (2026-10-05 "wown", quick 261005-scn). Виджеты теперь имеют собственников (их создателя) и пользователей (по-виджетовые полномочия): - соединение
widget_users(аддитивная миграция) + потолокusers.widgetQuotaLimitна собственника; NULL/fail-closed 0 — нетронутые установки видят ноль изменения поведения (решётчатый идиомmaxSponsoredUsersPhase-206). - CRUD виджетов снова включён для не-админ СОБСТВЕННИКОВ:POST /api/widgetsидёт через новое правоwidget:manage+ назначенный админом потолок (нарушение → 409{ error, quota: "widgets" }); лицензионные барьеры на уровне установки (widget_enabled/max_widgets) едут без изменений. GET список/детали, PUT настройки + привязки workspace достигают creator | widget-user | admin (чужие ID остаются неперечисляющими 404); DELETE остаётся creator | admin. - Новая админ-секция в зоне виджетов (/widgets?tab=quotas, вкладка Quota): строки на пользователя — user | owned | max-widgets — со assign / clear (null)-записями, пагинированным поиском, под авдитом (user.widget_quota.updated). - Новая по-виджетная вкладка "Пользователи" (WidgetDetail, в режиме правки): СОБСТВЕННИК выбирает пользователей виджета (PUT replace-set, зонд директории полномочий/widgets/owner-user-search— поверхность выбора личности, без email); получившие получают read-only руку. - Тесты: серверная матрица 30-pin (widgetOwnership.test.ts), фронтенд-руки (WidgetUsersTab/WidgetQuotasTab), e2e-путь (widget-ownership.spec.ts).
Fixed
- Разделение workspace по умолчанию выставляет роль editor, не viewer (2026-10-05 "viewer-default"). Сообщённый симптом — "получивший пользователь не может создавать новые чаты и писать в существующих чатах предоставленного workspace" — прослежен (через новую интеграционную пробу 4-pin,
granteeChatWriteAccess.test.ts) к РОЛИ выдавания, не к барьерам: avecWORKSPACE_ROLE_ENFORCEMENT=trueстрока WorkspaceAccess editor проходит GET /chats (200) И POST /chat (200) на сервере; строка VIEWER проходит руку чтения и BY DESIGN оценивается 403 на руке записи, а получатель без строки получает неперечисляющий 404 (SC-4). ОБЕ выдающие поверхности (диалог деления собственникаWorkspaceAccessDialogи панель SettingsSettingsWorkspaceAccess) предвыбирали viewer в одинарных И массовых селекторах ролей — админ, выдававший без смены меню, производил read-only выдачи. UI-умолчание также противоречило намерению схемы: комментарий миграции Phase 189 D-12 ставит бэкфилл унаследованных строк на editor ("NOT the spec's viewer suggestion"). Обе поверхности теперь ставят editor по умолчанию (одинарный + массовый); кабельные пины ролей (тест, который явно выбирает viewer и затем проверяет, что viewer проходит до конца) тронуты не были. Примечание: пользователи, получившие ДО этого исправления, сохраняют свою персистентную роль — повторное выдача (upsert роли) переоценивает. - "Суммируй прикреплённый файл" больше не лжёт с заготовленной фразой нет-результатов (закрытие щели прикреплённого-перед-ingest, 2026-10-05). Прикрепить файл в чате = загрузить его через ASYNC-конвейер ingest; сообщение спрашивает чуть позже, у документа ещё НОЛЬ чанков, поэтому per-route разрешение
attachedDocumentId(оба чат-руки в chat.ts) ничего не вводило: агент падал наrag_search— свежего документа ещё не было в векторном хранилище, и правило 3 buildSystemPrompt отвечало заготовкой "Non ho trovato informazioni su questo argomento nei documenti del workspace. Verifica che i file pertinenti siano stati caricati e indicizzati." — ложное утверждение (workspace может быть полон; только свежее прикрепление не готово). Обе точки разрешения теперь определяют руку ноль-чанков и вводят правдивое уведомление (buildAttachmentNoticeChunk(new utils/attachmentNotice.ts)): рука non-empty-ragContext оркестратора отнимаетrag_search, так что модель отвечает из уведомления — "ещё обрабатывается, попробуйте через мгновение" (pending/processing) или "обработка провалилась/отменена, загрузите заново" — и уведомление явно запрещает заготовленную фразу. Осознано в статусе и прогрессе (прогресс 0-100 collectorа появляется только когда > 0); DLP-сканирование rag-контекста идёт по уведомлению без изменений. 5 юнит-пинов на хелпере (руки pending/processing/failed/cancelled + анти-правило-3-инструкция). Фронтенд-продолжение — awaiter ingest чипа вложения: чип опрашивает каждые 1,5с новый endpointGET /api/documents/:id/attachment-status(тройная IDOR-проверка метаданных, проекция{ name, status, progress }, без чанков), показывает спиннер +%-прогресс во время ingest, ДЕРЖИТ барьер отправки (canSend+ путь Enter черезattachmentPending) до "completed" и роняет вложение с тостом наfailed/cancelled(404 → тихий дроп; транзитные сетевые ошибки продолжают опрос — заикание сервера не должно уничтожать здоровый ingest). Барьер = вежливость клиента: уведомление сервера остаётся честным оплот для устаревших вкладок. 4 endpoint-пина в documentIdor.test.ts (проекция access-user + руки D-04 админа); 2 пина ChatInputArea (pending отключает отправку + чип прогресса; completed возвращает). - Настройки → "Доступ к workspaces": строки выдач снова показывают имя пользователя (Q6-fast settings parity, 2026-10-05). Рабочий диалог доступа исправил именно этот дефект (цепочка имя-пользователя → ID), но панель Settings сохраняла ДО-Q6 цепочку
user ? displayName(user) : userId— а провод D-15 никогда не посылает встроенный объектuser(T-189-11), так что каждая развёрнутая строка панели падала на голый UUID. Панель теперь зеркалит цепь диалога (встроенный пользователь унаследованная форма →grant.username→ ID) и её люкс пинит истинную форму провода: статический макет выбросил никогда-не-посланныйuserполе, новая тестовая строка проверяет, что имя пользователя рендерится, а UUID — никогда (+1 тест). - Входная точка общего workspace: строки страницы Workspaces теперь ОТКРЫВАЮТСЯ (2026-10-05 "open-row"). Предоставленному на уровне workspace (viewer/editor БЕЗ доступа к проектам) никогда не удавалось дойти до выданного workspace в чат-UI: селекторы боковой панели = проекты-вперед — меню проектов едет по
GET /api/projects(перечисляет только создатель-свои / ProjectAccess проекты, без workspace-выдачной руки) и меню workspacesdisabledпока проект не выбран, отфильтровано поprojectId === selectedProjectId— тогда как страница Workspaces перечисляла общий workspace (OR-фильтрGET /api/workspacesвключает выдачную рукуaccessGrants.some(userId)), но её строки были только управленческими, без пути ввойти.WorkspaceRowполучает действие "Open" (первое в колонке действий, рендерится для КАЖДОЙ перечисленной строки — принадлежность списку подразумевает минимум viewer по серверному OR-фильтру; клиентский барьер = только UX, чат-маршруты восстанавливают барьер на сервере на каждый запрос), которое монтирует workspace через ChatContext (setWorkspaceId(id)+ navigate("/") — персистируетlastWorkspaceId, так что восстановление при загрузке снова входит в контекст без селекторов). Зеркальный эффект в App держит селекторные меню согласоваными, когда активный workspace меняется вне селекторов — ТОЛЬКО когда отцовский проект в списке проектов пользователя; чат-поверхность управляется толькоcurrentWorkspaceId. Селекторы боковой панели получают статическую запись "Поделено со мной" (сентинел__shared__в PROJECT-меню, рендерится только когда набор делений непуст): с выбранным проектом список workspaces по-прежнему ограничен тем проектом; выбрав сентинел (или пустой выбор при загрузке) список workspaces перечисляет множество невидимых-по-проекту (все OR-фильтр-достижимые workspace, чей отцовский проект отсутствует вGET /api/projects) — список делений только-workspace-получателя, прежде недостижимый, потому что контроль требовал сначала проект И Radix Select не может очистить конкретный идентификатор проекта после выбора (one-way тупик; названная запись = всегда-кликвый путь назад). Плейсхолдер ведётся по содержанию через общий хелперworkspaceSelectPlaceholderKeyвsidebar/navModel.tsx(просроченный пустой сентинел от отозванного деления деградирует к общей формулировке вместо включённого-пустого общего охвата;workspaceSelectDisabledставит барьер на монтажиAppNavOverlay— встроенныйAppSidebarNavрендерится на одних хост-записях). Ключи i18nworkspace.open+sidebar.selectSharedWorkspace+sidebar.sharedProjectsдобавлены ко всем 8 языкам — паритет зелёный. Новые пины: строки viewer показывают Open и никогда delete/edit, отсутствиеonOpenне рендерит контроля, страничный пин-спутник проверяет записьlastWorkspaceIdв localStorage через настоящий ChatProvider (WorkspacesPage), общая рука рендерится из хост-записей без проекта и прячется когда пусто (AppSidebarNav), выбор сентинела показывает общий плейсхолдер, тогда как просроченный сентинел деградирует, и матрица gating оверлея пинит включено/выключено по руке + плейсхолдер на desktop и mobil (AppNavOverlay). - Диалог "Accesso" workspace-ов (кнопка сверху боковой панели) больше не перелейстывает свой собственный борд (2026-10-05). Каждый полноширинный блок убегал за скруглённый край диалога: ui-kit
DialogContent=gridБЕЗ эксплицитногоgrid-template-columns, так что дети сидели на ОДНОЙ имплицитнойauto-дорожке, растущей до min-content-вклада наибольшего айтема — выдачная строка с неразбиваемым 36-знаковым UUID-именем пользователя вносила 503–549px против 488px контент-бокса (sm:max-w-[520px]), widening дорожку и РАСТЯГИВАЯ каждый полноширинный айтем (титул, строку селекторов, чек-стукатурку,<li>выдач) за борд. Двухэтажный фикс: база ui-kit теперь пинит одну эксплицитную ноль-минимум колонку (grid-cols-[minmax(0,1fr)]— вся Dialog-семья сжимает айтемы к крытому контент-боксу вместо выталкивания дорожки, app-wide), и сайт диалога добавляетbreak-wordsк интерполированному титулу +flex-wrapк выдачным/массовым строкам и каждой выдачной<li>(кнопки nowrap заворачиваются на свою строку под ~360px-вьюпортами). Playwright-проверено: ноль перелейста 380→1280px (имена выдач урезаются эллипсисом как спроектировано, ничего не пересекает борд); новыйWorkspaceAccessDialog.test.tsxпинит оба этажа — 174 люкса / 1487 фронтенд-тестов зелёные (+1 люкс, +4 теста). - Строки выдач "Accesso" снова показывают ИМЯ пользователя (Q6 быстрое продолжение, 2026-10-05). Серверный ответ D-15 несёт
{userId, username}, но НЕ встроенный объект пользователя (выброшенный с T-189-11, чтобы перестать течь email/роли), тогда как диалог рендерил имя ТОЛЬКО из того мёртвого опционногоuserполя — каждая строка назначенного пользователя падала на голый UUID. Выдачные строки теперь разрешают встроенного пользователя (унаследованная форма) →username→ ID;WorkspaceAccessGrantполучаетusernameполе, которое провод реально посылает. Новый пин: имя пользователя рендерится / ID никогда не течёт / унаследованные null-username строки держат ID-фолбэк (+1 тест). - Глубокая ссылка Settings восстановлена (2026-10-05). Полная загрузка
/settings(закладка, вхождение сайдбара, устаревшие навигационные элементы) перестала открывать диалог настроек с ревизии R-5 "настройки-как-диалог": при первом коммите auth-запросы были ещё в полёте (user = null), а эффект close-on-logout — объявленный после открывателя в том же компоненте — заново закрывал его в том же батче (открыть-потом-отменить), и пост-загрузочный путь уже был нормализован, так что ничего не переоткрывалось. Мягкая навигация (меню пользователя) не тронута. Открыватель теперь только на LOADED сессии, а нормализация сохраняет query-строку, так что?tab=-глубокие ссылки переживают монтаж диалога;?tab=-закладки также ВХОДЯТ в целевую секцию (та же рука, что клик по записи: страница группы открывается и запись помечаетdata-active).e2e/settings-left-menu-nav.spec.tsснова зелёный. - Проверено также не-баг:
POST/DELETE /api/system/settings/branding/icon= маршруты ENTERPRISE-плагина (simmetric-enterprise/brandingRoutes, Phase 145,mountProtected+adminLicense("white_label")) — community-сборка отвечает 404 и UI деградирует с документированным upgrade-тостом по design (grep-стража D-12whiteLabelExtractionGuard); EE-батарея 37 люксов / 737 тестов зелёные, community-стража зелёная. - Панель Settings больше не разлогинивает не-админ пользователей, открывающих её. Роутер настроек был забарьерирован от края до края
requireAdmin(правоadmin:settings), тогда как решётка видимости по-роли (SETTINGS_TAB_PERMISSIONS/RoleSectionVisibility) всё ещё рендерила секции — Профиль, Внешний вид, LLM-провайдеры, Продвинут — обычным пользователям. Открыть панель (или секцию) = оболочкаSettingsPageопрашивалGET /api/system/settings, получая 403, и страница путала 403 с 401: показывала "Session expired. Please log in again.", стирала токен и редиректила на /login 3 секунды спустя — уничтожая совершение валидную сессию из-за простого зазора права. Двухчастный фикс (2026-10-04 "settings-panel logout"): - Сервер:GET /api/system/settingsтеперь право-осведомлён. Админы (носителиadmin:settings) сохраняют полный консольный payload byte-идентично как раньше; каждый другой аутентифицированный пользователь получает только подмножествоNON_ADMIN_SETTINGS_READ_KEYS(общее) — не-чувствительные ключи, потребляемые не-админ-достижимыми поверхностями (BRANDING_* для вкладки Внешний вид / заголовок app,ALLOW_NON_ADMIN_UPLOADдля страницы Загрузок) — каждаяreadOnly: true(анти-несходство: никогда не рендерить форму, которую сервер отклонит 403). Секреты (SMTP_PASS,LLM_API_KEY,JWT_SECRET,DATABASE_URL,VECTOR_DB_API_KEY, ...) никогда не могут достичь не-админ ответа. Org-обзор (?organizationId=) и все маршруты записи (PUT /,POST /mail-test) остаются admin-единственными (fail-closed). - Фронтенд: только 401 (автухнение провалилось) разрушает сессию — в 3-секундном редирект-рукеSettingsPageи вuseMe-ошибочном рукеApp.tsx. 403 теперь рендерит отличимый баннер "У вас нет прав на эти настройки." (settings.errorNoPermission, все 8 языков) и сессия выживает. - Ingest Collectorа: файлы, загруженные с текстоподобным расширением (
.txt.md.csv) но несущие PDF magic bytes, больше не индексируются как сырой UTF-8 (инцидент AI-ACT.txt от 2026-10-01). Переименованную в txt и прочитанную как текст PDF векторный индекс получал только несжатыми PDF-внутренностями (Info-словарь, XMP-метаданные, словари объектов, элементы структуры) — 3321 чанков, ноль нормативного текста — тогда как корпус внутри FlateDecode-сжатых потоков деградировал в U+FFFD-мусор.parseFileтеперь нюхает%PDF--заголовок (первые 1024 байта, окно от ISO-32000-мусора) у textобъявленных файлов и направляет к PDF-парсеру; метаданные чанков получаютroutedAsPdf-штамп, а ложноположительный нюх (текстовый файл, лишь упоминающий "%PDF-" в начале) падает на текстовый парсер вместо провала ingest.
Added
- Коннекторы: существующие коннекторы теперь могут быть ИЗМЕНЕНЫ из админ-UI (Q1-quick) — существовавшая рука
PUT /api/connectors/:idнаконец всплыла: меню переполнения карты получает Редактор (первая запись) — открывающий заполненный заранее диалог над изменяемой поверхностью (имя, привязка архива знаний, приветственные/фолбэк-сообщения, fallback locale, ограничение скорости + дневной лимит сессий — пусто = null,0= UNLIMITED-сентинел роутера).isEnabledостаётся на Switch карты, TelegrampollModeна webhook-кнопках, а платформа/botToken/workspace остаются неизменяемыми по схеме. Рука PUT получает также аудиторное событиеchat_connector.updated(наравне с рукой DELETE, D-21). - Phase 221 (INSTLIC) — вставочная щель лицензии инстанса (upgrade enterprise без .env): новая админ-поверхность
GET/PUT/DELETE /api/license/instance(requireAdmin + tenant-мидлвьюэр; стекирование по-руке сохраняет GET /info и /diagnose нетронутыми) плюс панель Settings → Security → Лицензия. Вставка зонд-first-проверяет незаявленный RS256 JWT уровня ENTERPRISE (отклонённый ключ никогда не персистирует: закрытое перечисление{ error, reason }— invalid / expired / plugin_bound / community_tier), хранит ТОЛЬКО шифротекст AES-256-GCM в SystemConfig (ключINSTANCE_LICENSE— SMTP_PASS-дисциплина записи), инвалидит Redis-конфиг-кэш D-07, и применяется к ИДУЩЕЙ инстанции через ТОТ ЖЕ резолвер, что boot-шаг пробегает (setCachedLicense — инжектированные загрузчиком лимитные переступания выживают; булевы флаги оживают немедленно; очистка вставки возвращает next-boot состояние с boot-паритетом). Прецедентность, boot и runtime идентичны: щель → проверенная plugin-строка → переменная среды LICENSE_KEY → Community. Массовая запись PUT /api/system/settings отвергает ключ (доктрина выделенной писающей руки для chat-хранения) и листинг настроек никогда не выставляет шифротекст. - Phase 219 (SETP-01/02) — флаги видеть + изменять по-роли для каждой секции Settings: аддитивный
RoleSectionVisibility.canModify(отнимается от фолбэкаSETTINGS_SECTION_MODIFY_PERMISSIONS— никогда не даёт,admin:settingsконцевая),settingsModifyвGET /api/roles/me/menu-sections, серверный барьер на мутациях секции, отдающий403 { error, section }(мутации/api/providers→llm;POST /api/agency/users→team) и Изменить-колонка в визибилити-редакторе ролей (PUT /api/roles/:id/visibilityпринимаетcanModifyпо записям — оставить-как-есть когда отсутствует).
Fixed
- Диалоги подтверждения удаления сайдбара больше не перелейстируют (Q5-quick): подтверждения удаления чат-/папок были коробками
w-80, чья кнопка ui-kit =whitespace-nowrap shrink-0ПО ДИЗАЙНУ — длинные локализованные этикетки действий папки ("Mantieni N chat…" / "Elimina N chat permanentemente") убегали горизонтально из коробки, а длинные интерполированные имена папок переполняли титул. Коробки теперьw-[22rem] max-w-[calc(100vw_-_2rem)](шире + крыты вьюпортом), ряды кнопокflex-wrap, две длинные этикетки действий отталкивают nowrap (whitespace-normal text-left), а интерполированные титулы/тексты несутbreak-words. Три недостающих i18n-ключа диалога удаления чата (sidebar.deleteChat/.deleteChatConfirm/.deleteChatButton) вышли во всех 8 языках — итальянский интерфейс больше не показывает английские inline-фолбэки там. Q5-последствие: первая крышечка никогда не компилировалась —calc(100vw-2rem)внутри произвольного значения = НЕВАЛИДНЫЙ CSS (Tailwind требует подчёркивочный идиомcalc(100vw_-_2rem)для пробелов вокруг операторов), и правило было никогда тихонечно не выдано, оставляя 352px-коробку некрытой на узких вьюпортах. Исправленная утилита выходит в собранный CSS (max-width:calc(100vw - 2rem)проверен в dist-бунделе). - Разрешение модели: чат-ходы больше не приземляются на hardcodированный дефолт
qwen2.5:3bкогда нет эксплицитного пина (Q4-quick — корневая причина инцидента четырёх чатов: свежие чаты обслуживали маленькую локальную модель вместо дефолта workspace, 34-98с за ход). Два исправления: (1) дефолты схемыChat.modelиWorkspaceAgentConfig.modelсменились с"qwen2.5:3b"на сентинел""(аддитивная миграция —ALTER COLUMN … SET DEFAULT, существующие строки нетронуты): falsy-цепочка (model || chat.model || undefined, orchestrator:388/1059) теперь пробегает к agent-config-цепочке workspace ровно как"default"-брат-сентинел; (2) per-Workspace-localStorage модельная преференция теперь ТОЛЬКО-ЭКСПЛИЦИТНА — выбор из палитры пишетexplicit: true, autore-persist при reseeding исчез (разрешённый цепочкой дефолт не пользовательский выбор и не должен становиться липким), а унаследованные без-маркерные значения чистятся один раз и никогда больше не берутся как кандидаты. Прослывшая преференция (напр.qwen2.5:3b, всё ещё валидна-но-медленна в регистре) больше не может тень дефолт workspace на каждом новом чате. - Чат: "одно сообщение открывает N чатов" — клиент принимал серверный chatId только на SSE
done-событии (до ~90с позже на tool-богатых ходах), так что пользовательский retry в том окне отсылал chatId-менее и сервер СОЗДАВАЛ НОВЫЙ чат на retry, а идущий ход заканчивался в фоне осиротевшей строкой (Q3-quick). Stream-маршрут теперь эмитит аддитивноеchat-событие, несущее chatId В НАЧАЛЕ хода (перед любым токеном); клиент принимает немедленно (охраняемо + идемпотентно — посылка, уже несущая chatId, никогда пере-принимает) и обновляет сайдбар враз. Retry-и теперь приземляются в ОДНОМ чате. Byte-идентично для до-Q3 клиентов (неизвестые SSE-события игнорируются). - Вспоминание памяти: пара (пользователь, workspace) с НУЛЁМ сохранённых памятей теперь пропускает весь pre-LLM recall-хук (Q2-quick) — индексированный PostgreSQL счёт (
@@index([userId, workspaceId])) ставит барьер хуку перед любой работой: никакого запроса эмбеддинга, никакого collector-круга, и больше никакого per-ход[vector-store] Table "ws_user_memory_…" not foundварнинга с LanceDB-таблицы, законно не существующей до ПЕРВОЙ записи памяти (vectorStore-овскийensureTableсоздаёт при первом ingest; рука поиска падает открыто на []). ДБ-заикание в счёте падает открыто на до-барьерный recall (контракт хука = best-effort — никогда не блокировать чат). - shared: стража регрессии
FEATURE_FLAGSосталась позади от коннекторного EE-перехода (quick 260930-l4q) — пин поднят 12 → 13 дляconnectors_enabled.
Fixed
- Backup/восстановление: пробный прогон или восстановление, падающее с "Unable to decrypt (no key in chain matched)" (destination-учётные данные, записанные под прежней ENCRYPTION_KEY), теперь возвращает ведомую ошибку, называя причину, рычаги восстановления (LEGACY_PREVIOUS_ENCYPTING_KEYS / прежнюю ENCRYPTION_KEY, затем рестарт), число попробованных ключей, и runbook docs/ENCRYPTION_KEY_ROTATION.md — который получил Docker-раздел восстановления "Backup restore dry-run fails". Раскрывается только длина цепи ключей, никогда ключевой материал.
- Phase 220 (SPLIT-01/02) — coherence of the repo-split: docs/REPO_BOUNDARIES.md (матрица аудита модуль→репо для 4 репос с доктриной классификации по 7 критериям и установленными исключениями) + репо-борьба-грань: scripts/repo-boundary-check.cjs (grep-барьер по allowlist — ядро не должно ссылаться на приватные plugin-пакеты вне двух loader-швов и документированных контактных точек) + самопроверка scripts/repo-boundary-check.test.cjs + CI-работа repo-boundary-check wiring в build.needs
Added
- UI: полная ревизия оболочки (R-2 → R-5). Пустое состояние чата показывает элегантный монохромный словесный знак (глитч циан/магента анимация теперь ограничена хакерской темой); сайдбар становится минимальным Claude-рельс (новый чат + список чатов + нижнее меню) с полной навигацией в клавиатурно-дружественном cmdk оверлее (Cmd/Ctrl+/), сохраняющем точную RBAC-фильтрацию; меню пользователя переехало с верхней полосы в выделенный диалог (аккаунт, язык, тема, настройки, выход) с левым ящиком на мобильном; Settings открывается как большой мастер-деталь диалог (левое меню + полный детальный с рельс-переключателем и хлебными крошками) вместо роутед-страницы — главная область теперь только чат на каждой странице. Темы (light "Pearl", dark, hacker, system) получили согласованность с WCAG-AA контрастными исправлениями для приглушённого текста и чата, prefers-reduced-motion стражами для всех оверлеев и white-label-осведомлённым брендингом на словесном знаке.
- Docker: опциональный searXNG метапоисковый сервис добавлен в compose-стек (чистая инфраструктура — ничего от него не зависит пока; прикреплённая release-метка, healthcheck на wget, настройки авто-провижинены entrypoint'ом образа).
- Docker: приватный @simmetric-chat/saas плагин теперь монтируется в compose-сервер как enterprise-плагин (bind mount целого соседского репосitorium; saasLoader no-ops на MODULE_NOT_FOUND в community-режиме).
Fixed
- Seed: crash загрузки во время посева пользователей мог стереть роли и забарrikировать админа; seed-и теперь самолечатся — идемпотентная выдача заново применяет админскую роль к пережившему admin-пользователю на каждом boot (протоколировано в отложенных STATE.md пунктах).
- Tenancy: POST /api/auth/register теперь коммитит создание пользователя, выдачу роли по умолчанию и членство дефолт-org в единой транзакции, и каждый seed/boot путь заново пробегает идемпотентный membership-хелпер в своей "пользователь уже существует" руке — крах среди boot/запроса больше не может оставить пользователя без членства (обзор Phase 182 WR-01); надгробное воскрешение теперь вносит roleInOrg вызывающего, так что удаление понижает привилегии (WR-02).
- Enterprise-сборки: секция Settings → Advanced → Backup (и
sso/logsмаршруты) больше не остаются забарьерированными за "requires Enterprise" карточкой после app-логина или SSO-логена. Запрос манифеста enterprise-модулей (GET /api/enterprise/modules) теперь auth-забарьерен и реактивен сессии: прежде он загружался один раз, неаутентифицированным, при boot приложения и кэшировался на всю сессию (retry: false + staleTime: Infinity), так что на экране логина давал 401, а Enterprise-функции оставались скрытыми до полного перезагрузка С хранимым токеном. Хук теперь подписан на useMe() и перезагружает манифест, когда появляется сессия (парольный логин, SSO ?token= хендофер), и никогда не пробует endpoint без сессии вообще. - Docker: продакшн-помощник provision-encryption-key.sh больше не загоняет сервер в краш-петли, когда корневой .env разворачивается с незаполненным .env.example шаблонным заполнителем (<sostituire-con-valore-generato>) как API_KEY_HMAC_SECRET/ENCRYPTION_KEY значение — заполнитель теперь распознаётся как "не установлен" (громкое предупреждение) и provisionинг проваливается к restore/генерация рукам. Люой другой незаполненный невалидный значение всё равно проваливается громко на boot-баре.
- Безопасность: E2E helper-роутер (/api/__tests__, неаутемтифицированный старт/стоп эхо MCP-сервер) больше не монтируется, когда сервер идёт с NODE_ENV=production — продакшн-боots отвечают там 404, поддерживая документированный dev/test-only контракт роутера (нет неаутемтифицированного процесс-спавн endpoint в продакшне).
- Безопасность: контроль callback'a collectorа импорта архива (PUT /api/archives/import/:jobId/callback) теперь сравнивает X-Collector-Secret заголовок в константное время (crypto.timingSafeEqual с длиньй стражей), уравнивая timing-безопасную дисципплину, уже используемую статусcallback'ом документов и секретной барьерой collectorа — закрывая последнее plaintext-сравнение секрета на сервере.
- Authentification: POST /api/auth/login теперь валидирует своё тело на уровне маршрута через общий loginSchema (safeParse) — невалидное или пустое тело возвращает 400 {error, details} в согласовании со всем другим auth-маршрутом, вместо 401 с голым Zod-сообщением. Правильые-но-не те учётные данные всё ещё возвращают 401 "Invalid credentials".
- Authentification: login теперь пробегает манекенный bcrypt.compare против фиксированного выброшенного хэша, когда пользователь не найден, так что время ответа больше не различает существующий от несуществующего ника (укрепление перечисления). Обе руки всё ещё возвращают то же 401 "Invalid credentials".
- Authentification: POST /api/auth/admin-reset-password теперь валидирует своё тело с новым общим adminResetPasswordSchema (userId UUID + newPassword из 8-128 знаков, safeParse → 400 {error, details}) — последний auth-маршрут съезжает с ad-hoc деструктурированной телесной валидации.
- Authentification: закрытая-регистрация ветвь POST /api/auth/register больше не дублирует auth-логику inline — она использует статически импортированные verifyToken / isTokenRevoked / isAdmin / getCachedUserWithRoles (нет динамического await import, нет мёртвой заголовочной перепроверки, закэшированный пользовательский просмотр), сохраняя каждую форму отклика и возложение-revoke-до-просмотра порядок.
- Безопасность: cache-bust endpoint виджета (POST /api/config/:widgetId/cache-bust) теперь сравнивает X-Api-Key заголовок в константное время (crypto.timingSafeEqual с длиньй стражей) и проваливается закрыто, если ожидаемая клей не установлена, вместо прежнего plaintext-сравнения с fail-open expected && формой — клейконтроль виджета теперь походит на timing-безопасную дисципплину серверно- и collector-секретных барьеров.
- Release-конвейер: все пять Docker-образов теперь собираются только-amd64. QEMU-эмулированные linux/arm64 сборы вились по ~5ч (widget первый, затем all-in-one pgvector исходная компиляция) и исчерпывали диск раннера платных минут частного repo; setup-qemu удалён, каждая матричная запись публикует linux/amd64, и buildmatrix развязывается до max-parallel: 2. arm64 пользователи могут собрать локально сами (Dockerfile кросс-сборк-безопасны) или приложить self-hosted arm64 раннер — см. arm64 примечание в docs/DEPLOYMENT.md.
Changed
- Инструментарий: dead-code барьер теперь работает в CI (knip) и проваливается на неиспользованных файлах, зависимостях или экспортах вне документированных allowlists; repo-широкая выметка удалила ~3400 строк мёртвого кода, генерация widget API-ключа теперь живёт за стандартным admin-потоком, и оба i18n-скрипта репортят defined-но-нe-использованные translation-ключи как неблокирующий cleanup-наводку.
- Виджет: пер-виджетная response-модель — админская конфигурация каждого виджета теперь может выбрать LLM-провайдера + модель, обслуживающую чат-ответы этого видшета (responseProviderId/responseModel, аддитивная миграция add_widget_response_model). Пин разрешается на сервере из DB-строки видшета на каждый чат-запрос (никогда клиент-подаваемый — прокси видшета и визиторский UI нетронуты), имеет приоритет над workspace-дефолтом, и неустановленный виджет держит существующую workspace/глобальную цепочку разрешения. Виджет-чаты теперь записывают модель, действительно их обслуживаемую.
Added
- RAG: метаданные-фильтрация для rag_search — агент теперь может сузить retrieval на document types (documentTypes: pdf/md/txt/csv/docx/xlsx) и ingest даты (dateFrom/dateTo, ISO; дата-единственная верхняя граница включает весь день), исправляя режим сбоя "2023-документы для 2025-вопросов". Фильтры пробегают гибридный поиск и входят в обе retrieval-ноги: векторная нога достигает collector как истинные пре-фильтры на штампированных ingest-метаданных (pgvector JSONB предикаты + Qdrant payload must-клаузулы; LanceDB и Chromа игнорируют новые клей с залогированным предупреждением), а FTS-нога получает параметризованные SQL-предикаты на документной таблице. Серверный пост-выбор backstop против авторитетной таблицы документов гарантирует отфильтрованную правильность на каждом провайдере. Документы, ingested с этого момента всегда фильтруемые; наследованные векторы (до штампования) исключаются пока фильтр активен и regain filterability via admin re-embed — никаких изменений схемы, никакого ре-эмбеддинга существующих документов, и поведение без фильтров не меняется.
- DLP: четыре новых встроенных pattern'а для общих европейских (особ. итальянских) идентификаторов, посеянных идемпотентно data-only миграцией и отражённых в DB-вниз fallback наборе — it_vat_iva (Partita IVA; привязанный к этикетке, так что голая 11-цифренная цифра никогда не мачет), it_codice_fiscale (16-знаковый codice fiscale; только-заглавные, прозаические слова как "foschi" никогда не мачут), iban (страновой префикс + длина ≥ 15; компактная и пробель-сгруппированная формы, линейное время/ReDoS-безопасное), и eu_phone (телефон IT/EU; засеянный DISABLED для admin-проверки, поскольку любая структурная телефонная regex флаггует заказ-/ссылочные цифровые пробеги). Builtin-ы теперь 10 всего; существующие развёртыванй получают новые строки при migrate deploy с ON CONFLICT DO NOTHING реплевой безопасностью.
- Чат: админы теперь могут вскрыть или заново скрыть DLP-редактированные совпадавшие тексты по всей беседе сразу, через новый "Show DLP texts" / "Hide DLP texts" item в (+) меню more-actions чат-входа (глаза Eye/EyeOff, показанные только админам, поскольку только они могут развернуть DLP-уведомление). Предпочтение session-объёмное, по умолчанию скрытое, и применяется к каждому уведомлению, включая только что пришедшие сообщения; пер-уведомление глаз-кнопка всё ещё работает для ручных per-сообщение переопределений между глобальными тумблерами.
- DLP pattern-конфигурация: шесть DLP pattern'ов теперь DB-закреплены (dlp_patterns таблица, засеянная идемпотентно миграцией, так что существующие развёртывания получают built-ins) вместо hardcodед. Админы управляют ими в Settings → Advanced → DLP patternх: включить/отключить любой pattern по строке, переозначить built-ins (их regex заморожен — попытки модифицировать дают 400), и добавить до 50 пользовательских pattern'ов (snake_case имя, regex-источник с compileвалидацией при сохранении, флаги, замена) с живым тестовым превью, показывающим совпавшие сегменты и отредактированный текст. Сканы, которые могут загрузить DB набор — фильтр-плugin вход/выход и конечная ответная вспышка — используют настроенные patternы через 5-минутный TTL кэш с per-pattern кэшированием компилированных regex, падая на встроенный набор, когда база недостижима; token-по-token streaming всполох сохраняет builtin pattern'ы (документированное v1 ограничение). Вся панель локализована во всех 8 языках.
- DLP ролевая bypass: новая системная настройка DLP_BYPASS_ROLES (JSON массив имён ролей, дефолт []) позволяет админам изъять выбранные роли из ВСЕГО DLP сканирования и редактирования — фильтр-плugin вход/выход и streaming поступательный-всполох core (включая RAG-контекст скан и думание-редактирование, как для JWT-чата, так и для виджет-сервис-аккаунта) чествуют тот же барьер. Каждый обходной пробег запускает fire-and-forget dlp.bypassed аудиторное событие, записывающее КТО обошёл (пересечённые роли) и поверхность происхождения — никогда гсканированное содержание. Админ-UI: multi-select ролей с предупреждением об экспозиции в Settings → Advanced → DLP, и "Bypassed" метка действия в DLP Match History панели, локализованные во всех 8 языках.
- DLP аудиторное tagging и фильтрация источника: каждое dlp.* событие несёт source: "chat" | "widget" в свои метаданные — общее SSE stream ядро выводит таг из виджетного X-Widget-Id заголовка, так что виджет-беседы редактируются как чат-иес, но теперь различаются в DLP Match History панели (Settings → Advanced). Панель получает клиентские фильтры для источника (Все/Чат/Виджет), ID чата и пользователя.
- Админ_SETTINGS UI для upload-драфт reaperа: новая секция в Settings → Maintenance экспонирует включённый тумблер (upload_draft_reaper_enabled) и cadence cron выражение (upload_draft_reaper_cron) через PUT /api/system/settings, с клиентским пятиполым cron-формочепрека (сервер держит своё warn-and-fallback поведение для невалидных значений), локализованными успех/провал тостами, и описаниями во всех 8 языках. .env пример документирует reaper ключи как env-var переобъятия (upload_draft_reaper_enabled, upload_draft_reaper_cron, upload_draft_retention_days).
- Конфигурация: окружение-конфигурация теперь определена однажды в общем Zod schema (@simmetric-chat/shared), эффективная прецеденция документирована и test-пиннена как DB > ENV > deфолт (правки settings-UI выигрывают немедленно; 8-случая матрица), и ключи, которые будут игнорированы, потому что env-вар их перекроет, показываются с envOverridden знакомством в settings-UI.
- Конфигурация: repository-корень .env теперь единая runtime-конфигурация для сервера, collectorа и видшета — новый ноль-зависимость loader (loadRootEnv()) мергеит её под per-package .env переобъятиями (legacy), с once-per-boot deprecation предупрежением, перечисляющим аффектированные клей имена (никогда значения); Docker и Tauri выравнены (compose env_file указывает на корневой файл, никакой имаж не печёт секреты).
- Конфигурация: все три package .env.example файла регенерированы из их Zod схемов (83/15/6 ключи, сгруппированы, дефолты синхронизированы) с Jest tripwire'ами, проваливающимися, когда будущая schema-клей остается недокументированной; raw process.env чтения, хранимые вне схемы (HF/openai-причуды, encryption/HMAC ключи, LOG_LEVEL, test gates), пиннены 44 поведенческими guard-пробами.
- UX: удаление workspace теперь спрашивает стандартный подтверждающий диалог (был inline мини-confirm), workspaces поддерживают multi-select bulk delete с подтверждающим диалогом и результатным тостом, подсчётывающим пропущенные строки (нет-прав строки пропускаются, никогда тихий провал), OCR превью бок-о-бок держит вытянутый текст, привязанный к той же странице-срезе, что и image-панель, и синтез-прогон деталь показывает апрув-кнопку отключённой с явной причиной на прогонах, которые еще не могут быть апрувлены.
- UX: workspace выборочные чекбоксы и bulk delete локализованы во всех 8 языках.
Changed
- Upload-draft reaper теперь вполне конфигурируем через системные настройки: enabled/disabled тумблер (upload_draft_reaper_enabled, дефолт "true") и cadence переобъятие (upload_draft_reaper_cron, дефолт ежедневно 03:00 UTC), оба управляемые через PUT /api/system/settings. Невалидные cron значения логируют предупреждение и падают на дефолт cadence вместо провала boot'а; дефолтное поведение без изменений.
Fixed
- Исправлен Docker продакшн boot crash-loop, где server start умирал с unique-constraint failure на api_keys.prefix: виджет API-ключ seeder теперь деривует дисплейный prefix с HMAC digest'a и толерирует prefix'овые коллизии (re-проверка от key hash'а, warning вместо crash'а), и API-key generation retry'ит свежым ключом при prefix-коллизии (макс 3 попытки).
- Чат-цитации теперь отражают источники, на которых ответ реально основан, вместо того, что retrievаль-инструменты подхватили: после цитатной дедупликации grounding-фильтр каппит цитации per documento на top-2 по релевантности и дропает chunk-цитации, чей текст не делит значимого лексикального наложения с финальным ответом (консервативный барьер — wiki/архивные страницы, web/память результаты, цитации без chunk-текста и очень короткие ответы всегда сохраняются). "Нет больше info"-ответы больше не показывают "Fonti (5)" одного документа при ~1.5% релевантности; "Нет поддерживающих доказательств" дискеймер судится аналогично на отфильтрованном наборе (и streaming, и non-streaming рукам). Только цитация-конвейер — никогда UI, i18n, или схема-взросление.
- Docker: ollama сервис healthcheck заменил curl пробу (ollama/ollama имаж не несёт curl/wget/нос — контейнер репортил "unhealthy" навсегда) с ollama ls пробой; qdrant healthcheck заменил wget пробу (имаж не несёт wget/busybox) с bash /dev/tcp сокет-проверкой — оба сервиса теперь репортят healthy. Единственно-контейнерный docker/entrypoint.sh теперь source'ит provision-encryption-key.sh (паритет с split имажем): продакшн стендэлоун-бооты авто-provision'ят ENCRYPTION_KEY + API_KEY_HMAC_SECRET вместо провала на баре. Все пять Dockerfile'ов lint'ятся чисты под hadolint'а (документированная непинненная-deps политика + placeholder-ENV waivers).
- DLP-аудит хронология-события больше не показывают "нет деталей совпадения", когда совпавший контент прибыл в ранний stream'ный chunk: streaming dlp.output_match событие теперь выводит свои match-типы И список совпавших текстов из совпадений, накопленных через весь run (все прогрессивные всполохи плюс финальный хвостовой скан) вместо одного только последнего chunk'ового скана — исторические строки могли хранить пустой matchTypes array, пока строка всё ещё показывала match-тип badge. Совпавший текст хранится по DLP filter-plugin прецеденту (dlp.input_match / non-streaming dlp.output_match уже несут его); аудит-панель admin-only и Enterprise-гаррaged. Развёрнутая вьюха аудит-панели теперь рендерит сдетектированный список типов с "type-only" намёком для событий, записывающих match-типы без совпадений (локализовано во всех 8 языках), удерживая "no details" fallback только для действительно пустых метаданных.
- Загруженные файлы остаются доступными для retry: collector'овые терминальные статусные callback'и больше не удаляют staged draft'евые файлы из storage/uploads/drafts/ (callback любого собратья-документа мог стереть файл, от которого зависит каждый retry), и /retry//assign самолечатся восстановлением staged файла из persistent'ной OCR копии (storage/ocr-sources/) когда присутствует, вместо сбоя "больше не существует на диске".
- Удаление провального устаревшего upload draft'а больше не возвращает заблокировано (409) — DELETE /api/uploads/:id теперь трактует терминальные (comплетнутые или провальные) leg состояния как удаляемые и отвергает только draft'ы с действительно in-flight (включённой нон-терминальной) leg'ой, и фронтендовый bulk delete использует то же in-flight предикат, что и filter'овые чипы, так что то, что UI предлагает удалить — именно то, что сервер принимает.
- RAG/KB retry загрузки с пропавшим staged draft-файлом теперь возвращает ясную ошибку re-upload'а (400) вместо фальшивого success-тоста, проваллые collector'овые ноги больше не удаляют staged draft-файл из storage/uploads/drafts/ (будущие retries остаются возможными — 24h reaper и DELETE маршрут остаются единственными draft-файл удаления), и retry-тосты теперь отражают per-leg исход: отвергнутая нога при HTTP 200 показывает локализованный error-тост и bulk retry считает её провалленной.
- Docker продакшн развертывания авто-provision'ят постоянную ENCRYPTION_KEY (серверно-сторедж вольюм, /app/storage/.encryption-key) вместо crash-loop'инга на жёстком дефолте. Серверный entrypoint генерирует ключ однажды перед любым Prisma-шагом, реставрирует его при каждом перезаголовке/ребилде, предпочитает оператор-подаваемое значение и fail-loud на корроптированном persistированном ключе. .env пример и docs/DEPLOYMENT.md обновлены, чтобы пометять переменную как Обязательно-в-продакшн. Смотрите docs/ENCRYPTION_KEY_ROTATION.md для Docker нот.
- KB OCR страницы больше не проваливаются, когда Ollama visionstream закрывается модельным движком без финального done маркера после полного транскрипта (наблюдаемо с кастомным glm-ocr движком): ocrPage теперь сальватит аккумулируемый неверный вывод как страницу-результат с truncated flag'ом и warning-логом, вместо выбрасывания реального транскрипта за "Did not receive done or success response in stream." Done-безный stream без содержания всё ещё проваливает страницу.
- OCR job summaries правдивы о здоровье страниц: страницы, заканчивающиеся [FAILED: маркером, считаются и хранятся как failedPages в job result JSON, и комплетион-логи эмитят [ocr] Job completed with failed pages предупреждение, так что COMPLETED job с проваленными страницами больше не неразличим от чистого успеха (ноль token'ов + floor качество score сами по себе были амбигвальны).
- Документировано и исправлено mode отказа для Enterprise развертываний: когда @simmetric-chat/shared получает НОВЫЕ исходные файлы, out-of-date enterprise file: зависимость-снимок может крáшить сервер при boot'е с "Cannot find module './env.schema'" — pnpm снимает file: пакет в node_modules/.pnpm и файлы, созданные после последнего pnpm install, никогда не достигают того снимка (hardlinks обновляют лишь уже-существующие файлы), так что schemas/index.js снимка требует модуль, не существующий на enterprise стороне, и fail-loud политика loader'а превращает провал регистрации плагина в boot crash-loop. Исправление отгружается как документация: enterprise repo'ский rebuild runbook теперь покрывает re-install-not-rebuild расстояние (рефреш снимкка через pnpm install, затем pnpm build), и air-gap установка-рунбук в docs/ENTERPRISE_PLUGIN.md получает сответствующую troubleshooting-подсеченцию (симптом, причина, фикс, sanity проверка), так что операторы без приватного repo всё ещё найдут remedy.
Долговой свып и релизная гигиена (было: v1.5.0)
Added
- docs/LICENSE_DECISION.md — анализ лицензионной модели для дуального дистрибуции (community + enterprise). Решение: AGPL-3.0 (community) + пропитарная коммерция (enterprise).
- LICENSE заменён с Apache-2.0 на GNU AGPL-3.0 (community repo).
- LICENSE_EE.md — пропитарная коммерческая лицензия для enterprise-плагина (@simmetric-chat/enterprise).
- NOTICE — двойное-лишенство объяснение в repo-корне.
- CLA.md — Contributor License Agreement (v1.0) — копирайт + патентный грант, двойная-лицензия-aware. Требуется для всех внешних вкладов.
- AGPL-3.0 копирайтные заголовки добавлены ко всем 981 исходным файлам через 5 пакетов (.ts/.tsx). Приложено через scripts/add-headers.cjs (идемпотент).
- docs/ENTERPRISE_LICENSE_TERMS.md — оператор-направленные коммерческие/SLA условия для enterprise-клиентов.
- .github/workflows/release.yml — GitHub Release + GHCR-имаж-публикация на tag push.
- CHANGELOG.md (этот файл) — Keep-a-Changelog формат, засеянный из milestone истории.
- .planning/milestones/v1.5-debt-sweep-ROADMAP.md — долг-оценочный milestone scope (E2E carry-forward, debug сессии, quick-задача triage, версия-штамп синхрон-дисциплина).
Changed
- package.json версия подкачана 0.17.0 → 1.4.0 (синхронно с последним git тегом v1.4). Per-package версии остаются на 0.22.0/0.23.0 (приватные пакеты, не опубликованы в любом registry).
- package.json + все packages/*/package.json license поле установлено в AGPL-3.0-or-later.
- README.md лицензный бейдж обновлён Apache-2.0 → AGPL-3.0; лицензная секция обновлена для описания дуально-лицензной модели; Contributing секция добавлена с CLA требованием.
- CONTRIBUTING.md обновлён с CLA подписывательскими инструкциями.
Горизонтальный масштаб (завершение Redis слоя) (было: v1.4)
Цель: закрыть 4 SCALE deferred (CSW-19 + SCALE-02/03/04) и доказать горизонтальное масштабирование реальным multi-instance интеграционным тестом — включив безопасное multi-instance серверное развертывание за лоад-балансером.
Вышло: 7 фаз (161-167), 13 планов, 29 задач. Все 22/22 требования удовлетворены. Git тег: v1.4.
Added
- SCALE-01 (CSW-19): 3 оставшихся шедулера lock-wrapped → затем все 8 мигрированы на pg-boss cron jobs; setInterval + isRunning + withDistributedLock удалены со всех 8 мигрированных шедулеров.
- SCALE-02: scryptSync ENCRYPTION_KEY fallback удалён в продакшне — fail-loud boot (logger.error + process.exit(1)) + defense-in-depth service throw; dev/test сохраняет scrypt; ротационный runbook обновлён.
- SCALE-03: bcrypt-loop API-ключа verification → keyed-HMAC O(1) findUnique({key_hash}) digest lookup; breaking DROP COLUMN hashedKey + ADD COLUMN key_hash миграция; API_KEY_HMAC_SECRET env-var; операторный миграционный runbook docs/API_KEY_MIGRATION.md.
- SCALE-04: 8 setInterval шедулеров мигрированы на pg-boss (Postgres-backed job queue, air-gap friendly); 2 латентночувствительных 10s poller'а (OCR + синтез) остаются setInterval; pg-boss singleton (jobQueue.ts) с грациозной деградацией.
- SSE fan-out verification: cross-instance deferral закрыта — 4 mock-based cross-instance теста, доказывающие Redis pub/sub SSE relay (A публикует → B релеит, origin-skip, teardown, деградация).
- Операторные docs: docs/SCALING.md (377 строк) — multi-instance развертывание топологии, REDIS_URL требование, pg-boss Postgres зависимость, SCALE-02/03 перекрёстные референсы, 10s poller'ы, грациозная деградация.
Прорывные перемены — операторное действие требуется
- ENCRYPTION_KEY должна быть установлена в production .env (base64, 32 байта) или сервер отказывает boot. Смотрите docs/ENCRYPTION_KEY_ROTATION.md.
- API ключи сменились с bcrypt (apiKeyHash) → HMAC-SHA256 (key_hash). Операторы должны выдать widget/collector API ключи заново. Смотрите docs/API_KEY_MIGRATION.md.
Инфраструктура
- Новая зависимость: pg-boss v12.28.0 (сервер). Новая env-переменная: API_KEY_HMAC_SECRET. Boot порядок изменился — pg-boss init теперь 8-й async init шаг с graceful-shutdown teardown.
Concerns Sweep (было: v1.3)
Цель: выпести 2026-08-26 CONCERNS.md аудит — исправить все легкие победы напрямую, атаковать medium-усилия items в фокусированных фазах, и оценить (defer'ить или документировать) blocking/большой-усилие пункты.
Вышло: 7 фаз (154-160), 14 планов. Brownfield strengthening milestone; ни одной новой user-facing функции.
Added
- docs gitignore фикс, chat.ts file map, lint rule флипы.
- code укрепление легкие победы (apiKeyMiddleware take:10 cap, и т.д.).
- фронтенд lint долг reduction (3 плана, warn→error флипы).
- ENCRYPTION_KEY продакшн-бум предупреждение (CSW-11) + батчированная generation заголовка+теги+followUps.
- модульные сплиты (провайдер parser извлечение, capability регистры).
- тестовое покрытие (chatImportService, chatExportService, apiKeyService, 5 оставшихся сервисов; collector + widget проволочены в root jest конфиг).
- оценено и документировано (.env лицензионные JWT entitlements, лицензия signing-key rotation runbook, устаревшая AGENTS.md claim фикс).
Рафинирование (было: v1.2)
Цель: брендинг, AI-дисклеймер, TypeScript-реимплементация графического wiki-алгоритма, RAG-поисковые фиксы, MCP-укрепление, Setup Wizard перво-запуск.
Вышло: 6 фаз (149-153 + 153.1).
Added
- BRAND-01/02: SVG S-монограмм favicon, повторно-используемый Monogram компонент, приглушенный локализованный AI-дисклеймер под каждым ассистент-сообщением (7 языков).
- MCP-01/02/03: MCP-укрепление — per-session SSE-транспорт Map, MCP_API_KEY Bearer auth барьер, list_workspaces IDOR закрыт, getMCPToolsForWorkspace проводочен в resolveSkillsForChat.
- RAG-01/02/03: FTS-локальный фикс (multi-locale токенизация для it/ru/de/es/fr/zh), дедупликация результатов rag_search↔wiki_query fallback'ов, LLM affordance prompt для rag_search vs wiki_query.
- WIKI-01: TypeScript-реимплементация Graphify wiki-алгоритма (community detection + wiki markdown генерация, Apache-2.0 чисто реимплементия, ~280 LOC Python → TS).
- WIZ-01/02: Setup-Мастер первозапускный UI (multi-этапный: admin счёт + провайдерная конфигурация + векторная БД, потребление /api/system/initialize); SEED_BOOTSTRAP_ADMIN отключается при активном мастере.
- .1: v1.2 тех-долг уборка (React Compiler warn→error флип).
Enterprise плагин-архитектура (было: v1.1)
Цель: refactor лицензионной модели с "runtime feature flags в общественном источнике" на "отдельный приватный enterprise плагин": enterprise функции (SSO, audit log, white-label, backup) живут в приватном пакете, доставляемом только платющим клиентам; community repo не содержит enterprise feature кода. Коммодитные функции становятся всегда-ВКЛ в community.
Вышло: 9 фаз (140-148), 19 планов, 26 задач.
Added
- EPA-01: PluginContext/EnterprisePlugin types в shared; loader шов loadEnterprisePlugin(app) / shutdownEnterprisePlugin() с two-step require.resolve→require (MODULE_NOT_FOUND = community no-op, другие ошибки = process.exit(1) fail-loud).
- EPA-02: 9 commodity флагов удалены из FEATURE_FLAGS (20→10): web_search, webhooks, push_notifications, memory, lead_export, widget_analytics, auto_title, synthesis_rate_limit. Commodity маршруты всегда-ВКЛ.
- EPA-03: SSO (SAML + OIDC + SCIM 2.0) извлечено в enterprise плагин.
- EPA-04: Audit log (неизменная event_log таблица + INSERT-only DB роль) извлечено в enterprise плагин.
- EPA-05: White-label брендинг (config-ключевой валидатор IoC hook) извлечено в enterprise плагин.
- EPA-06: Backup (~1400 LOC: backupService, провайдер-реестр, Bree шедулер, восстановление, маршруты, UI) извлечено в enterprise плагин.
- EPA-07: overrideFeatureLimit резолвер с реактивной ревокацией; GET /api/enterprise/modules манифест; React.lazy условная загрузка 4 enterprise панелей.
- EPA-08/09/10: priority_support удалён; custom_agents = численный лимит (community 3, enterprise Infinity); licensePayloadSchema byte-идентичен (RS256).
- EPA-12: docs/env выравнивание (docs/ENTERPRISE_PLUGIN.md, docker-compose volume mount, .env.example раскол), air-gap CI профиль (grep барьер на license service для HTTP примитивов).
Публичной выпуск-кандидат (simmetric-chat) (было: v1.0)
Цель: подготовить и опубликовать проект как чистый публичный repo, переименован в simmetric-chat, без секретов/личных данных/локальных путей, с лицензиями зависимостей, enterprise keygen инструментом (отдельный, не опубликован), и полной документацией.
Вышло: 8 фаз (132-139), 12 планов, 16 задач. Публичный repo опубликован, переименование завершено, лицензионный аудит, enterprise keygen (HS256 → мигрировано на RS256 после v1.0), канонические доки, CI GitHub Actions, чистый repo.
Widget UX, i18n и надёжность
Вышло: 7 фаз (125-131), 32 планы, 75 задач, 16/16 требования удовлетворены.
Added
- WID-ADMIN: Widget create/edit таб-страница (/widgets/:id + /widgets/new), live-превью панель, dirty-guard, вкладка leads.
- WID-I18N: JSON-блочный транспорт (убивает %20 баг), visitor→widget-дефолт→en fallback цепочка, 7-языковой chrome + content локализация, locale threading widget→proxy→сервер→orchestrator.
- WID-QUEST: три-состояния suggested questions (Default/None/Custom), 7-языковые группы, line-clamp overflow фикс.
- WID-CREDITS: футовые кредиты + AI бейдж, white-label gated удаление, real-embed поп-ап relay верифицирован.
- WID-BUGS: 44px input кнопки, host-FAB туда-вернуться, SVG-only иконный контракт, контакный баннер, wiki-петля, lead персистенция, RAG-деградация i18n.
Диагностика и Долги
Вышло: 5 фаз (120-124). Лицензная диагностика (boot логи, admin-only diagnose endpoит, pnpm license:check скрипт), тестовый долг (ESM drift, LoginPage QueryClientProvider, fixture-формы spec), тех-долг оценка (Redis-scale-слой спайк — deferred к v1.4).
Стабилизация
Вышло: 4 фазы (116-119).
2026-02 — Enterprise и Полировка
Вышло: 8 фаз (108-115).
Более ранние milestones (v0.1–v0.16) документированы в .planning/PROJECT.md (Milestone History секция). Полная фазовая история живёт в .planning/MILESTONES.md.