{ "index": 55, "slug": "editorial-2026-06-field-multi-runtime", "title": "Когда PHP, JavaScript и D говорят разное: как закрыть границу контракта", "excerpt": "Разные runtime не становятся одной системой от похожего JSON. Разберём наблюдаемый симптом, явный контракт, отрицательный путь и критерий, который отделяет проверяемую модель от заявления об интеграции.", "contentHtml": "
Запрос проходит через PHP, затем попадает в JavaScript и заканчивается обработкой на D. Пользователь видит ошибку без понятной причины, а оператор не может связать запись D с исходным запросом. Иногда все три части возвращают похожий JSON, но одна сторона считает ошибку исключением, другая — обычным значением, а третья записывает время в другой шкале. Внешне система работает. Внутри она уже потеряла общий смысл.
\nЦена такой ошибки выше, чем один неудачный запрос. Команда может повторить несовместимый ответ, принять его за временный сбой и включить повторную попытку там, где операция уже выполнена. Можно получить двойное списание, зависшую задачу или неверный отчёт. Похожая форма данных не защищает от разных правил обработки.
\nТри runtime можно соединить только через узкий, именованный контракт. Он должен назвать операцию, форму успешного значения, форму ошибки и основание времени. Каждый адаптер обязан сохранить эти поля без скрытого приведения. Если поле нельзя сопоставить, система должна остановиться и назвать причину. Молчаливое «примерно подходит» опаснее явного отказа.
\nВ этом материале проверяется модель границы. Учебный пример хранит запись в памяти JavaScript и читает её обычной функцией. Он не запускает PHP, JavaScript как отдельный процесс или D. Он не обращается к сети, диску, часам, телеметрии и сервисам. Поэтому его результат говорит только о внутренней сопоставимости записи. Это ограничение входит в смысл примера.
\nСначала назовите операцию. Строка fixed-order-decision лучше, чем общий «обработчик заказа»: у неё есть конкретная граница. Затем задайте value tag. В примере это order-ready. Число 4200 получает единицу и валюту. Без tag и единицы потребитель может принять копейки за рубли или число лимита за сумму.
Ошибка получает named envelope. В нём явно присутствуют semantics, code и retry. Значение code: null означает отсутствие кода внутри известного envelope. Отсутствующий сам envelope означает другую проблему. Эти два случая нельзя сливать в одну пустую строку.
Время в изолированной модели задаётся ordered fixed logical ticks. Пара 100..108 показывает порядок и интервал из восьми условных шагов. Это не миллисекунды, не latency и не SLA. Если нужен production-замер, он требует отдельного источника времени, политики измерения и проверки среды.
const contract = {\n schemaVersion: 'fixed-boundary-1',\n operation: 'fixed-order-decision',\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 = ['php', 'javascript', 'd'].map((model) => ({\n model,\n contractVersion: 'fixed-boundary-1',\n valueTag: 'order-ready',\n errorSemantics: 'named-envelope',\n timeBasis: 'fixed-logical-ticks',\n mapping: 'exact'\n}));\n\nfunction check(record) {\n if (record.schemaVersion !== 'fixed-boundary-1') {\n return { status: 'stop-incomplete-contract', reason: 'schema-version-missing' };\n }\n if (record.adapters.some((item) => item.mapping !== 'exact')) {\n return { status: 'stop-incomparable-adapter', reason: 'mapping-is-not-exact' };\n }\n return { status: 'synthetic-review-hand-off', observedEffect: 'none-observed' };\n}\n\nconsole.log(check({ ...contract, adapters }));\nПример учебный. Он показывает форму проверки, а не совместимость библиотек. В нём имена php, javascript и d — значения поля model. Они не доказывают, что три языка обменялись данными. Положительный результат означает лишь: запись содержит названные поля, версии совпадают, а mapping не скрывает преобразование.
Одинаковый текст ошибки не задаёт одинаковое действие. PHP может вернуть envelope, JavaScript — выбросить значение, а D — записать код в отдельное поле. Потребитель, который проверяет только сообщение, потеряет режим завершения. Сравнивайте не текст, а семантику: кто владеет ошибкой, можно ли повторить операцию и какие данные сохраняются.
\nОдинаковое число времени тоже ничего не гарантирует. Один адаптер может передать логические шаги, другой — epoch seconds. Числа совпадут случайно, а вывод окажется ложным. Поэтому basis должна быть полем контракта и каждого адаптера. Пропущенный closed должен закрывать проверку, а не заменяться текущим временем.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Ответы похожи, но retry ведёт себя по-разному | Envelope смешан с thrown value | Сравнить error semantics и наличие code/retry | Выровнять envelope или вернуть stop |
| Время совпадает только на одном стенде | Разные time basis | Проверить basis и обе границы интервала | Назвать одну шкалу или прекратить сравнение |
| Сумма проходит проверку, но меняет порядок величины | Tag, unit или currency угадываются по имени | Проверить value tag, amountMinor и currency | Добавить явное поле и запретить inference |
| Адаптер «почти» совпадает с contract | Скрытая coercion при mapping | Потребовать mapping: exact | Описать преобразование явно либо остановить hand-off |
| Нельзя связать запись D с запросом PHP | Нет общего correlation field | Проверить, входит ли идентификатор в contract | Добавить поле в новый контракт; не восстанавливать связь по времени |
Проверка должна отказываться от удобного вывода. Возьмём запись, где у одного адаптера errorSemantics: 'thrown-value', а у контракта остаётся named-envelope. Такой набор нельзя объявить совместимым. Функция возвращает stop-incomparable-adapter и оставляет владельцу конкретное действие: выровнять семантику.
Другой случай: time.basis равен wall-clock, а closed отсутствует. Здесь нельзя написать «обработка заняла неизвестное время» и нельзя вычислить значение из текущих часов. Проверка должна вернуть stop-undetermined-time-boundary. Она не спорит о точности. Она фиксирует отсутствие основания для сравнения.
Третий случай — coercion. Если адаптер превратил строку в число, округлил сумму или заменил пустое значение значением по умолчанию, результат уже не exact mapping. Приведение может быть правильным в конкретной программе, но оно требует правила, единицы и теста. До этого момента оно скрывает смысл и закрывает hand-off.
\namountMinor и currency.Допустимая формулировка узкая: «фиксированная запись соответствует названным правилам и может перейти на synthetic review». Нельзя писать «PHP, JavaScript и D совместимы», «адаптер работает», «интеграция подтверждена» или «latency равна восьми». У модели нет runtime, транспорта, хоста, зависимостей, прав, пользовательских данных и production-метрик.
\nЭто не бюрократическая оговорка. Явная граница защищает решение от расширения смысла при копировании. Читатель видит, какой факт проверен, а какой ещё требует отдельного эксперимента. Если понадобится связать PHP-запрос и запись D, добавьте correlation id и проверьте его на реальном пути. Не выводите связь из одинакового времени, порядка строк или похожего JSON.
\nМодель не описывает ABI, сериализацию, nullable policy, иерархию классов, stack unwinding, сборку мусора, планировщик, retry конкретного клиента или схему регистрации сервисов. Эти свойства нельзя спрятать в поле details. Если свойство влияет на решение, назовите его отдельным полем и задайте проверку. Если назвать его нельзя, остановите границу.
Модель также не заменяет контракт домена. Tag order-ready говорит о форме значения, но не доказывает, что бизнес действительно разрешает выдавать заказ. Доменный смысл проверяет владелец операции. Техническая проверка должна передать ему точное значение и не присваивать себе его решение.
Граница готова к synthetic hand-off, если выполнены все условия: есть schema version и operation; value имеет tag и единицы; error содержит named semantics, code и retry; time имеет одну basis и целые ordered ticks; присутствуют все три model labels; каждый адаптер сохраняет version, tag, error semantics, time basis и exact mapping; отрицательные случаи возвращают именованные stop; результат содержит observedEffect: none-observed.
Проверяемый результат можно повторить на той же записи и получить тот же status. Если status меняется от текущих часов, сети, окружения или неявной нормализации, это уже не изолированная проверка. Если положительный status требует доверия к словам о запущенном сервисе, он выходит за границу примера. В обоих случаях работу нужно остановить и уточнить новый scope.
\n