Мой продукт — программа для ретуши и плагин для Photoshop с локальной нейросетью. Чтобы выпуская программу и обеспечить её безопасность, я выстроила десять слоев безопасности: защищённый канал между плагином и системной службой, регистрация устройства, подпись артефактов с меткой времени, аттестация сборки, канал обновлений, шифрование модели, водяной знак. Части написаны на C++ и Go, живут в разных процессах и ставятся разными установщиками.
Каждая такая граница — это не модуль, а договорённость двух сторон: одно имя службы, одна версия протокола, один набор маршрутов, одна строка подтверждения. Сейчас 54 связи на этих границах держат 188 сравнений значений (468 мест в коде) и 105 сравнений множеств. Компилятор не видит ни одного из них: стороны написаны на разных языках.
Я назвала такие места швами — точками, где разные компоненты должны точно совпадать:
маршрут, который вызывает клиент и регистрирует сервер;
заголовок с версией протокола;
поля DTO в Go и TypeScript;
переменные окружения, которые читает сервис и которые указаны в
.env.example;правило округления цены, реализованное на двух языках.
Типы, тесты и линтеры хорошо работают внутри одного языка, но такие договорённости обычно не проверяют. Они держатся на памяти разработчиков.
Инструмент называется Crossweft (уток, weft, — поперечная нить в ткани: она проходит через все продольные и держит полотно вместе).
С чего все началось
Разработку я начала в январе этого года. За это время я потратила на подписки больше $9 000 — по самым скромным прикидкам это больше миллиарда токенов. И всё это время я не понимала, почему у меня не сходится безопасность. Сама программа работала прекрасно, а соединить слои защиты не получалось. Я даже сделала отдельное приложение, которое следило за «сердцебиением» слоёв, чтобы свести их вместе, — но и это было безуспешно.
Под капотом у программы четыре части: клиент, системная служба, бэкенд и установщики. И вот три случая, после которых у меня наконец сложилось понимание, как свести агентскую разработку воедино…
Один идентификатор — три копии. Имя службы определяли в трёх почти одинаковых функциях. На чистой машине служба ещё не была установлена, поиск заканчивался ошибкой, и пустая машина выглядела как старая несовместимая установка — установщик останавливался с кодом 76. Исправила одну копию — две другие остались сломанными.
Две половины одной зависимости. Сборка брала библиотеку из одного каталога, а связанные с ней инструменты — из другого. Конфигурация только предупреждала, и проблема проявилась гораздо позже, там, где причину искать было уже трудно.
Сторож, который ничего не сторожил. Валидатор одного из правил безопасности из-за ошибки в пути несколько недель проверял ноль файлов и каждый раз говорил OK. Проверка проходила именно потому, что не выполнялась.
Причина у всех трёх одна: у договорённости две или три стороны, а их совпадение держится на внимании людей.
При чём тут AI-агенты
Значительную часть кода у меня пишут агенты, в основном Claude Code и Codex. С межкомпонентными связями они справляются хуже людей.
Типичный сценарий: агент меняет Go-структуру, тесты зелёные, агент пишет «Готово» — и даже не открывает TypeScript-интерфейс на другой стороне.
Причины одни и те же:
Локальные правки. Вторая сторона шва лежит в другой папке, на другом языке, в другом процессе.
Ограниченный контекст. Агент не держит в голове весь продукт, а компилятор не скажет ему, что где-то есть клиент, зависящий от изменённой структуры.
Узкое «готово». Для агента это «все проверки, которые я вижу, прошли». Если ни одна не смотрит на шов, расхождение незаметно до поломки.
Зато агенты хорошо выполняют точные локальные инструкции. Если сразу после правки сказать, какой файл на другой стороне открыть и что там должно совпасть, агент это сделает. А если не отпускать его, пока стороны расходятся, ошибка не попадёт в коммит.
Идея: каждая связь сама говорит, как её проверять
Правило простое: каждая связь между компонентами объявляет, как её стороны согласуются. Если договорённость поддерживается вручную с обеих сторон, у неё должен быть сторож — проверка, которая читает обе стороны.
Связи описываются в JSON рядом с кодом. В карте есть блоки — программы, сервисы, хранилища, конфиги — и связи между ними: HTTPS, named pipe, файлы, генерация кода, переменные окружения. Каждое утверждение карты привязано якорем к конкретному литералу в коде: если код переехал, карта не продолжает молча описывать прошлое — проверка падает.
У каждой связи есть поле contract.enforcement:
Способ |
Кто держит равенство сторон |
Чего требует проверка |
|---|---|---|
|
символ, который импортируют обе стороны |
|
|
генератор из одного источника |
|
|
общие тестовые векторы |
|
|
никто, копия руками |
сторож, читающий файл на каждой стороне |
|
никто, договорённость |
сторож, читающий файл на каждой стороне |
|
никто |
сторож, читающий файл на каждой стороне |
Уже сама классификация полезна: вопрос «где я полагаюсь на чью-то память?» превращается в список, который можно посчитать. Сторож, читающий одну сторону, не засчитывается: инструмент проверяет, что он читает файлы двух разных концов связи.
Три сторожа: join, set и pair
Сторожа намеренно простые: регулярные выражения, сравнение значений или множеств. Проверка занимает миллисекунды, не требует сборки и работает с любым языком.
join — значение совпадает везде. Версия API в TypeScript-клиенте и в Go-сервере:
{"id": "api-version", "link": "web-orders", "points": [ {"path": "web/src/api.ts", "side": "web", "regex": "API_VERSION = \"([^\"]+)\""}, {"path": "server/main.go", "side": "api", "regex": "APIVersion = \"([^\"]+)\""}]}
Сравниваются все вхождения, а не первое: в первой версии вторая копия значения ниже по файлу могла проскочить.
set — состав совпадает. Поля TypeScript-интерфейса и JSON-теги Go-структуры; статусы, которые пишет Python-воркер, и константы в Go; переменные окружения в коде и в .env.example. Намеренное различие вносится в allow с причиной, а ставшая ненужной запись allow сама роняет проверку.
pair — дублированная логика без одного значения. Например, округление цены на Go и TypeScript. Участки помечаются crossweft:begin price-rounding / crossweft:end price-rounding, Crossweft хэширует оба. Изменился любой — проверка падает, пока кто-то не перечитает двойника и не выполнит crossweft attest price-rounding --reason "...". Причина попадает в lock-файл и в ревью.
Самый короткий для агента путь из красной проверки — аттестовать изменённую сторону, не трогая вторую. Поэтому если с прошлой аттестации поменялся только один участок, attest отказывает: сначала приведите в порядок двойника, а если он действительно уже эквивалентен — скажите это явно флагом --other-side-unchanged.
Есть проверки крупнее. Маршруты сервера должны быть на карте — Go chi разбирается напрямую (а также Express и Fastify, FastAPI, Flask, gin и echo), остальное описывается регулярным выражением. Каждый вызов вида /v1/... в клиенте должен объясняться связью. Блок может отправлять данные, только если создал или получил их. Каждая папка с кодом должна быть на карте.
Если встроенных видов мало, репозиторий добавляет свои плагинами. У меня их 13: один сверяет 916 мест, где Go-сервер обращается к SQL, со 161 таблицей и 276 триггерами схемы, другой — 185 маршрутов сервера, третий — 27 пар «ожидающий ≥ работа + запас» для таймаутов. Плагин, который не импортировался, упал или ничего не сравнил, — ошибка, а не зелёный результат.
Что видит агент
Сама проверка — половина решения. Важно, чтобы агент узнал о шве сразу после правки.
Для этого три хука:
Старт сессии — короткое описание: в репозитории есть карта швов, сколько их и как работать.
После каждой правки — если файл лежит на шве, агенту называют вторую сторону и сторожа.
Перед завершением —
crossweft check; если шов расходится, хук не даёт закончить и объясняет, что делать.
После правки Go-сервера в демо агент видит:
crossweft: server/main.go is part of 3 cross-component seam(s). Keep both sides in agreement: - link web-orders (web -> api, https; Orders API [duplicated]): you changed its to side. Re-read: web/src/api.ts. Guarded by: join:api-version, join:version-header, set:order-fields, set:web-statuses, pair:price-rounding. ... Run `crossweft check` before you finish.
Поднял версию API в Go и пытается закончить — хук возвращает его:
crossweft check fails: - join:api-version disagrees: web@web/src/api.ts:3='2026-09-01'; api@server/main.go:14='2026-10-01' [server/main.go:14] (key: join:api-version:b5dfb7e5) A seam is out of agreement: bring the other side in line with the one you changed (`crossweft impact <file>` names it).
Совет зависит от вида проблемы: если в коде пропал литерал, на который указывает карта, агенту скажут «обнови якорь или верни код», а не «исправь другую сторону». Ключ заканчивается дайджестом того, какой файл держит какое значение: зарегистрированное расхождение прощает только себя.
У stop-хука две типичные беды — бесконечный цикл и тихий пропуск. Он возвращает агента, только пока тот продвигается (набор падающих ключей меняется), и не больше четырёх раз подряд. Нет прогресса — агент может остановиться, но человек получает явное сообщение, что проверка красная.
Для Claude Code есть плагин с хуками и skill. Для Codex — skill-плагин. Любой агент с поддержкой MCP может подключить crossweft mcp: проверка, анализ влияния правки и «какой файл перечитать» как инструменты только на чтение. Адаптеры хуков для Gemini CLI, Copilot и Cursor пока экспериментальные: проверена форма вывода, а не полный сценарий.
Проверки, которые не могут молча пройти
Третья история — валидатор, который неделями сканировал ноль файлов. Не каждое правило сводится к сравнению двух значений: иногда нужно доказать, что у константы один источник. Такие правила живут в маленьких скриптах, и именно они ломаются незаметно.
Мета-раннер crossweft validators считает валидатор провалившимся, если тот вернул ненулевой код, не вывел SCANNED: files=<n> items=<n> или вывел нули, не прошёл self-test с подложенной ошибкой (строка SELF-TEST: checks=<n>) или не уложился в таймаут. Честно неприменимый валидатор выводит APPLICABILITY: <причина> — это SKIP, а не PASS.
Тот же принцип у самого Crossweft. Три исхода, а не два: 0 — сошлось, 1 — расхождения, 2 — проверка не состоялась (модель не читается, ничего не просканировано). Код 2 никогда не выглядит как успех.
Реестр расхождений, который не гниёт
Не каждое расхождение можно исправить в том же PR. Тогда его записывают как finding: ключ, владелец, следующий шаг. Пока проблема воспроизводится, проверка проходит; когда её исправили, запись становится протухшей и сама роняет проверку. Сломанные якоря finding’ом не прикрыть: карта не имеет права описывать код неправильно.
Для внедрения в репозиторий, где расхождения уже накопились, есть crossweft baseline: он записывает их как открытые findings с владельцем. Проверка зеленеет, новый дрейф её роняет, а исправленное само требует закрытия.
Что это дало: цифры
С середины июля установщик ни разу не доводил системную службу до запуска на чистой виртуальной машине. Проверяемые швы я добавила 26–28 сентября, 29 сентября служба впервые запустилась. Это совпадение по времени, а не доказательство причины: параллельно чинилось и другое.
Ошибки все равно остались но их характер изменился. Три из первых падений 28 сентября были именно швами: код выхода 76 (имя службы в трёх копиях), отказ сервера, потому что загрузчик регистрировался по версии артефакта, а запрашивался по версии протокола, и HTTP 500, потому что два пути публикации убирали старый пакет, а третий — нет. Теперь каждое закрыто отдельным сторожем. Две следующие проблемы были уже обычными багами, а не «одним значением в нескольких местах».
Что показывает карта (пересчитано по истории репозитория):
27.09 |
29.09 |
4.10 |
|
|---|---|---|---|
Блоки / связи |
130 / 160 |
134 / 166 |
138 / 192 |
join-сторожа |
10 |
220 (529 точек) |
261 (644 точки) |
set-сторожа |
4 |
125 |
138 |
Реестр расхождений |
58, все открыты |
106: 93 закрыто, 10 открыто, 3 приняты |
146: 117 закрыто, 25 открыто, 4 приняты |
Первый проход по правилу «у каждой связи объявлен способ согласования» сделали сами агенты: за день добавили 185 join- и 107 set-сторожей и нашли 15 настоящих расхождений в коде. Из 93 закрытых расхождений у 91 есть исправляющий коммит, и в 58 из них сообщение называет идентификатор из карты.
Карта без проверки устаревает за неделю. Первый замер через неделю показал 96 проблем, и 94 из них — устаревшая карта, а не баги кода. Поэтому карта проверяется на каждой остановке агента. Валидаторов стало 50 вместо 4 в июне, и каждый доказывает, что действительно что-то просканировал.
Выход в свет
Когда безопасность у меня наконец сошлась, я поняла: наверняка есть куча людей с такими же проблемами. У всех, кто пишет продукт на нескольких языках вместе с агентами, швы расходятся одинаково. Значит, это может пригодиться не только мне. Так Crossweft стал отдельным открытым инструментом: командой crossweft, плагином для Claude Code и Codex и сервером MCP для остальных агентов.
А разве такого ещё нет?
Каждая часть по отдельности существует. Сочетания и правила «каждая связь объявляет способ согласования, и каждый рукописный шов имеет сторожа» я не нашла.
Инструмент |
Что делает |
Чем отличается |
|---|---|---|
Google |
«изменил блок — измени и тот файл» |
проверяют, что дифф задел файл; join сравнивает значения на каждом прогоне |
равенство значения в разных файлах |
нет множеств, участков кода и карты |
|
контрактные тесты и совместимость схем |
правильный ответ, если схема есть; Crossweft строит сторожей из OpenAPI и proto для рукописной стороны |
|
ArchUnit, dependency-cruiser, import-linter |
архитектурные правила |
внутри одного языка |
архитектурные инварианты и skills для агентов |
ближе всех по духу, но структура, а не значения между языками |
|
граф кода и анализ влияния для агентов, MCP |
граф для запросов, а не падающая проверка; дополняют друг друга |
|
fiberplane/drift, Swimm |
документация, привязанная к коду |
хэш падает на любую правку; якорь — только когда исчез сам факт |
Подход придуман не мной. Бригитта Бёкелер описывает harness engineering: «направляющие» до того, как агент пишет код, и «датчики» после; детерминированные проверки против проверок языковой моделью. OpenAI пишет о том же в Harness engineering: leveraging Codex in an agent-first world. Crossweft — детерминированный датчик для межкомпонентных швов, чей вывод говорит агенту, что именно чинить.
Ограничения
Регулярки, а не разбор кода. Переформатировали объявление — проверка громко упадёт, регулярку придётся поправить. Но слишком широкий шаблон может пропустить реальное расхождение, поэтому значения стоит просматривать, а поведение проверять тестами.
Честность способа согласования проверяет человек. Агенты строят карту, Crossweft не даёт им придумать связь или пропустить папку, но
shared-codeэто или две копии — иногда знает только разработчик.Внедрение стоит труда. На маленьком проекте выгода невелика; окупается там, где языков и процессов много.
Пользователь пока один — я. Полностью проверена работа с Claude Code; остальные агенты — через MCP и skill, адаптеры их хуков экспериментальные.
check --changedускоряет pre-commit, но пока не тогда, когда правка задела роутер или клиент.
Попробовать
Быстрее всего — сломать демо (Go-API, TypeScript-клиент, Python-воркер):
pip install "git+https://github.com/AnastasiyaW/crossweft.git@v0.2.0" git clone --branch v0.2.0 --depth 1 https://github.com/AnastasiyaW/crossweft cd crossweft/examples/polyglot-shop && crossweft check # RESULT: PASS # поменяйте API_VERSION только в web/src/api.ts: crossweft check # join:api-version disagrees
Каждая строка таблицы проверена на копии демо: проверка падает с кодом 1 и называет нужного сторожа.
Изменение |
Кто ловит |
|---|---|
поднять |
|
добавить JSON-поле только в Go-структуру |
|
добавить статус только в Python-воркер |
|
читать в воркере переменную, которой нет в |
|
вызвать |
|
изменить округление в |
|
В своём репозитории: crossweft init (init --example — чтобы сначала увидеть рабочий шов), дальше агент строит карту по skill, crossweft check его ведёт. В Claude Code: /plugin marketplace add AnastasiyaW/crossweft, затем /plugin install crossweft@crossweft. В CI: uses: AnastasiyaW/crossweft@v0.2.0 или crossweft check --format sarif для GitHub code scanning.
Репозиторий Crossweft охраняет собственные швы Crossweft’ом: версию в трёх файлах, события хуков, команды из документации.
Crossweft распространяется под Apache-2.0. Issues и pull requests — в github.com/AnastasiyaW/crossweft. Английская версия — happyin.work/blog/crossweft-seams.
Комментарии (4)

