Files

8 lines
27 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": 56,
"slug": "editorial-2026-06-mechanism-multi-runtime",
"title": "Один payload, три runtime: как сохранить смысл на границе",
"excerpt": "Похожая структура данных не делает PHP, JavaScript и D взаимозаменяемыми. Разбираем контракт type, error и time, fail-closed проверку и границы вывода.",
"contentHtml": "<p>Наблюдаемый симптом — один и тот же заказ получает разные решения. В одном заказе три участника видят почти одинаковый JSON: <code>amount</code>, <code>currency</code> и <code>status</code>. PHP считает <code>status=ready</code> успешным завершением. JavaScript проверяет только наличие строки и продолжает обработку. D ждёт отдельный код результата и оставляет операцию незавершённой. На экране это один payload, но решения уже расходятся.</p>\n<p>Цена расхождения появляется после сбоя. Клиент может повторить уже принятый заказ, worker — пропустить отказ, а оператор — связать события по совпавшим полям, хотя они относятся к разным стадиям. Попытка быстро исправить ситуацию обычно выглядит невинно: привести значение к строке, подставить код по умолчанию или включить retry. Но каждое молчаливое преобразование переносит неопределённость дальше по цепочке.</p>\n<p>Здесь полезно проверять не сходство объектов, а сохранение смысла. Минимальная граница состоит из версии схемы, именованного типа значения, режима ошибки и шкалы времени. Если хотя бы одна ось не описана, адаптер должен остановиться с конкретной причиной. Такой подход не делает языки одинаковыми; он показывает, в каком месте они перестают быть сопоставимыми.</p>\n<h2>Сначала отделим форму от смысла</h2>\n<p>JSON описывает синтаксическую форму обмена, но не решает, что означает поле <code>amount</code> или можно ли повторить операцию. Число <code>4200</code> может быть суммой в копейках, лимитом или идентификатором. Строка <code>ready</code> может быть именем состояния, текстом интерфейса или значением, которое случайно прошло нестрогое сравнение. Одинаковый shape не является доказательством одинакового поведения.</p>\n<p>Поэтому полезная запись на границе называет смысл явно. В учебном примере <code>value.tag</code> фиксирует тип полезной нагрузки, <code>amountMinor</code> — целое число в минимальных денежных единицах, а <code>currency</code> — код валюты. Это проектные решения конкретного примера, а не свойства PHP, JavaScript или D. Их нужно закрепить в API-схеме и тестах своего продукта.</p>\n<p>Версия схемы отвечает на вопрос «какую форму мы сейчас проверяем». Она не заменяет версию runtime и библиотеки. Если PHP меняет правила приведения скаляров, а JavaScript или D иначе представляют ошибку, одна версия JSON не устраняет различие. Версия нужна, чтобы не подменять новый договор старым объектом; остальные зависимости проверяются отдельно.</p>\n<figure><img src=\"/assets/editorial/2026/multi-runtime-2026-error-type-time-matrix.svg\" alt=\"Матрица проверки type, error, time и adapter для PHP, JavaScript и D\" loading=\"lazy\" /><figcaption>Четыре строки отвечают на четыре разных вопроса: назван ли тип значения, описан ли режим ошибки, определена ли шкала времени и выполнено ли точное сопоставление. Красная точка означает остановку проверки, а не дефект конкретного языка.</figcaption></figure>\n<h2>Четыре независимые оси контракта</h2>\n<p><code>type</code> должен быть именованным тегом, а не выводом из соседних полей. Без него адаптер не знает, является ли <code>4200</code> суммой или лимитом. Тег также не заменяет проверку содержимого: после <code>order-ready</code> всё равно нужно проверить диапазон суммы, код валюты и обязательные поля.</p>\n<p><code>error</code> описывает не текст сообщения, а режим завершения. В примере есть <code>semantics</code>, <code>code</code> и <code>retry</code>. Пара <code>code: null</code> и <code>semantics: named-envelope</code> означает только отсутствие кода в этой записи. Она не доказывает, что в системе не было ошибки. Поле <code>retry</code> сообщает намерение или результат политики, но не делает повтор безопасным без ключа идемпотентности и правила побочных эффектов.</p>\n<p><code>time</code> должен назвать шкалу и обе границы интервала. В локальном примере используются фиксированные логические такты: по ним можно проверить порядок <code>opened &lt;= closed</code>. Это не миллисекунды и не измерение производительности. Для latency понадобятся источник часов, единицы измерения, точки старта и окончания, а также правило, где именно начинается операция.</p>\n<p><code>adapter</code> фиксирует перевод между внутренним представлением и этим envelope. Участник не может объявить себя совместимым по одному имени: проверяются версия, tag, режим ошибки, шкала времени и способ mapping. Если в одном месте происходит coercion, это отдельное правило преобразования с тестами, а не «точное» сопоставление.</p>\n<h2>Что действительно различается в runtime</h2>\n<p>Официальная документация PHP прямо описывает важную ловушку: по умолчанию скалярные значения могут быть приведены к объявленному типу, а <code>declare(strict_types=1)</code> меняет проверку для вызовов из конкретного файла. Несовпадение может закончиться <code>TypeError</code>. Поэтому PHP-тип параметра нельзя автоматически считать описанием внешнего JSON-контракта: поведение зависит от места вызова и от того, где стоит граница сериализации.</p>\n<p>Спецификация ECMAScript описывает language types и completion records JavaScript. Это модель выполнения программы, а не готовый HTTP-envelope с полями <code>code</code> и <code>retry</code>. Преобразование исключения или результата в такой envelope — решение адаптера. Его нельзя приписать самому JSON или назвать общим свойством всех трёх runtime.</p>\n<p>Спецификация D описывает собственную модель ошибок и раскрутки стека. Она помогает понять поведение D-кода внутри его среды, но не задаёт внешний договор с PHP и JavaScript. На транспортной границе нужно отдельно решить, какие ошибки становятся <code>error.code</code>, что происходит с незавершённой операцией и кто может инициировать повтор.</p>\n<p>Это различие важно для расследования. Фраза «язык вернул ошибку» слишком широка. Нужно записать наблюдаемый слой: тип значения до сериализации, байты или JSON на транспорте, результат десериализации, решение адаптера и побочный эффект операции. Только так можно понять, где исчезло поле или возникло неявное приведение.</p>\n<h2>Воспроизводимая fail-closed проверка</h2>\n<p>Ниже — самостоятельный пример для Node.js 18+. Он проверяет объект в памяти и не вызывает сеть, часы, PHP или D. Все значения внутри него учебные. Смысл функции в другом: неполная запись получает именованную причину остановки, а положительный результат появляется только при полном наборе независимых признаков.</p>\n<pre><code>const validRecord = {\n schemaVersion: 'boundary-1',\n operation: 'create-order',\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 checkEnvelope(record, participants) {\n if (!record || record.schemaVersion !== 'boundary-1' ||\n record.operation !== 'create-order') {\n return 'stop-incomplete-contract';\n }\n const requiredModels = new Set(['php', 'javascript', 'd']);\n const actualModels = new Set(participants.map((item) =&gt; item.model));\n if (actualModels.size !== 3 ||\n [...requiredModels].some((model) =&gt; !actualModels.has(model))) {\n return 'stop-incomplete-adapter-set';\n }\n if (record.value?.tag !== 'order-ready' ||\n !Number.isInteger(record.value?.amountMinor) ||\n typeof record.value?.currency !== 'string') {\n return 'stop-incomplete-value';\n }\n if (record.error?.semantics !== 'named-envelope' ||\n !Object.hasOwn(record.error, 'code') ||\n typeof record.error.retry !== 'string') {\n return 'stop-incomplete-error';\n }\n if (record.time?.basis !== 'fixed-logical-ticks' ||\n !Number.isInteger(record.time.opened) ||\n !Number.isInteger(record.time.closed) ||\n record.time.closed &lt; record.time.opened) {\n return 'stop-undetermined-time-boundary';\n }\n const valid = participants.every((item) =&gt;\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 valid ? 'accepted-fixed-contract' : 'stop-incomparable-adapter';\n}\n\nconst copy = () =&gt; JSON.parse(JSON.stringify(validRecord));\nconsole.log(checkEnvelope(validRecord, adapters));\nconst unknownTag = copy();\nunknownTag.value.tag = 'order-paid';\nconsole.log(checkEnvelope(unknownTag, adapters));\nconst mismatchedAdapter = adapters.map((item) =&gt; ({ ...item }));\nmismatchedAdapter[1].valueTag = 'order-paid';\nconsole.log(checkEnvelope(validRecord, mismatchedAdapter));\n\n// accepted-fixed-contract\n// stop-incomplete-value\n// stop-incomparable-adapter</code></pre>\n<p>Проверка начинается с операции и набора участников, поэтому три записи одного и того же runtime не проходят как «три среды». Затем она валидирует значение, ошибку и время по отдельности. Вызов <code>Object.hasOwn</code> не даёт превратить отсутствие кода в неявный default. На последнем шаге адаптеры сравниваются с записью, а не друг с другом: взаимное совпадение трёх одинаково ошибочных переводов не считается доказательством.</p>\n<p>Пример можно запустить, сохранив код в чистый файл и выполнив <code>node checker.mjs</code>. Для настоящего API следует заменить учебную запись схемой продукта, добавить проверку неизвестных полей и зафиксировать правила сериализации. Результат функции не является сертификатом совместимости: он показывает, что именно проверено и где проверка отказалась делать вывод.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class=\"table-scroll\"><table><caption>Минимальная карта расследования неоднозначного payload</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Вероятная причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Одинаковый shape даёт разные решения</td><td>Не названы tag, версия или единицы полей</td><td>Сверить обязательные поля до бизнес-ветки</td><td>Расширить envelope; не выводить смысл из shape</td></tr><tr><td>Один участник бросает ошибку, другие возвращают объект</td><td>Смешаны режимы завершения</td><td>Сопоставить semantics, code и момент завершения</td><td>Задокументировать адаптер или остановить mapping</td></tr><tr><td>После таймаута создаётся второй заказ</td><td>retry есть, а идемпотентность не определена</td><td>Повторить вызов с тем же ключом на тестовой записи</td><td>Запретить автоматический повтор до правила эффекта</td></tr><tr><td>Отчёт говорит «быстрее», но метрики нет</td><td>Логические такты приняты за latency</td><td>Проверить basis, часы, единицы и границы</td><td>Удалить вывод о скорости или завести отдельное измерение</td></tr><tr><td>Неполный объект считается успешным</td><td>Валидатор подставляет default</td><td>Удалить поле и проверить точную stop-причину</td><td>Сделать обязательность явной и покрыть missing-case</td></tr></tbody></table></div>\n<h2>Отрицательный путь должен быть частью договора</h2>\n<p>Положительная запись показывает только счастливую ветку. Дисциплину проверяют изменения, которые инженер обязан отвергнуть. Удалите <code>schemaVersion</code> или замените <code>operation</code>: функция возвращает <code>stop-incomplete-contract</code>. Она не угадывает версию по текущей сборке и не пытается «помочь» вызывающему коду.</p>\n<p>Замените <code>value.tag</code> на неизвестное значение. Ожидаемая причина — <code>stop-incomplete-value</code>: запись больше не соответствует выбранному типу. Чтобы проверить несовместимый адаптер, измените у JavaScript-участника <code>valueTag</code>. Тогда функция вернёт <code>stop-incomparable-adapter</code>. То же происходит, если JavaScript сообщает <code>thrown-value</code> вместо <code>named-envelope</code>. Нельзя объявить эти варианты равными только потому, что оба заканчиваются словом «ошибка».</p>\n<p>Поставьте <code>time.closed: null</code> или поменяйте <code>basis</code> на <code>wall-clock</code>. Результат — <code>stop-undetermined-time-boundary</code>. Если нужна настоящая длительность, добавьте отдельный контракт с единицами и источником измерения. Не переводите условный такт в миллисекунды задним числом.</p>\n<p>Проверьте также дубликат модели: замените D-участника вторым PHP-участником. Функция вернёт <code>stop-incomplete-adapter-set</code>. Это маленькая деталь, но без неё матрица доказывает лишь три строки данных, а не участие трёх заявленных runtime. Такой отрицательный тест должен жить рядом с положительным и запускаться на каждое изменение схемы.</p>\n<h2>Как встроить проверку в реальный обмен</h2>\n<ol><li>Назовите операцию и владельца её бизнес-смысла. «Три языка совместимы» — слишком широкое утверждение для теста.</li><li>Опишите версию схемы, tag, единицы чисел, обязательные поля, допустимый <code>null</code> и поведение неизвестных полей.</li><li>Разделите результат и ошибку. Для каждого error code укажите, сохранён ли эффект, разрешён ли повтор и кто принимает решение.</li><li>Выберите одну шкалу времени для каждого измерения. Порядковые метки, календарные даты и latency не смешивайте в одном поле.</li><li>Зафиксируйте вход и выход каждого адаптера. Логируйте идентификатор операции, но не подменяйте им доказательство корректности.</li><li>Запустите положительный случай и минимум четыре отрицательных: неполная схема, неизвестный tag, смешанная ошибка и незакрытый интервал.</li><li>Проверьте побочные эффекты отдельно: повторная доставка, транзакция, дедупликация и восстановление после таймаута не следуют из формы JSON.</li><li>Только после этого подключайте transport, конкретные версии runtime и наблюдаемость. Результат локального теста не переносите на production без отдельного измерения.</li></ol>\n<h2>Ограничения применимости</h2>\n<p>Матрица проверяет согласованность выбранного envelope, а не эквивалентность языков. Она не описывает сборщик мусора, правила ABI, сериализатор, планировщик, права, транзакции, порядок доставки сообщений или лимиты сети. Каждое из этих свойств может изменить результат и требует собственной проверки.</p>\n<p>Даже официальный документ языка отвечает на вопрос о языке, а не о вашем API. PHP может привести скаляр до вызова функции; JavaScript может завершить функцию значением или исключением; D использует свою модель ошибок. Между этим поведением и внешним JSON находится ваш код. Именно его контракт и тесты должны объяснить, что увидит соседний участник.</p>\n<p>Учебный пример допускает только три фиксированные модели и одну шкалу времени. В рабочем проекте может быть больше адаптеров, несколько версий схемы или асинхронная доставка. Тогда нужно версионировать набор правил и явно описать совместимость между версиями. Нельзя расширить список участников молча и сохранить старый критерий готовности.</p>\n<p>Наконец, наличие <code>error.code</code> не доказывает, что операция безопасна для повтора. Для этого нужны идемпотентный ключ, граница фиксации эффекта и тест повторной доставки. Если такие условия не помещаются в текущую запись, вывод ограничивается проверкой формы и не распространяется на бизнес-результат.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Граница готова к интеграционному тесту, если инженер без устного пояснения может назвать операцию, версию схемы, допустимый tag, единицы каждого поля, режим ошибки, смысл retry и шкалу времени. Для каждого отрицательного случая заранее известна точная причина остановки.</p>\n<p>Дополнительно должны выполняться три условия: каждый runtime проходит через явный адаптер; преобразования записаны и покрыты тестом; положительный результат не утверждает latency, deployment или успешную транзакцию, если эти свойства отдельно не наблюдались. Если одно поле приходится угадывать, правильный результат проверки — отказ с именем причины, а не зелёная строка для удобства отчёта.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://www.rfc-editor.org/rfc/rfc8259\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format</a> — нормативное описание синтаксиса и типов JSON. Оно не определяет бизнес-смысл полей или политику повторов.</li><li><a href=\"https://262.ecma-international.org/16.0/\" target=\"_blank\" rel=\"noopener noreferrer\">ECMAScript 2025 Language Specification, ECMA-262, 16th edition</a> — официальная спецификация типов языка и completion records JavaScript. Она не задаёт внешний envelope для PHP или D.</li><li><a href=\"https://www.php.net/manual/en/language.types.declarations.php\" target=\"_blank\" rel=\"noopener noreferrer\">PHP Manual: Type declarations</a> — официальное описание проверки объявленных типов, приведения скаляров, strict typing и <code>TypeError</code>. Конкретный boundary зависит от места вызова и версии PHP.</li><li><a href=\"https://dlang.org/spec/errors.html\" target=\"_blank\" rel=\"noopener noreferrer\">D Programming Language Specification: Errors</a> — официальное описание модели ошибок D. Документ не устанавливает общий транспортный контракт с другими runtime.</li></ul>"
}