Перейти к основному содержимому

ADR 0066 — Читаемое имя папки workspace

Статус​

Принят.

Этот ADR пересматривает ADR 0055, который остаётся принятым и не редактируется. 0055 ввёл projectScopeRoot как ЕДИНСТВЕННУЮ функцию, выводящую <PROJECT_WORKSPACES_ROOT>/<projectId>, — общую, потому что permissions.json и область терминала (RN-075) должны соглашаться в том, где находится папка проекта. Этот документ меняет то, ЧТО подставляется вместо <projectId>, сохраняя гарантию того, что два вывода (api и engine) продолжают совпадать.

Контекст​

Физическая папка каждого проекта на диске называлась чистым UUID (<raiz>/3f2b1c8e-0a5d-4f6b-9c1e-2d7a8b3c4d5e/) — тем же projectId, что идентифицирует строку в базе. Просматривая папку в Finder/Explorer (что docs/getting-started.md стал позволять, направляя PROJECT_WORKSPACES_HOST_DIR на реальный путь на диске), пользователь видел список UUID, не имея способа узнать, какая папка какому проекту соответствует, не открывая каждую.

Запрос: ЧИТАЕМОЕ имя папки, основанное на slug проекта, с сохранением уникального id. Были рассмотрены два механизма (см. «Рассмотренные альтернативы»); пользователь явно выбрал переименование физической папки вместо symlink, указывающего на нетронутую папку с UUID.

PROJECT_WORKSPACES_ROOT — ОДИН корень на весь экземпляр, ОБЩИЙ для всех workspaces: в двух workspaces может быть по проекту со slug api. Один slug не уникален глобально, и именно это делает «уникальный id рядом с читаемым именем» реальным требованием, а не капризом.

Решение​

1. projects.workspace_dir_name — имя папки, записанное в базу​

Новый столбец workspace_dir_name (text, NOT NULL, UNIQUE) в projects. projectScopeRoot (ADR 0055) начинает получать это значение и БОЛЬШЕ НИКОГДА голый projectId — оба потребителя, которых защищает функция (permissions.json и область терминала), продолжают выводить ОДИН И ТОТ ЖЕ корень, потому что продолжают вызывать одну и ту же функцию, только с правильным аргументом.

2. Новый проект рождается с <slug>-<8 chars do id>; старый проект сохраняет UUID​

workspaceDirNameFor(id, slug) (чистая, в project-workspaces-root.ts) составляет имя — то же соглашение из 8 символов, которое apps/web/src/lib/session-label.ts уже использует для метки сессии. CreateProjectUseCase начал генерировать id в КОДЕ (crypto.randomUUID(), а не defaultRandom() Postgres): имени папки нужен id ДО INSERT, и есть лишь два способа получить его — сгенерировать id вне базы или сделать два обращения (вставить, прочитать сгенерированный id, UPDATE). Первый проще и имеет прецедент в самом коде (token-factory.ts, emitir-sessao.use-case.ts).

Проект, созданный ДО этой миграции, сохранил физическую папку, которая у него уже была: миграция ставит workspace_dir_name = id для каждой существующей строки — то же значение, что уже было правдой на диске, — и никогда не переименовывает ни одного каталога. Переименование рабочего дерева, возможно открытого активным агентом, — реальный риск без необходимости: старый проект продолжает работать ровно так, как работал.

3. Имя ЗАМОРАЖИВАЕТСЯ при создании и никогда не пересчитывается​

UpdateProjectUseCase позволяет редактировать slug позже — и это не трогает workspace_dir_name. Альтернатива (пересчитывать имя при смене slug) потребовала бы перемещать каталог с рабочим деревом и worktrees агентов, возможно используемыми, а ценность «заморозки» как раз в том, чтобы не обрабатывать этот случай: физическая папка определяется один раз и навсегда.

4. Trigger BEFORE INSERT — страховочная сеть, а не основной путь​

CreateProjectUseCase всегда записывает workspace_dir_name явно, ДО INSERT, — это реальный путь. Trigger (projects_workspace_dir_name_default_trg) применяет ТОТ ЖЕ fallback (id::text) для любого INSERT, пришедшего без поля. Решение добавить trigger — первый за всю историю migrations продукта — не входило в исходный запрос, и стоит зафиксировать причину: более пятидесяти тестовых spec api вставляют projects прямо в базу, не зная (и не нуждаясь знать) понятия имени папки. Без trigger сделать столбец NOT NULL сломало бы их все, а альтернатива — отредактировать пятьдесят файлов, выдумывая правдоподобный workspace_dir_name в каждом и заботясь о ГЛОБАЛЬНОЙ уникальности между ними, — обменяла бы один долг на другой, больший, не доказав ничего сверх о самой функциональности. Trigger никогда не перезаписывает ненулевое значение: тот, кто записывает явно, сохраняет контроль.

