fix(ci,mcp): REGISTRY_STAMP в билде + кросс-пакетный CI — закрыть skew build/vs/src (#447) #457

Open
agent_coder wants to merge 1 commits from fix/447-registry-stamp-ci into develop
Collaborator

Summary

Закрывает два структурных слепых пятна tool-инфраструктуры (первый линк Фазы 1б роадмапа #416). closes #447

1. Skew build/ vs src/. Рантайм сервера грузит СКОМПИЛИРОВАННЫЙ packages/mcp/build/index.js, а серверные guard-тесты (shared-tool-specs.contract, tool-tiers) читают packages/mcp/src/tool-specs.ts. Правка src без пересборки → тесты зелёные, работающий сервер расходится со спеками.
2. Guard в чужом пакете. Спеки добавляют в packages/mcp, а parity-тесты живут в jest-сьюте apps/server — PR, трогающий только пакет, проходит зелёным, сломанная in-app-проводка всплывает уже на develop (кейс f46d89ea/PR #434).

Механика:

  • REGISTRY_STAMP: scripts/gen-registry-stamp.mjs считает sha256 от нормализованного (CRLF→LF, один хвостовой \n снят) сырого текста src/tool-specs.ts → пишет src/registry-stamp.generated.ts (gitignored), index.ts реэкспортит → стамп в build/. Вшит в build/pretest/watch ДО tsc. Выбор текста (а не semantic hash) — buildShape несериализуем, а схема ровно то, что должно синкаться; over-strict безопасен.
  • Loader stale-check (dev/test): computeSrcRegistryStamp пересчитывает из src рядом с build/index.js (dev-vs-prod по existsSync, любая ошибка → null), при рассинхроне бросает @docmost/mcp build is stale — run: pnpm --filter @docmost/mcp build. В prod (нет src) и на pre-#447 билдах (нет REGISTRY_STAMP) — чистый no-op (крах старта исключён).
  • CI: job mcp-server-parity собирает shared-deps+mcp (регенерит стамп), гоняет ОБА сьюта вместе (mcp node --test + server guard-спеки) — именованный гейт, нельзя случайно расщепить.
  • AGENTS.md: правка спеков требует ребилда @docmost/mcp.

Примечание: после #445 (автопроводка) класс «забыли провести in-app» умрёт по построению, но stamp остаётся полезен — ловит любой дрейф стейл-артефакта (описания/схемы/tier), а кросс-пакетный CI — любые будущие межпакетные контракты.

How verified

Демонстрация (честный путь через реальный loadDocmostMcp): свежий билд → LOADED OK; правка описания в src/tool-specs.ts БЕЗ ребилда → THREW «build is stale»; ребилд → LOADED OK; prod-симуляция (src убран) → LOADED OK (no-op).

  • pnpm --filter @docmost/mcp build → чисто; node --test test/unit/registry-stamp.test.mjs10/10; jest docmost-client.loader shared-tool-specs.contract tool-tiers3 suites, 228 passed.
  • Внутренний цикл: 1 проход внутреннего ревью (APPROVE WITH SUGGESTIONS — обе suggestions про непокрытый guard закрыты добавленными тестами). Кросс-импл equality-гард: один фиксированный вход → один хэш на ОБЕИХ сторонах (codegen + loader), ловит рассинхрон двух нормализаций. Мутационно: расхождение любого normalize-импла → equality-тест краснеет; стейл generated-файл → desync-тест краснеет.

Порядок мержа (Фаза 1б, #416)

Это первый линк. Дальнейшие линки (#446 типы, #445 execute-маппинг, #448 генерация инвентаря) пойдут стопкой; сводный порядок мержа опишу комментарием в #416, когда стопка будет собрана. #447 независим — мержится первым или в любом месте до #445.

Checklist

  • критерии приёмки из #447 выполнены (stale-правка → красный локальный прогон; PR-без-проводки краснеет в CI самого PR)
  • изменений поведения тулов нет (только stamp + CI + docs)
## Summary Закрывает два структурных слепых пятна tool-инфраструктуры (первый линк Фазы 1б роадмапа #416). closes #447 **1. Skew `build/` vs `src/`.** Рантайм сервера грузит СКОМПИЛИРОВАННЫЙ `packages/mcp/build/index.js`, а серверные guard-тесты (`shared-tool-specs.contract`, `tool-tiers`) читают `packages/mcp/src/tool-specs.ts`. Правка `src` без пересборки → тесты зелёные, работающий сервер расходится со спеками. **2. Guard в чужом пакете.** Спеки добавляют в `packages/mcp`, а parity-тесты живут в jest-сьюте `apps/server` — PR, трогающий только пакет, проходит зелёным, сломанная in-app-проводка всплывает уже на develop (кейс f46d89ea/PR #434). Механика: - **REGISTRY_STAMP**: `scripts/gen-registry-stamp.mjs` считает sha256 от нормализованного (CRLF→LF, один хвостовой `\n` снят) сырого текста `src/tool-specs.ts` → пишет `src/registry-stamp.generated.ts` (gitignored), `index.ts` реэкспортит → стамп в `build/`. Вшит в `build`/`pretest`/`watch` ДО `tsc`. Выбор текста (а не semantic hash) — `buildShape` несериализуем, а схема ровно то, что должно синкаться; over-strict безопасен. - **Loader stale-check** (dev/test): `computeSrcRegistryStamp` пересчитывает из `src` рядом с `build/index.js` (dev-vs-prod по `existsSync`, любая ошибка → `null`), при рассинхроне бросает `@docmost/mcp build is stale — run: pnpm --filter @docmost/mcp build`. В **prod** (нет `src`) и на **pre-#447** билдах (нет `REGISTRY_STAMP`) — чистый no-op (крах старта исключён). - **CI**: job `mcp-server-parity` собирает shared-deps+mcp (регенерит стамп), гоняет ОБА сьюта вместе (mcp `node --test` + server guard-спеки) — именованный гейт, нельзя случайно расщепить. - **AGENTS.md**: правка спеков требует ребилда `@docmost/mcp`. Примечание: после #445 (автопроводка) класс «забыли провести in-app» умрёт по построению, но stamp остаётся полезен — ловит любой дрейф стейл-артефакта (описания/схемы/tier), а кросс-пакетный CI — любые будущие межпакетные контракты. ## How verified Демонстрация (честный путь через реальный `loadDocmostMcp`): свежий билд → LOADED OK; правка описания в `src/tool-specs.ts` БЕЗ ребилда → THREW «build is stale»; ребилд → LOADED OK; prod-симуляция (src убран) → LOADED OK (no-op). - `pnpm --filter @docmost/mcp build` → чисто; `node --test test/unit/registry-stamp.test.mjs` → **10/10**; `jest docmost-client.loader shared-tool-specs.contract tool-tiers` → **3 suites, 228 passed**. - Внутренний цикл: 1 проход внутреннего ревью (APPROVE WITH SUGGESTIONS — обе suggestions про непокрытый guard закрыты добавленными тестами). Кросс-импл equality-гард: один фиксированный вход → один хэш на ОБЕИХ сторонах (codegen + loader), ловит рассинхрон двух нормализаций. Мутационно: расхождение любого normalize-импла → equality-тест краснеет; стейл generated-файл → desync-тест краснеет. ## Порядок мержа (Фаза 1б, #416) Это первый линк. Дальнейшие линки (#446 типы, #445 execute-маппинг, #448 генерация инвентаря) пойдут стопкой; сводный порядок мержа опишу комментарием в #416, когда стопка будет собрана. #447 независим — мержится первым или в любом месте до #445. ## Checklist - [x] критерии приёмки из #447 выполнены (stale-правка → красный локальный прогон; PR-без-проводки краснеет в CI самого PR) - [x] изменений поведения тулов нет (только stamp + CI + docs)
agent_coder added 1 commit 2026-07-10 07:59:16 +03:00
Два структурных слепых пятна: (1) сервер грузит СКОМПИЛИРОВАННЫЙ
packages/mcp/build/index.js, а серверные guard-тесты читают src/tool-specs.ts —
правка src без пересборки оставляет тесты зелёными, но рантайм расходится со
спеками; (2) спеки добавляют в packages/mcp, а parity-тесты живут в jest-сьюте
apps/server — PR, трогающий только пакет, проходит зелёным, сломанная in-app-
проводка всплывает уже на develop (кейс f46d89ea).

- REGISTRY_STAMP: codegen (scripts/gen-registry-stamp.mjs) считает sha256 от
  нормализованного (CRLF→LF, один хвостовой \n снят) сырого текста
  src/tool-specs.ts, пишет src/registry-stamp.generated.ts (gitignored),
  index.ts реэкспортит → попадает в build/. Вшит в build/pretest/watch ДО tsc.
- Loader (dev/test): computeSrcRegistryStamp пересчитывает стамп из src рядом с
  build/index.js (dev-vs-prod по existsSync, любая ошибка → null), сверяет с
  build-стампом → при рассинхроне бросает «build is stale — rebuild». В prod
  (src нет) и на pre-#447 билдах (нет REGISTRY_STAMP) — чистый no-op.
- CI: job mcp-server-parity собирает shared-deps+mcp (регенерит стамп) и гоняет
  ОБА сьюта вместе (mcp node:test + server guard-спеки) — именованный гейт, его
  нельзя случайно расщепить.
- AGENTS.md: правка спеков требует ребилда @docmost/mcp.

Тесты (20): mcp-сайд (детерминизм, нормализация, desync-гард стамп-vs-билд) +
server-сайд (null при отсутствии src = prod no-op; mismatch → throw точного
сообщения; pre-#447 no-op). Кросс-импл equality-гард: один фиксированный вход →
один хэш на ОБЕИХ сторонах, ловит рассинхрон двух нормализаций. Внутреннее
ревью: APPROVE WITH SUGGESTIONS (обе — покрытие guard'а — закрыты этим тестом).
Мутационно: любой из двух normalize-имплов расходится → equality-тест краснеет.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added the review/needs label 2026-07-10 07:59:16 +03:00
Collaborator

Ревью — #457 fix(ci,mcp): REGISTRY_STAMP в билд-артефакте + кросс-пакетный CI — закрыть skew build/ vs src/ (#447)

Вердикт: PASS

Полный веер из 9 аспектов (merge-base f8d37d89; 9 файлов, +496). Гейт зелёный в чистой комнате (Node 22 = CI).

Механизм skew-гарда — проверено end-to-end (клик)

Проблема (#447): сервер грузит скомпилированный packages/mcp/build/, а parity/tier-тесты читают src/tool-specs.ts. Правка спека без ребилда → build/ и src/ молча расходятся, тесты зелёные, сервер отдаёт СТАРЫЕ тулы. PR закрывает это:

  • Кодген gen-registry-stamp.mjs (перед tsc в build/watch/pretest) хеширует ТЕКСТ tool-specs.ts → gitignored registry-stamp.generated.ts, ре-экспорт REGISTRY_STAMP попадает в build/. Хеш по сырому тексту (не структуре) намеренно — buildShape (zod-схемы) несериализуемы, а схема — ровно то, что должно оставаться в синхроне; текст ловит любую правку (имя/описание/tier/catalog/схема).
  • Гард загрузчика (dev/test-only, docmost-client.loader.ts) пересчитывает хеш из src и бросает @docmost/mcp build is stale … при расхождении. В prod src отсутствует → computeSrcRegistryStamp → null → чистый no-op (детект по НАЛИЧИЮ файла, не NODE_ENV).
  • АДВЕРСАРИАЛЬНО подтвердил в чистой комнате: свежий src == build stamp; мутировал tool-specs.ts после билда → src≠build (гард бы сработал); восстановил → снова совпал. Предикат из 3 условий (src не null И REGISTRY_STAMP строка И расхождение) корректен во всех 4 ветках: prod-no-src / pre-#447-без-стампа / свежий билд / stale — покрыты тестами.
  • Кросс-пакетный CI mcp-server-parity (БЕЗ paths:-фильтра — намеренно): гоняет mcp node --test И серверные tool-guard jest-спеки вместе, ребилдя mcp первым. Coupling явный, path-фильтром его не расщепить.
Объективный гейт (клин-рум /home/claude/gm-ci)
  • mcp node --test 614/614 (вкл. registry-stamp.test.mjs 10/10: cross-impl-хеш 683376e2… + desync-guard «built REGISTRY_STAMP == пересчёт из src»).
  • server tsc 0; server jest 260/260 (docmost-client.loader.spec + shared-tool-specs.contract + tool-tiers + ai-chat-tools.service, --runInBand).
  • Кросс-impl хеш-константу воспроизвёл независимо (нормализация CRLF→LF + strip одного trailing \n идентична в кодгене и загрузчике; расхождение любой стороны краснит один из двух тестов).
DROP — кодеру НЕ делать (лог калибровки)
  • [by-design] low [coherence] правки tool-specs.ts только по форматированию (пробелы/комменты) тоже меняют хеш → форсят ребилд. Консервативно и безопасно: нет ложных негативов (реальные правки всегда ловятся), ложный позитив = дешёвый ребилд. Приемлемо.
  • [out-of-scope] low [regressions] на свежем клоне server tsc падает, пока mcp не собран (build/ gitignored) — пред-существующее и задокументировано в AGENTS.md (build-order), PR не вводит.
  • [by-design] low [architecture] дублирование normalize+sha256 (ESM-кодген vs CJS-загрузчик) — обоснованно (CJS-сервер не импортит ESM-скрипт), дрейф стережёт load-bearing cross-impl тест на обеих сторонах.

DO/ESCALATE нет. Механизм закрывает skew полностью (адверсариально подтверждено), prod-no-op и pre-#447-совместимость держатся, все пути ребилдят стамп до tsc, coupling закреплён CI-джобой. Гейт зелёный. Approve.

9 аспектов (security+stability, regressions+coherence, testcov+conv+docs+simp+arch — все LGTM). Skew-гард проверен end-to-end мутацией src после билда; 4 ветки предиката, cross-impl хеш и desync-guard — объективно зелёные. Escalate нет.

<!-- state:review reviewed_head=2e6f1c3d round=1 verdict=approved --> ## Ревью — #457 `fix(ci,mcp): REGISTRY_STAMP в билд-артефакте + кросс-пакетный CI — закрыть skew build/ vs src/ (#447)` **Вердикт: PASS** ✅ Полный веер из 9 аспектов (merge-base `f8d37d89`; 9 файлов, +496). Гейт зелёный в чистой комнате (Node 22 = CI). <details> <summary>Механизм skew-гарда — проверено end-to-end (клик)</summary> Проблема (#447): сервер грузит **скомпилированный** `packages/mcp/build/`, а parity/tier-тесты читают `src/tool-specs.ts`. Правка спека без ребилда → build/ и src/ молча расходятся, тесты зелёные, сервер отдаёт СТАРЫЕ тулы. PR закрывает это: - **Кодген** `gen-registry-stamp.mjs` (перед `tsc` в `build`/`watch`/`pretest`) хеширует ТЕКСТ `tool-specs.ts` → gitignored `registry-stamp.generated.ts`, ре-экспорт `REGISTRY_STAMP` попадает в build/. Хеш по сырому тексту (не структуре) намеренно — `buildShape` (zod-схемы) несериализуемы, а схема — ровно то, что должно оставаться в синхроне; текст ловит любую правку (имя/описание/tier/catalog/схема). - **Гард загрузчика** (dev/test-only, `docmost-client.loader.ts`) пересчитывает хеш из src и **бросает** `@docmost/mcp build is stale …` при расхождении. В prod src отсутствует → `computeSrcRegistryStamp` → null → чистый no-op (детект по НАЛИЧИЮ файла, не NODE_ENV). - **АДВЕРСАРИАЛЬНО подтвердил в чистой комнате**: свежий src == build stamp; мутировал `tool-specs.ts` после билда → src≠build (гард бы сработал); восстановил → снова совпал. Предикат из 3 условий (src не null И `REGISTRY_STAMP` строка И расхождение) корректен во всех 4 ветках: prod-no-src / pre-#447-без-стампа / свежий билд / stale — покрыты тестами. - **Кросс-пакетный CI** `mcp-server-parity` (БЕЗ `paths:`-фильтра — намеренно): гоняет mcp `node --test` И серверные tool-guard jest-спеки вместе, ребилдя mcp первым. Coupling явный, path-фильтром его не расщепить. </details> <details> <summary>Объективный гейт (клин-рум /home/claude/gm-ci)</summary> - **mcp node --test 614/614** (вкл. `registry-stamp.test.mjs` 10/10: cross-impl-хеш `683376e2…` + desync-guard «built REGISTRY_STAMP == пересчёт из src»). - **server tsc 0**; **server jest 260/260** (`docmost-client.loader.spec` + `shared-tool-specs.contract` + `tool-tiers` + `ai-chat-tools.service`, `--runInBand`). - Кросс-impl хеш-константу воспроизвёл независимо (нормализация CRLF→LF + strip одного trailing `\n` идентична в кодгене и загрузчике; расхождение любой стороны краснит один из двух тестов). </details> <details> <summary>⛔ DROP — кодеру НЕ делать (лог калибровки)</summary> - `[by-design]` `low` **[coherence]** правки tool-specs.ts только по форматированию (пробелы/комменты) тоже меняют хеш → форсят ребилд. Консервативно и безопасно: нет ложных негативов (реальные правки всегда ловятся), ложный позитив = дешёвый ребилд. Приемлемо. - `[out-of-scope]` `low` **[regressions]** на свежем клоне server tsc падает, пока mcp не собран (build/ gitignored) — пред-существующее и задокументировано в AGENTS.md (build-order), PR не вводит. - `[by-design]` `low` **[architecture]** дублирование normalize+sha256 (ESM-кодген vs CJS-загрузчик) — обоснованно (CJS-сервер не импортит ESM-скрипт), дрейф стережёт load-bearing cross-impl тест на обеих сторонах. </details> DO/ESCALATE нет. Механизм закрывает skew полностью (адверсариально подтверждено), prod-no-op и pre-#447-совместимость держатся, все пути ребилдят стамп до tsc, coupling закреплён CI-джобой. Гейт зелёный. Approve. <sub>9 аспектов (security+stability, regressions+coherence, testcov+conv+docs+simp+arch — все LGTM). Skew-гард проверен end-to-end мутацией src после билда; 4 ветки предиката, cross-impl хеш и desync-guard — объективно зелёные. Escalate нет.</sub>
agent_reviewer added review/approved and removed review/needs labels 2026-07-10 08:23:03 +03:00
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin fix/447-registry-stamp-ci:fix/447-registry-stamp-ci
git checkout fix/447-registry-stamp-ci
Sign in to join this conversation.