{ "index": 56, "slug": "editorial-2026-06-mechanism-multi-runtime", "title": "Один payload, три runtime: как сохранить смысл на границе", "excerpt": "Похожая структура данных не делает PHP, JavaScript и D взаимозаменяемыми. Разбираем контракт type, error и time, отрицательный путь и проверяемый критерий совместимости.", "contentHtml": "

Сервис принял заказ и вернул объект с полями amount, currency и status. PHP назвал статусом готовности строку ready. JavaScript обработал её как обычный результат. D получил тот же набор полей, но считает отсутствие кода ошибки отдельным состоянием. На границе всё выглядит одинаково. После сбоя команда видит три разных решения, а в логах остаётся один красивый JSON.

\n

Цена такой ошибки — не только неверное сообщение. Клиент может повторить уже принятый заказ, worker может пропустить отказ, а расследование свяжет события по совпадающим полям, хотя они относятся к разным состояниям. Исправление обычно начинается с догадки: добавить retry, привести значение к строке или считать пустой код успехом. Каждая такая догадка расширяет зону риска.

\n

Тезис статьи простой: общий boundary contract должен называть не только поля, но и их смысл. Для минимальной проверки достаточно разделить три независимые оси: type отвечает за вид значения, error — за режим завершения, time — за определённую шкалу и порядок. Если одна ось не задана или подменена другой, проверка должна остановиться.

\n

Почему одинаковый JSON не равен общему контракту

\n

JSON переносит форму. Он не переносит договор о поведении. Число 4200 может означать сумму в копейках, лимит, внутренний идентификатор или случайный счётчик. Строка ready может быть именем состояния, текстом для интерфейса или результатом нестрогого сравнения. Без названного типа consumer вынужден угадывать.

\n

Ошибки создают второй разрыв. Один runtime может вернуть объект результата с полем error. Другой может завершить операцию исключением. Третий может вернуть код и продолжить выполнение. Человек способен описать эти случаи одной фразой «обработка ошибки». Адаптеру такой фразы недостаточно: ему нужно знать, можно ли повторять операцию, сохранено ли значение и кто владеет решением.

\n

Время создаёт третий разрыв. Длительность из monotonic clock нельзя без оговорки сравнивать с календарным timestamp. Два числа без шкалы не доказывают latency. Даже одинаковые начало и конец могут быть только порядковыми метками внутри тестового объекта, а не наблюдением работающей системы.

\n
\"Матрица
Иллюстрация разделяет три вопроса boundary. Матрица показывает структуру проверки, а не сравнительную характеристику PHP, JavaScript и D и не результат запуска в конкретной среде.
\n

Три оси контракта

\n

type должен содержать named tag. Не выводите его из соседних полей. В примере order-ready — это фиксированная метка значения, а amountMinor и currency — дополнительные поля с собственной единицей и форматом. Если адаптер заменяет tag числом или оставляет его пустым, shape больше нельзя считать сохранённым.

\n

error должен описывать режим завершения. Удобная минимальная форма — semantics, code и retry. Значение code: null означает отсутствие кода в данном envelope. Оно не означает «в системе ошибок нет». Значение retry: not-requested не запускает повтор и не обещает, что повтор безопасен. Для этого нужны отдельные правила идемпотентности.

\n

time должен называть basis и обе границы интервала. Fixed logical ticks подходят для проверки порядка внутри заранее заданной записи. Они не являются миллисекундами. Если контракт требует наблюдаемую длительность, ему нужны источник измерения, единицы, точка начала и точка окончания. Нельзя подставить текущие часы, чтобы получить зелёный результат.

\n

Три оси проверяются отдельно. Ошибка не сообщает тип значения. Числовое поле не задаёт единицу времени. Наличие timestamp не подтверждает, что операция завершилась успешно. Такое разделение кажется избыточным только до первого неоднозначного отказа.

\n

Учебный пример проверки

\n

Ниже приведён ограниченный JavaScript-пример. Он проверяет только объект в памяти и возвращает причину остановки. Он не запускает PHP или D, не вызывает сеть, не читает часы, не измеряет производительность и не подтверждает поведение сервиса. Его задача — показать форму fail-closed проверки.

\n
const contract = {\n  schemaVersion: 'boundary-1',\n  value: { tag: 'order-ready', amountMinor: 4200, currency: 'RUB' },\n  error: { semantics: 'named-envelope', code: null, retry: 'not-requested' },\n  time: { basis: 'fixed-logical-ticks', opened: 100, closed: 108 }\n};\n\nconst adapters = [\n  { model: 'php', version: 'boundary-1', valueTag: 'order-ready',\n    errorSemantics: 'named-envelope', timeBasis: 'fixed-logical-ticks', mapping: 'exact' },\n  { model: 'javascript', version: 'boundary-1', valueTag: 'order-ready',\n    errorSemantics: 'named-envelope', timeBasis: 'fixed-logical-ticks', mapping: 'exact' },\n  { model: 'd', version: 'boundary-1', valueTag: 'order-ready',\n    errorSemantics: 'named-envelope', timeBasis: 'fixed-logical-ticks', mapping: 'exact' }\n];\n\nfunction review(record, participants) {\n  if (!record.schemaVersion || participants.length !== 3) {\n    return 'stop-incomplete-contract';\n  }\n  if (record.time.basis !== 'fixed-logical-ticks' ||\n      record.time.closed === null || record.time.closed < record.time.opened) {\n    return 'stop-undetermined-time-boundary';\n  }\n  const valid = participants.every((item) =>\n    item.version === record.schemaVersion &&\n    item.valueTag === record.value.tag &&\n    item.errorSemantics === record.error.semantics &&\n    item.timeBasis === record.time.basis &&\n    item.mapping === 'exact'\n  );\n  return valid ? 'accepted-fixed-contract' : 'stop-incomparable-adapter';\n}\n\nconsole.log(review(contract, adapters)); // accepted-fixed-contract
\n

Пример сравнивает ровно те сведения, которые записаны в объекте. Он не делает вывод о типовой системе языка по полю model. Подписи php, javascript и d здесь лишь заранее названные участники матрицы. Версия runtime, библиотека, transport и формат сериализации в этот объект не входят.

\n

В реальном коде такой boundary нужно реализовать в согласованном контракте и покрыть тестами конкретного продукта. Нельзя скопировать функцию и объявить интеграцию проверенной. Учебный объект специально маленький: он помогает увидеть missing field и смешанную семантику, но не заменяет контракт API.

\n

Симптом → причина → проверка → действие

\n
Признаки потери смысла на границе и минимальная реакция
СимптомПричинаПроверкаДействие
Одинаковый shape даёт разные решенияНет named value tag или версии схемыСверить tag, schemaVersion и единицу каждого поляДобавить обязательные поля; не выводить смысл из shape
Один участник бросает ошибку, другие возвращают envelopeСмешаны error semanticsСравнить режим завершения, code и retryВыбрать один boundary mode или остановить mapping
Отчёт говорит «быстрее», но метрики нетLogical ticks приняли за latencyПроверить basis, источник часов и обе границыУдалить вывод о скорости либо завести отдельное измерение
Повтор после таймаута создаёт второй заказRetry назван, но идемпотентность не определенаПроверить operation key и эффект повторного вызоваОстановить повтор; согласовать ключ и политику отдельно
«Успех» появляется при неполном объектеПроверка подставляет default вместо отказаУдалить default и прогнать missing-caseВернуть точную stop-причину владельцу контракта
\n

Отрицательный путь важнее зелёного примера

\n

Положительный объект удобен, но он почти ничего не говорит о дисциплине контракта. Настоящая проверка начинается с испорченной записи. Удалите schemaVersion. Ожидаемый результат — stop-incomplete-contract. Не подставляйте текущую версию автоматически: иначе тест перестанет замечать несовместимый участник.

\n

Замените у JavaScript-участника errorSemantics на thrown-value. Ожидаемый результат — stop-incomparable-adapter. Проверка не должна оборачивать исключение в envelope задним числом. Такое преобразование может быть правильным решением продукта, но тогда оно должно быть отдельным адаптером с названными правилами, а не скрытой операцией review.

\n

Измените time.basis на wall-clock и оставьте closed: null. Ожидаемый результат — stop-undetermined-time-boundary. Нельзя сказать «интервал неизвестен, но примерно короткий». В этом объекте нет факта, который поддерживает такую оценку.

\n

Такой путь защищает и от незаметной нормализации. Coercion может сделать данные удобнее для одного consumer, но скрыть различие между целым числом и tagged value. Если преобразование нужно, его надо назвать, версионировать и проверить как новую границу. Молчаливое приведение не является доказательством совместимости.

\n

Порядок действий

\n
  1. Назовите одну операцию и владельца её смысла. Не начинайте с общего утверждения «три языка совместимы».
  2. Запишите schema version, value tag, единицы полей и допустимые пустые значения.
  3. Определите один error envelope или другой единый режим завершения. Отдельно назовите retry и условие его безопасности.
  4. Выберите time basis. Для логического порядка зафиксируйте обе границы; для latency укажите источник измерения и единицы.
  5. Составьте по одной записи для PHP, JavaScript и D. Сравнивайте каждую с контрактом, а не одну реализацию с другой.
  6. Запустите positive-case и четыре negative-case: missing schema, mixed error, неизвестный tag и незакрытый интервал.
  7. Для каждого отказа сохраните точную причину. Не заменяйте её общим «adapter error».
  8. Только после успешной проверки границы подключайте конкретный transport, runtime и наблюдение. Их результаты нельзя приписывать синтетическому объекту.
\n

Ограничения применимости

\n

Контрактная матрица не делает разные языки одинаковыми. Она не описывает сборщик мусора, правила приведения типов, исключения, ABI, сериализатор или планировщик. Эти свойства могут влиять на интеграцию и требуют отдельных источников и тестов. Матрица только не даёт спрятать их за одинаковым полем.

\n

Даже официальный документ языка отвечает на вопрос о данном языке, а не о совместимости трёх систем. Например, строгая типизация PHP может изменить момент отказа, completion record ECMAScript описывает семантику спецификации JavaScript, а D документирует собственную обработку ошибок. Ни один из этих фактов сам по себе не доказывает общий API.

\n

Матрица также не проверяет бизнес-смысл суммы, права пользователя, повторную доставку сообщения или транзакцию. Для них нужны свои поля, владельцы и отрицательные сценарии. Если boundary не может выразить важное условие, нельзя считать его достаточным только потому, что все участники прошли текущую проверку.

\n

Проверяемый критерий готовности

\n

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

\n

Дополнительное условие — все три участника сохраняют contract shape без неявного приведения, а положительный результат не содержит утверждения о latency, deployment или реальном поведении среды. Если хотя бы одно поле приходится угадывать, проверка должна закончиться именованной stop-причиной. Это и есть полезный результат: команда видит границу знания до того, как похожий payload станет ошибочным действием.

\n

Проверяемые источники

\n" }