Пересмотр роли проектов: активность глобальна, проект — атрибуция [L1-3]
#1. Цель [L3-5]
Разделить четыре роли, которые сегодня неявно выполняет project_id, и убрать
жёсткую привязку активности (раны, триггеры, операции, вопросы) к проектам.
Проект остаётся доменом знаний и рабочих артефактов, но перестаёт быть
partition'ом для всего интерфейса и API.
#2. Ограничения предыдущей модели [L5-21]
Модель «всё фильтруется по текущему выбранному проекту» показала пределы:
- сущности без проекта (вебхук-триггеры, системный
intake) трудно найти: списки ранов, триггеров и вопросов фильтруются выбранным проектом; - открытые вопросы требовали кросс-проектного опроса вместо честной видимости;
- системный проект
intakeприходилось специально показывать/прятать в селекторе; - переходы между панелями требовали осознания «в каком я проекте», хотя пользователь думает категорией «что происходит в моей установке».
Общий корень: проект — обязательный разрез всего. Любой поток событий, приходящий без проекта (вебхук, сообщение в канале, расписание), нуждается в маршрутизации до того, как система сможет его показать.
#3. Что такое проект сегодня [L21-45]
Фактическая карта привязок (миграции 000018–000059, controlplane, UI):
| Роль | Где закреплена | Нужна ли после пересмотра |
|---|---|---|
| Домен знаний | knowledge: scope_kind=project по рождению, промоушен в org/organization | да — это главная роль |
| Рабочие артефакты | песочница .sandbox/projects/<id>/…, задачи/раны работают «в проекте» |
да — как контекст исполнения рана |
| Граница доступа | gate.Allow(..., project): пустой проект = 403 для не-админов; project_member; связи с org-юнитами |
нет — доступ через org-структуру и членство; проект остаётся фильтром |
| Граница учёта | kernel_run_quota_day, cost-ролапы по проектам | частично — проект остаётся разрезом отчётов, не границей лимитов |
| Partition активности | workspace_task/run/trigger.project_id NOT NULL, все списки UI |
нет — становится атрибутом-атрибуцией |
Журнал (observation_events.project_id TEXT NOT NULL DEFAULT '') уже мягкий:
события без проекта возможны. Это подсказывает направление.
#4. Принцип [L45-60]
Активность глобальна, проект — атрибуция.
- Ран, задача, операция, вопрос, доставка вебхука — глобальные сущности.
Каждая несёт
project_idкак атрибуцию «для какого домена выполнена работа», но списки не делятся жёстко по проектам. - Триггер — глобальная сущность с целью маршрутизации (routing target): в какой проект складывать порождённые раны и знания. Не «принадлежит проекту», а «настраивает маршрут».
- Знание — по-прежнему рождается в проекте (scope_kind=project) и промоутится в org_unit/organization. Здесь ничего не меняется.
- Таймлайн — глобальный поток журнала; проект — один из фильтров.
- Доступ к активности — через org-структуру (видимые юниты + членство в проектах), фильтрация на сервере, а не 403 на «не тот» запрос.
Инвариант: у рана, который трогал артефакты проекта, проект известен всегда (через задачу/триггер/классификацию) — атрибуция не становится опциональной для ранов, работающих с проектными данными. Опциональна только фильтрация.
#5. Модель доступа [L60-78]
Сегодня: пустой ?project= разрешён только принципалам с доступом во все
проекты (controlplane.Gate.Allow, контроль 403 «may not query across
projects»). Для остальных любое глобальное представление недоступно.
Целевое поведение:
- запрос списка без фильтра проекта возвращает пересечение с разрешённым: сервер сам вычисляет множество видимых проектов принципала (видимые org-юниты → связанные проекты ∪ прямые членства) и фильтрует;
Principalполучает не толькоAllowsProject(id) bool, но иAllowedProjects() []string— источник дляWHERE project_id = ANY($n);- пустой фильтр перестаёт быть ошибкой для скоупленного пользователя: он видит «все СВОИ проекты сразу» — это и есть ожидание «покажи что происходит»;
- точечный запрос чужого проекта — по-прежнему 403 (не расширяет доступ, а лишь убирает необходимость знать список заранее).
Для single-team установок (все видят всё) поведение вырождается в глобальные списки без фильтров — без отдельного режима.
#6. Триггеры и маршрутизация [L78-95]
- CRUD триггеров становится глобальным: список, создание, редактирование не требуют «быть в проекте»;
- у триггера появляется явное поле маршрутизации:
target_project_id(куда стартуют раны; пустое → классификация через intake-агента); - URL вебхука теряет обязательный
/{projectID}/: путь становится глобальным (/v1/webhook/{path}), старый формат — совместимый алиас; - доставки вебхуков — глобальный фид (проект-атрибуция в каждой записи);
- intake-агент остаётся классификатором «сообщение → проект», но перестаёт
быть проектом-«гетто», куда случайно падают раны: он не выбирается
руками, скрыт из селектора навсегда, а его раны видны в глобальном
списке ранов с атрибуцией
intake.
#7. Что меняется в UI [L95-109]
- Селектор проекта в хедере превращается из «режима» в «фильтр»: «Все проекты» по умолчанию + конкретный проект; выбор не обязателен для просмотра панелей;
- «Запуски»: глобальный список с колонкой проекта; фильтр по проекту — один из фильтров; вписываются и раны intake (атрибутация видна);
- «Триггеры»: глобальный список, у вебхука показан целевой проект; секция доставок — глобальный фид;
- «Операции», «Входящие», бейдж вопросов — уже глобальные (бейдж починен кросс-проектным опросом; здесь это становится нормой, а не обходом);
- «Знания» и их таймлайн остаются проектными (существующие промоушены org/organization поверх);
- запоминание «последнего проекта» из списка правок этой недели становится ненужным: «Все проекты» — стабильное состояние по умолчанию.
#8. Фазы [L109-126]
#Фаза 1 — списки без обязательного проекта (без смены схемы) [L112-116]
AllowedProjects()у принципала; серверная фильтрация пустого фильтра вместо 403 (журнал, human-requests, runs-листинг, триггеры, операции);- UI: селектор → фильтр «Все проекты»; колонка проекта в ранах и триггерах; intake скрыт из фильтра, но его раны видны в глобальном списке.
#Фаза 2 — триггеры глобально [L116-121]
target_project_idв конфиге триггера (миграция: разрешить NULL-语义 через отдельное поле или оставить project_id как цель маршрутизации);- глобальный путь вебхука + алиас старого;
- глобальный фид доставок.
#Фаза 3 — разгрузка учёта и очистка [L121-126]
- квоты/лимиты на org-юнит/установку, проект — разрез отчёта;
- удаление обходов этой недели (тег «система», кросс-проектный бейдж как специальный код);
- intake-проект окончательно системный: не выбирается, не отображается в фильтрах, раны видны глобально.
#9. Критерии готовности [L126-140]
#Фаза 1 [L127-131]
- свежая установка: после онбординга сразу виден глобальный список ранов без выбора проекта;
- скоупленный пользователь видит раны всех своих проектов без перебора селектором; чужие — нет;
- созданный «не в том проекте» триггер и его раны находятся поиском без знания о проекте.
#Фаза 2 [L131-135]
- создание вебхука не требует проекта; раны маршрутизируются по target_project_id или классификации;
- старые URL вебхуков работают.
#Фаза 3 [L135-140]
- квота не «утекает» через перебор проектов; отчёты по затратам агрегируются по установке/org-юниту с разрезом по проектам.
#10. Открытые вопросы [L140-150]
- Оставлять ли
workspace_run.project_id NOT NULL(атрибуция всегда известна) или разрешать пустой для «внерейловых» активностей — предложение: оставить NOT NULL, атрибуция обязательна. - Миграция существующих триггеров:
project_id→target_project_idодин-в-один, без изменения поведения доставок? - Нужен ли режим «жёсткой изоляции проектов» для потенциальных мульти tenants-установок (установка=компания — текущее предположение; если появится SaaS-мультиарендность, изоляция вернётся на уровне установки, не проекта).