{ "index": 57, "slug": "editorial-2026-06-practice-multi-runtime", "title": "PHP, JavaScript и D: как удержать общий контракт на границе runtime", "excerpt": "Когда один ответ проходит через PHP, JavaScript и D, похожие поля ещё не означают одинаковый смысл. Разбираем узкий контракт, fail-closed проверку и границу между учебной моделью и реальной интеграцией.", "contentHtml": "
Симптом обычно выглядит безобидно: PHP возвращает объект заказа, JavaScript показывает его как готовый, а D-обработчик принимает тот же пакет после адаптации. В логах остаются одинаковые поля, но в редком случае одно отсутствие превращается в 0, другая ветка сохраняет исключение, а третья считает время по другой шкале. Ошибка обнаруживается уже после передачи данных. Цена — неверное решение, повторная обработка или часы разбора, потому что команда спорит о runtime вместо формы сообщения.
Тезис простой: общий контракт нужно проектировать на границе задачи, а не выводить из сходства языков. Для учебной проверки достаточно одного объекта в памяти. В нём надо явно назвать операцию, вид значения, семантику ошибки, шкалу времени и правила адаптеров. Если хотя бы одно поле нельзя сравнить, проверка должна остановиться. Такой результат подтверждает только внутреннюю согласованность модели. Он не подтверждает работу PHP, JavaScript, D или production-сервиса.
\nJSON-подобная форма скрывает решения. Число может означать деньги в минимальных единицах, счётчик или результат преобразования. Пустое поле может означать отсутствие значения, ошибку или значение по умолчанию. Время может быть timestamp, длительностью или логическим порядком событий. Если контракт не называет эти свойства, каждый адаптер заполняет пробел своим правилом.
\nНужен узкий boundary contract. Он не пытается описать всю систему и не переносит внутренние классы, stack trace, сборщик мусора или планировщик. Он отвечает на один вопрос: сохраняют ли три представления одну заранее названную форму. Поэтому в нём нет неявного default. Отсутствующее поле ведёт к отказу, а не к удобной подстановке.
\nВ примере операция называется fixed-order-decision. Поле value.tag отделяет вид значения от его представления. amountMinor: 4200 — учебное целое число; оно не объявляет денежный протокол и не должно автоматически превращаться во float. Поле error использует именованный конверт: в нём есть семантика, код и правило повтора. Это не объект исключения и не текст сообщения.
Время задаётся двумя упорядоченными логическими отметками. Числа 100 и 108 дают разность восемь внутри учебной шкалы. Они не являются timestamp и не показывают latency. Каждый адаптер получает ту же версию схемы, тот же tag, ту же семантику ошибки, ту же шкалу времени и результат exact. Приведение типа скрывает потерю смысла, поэтому его надо отклонять.
| Поле | Зачем оно нужно | Когда остановиться |
|---|---|---|
schemaVersion | Связывает верхний объект и адаптеры. | Версия пустая или различается. |
value.tag | Называет вид значения до преобразования. | Tag отсутствует или подменён. |
error | Фиксирует code и retry без object identity. | Нет именованного конверта. |
time | Задаёт одну сравнимую шкалу. | Нет двух упорядоченных отметок. |
mapping | Показывает сохранение формы. | Используется coercion вместо exact. |
Ниже выполняется только JavaScript-код, который читает заранее заданный объект. Строки php, javascript и d — метки взглядов на форму, а не запущенные процессы. Пример полезен для проверки правил и отрицательных веток. Он не доказывает совместимость библиотек, транспортов или окружений.
const record = {\n id: 'named-contract-v1',\n schemaVersion: 'fixed-boundary-1',\n contract: {\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 adapters: [\n { model: 'php', contractVersion: 'fixed-boundary-1', valueTag: 'order-ready', errorSemantics: 'named-envelope', timeBasis: 'fixed-logical-ticks', mapping: 'exact' },\n { model: 'javascript', contractVersion: 'fixed-boundary-1', valueTag: 'order-ready', errorSemantics: 'named-envelope', timeBasis: 'fixed-logical-ticks', mapping: 'exact' },\n { model: 'd', contractVersion: 'fixed-boundary-1', valueTag: 'order-ready', errorSemantics: 'named-envelope', timeBasis: 'fixed-logical-ticks', mapping: 'exact' }\n ]\n};\n\nconst models = new Set(record.adapters.map(({ model }) => model));\nconst accepted =\n record.schemaVersion === 'fixed-boundary-1' &&\n record.contract.time.closed >= record.contract.time.opened &&\n ['php', 'javascript', 'd'].every((model) => models.has(model)) &&\n record.adapters.every((adapter) =>\n adapter.contractVersion === record.schemaVersion &&\n adapter.valueTag === record.contract.value.tag &&\n adapter.errorSemantics === record.contract.error.semantics &&\n adapter.timeBasis === record.contract.time.basis &&\n adapter.mapping === 'exact'\n );\n\nconsole.log({ accepted, externalEffect: 'not-checked' });\nПоложительный результат означает: поля учебного объекта соответствуют названным правилам. externalEffect: not-checked удерживает смысл результата рядом с кодом. Если его убрать, читатель легко примет accepted: true за доказательство, что три системы связаны.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Пропущенный amount читается как ноль. | Нет отдельного tag для отсутствия. | Сверить value.tag и наличие поля. | Добавить именованный вариант или остановить проверку. |
| Один адаптер хранит thrown value. | Смешаны semantics ошибки. | Сравнить errorSemantics буквально. | Выровнять конверт или вернуть stop-incomparable-adapter. |
| Для одного ответа считают duration. | Нет общей шкалы и закрывающей отметки. | Проверить basis, opened и closed. | Задать ordered fixed ticks; не подставлять часы. |
| Адаптер возвращает похожее число. | Форма прошла coercion. | Проверить mapping на exact. | Убрать приведение или описать новое поле и версию. |
| Пример называют интеграционным тестом. | Метки моделей приняли за процессы. | Перечислить реально запущенные компоненты. | Сузить вывод до проверки объекта в памяти. |
Пустая версия или отсутствующий адаптер должны вернуть stop-incomplete-contract. Не надо принимать частичный объект ради продолжения разбора. Если JavaScript записывает thrown-value, а два других адаптера используют named-envelope, результат — stop-incomparable-adapter. Это не утверждение о поведении языков. Это точное описание несовпадения полей.
Если closed отсутствует или basis равен wall-clock, верните stop-undetermined-time-boundary. Нельзя восстановить длительность из незаписанного события и нельзя превратить учебные ticks в метрику. Если новые требования делают эти поля недостаточными, создайте новую версию контракта. Не прячьте смысл в поле metadata.
Модель не описывает nullable semantics, ABI, сериализацию, transport, версии пакетов, доступы, retry конкретного клиента или бизнес-значение заказа. Она не запускает PHP, D или отдельный JavaScript runtime, не читает сеть и диск, не использует часы, не собирает telemetry, trace или profile и не содержит пользовательских данных. Поэтому из неё нельзя вывести latency, SLA, безопасность, совместимость релиза или готовность deploy.
\nВ production такой контракт может стать частью отдельного теста совместимости, но это потребует реальных входов, владельцев, версий и наблюдаемого результата. Учебный объект не заменяет этот тест. Он только не даёт начать разговор с ложного утверждения.
\nМатериал и его пример готовы, если независимый читатель может повторить проверку по одному объекту и получить одно из двух: accepted с перечисленными полями или точный stop reason. При accepted все три model label присутствуют, версия совпадает, tag и error semantics совпадают, время упорядочено, mapping равен exact, а итог прямо говорит externalEffect: not-checked. При отказе причина указывает на конкретное недостающее поле. Ни один результат не использует слова «интеграция подтверждена» без отдельного runtime-доказательства.