5. Engine читает ТОТ ЖЕ столбец и никогда не вычисляет имя заново​

Engine.Projects.Project.workspace_dir_name/1 запрашивает столбец напрямую. Engine.Actions.Workspace.workspace_dir/1 (который раньше был Path.join(root, project_id), чистой функцией) начал определять имя этим запросом, с fallback на голый project_id, когда запрос не находит строку, когда у project_id нет формы UUID или когда запрос падает по любой причине (широкие rescue/catch — деградация всегда предпочтительнее распространения сбоя определения ПУТИ тому, кому нужен был лишь каталог).

Это то место, которое гарантирует, что инвариант ADR 0055 переживает изменение: api и engine не реализуют две формулы, которые должны совпадать случайно, — обе читают ОДНУ строку ОДНОЙ базы. Если однажды они разошлись бы, то потому, что одна перестала запрашивать, а не потому, что формула изменилась с одной стороны.

workspace_dir/1 не горячий путь: цикл инструментов dev agent (search_workspace, write_file, read_file) уже получает ГОТОВЫЙ ctx[:workspace_root] — определённый один раз, когда Engine.Dev.WorktreeManager.create/3 собирает worktree агента, — и лишь обращается к workspace_dir/1 как к fallback для вызывающих, которые сегодня не проходят через этот ctx (worktree_manager внутренне, instruction_files, TerminalExecutor через ensure_remoto). Ни один из них не выполняется на каждый вызов инструмента.

Engine.Dev.WorktreeCleanup (периодическая обрезка осиротевших worktrees) был исключением, потребовавшим собственного дизайна: до RN-109 имя папки БЫЛО project_id, и сканирование диска (File.ls(root)) с трактовкой каждой записи как id было допустимым прочтением. С читаемым именем папки папка перестала быть id, и вернуть одно к другому без запроса невозможно. Исправление меняет источник итерации: вместо сканирования диска оно запрашивает Project.all_workspace_dirs/0 (все {id, workspace_dir_name} одним запросом) и использует разрешённый work_dir напрямую — ни эта функция, ни WorktreeManager не делают второго запроса на проект, чтобы снова узнать имя.

Последствия​

Принятые. Один новый столбец, один новый trigger (первый в продукте — см. пункт 4), две чистые функции (workspaceDirNameFor в api, workspace_dir_name/1 в engine) и расширение Workspace.workspace_dir из чистой в DB-aware-с-fallback. WorktreeCleanup сменил источник итерации с диска на базу — та же цена в ОДИН запрос, никогда не на проект.

Вне охвата. Переименование папки существующего проекта в читаемый формат не реализовано и не запрашивалось: это потребовало бы перемещать рабочее дерево, возможно открытое активным агентом, а ценность читаемого имени косметическая, она не стоит риска. Кто хочет читаемую папку, создаёт новый проект.

Что НЕ меняется. RN-075 (область терминала) и RN-092 (чтение кода, вкладка Code) продолжают указывать на ТУ ЖЕ папку, которую engine действительно использует, — именно это согласие, а не форму имени, защищают оба ADR.

Рассмотренные альтернативы​

  • Symlink, указывающий на нетронутую папку с UUID — меньший риск (физическое рабочее дерево никогда не меняет места, лишь получает читаемый ярлык рядом). Это была первоначальная рекомендация. Пользователь явно выбрал другой вариант: по-настоящему переименовать папку, без двух записей на проект на диске.
  • Пересчитывать имя при каждой смене slug — отвергнуто: это перемещало бы потенциально используемый каталог ради косметического выигрыша. «Заморожено при создании» исключает целую категорию ошибок.
  • Столбец NOT NULL без trigger, с редактированием пятидесяти тестовых spec — отвергнуто из-за цены/риска: пятьдесят файлов, отредактированных вручную, чтобы выдумать правдоподобные имена, с дополнительной заботой о ГЛОБАЛЬНОЙ уникальности между ними, не доказывают о функциональности ничего сверх того, что trigger уже доказывает тремя тестами.

Ссылки​

  • RN-109 — имя папки замораживается при создании, а старый проект сохраняет UUID.
  • RN-075 — область пути в политике терминала (то, что этот ADR сохраняет).
  • RN-092 — сдерживание пути при чтении кода (то же).
  • ADR 0055 — пересматриваемый документ.