Sonia_Black Автор
11.10.2026 16:37Спасибо! Частично это уже есть: Crossweft проверяет документацию там, где в ней записано конкретное значение. Команды из README обязаны существовать в CLI, версия в трёх файлах сверяется, каждое утверждение карты привязано к литералу в коде. А вот «высокодоступная система» — это уже не значение, а суждение, тут без модели не обойтись. Crossweft я специально держу детерминированным, чтобы зелёная проверка значила одно и то же при каждом запуске. Ваш слой до кода выглядит хорошим соседом: если ваша проверка умеет выдавать код возврата, её можно подключить к тем же хукам как ещё один валидатор, раннер
crossweft validatorsдля этого и сделан. Напишите в issues на GitHub, давайте посмотрим.

bleedly
11.10.2026 16:37Отличная статья!) Подскажите, вкратце вопрос: Crossweft интегрируется в реальный CI/CD? Есть ли возможность автоматического прогона регрессионных тестов для ИИ-агентов при изменении схемы данных, или сейчас инструмент больше ориентирован на runtime-мониторинг и алертинг разработчиков?

gotham_engineer
11.10.2026 16:37доброго времени!) Зцепила цифра в $9 000...Очевидно, что рассинхронизация данных генерирует тонны пустых запросов и цикличных ошибок, сжигая бюджет...Проводили ли вы замеры: сколько бюджета на токены экономит Crossweft за счет того, что вовремя ловит несоответствие контрактов и не дает агенту уйти в бесконечный цикл саморефлексии? Окупилась ли разработка инструмента чисто на экономии токенов?
Oleg_Sche
Отличная статья! Проблема «швов» (seams), которые ломают AI-агенты из-за ограниченного контекста, сейчас одна из самых острых. Но эта боль начинается еще раньше — на уровне архитектуры и ТЗ. Агент может легко описать систему как «высокодоступную», упустив, что там «один мастер БД + асинхронная репликация + ручные бэкапы по SSH». Это классический SPOF (ровно тот, что положил GitLab на 6 часов в 2017-м), и тесты это не поймают. Выход, который мы видим — двухуровневая защита: 1. До кода: Детерминированная проверка ТЗ и архитектурных описаний на логические противоречия (мы делаем это через взвешенные паттерны + LLM-дебаты, находя такие «бомбы» за секунды). 2. В коде: Ваш подход с crossweft и жесткими сторожами (guards) на границах компонентов. Если связать валидацию архитектуры с вашими хуками, можно вообще блокировать генерацию кода агентом, если его предложение противоречит базовым принципам отказоустойчивости. Планируете ли вы в будущем валидировать сами архитектурные описания (ADR/README), а не только код? Было бы круто обсудить стык наших подходов!