refactor(mcp): структурные инварианты конкурентной записи — UUID-assert, self-resolve seams, no-await-guard (#449) #475
Reference in New Issue
Block a user
Delete Branch "refactor/449-write-invariants"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Механика конкурентной записи (
withPageLock→acquireCollabSession→session.mutate) была корректна, но держалась на цепочке конвенций в комментариях. Новый write-метод без знания правил компилировался и уходил в прод (класс #260/#152/#159). Закрепляем ключевые инварианты кодом. Рефакторинг, поведение существующих путей не меняется. closes #449Стоит на develop напрямую (весь предыдущий стек #412…#425 уже влит).
withPageLockтеперь fail-fast кидает при не-UUID ключе ДО любой работы — забытый resolve / переданный slugId больше не даёт тихую потерю сериализации под другим ключом.UUID_RE/isUuidвынесены вpage-lock.tsкак единственный источник,client.tsимпортирует оттуда (resolver и assert не разъедутся). SeamsmutatePage/replacePageсталиasyncи сами резолвят — ключ канонический даже если вызывающий забыл (для уже-UUID это cached no-op).replaceImage(один внешний лок) не тронут.MUTATE-CRITICAL-WINDOW BEGIN/ENDвокруг синхронногоfromYdoc→applyDocToFragment(INVARIANT 1); тест читает исходник и краснеет наawait/yieldв окне. Случайныйawaitбольше не тихо клоббит живые правки человека..env.example.MCP_COLLAB_SESSION_MAX_AGE_MS(10 мин) — задокументированный trade-off в.env.example; push-инвалидации нет (осознанно).How verified
mcptscчисто. #449-специфичные тесты (page-lock fail-fast + accept, no-await-guard нейтер-проверен, обновлённые фикстуры на валидные UUID) — зелёные.withPageLock(их два —collaboration.ts:258иreplaceImage), провенанс ключа у каждого достижимого write-пути (~14 сайтов) — все передают резолвленныйpageUuid; новый throw недостижим на легитимных путях. UUID-предикат сверен с серверным форматом (gen_uuid_v7).Checklist
awaitв критическое окноmutate→ красный тестСтоит на develop напрямую (предыдущий стек уже влит через #474). Рефакторинг-хардеринг write-path: конвенции конкурентной записи из комментариев → в код.
Что: withPageLock fail-fast на не-UUID ключе (единый isUuid в page-lock.ts), self-resolving seams mutatePage/replacePage, машинно-проверяемое no-await окно в mutate, документация multi-replica/sticky + окна устаревших прав.
Проверка: mcp tsc чисто; #449-специфичные тесты (page-lock fail-fast/accept, no-await нейтер-guard, фикстуры на UUID) зелёные.
Внутренний цикл: 1 проход (адверсарно). Главная проверка — полный обход ВСЕХ вызовов withPageLock (их два) и провенанс ключа у каждого достижимого write-пути (~14 сайтов, включая drawio-тулы #425): все передают резолвленный pageUuid → новый throw недостижим на легитимных путях, регрессии нет. UUID-предикат сверен с серверным gen_uuid_v7. Блокеров/багов не найдено.
Прим.: в свежем worktree часть runtime-deps mcp не подтянута (rfc6902/diff/@tiptap/pm/re2) → пара НЕ относящихся к #449 тестов падает на отсутствии модулей (и на базе тоже); полный install в CI это снимает.
Ревью — #475
refactor(mcp): структурные инварианты конкурентной записи — UUID-assert, self-resolve seams, no-await-guard (#449)Вердикт: PASS ✅
Полный веер из 9 аспектов по изолированной дельте (на develop
bec2156eнапрямую; ревьюилbec2156e..9a8671c3= 11 файлов, +321/−31, mcp+config). Рефакторинг, поведение существующих путей не меняется. Гейт зелёный, no-await-инвариант адверсариально пришпилен.Инварианты закреплены КОДОМ — проверено (клик)
Механика конкурентной записи была корректна, но держалась на цепочке комментариев-конвенций (новый write-метод мог скомпилиться с багом — класс #260/#152/#159). Теперь:
withPageLock(page-lock.ts) fail-fast кидает на не-UUID ключе ДО взятия мьютекса.UUID_RE/isUuid— единственный источник в page-lock.ts, client.ts импортит (resolver+assert не разъедутся). SeamsmutatePage/replacePageстали async и САМИ резолвят (resolve→lock порядок верный; resolvePageId идемпотентен → для UUID cached no-op). Все 7 вызывателей уже await'ят → async-переход прозрачен.replaceImage(один внешний лок) НЕ тронут, зовёт non-locking helper → deadlock'а нет. Ни один легитимный путь на assert не попадает.MUTATE-CRITICAL-WINDOW BEGIN/ENDвокруг СИНХРОННОГО fromYdoc→applyDocToFragment (collab-session.ts:452-490); transform типизирован sync (не async). Тест сканирует .ts-ИСХОДНИК, краснеет на await/yield в окне. Адверсариально: вставил await между маркерами → тест ПАДАЕТ с точным сообщением («await reopens the clobber-live-edits race #152»). Тест не вакуумен (3-частный гард: маркеры-ровно-по-одному + no-await-скан + presence fromYdoc/applyDocToFragment).Объективный гейт
⛔ DROP — кодеру НЕ делать
[inherited]low[stability]server tsc --noEmitкраснеет на import.meta в mcp-исходнике — но #475 НЕ трогает НИ ОДНОГО apps/server файла, значит унаследовано от develop (латентный server-classic-резолв mcp-src, вошёл с #472/drawio-стадия-3; см. коммент к #472). НЕ CI-проверка (CI гоняет server через jest), runtime/.d.tsчисты. Не #475.[below-threshold]low[documentation] нет CHANGELOG (паттерн с прошлыми PR).DO/ESCALATE нет. Инварианты (UUID-assert fail-fast, self-resolve, no-await-окно, single-source) закреплены кодом+машинными тестами; #260 закрыт двумя слоями; seams прозрачны, replaceImage без deadlock, окно атомарно; multi-replica-ограничение осознанно задокументировано. Гейт зелёный. Approve.
#449, на develop. 9 аспектов (stability+regressions+security+coherence, testcov+conv+docs+simp+arch — все LGTM). UUID-assert/self-resolve/replaceImage-single-lock/окно-атомарность проверены; no-await-гард адверсариально краснит; #260 закрыт двумя слоями. server-tsc-red унаследован (#475 не трогает server; не CI). Escalate нет.