Files

8 lines
25 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 55,
"slug": "editorial-2026-06-field-multi-runtime",
"title": "PHP, JavaScript и D на одной границе: как доказать совместимость",
"excerpt": "Практический разбор расхождения между PHP, JavaScript и D: явный контракт значения, ошибки и времени, отрицательные проверки и границы вывода.",
"contentHtml": "<p>Часть запроса проходит через PHP, затем попадает в JavaScript, а результат обрабатывает компонент на D. Пользователь получает ошибку без понятной причины, а оператор не может связать запись D с исходным запросом. На границе всё выглядит правдоподобно: поля называются одинаково, JSON похож, а число времени совпадает. Но один участник возвращает значение, другой выбрасывает исключение, третий записывает код отдельно. Это не совместимость, а потеря смысла, которую пока не видно.</p>\n<p>Цена ошибки — повторная операция, пропущенный отказ или расследование без доказательства связи событий. Клиент может повторить уже принятую команду, worker — принять неполный ответ за успех, а команда — объявить проблему задержкой, хотя сравнивает разные шкалы времени. Статья показывает, как проверить одну узкую границу и получить воспроизводимый результат. Она не объявляет три языка совместимыми и не заменяет тест реального сервиса.</p>\n<h2>Вопрос, на который отвечает проверка</h2>\n<p>Нужно ответить не на вопрос «могут ли три языка работать в одной системе», а на более точный: «сохраняет ли каждый участник заранее названный смысл конкретной записи». Для этого у записи должны быть версия схемы, операция, описанное значение, режим ошибки и основание времени. Участники сравниваются с этим контрактом по одинаковым правилам. Сравнение PHP с JavaScript напрямую не заменяет сравнение каждого из них с общей спецификацией.</p>\n<p>Такой подход отделяет факт от предположения. Факт — в записи есть <code>schemaVersion: 'boundary-1'</code>, значение помечено тегом <code>order-ready</code>, а интервал задан логическими шагами от 100 до 108. Предположение — что этот объект действительно прошёл через PHP, JavaScript и D. Поля <code>model</code> с названиями языков не превращаются в доказательство запуска. Для последнего нужны логи, транспорт, версии сборок и тест конкретного приложения.</p>\n<h2>Контракт начинается со смысла, а не с формы JSON</h2>\n<p>JSON описывает синтаксическую форму, но не объясняет значение числа или строки. Число <code>4200</code> может быть суммой в минимальных единицах, лимитом, идентификатором или счётчиком. Строка <code>ready</code> может быть состоянием домена, текстом интерфейса или случайным результатом сравнения. Поэтому поле <code>value.tag</code> должно называться явно, а денежное значение — иметь единицу и валюту. Если потребитель угадывает смысл по имени поля, контракт уже неполон.</p>\n<p>Операция и версия нужны для защиты от тихой подмены. Контракт для <code>fixed-order-decision</code> нельзя автоматически применять к другому действию только потому, что там тоже есть <code>amountMinor</code>. Версия сообщает, по какому набору правил читать запись. Это не версия PHP, Node.js или компилятора D. Версии runtime и библиотек остаются отдельными атрибутами интеграционного теста.</p>\n<p>Ошибку тоже нужно описывать как данные о поведении. В примере <code>error.semantics</code> равен <code>named-envelope</code>, <code>code</code> может быть <code>null</code>, а <code>retry</code> имеет значение <code>not-requested</code>. Последняя строка не означает, что повтор безопасен: она лишь фиксирует, что данная запись не запрашивает повтор. Идемпотентность, эффект повторного вызова и политика клиента требуют отдельного контракта.</p>\n<p>Время должно иметь <code>basis</code>. Фиксированные логические шаги подходят для проверки порядка в заранее созданном объекте. Они не являются миллисекундами, latency или SLA. Для наблюдаемой длительности нужны источник часов, единицы измерения, границы интервала и правило обработки рассинхронизации. Подстановка текущего времени в пропущенное поле делает пример менее воспроизводимым и скрывает ошибку.</p>\n<h2>Воспроизводимая проверка в памяти</h2>\n<p>Ниже — самостоятельный пример на JavaScript. Его можно сохранить в файл и запустить в среде с поддержкой современного синтаксиса JavaScript. Он проверяет только два заранее заданных объекта в памяти: полный набор участников и набор с изменённой семантикой ошибки. Пример не запускает PHP и D, не вызывает сеть, не читает системные часы, не измеряет скорость и не проверяет сериализацию.</p>\n<pre><code>const contract = {\n schemaVersion: '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 participants = [\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, items) {\n if (record?.schemaVersion !== 'boundary-1' ||\n record.operation !== 'fixed-order-decision' || items.length !== 3) {\n return 'stop-incomplete-contract';\n }\n if (record.value?.tag !== 'order-ready' ||\n record.value.currency !== 'RUB' || !Number.isInteger(record.value.amountMinor)) {\n return 'stop-untagged-value';\n }\n if (record.error?.semantics !== 'named-envelope' ||\n !Object.hasOwn(record.error, 'code') || !Object.hasOwn(record.error, 'retry')) {\n return 'stop-ambiguous-error';\n }\n if (record.time?.basis !== 'fixed-logical-ticks' ||\n !Number.isInteger(record.time.opened) || !Number.isInteger(record.time.closed) ||\n record.time.opened &gt; record.time.closed) {\n return 'stop-undetermined-time-boundary';\n }\n const knownModels = new Set(['php', 'javascript', 'd']);\n const exact = items.every((item) =&gt;\n knownModels.has(item.model) &amp;&amp;\n item.version === record.schemaVersion &amp;&amp;\n item.valueTag === record.value.tag &amp;&amp;\n item.errorSemantics === record.error.semantics &amp;&amp;\n item.timeBasis === record.time.basis &amp;&amp;\n item.mapping === 'exact'\n );\n return exact ? 'accepted-fixed-contract' : 'stop-incomparable-adapter';\n}\n\nconsole.log(review(contract, participants));\nconst mixedError = participants.map((item) =&gt;\n item.model === 'javascript' ? { ...item, errorSemantics: 'thrown-value' } : item\n);\nconsole.log(review(contract, mixedError));</code></pre>\n<p>Ожидаемый вывод — сначала <code>accepted-fixed-contract</code>, затем <code>stop-incomparable-adapter</code>. Первый результат означает только то, что все проверенные поля совпали с проектными литералами. Второй показывает, что один участник описывает ошибку иначе. Валидатор не пытается угадать, можно ли преобразовать исключение в envelope. Такое преобразование допустимо только как явно реализованный адаптер с собственными тестами и правилами.</p>\n<p>В коде намеренно проверяются наличие полей, целые границы времени и список допустимых участников. Проверка <code>Object.hasOwn</code> отличает явный <code>null</code> от отсутствующего поля. Это важно: отсутствие <code>code</code> может означать, что данные потеряны, тогда как <code>code: null</code> — осознанная часть конкретного формата. Проект может выбрать другую политику, но её нужно записать и одинаково применить к каждому участнику.</p>\n<h2>Как читать отрицательный результат</h2>\n<p>Отказ не доказывает, что реализация плохая. Он доказывает более узкое утверждение: текущая запись не позволяет честно объявить соответствие выбранному контракту. Причина должна вести к следующему действию. Для неполной версии нужно найти владельца схемы; для неразмеченного значения — определить единицу и тег; для смешанной ошибки — выбрать границу преобразования или сохранить разницу.</p>\n<p>Проверка времени должна быть особенно строгой. Если <code>closed</code> отсутствует, нельзя вычислять его из текущих часов. Если basis одного участника — epoch seconds, а другого — логические шаги, совпадающие числа не создают общего интервала. Если нужны реальные миллисекунды, пример следует заменить измерением на конкретном пути и отдельно указать часы, нагрузку, версию сборки и способ повторения.</p>\n<p>Нельзя исправлять отказ неявной coercion. Превращение строки в число, округление суммы или замена пустого значения default-ом может быть осмысленной операцией. Но она меняет границу данных. Её следует назвать, покрыть тестом на исходное и полученное значение и включить в версию адаптера. Пока правило не названо, статус <code>exact</code> будет ложным.</p>\n<div class='table-scroll'><table><caption>Симптомы потери смысла, проверка и безопасное действие</caption><thead><tr><th scope='col'>Симптом</th><th scope='col'>Вероятная причина</th><th scope='col'>Минимальная проверка</th><th scope='col'>Действие</th></tr></thead><tbody><tr><td>Похожие JSON дают разные решения</td><td>Нет версии или value tag</td><td>Сверить схему, tag, единицу и валюту</td><td>Сделать поля обязательными; не выводить смысл из shape</td></tr><tr><td>Один участник бросает ошибку, другие возвращают объект</td><td>Смешаны режимы завершения</td><td>Сравнить semantics, code и retry</td><td>Ввести явный адаптер или остановить передачу</td></tr><tr><td>В отчёте появилась latency из двух чисел</td><td>Логические шаги приняты за часы</td><td>Проверить basis, источник и единицы</td><td>Убрать вывод о скорости или провести измерение</td></tr><tr><td>Повтор создаёт вторую операцию</td><td>Retry назван без идемпотентности</td><td>Проверить ключ операции и повторный эффект</td><td>Остановить retry до отдельного решения</td></tr><tr><td>Неполная запись считается успешной</td><td>Валидатор подставляет default</td><td>Удалить default и добавить missing-case</td><td>Вернуть именованную причину отказа</td></tr><tr><td>Лог D нельзя связать с запросом PHP</td><td>Нет общего correlation id</td><td>Проверить идентификатор в каждом событии</td><td>Добавить его в новый контракт; не связывать по времени</td></tr></tbody></table></div>\n<h2>Где проходит граница ответственности</h2>\n<p>Контракт отвечает за представление и правила передачи. Он не решает, разрешено ли выдавать заказ, имеет ли пользователь право на операцию или безопасно ли повторять вызов. Эти решения принадлежат доменному владельцу и должны иметь собственные условия и тесты. Метка <code>order-ready</code> описывает значение в технической записи, но не выдаёт бизнес-разрешение.</p>\n<p>Адаптер отвечает за явное преобразование между своим представлением и контрактом. Он не должен скрывать потерю поля, менять валюту без правила или превращать исключение в успех. Если адаптер не может выразить состояние, правильный результат — отказ с причиной. Клиент отвечает за реакцию на эту причину, а не за угадывание пропущенного значения.</p>\n<p>Наблюдаемость отвечает за доказательство реального пути. Для связи событий нужны correlation id, идентификатор операции, версия участника, время с известной шкалой и запись результата. Один фиксированный объект в памяти не содержит этих свидетельств. Поэтому его положительный статус нельзя переносить на production, нагрузочное сравнение или гарантию доставки.</p>\n<figure><img src='/assets/editorial/2026/multi-runtime-2026-integration-evidence-loop.svg' alt='Схема проверки контракта между PHP, JavaScript и D с отдельными ветками отказа при неполной записи и точного сопоставления' loading='lazy' /><figcaption>Схема показывает порядок чтения фиксированной записи: граница, семантика и точное сопоставление. Она не изображает сетевой вызов, запуск трёх runtime или результат production-наблюдения.</figcaption></figure>\n<h2>Порядок проверки в настоящем проекте</h2>\n<ol><li>Назовите одну операцию, владельца доменного смысла и версию контракта.</li><li>Зафиксируйте value tag, формат числа, единицы, валюту и допустимые пустые значения.</li><li>Опишите один режим ошибки. Отдельно укажите code, retry и условие, при котором повтор безопасен.</li><li>Выберите time basis. Для latency назовите источник часов и единицы; для порядка используйте отдельные логические метки.</li><li>Составьте по одному образцу для PHP, JavaScript и D. Сравнивайте каждый образец с контрактом.</li><li>Добавьте отрицательные случаи: пропущенная версия, неизвестный tag, смешанная ошибка, отсутствующая граница времени и скрытое преобразование.</li><li>Сохраните для каждого отказа код причины и поле, которое его вызвало. Общий статус <code>adapter error</code> не помогает исправлению.</li><li>Проверьте сериализацию и транспорт отдельными тестами. Зафиксируйте версии runtime, библиотеки, сборки и окружения.</li><li>Только после этого проверяйте реальный путь по логам и correlation id. Не приписывайте фиксированному примеру эффекты работающей системы.</li></ol>\n<h2>Ограничения применимости</h2>\n<p>Модель не описывает ABI, правила приведения типов, сериализатор, сетевые таймауты, порядок доставки, транзакции, сборку мусора, планировщик и раскладку памяти. Эти свойства могут менять результат реальной интеграции. Их нельзя считать проверенными по совпавшему JSON. Для каждого свойства нужен отдельный источник, тест или наблюдение в заявленной среде.</p>\n<p>Валидатор также не измеряет производительность. Числа 100 и 108 — проектные целые литералы, показывающие порядок и интервал внутри fixture. Они не означают 8 миллисекунд, 8 секунд или любую другую физическую величину. Нельзя сравнивать такой объект с production latency и делать вывод о быстродействии D, PHP или JavaScript.</p>\n<p>Официальная спецификация отдельного языка не является сертификатом межъязыковой совместимости. Документация PHP описывает его исключения, спецификация ECMAScript — семантику JavaScript, спецификация D — собственные исключения и безопасность их обработки. Общий API подтверждается только контрактом приложения и тестом всех границ. Если важное условие нельзя выразить в контракте, область вывода нужно сузить, а не заполнить догадкой.</p>\n<h2>Критерий готовности</h2>\n<p>Граница готова к интеграционному тесту, если другой инженер без устного объяснения может назвать операцию, версию, смысл каждого значения, режим ошибки, правило retry и шкалу времени. Для полного и неполного объектов есть разные ожидаемые статусы. Каждый участник имеет идентификатор, версию и exact mapping либо описанное преобразование. Отрицательные случаи не превращаются в успех за счёт default-ов.</p>\n<p>Итоговая формулировка должна оставаться узкой: «этот набор полей соответствует контракту на уровне структуры и названных семантик». Формулировки «интеграция подтверждена», «латентность известна» и «три языка совместимы» требуют дополнительных доказательств. Если их нет, именованный отказ — полезный результат: он показывает, какое именно наблюдение нужно получить дальше.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href='https://www.php.net/manual/en/language.exceptions.php' target='_blank' rel='noopener noreferrer'>PHP Manual: Exceptions</a> — официальное описание <code>throw</code>, <code>catch</code>, распространения исключения по стеку и требований к выбрасываемому объекту. Используется для ограничения утверждений о PHP, а не для доказательства общего API.</li><li><a href='https://262.ecma-international.org/16.0/' target='_blank' rel='noopener noreferrer'>ECMAScript 2025 Language Specification, ECMA-262</a> — официальная спецификация JavaScript, включая типы языка и completion records. Она описывает семантику ECMAScript и не подтверждает поведение конкретного адаптера.</li><li><a href='https://dlang.org/spec/errors.html' target='_blank' rel='noopener noreferrer'>D Language Specification: Errors</a> — официальное описание обработки ошибок и распространения исключений в D. Источник не задаёт envelope, transport или совместимость с PHP и JavaScript.</li></ul>"
}