function escapeHtml(value) { return String(value).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); } const p = (text) => '

' + text + '

'; const h2 = (text) => '

' + text + '

'; const code = (text) => '
' + escapeHtml(text) + '
'; const ol = (items) => '
    ' + items.map((item) => '
  1. ' + item + '
  2. ').join('') + '
'; const figure = (src, alt, caption) => '
' + escapeHtml(alt) + '
' + caption + '
'; const table = (caption, headers, rows) => '
' + headers.map((cell) => '').join('') + '' + rows.map((row) => '' + row.map((cell) => '').join('') + '').join('') + '
' + caption + '
' + cell + '
' + cell + '
'; function plainText(html) { return html.replace(/<[^>]+>/g, ' ').replace(/&(?:quot|amp|lt|gt|#039);/g, ' ').replace(/\s+/g, ' ').trim(); } function bodyText(html) { return plainText(html.replace(/

Проверяемые источники<\/h2>[\s\S]*?(?=

|$)/, '')); } const REFERENCES = Object.freeze({ cuser: { title: 'Главный модуль: CUser — документация 1С-Битрикс', url: 'https://dev.1c-bitrix.ru/api_help/main/reference/cuser/index.php', version: 'документация API, проверена 31 July 2026; класс доступен с версии 3.0.6', }, loader: { title: 'CModule::IncludeModule — документация 1С-Битрикс', url: 'https://dev.1c-bitrix.ru/api_help/main/reference/cmodule/includemodule.php', version: 'документация API, проверена 31 July 2026; CModule с версии 3.0.1', }, filter: { title: 'PHP Manual: filter_var', url: 'https://www.php.net/manual/en/function.filter-var.php', version: 'PHP Manual, актуальная страница, проверена 31 July 2026', }, semver: { title: 'Semantic Versioning 2.0.0', url: 'https://semver.org/spec/v2.0.0.html', version: 'Version 2.0.0, 2013', }, }); function sources(entries) { return ''; } export function chooseLegacyBoundary(input) { const callers = Number(input?.callers ?? 0); const hasTests = Boolean(input?.characterizationTests); const hasSideEffects = Boolean(input?.unknownSideEffects); const hasCanonicalContract = Boolean(input?.canonicalContract); if (hasSideEffects || !hasTests) return { action: 'keep', reason: 'сначала сохранить наблюдаемое поведение и добавить тесты' }; if (callers > 1 && !hasCanonicalContract) return { action: 'wrap', reason: 'несколько callers требуют одной адаптационной границы' }; if (hasCanonicalContract) return { action: 'replace', reason: 'новый контракт проверен тестами и отделён от legacy API' }; return { action: 'wrap', reason: 'изменение изолируем до появления полного контракта' }; } export function inspectBitrixSurface(input) { const methods = new Set(input?.methods ?? []); const moduleLoaded = Boolean(input?.moduleLoaded); const version = String(input?.version ?? 'unknown'); if (!moduleLoaded) return { status: 'module-not-loaded', action: 'проверить подключение iblock или main до вызова' }; if (methods.has('CUser::GetByID') && methods.has('CUser::Update')) return { status: 'legacy-surface-present', action: 'использовать адаптер с явной проверкой версии', version }; if (methods.has('Bitrix\\Main\\UserTable')) return { status: 'd7-surface-present', action: 'проверить mapping полей перед заменой', version }; return { status: 'unknown-surface', action: 'остановить миграцию и получить контракт установленной версии', version }; } export function migrateUserFields(record) { const source = { ...record }; const phone = String(source.PERSONAL_PHONE ?? '').trim(); const email = String(source.EMAIL ?? '').trim().toLowerCase(); const result = { ...source, phone, email }; delete result.PERSONAL_PHONE; delete result.EMAIL; return { legacy: source, canonical: result, reversible: true }; } function revision(meta, parts, referenceEntries) { const contentHtml = parts.join('') + h2('Проверяемые источники') + sources(referenceEntries); const proseLength = bodyText(contentHtml).length; if (proseLength < 5000 || proseLength > 15000) throw new Error(meta.slug + ': body length ' + proseLength); return Object.freeze({ ...meta, contentHtml, proseLength }); } const practice = revision({ slug: 'editorial-2027-02-practice-bitrix-lessons', title: 'Bitrix legacy: когда сохранить код, когда обернуть, когда заменить', categories: ['Bitrix', 'Инженерные практики'], cover: '/assets/editorial/2027/bitrix-lessons-2027-keep-wrap-replace-tree.svg', excerpt: 'Практическое дерево решения для старого API: сохранить поведение, поставить адаптер или перейти на новый контракт.', readingMinutes: 15, }, [ p('Проблема legacy-кода в Bitrix редко состоит в возрасте файла. Старый вызов может держать неявные значения, порядок хуков, формат ошибок и поля, которые читает соседний модуль. Если заменить его только потому, что новый API выглядит аккуратнее, цена проявится позже: потеряется поведение, а исправлять его придётся уже по косвенным симптомам. Поэтому первый вопрос — не «как переписать», а «какой контракт нельзя сломать».'), p('Решение удобно разделить на три действия: сохранить, обернуть или заменить. Сохранить — значит оставить вызов и зафиксировать его наблюдаемое поведение. Обернуть — поставить адаптер между legacy API и остальным кодом, чтобы callers перестали знать детали. Заменить — удалить старую границу только после того, как новый контракт описан тестами. Это не догма: выбор зависит от числа callers, побочных эффектов и качества проверки.'), h2('Сначала отделяем возраст от риска'), p('Класс CUser относится к старому API Bitrix, но само имя класса не говорит, что его можно безопасно удалить. Документация перечисляет поля и методы, а также указывает аналог в D7. Для проекта этого мало: нужно увидеть, какие поля реально передаются, какие значения возвращаются и что вызывается после сохранения. Пока это не известно, сохранение или узкий wrapper дешевле полной миграции.'), p('Риск растёт, если один вызов смешивает несколько задач. Например, функция может одновременно нормализовать email, создавать пользователя, запускать событие и возвращать ID. Переписать её на новый класс без раздельных тестов значит поменять четыре контракта за один commit. Адаптер позволяет сначала выделить форму данных и ошибку, а уже потом менять внутреннюю реализацию.'), figure('/assets/editorial/2027/bitrix-lessons-2027-keep-wrap-replace-tree.svg', 'Дерево выбора для Bitrix legacy: проверить callers и контракт, затем выбрать сохранение, адаптер или замену.', 'Схема помогает выбрать границу изменения. Красная ветка означает, что сначала нужно получить недостающий контракт, а не переписывать вызов.'), table('Три действия для legacy-вызова', ['Действие', 'Когда подходит', 'Цена', 'Критерий перехода'], [ ['Сохранить', 'побочные эффекты не описаны, callers мало', 'остаётся старый долг', 'характеризующие тесты и список полей'], ['Обернуть', 'callers несколько, контракт можно выделить', 'появляется адаптер', 'внешний код видит canonical input/output'], ['Заменить', 'новый контракт и тесты готовы', 'нужен полный regression', 'старый путь больше не нужен'], ['Остановиться', 'нет версии, полей или воспроизводимого результата', 'изменение откладывается', 'собраны факты о границе'], ]), h2('Учебная функция выбора границы'), p('Функция ниже не изображает Bitrix runtime. Она показывает рабочую механику решения: входом являются количество callers, наличие characterization tests, неизвестные побочные эффекты и canonical contract. Возвращается одно действие и причина. Эти поля можно заполнить из code search, тестов и документации, а не из ощущения «код старый».'), code(`import { chooseLegacyBoundary } from './upgrade-2027-02.mjs'; const cases = [ { callers: 1, characterizationTests: false, unknownSideEffects: true, canonicalContract: false }, { callers: 4, characterizationTests: true, unknownSideEffects: false, canonicalContract: false }, { callers: 4, characterizationTests: true, unknownSideEffects: false, canonicalContract: true }, ]; for (const item of cases) console.log(chooseLegacyBoundary(item)); // keep -> сохранить наблюдаемое поведение и добавить тесты // wrap -> несколько callers требуют одной адаптационной границы // replace -> новый контракт проверен тестами и отделён от legacy API`), p('Первый результат намеренно не предлагает «сразу новый класс»: неизвестные эффекты сильнее желания обновиться. Второй показывает пользу wrapper, когда callers несколько. Третий разрешает замену только при наличии canonical contract. В рабочем репозитории эту функцию заменит ADR или короткая карточка изменения, а значения подтвердят тесты и просмотр вызовов.'), h2('Что должен скрывать адаптер'), p('Адаптер не должен превращаться в копию всего legacy API. Он принимает только нужные поля, проверяет обязательные значения и переводит ошибку в понятный тип. Например, внешняя функция может принимать { email, phone }, а внутри временно собирать массив полей для CUser::Update. Так callers перестают зависеть от названий PERSONAL_PHONE и от способа загрузки модуля.'), p('Нужно заранее решить, кто владеет преобразованием. Если один caller исправляет телефон, второй передаёт его как есть, а третий пишет пустую строку, wrapper не создал контракт — он спрятал разнобой. Входная нормализация должна быть в одном месте, а правила обратного преобразования — рядом с ней. В таблице изменений укажите поле, источник, допустимую пустоту и способ проверить результат.'), h2('Действия по порядку'), ol([ 'Найти все callers legacy-функции и выписать фактические поля, значения по умолчанию и обработку ошибок.', 'Проверить, подключён ли нужный Bitrix-модуль, и зафиксировать установленную версию вместе с документацией API.', 'Добавить characterization tests на текущий результат: успешное сохранение, пустое поле, повторный вызов и ошибку.', 'Выбрать keep, wrap или replace; для wrapper описать canonical input/output и список скрываемых legacy-деталей.', 'После изменения повторить те же проверки и отдельно проверить события, права и формат возвращаемого ID.', ]), h2('Ограничения и следующий шаг'), p('Без доступа к конкретной установке нельзя обещать совместимость версии, поведение событий или одинаковые тексты ошибок. Документация Bitrix описывает API, но не локальные обработчики и не поля, добавленные проектом. Полная замена особенно опасна, если legacy-вызов участвует в транзакции, импортирует данные или используется административной формой.'), p('Следующий шаг — взять одну функцию с двумя callers и оформить её canonical контракт. Если тесты не удаётся написать без сложной среды, это сигнал оставить код на месте и сначала сократить границу побочных эффектов. Такая остановка тоже инженерное решение: она сохраняет обратимость и делает следующий commit проверяемым.'), ], [ { key: 'cuser', use: 'Названия CUser, поля и наличие аналога UserTable сверены с документацией Bitrix.', boundary: 'Документация не описывает локальные события, права, обработчики и фактический набор callers проекта.' }, { key: 'loader', use: 'Проверка подключения модуля до вызова API используется как отдельное условие адаптера.', boundary: 'Страница не подтверждает, что нужный модуль установлен в конкретной среде.' }, { key: 'filter', use: 'PHP-проверка входных значений упомянута как часть нормализации boundary.', boundary: 'Manual не определяет Bitrix-поля и не заменяет тесты проекта.' }, ]); const mechanism = revision({ slug: 'editorial-2027-02-mechanism-bitrix-lessons', title: 'Bitrix API и версия: имя метода не обещает одинаковый контракт', categories: ['Bitrix', 'Архитектура'], cover: '/assets/editorial/2027/bitrix-lessons-2027-version-boundary-matrix.svg', excerpt: 'Как проверять установленную поверхность API и не считать название класса доказательством совместимости.', readingMinutes: 16, }, [ p('Проблема версионной совместимости выглядит как простой поиск: в документации найден метод, значит его можно вызвать. В Bitrix такой вывод часто слишком сильный. Один и тот же смысл может жить в legacy-классе и в D7, а конкретная установка может не содержать нужный модуль или иметь изменённое поле. Цена ошибки — адаптер, который компилируется и падает только на редкой форме или после обновления.'), p('Надёжная граница строится из трёх фактов: модуль подключён, поверхность методов действительно доступна, а вход и выход совпадают с нужным проекту контрактом. Версия помогает сузить поиск, но не заменяет проверку. Даже правило Semantic Versioning применимо только там, где поставщик соблюдает его для данного API; имя Update само по себе не обещает семантическую совместимость.'), h2('Подключение модуля — часть контракта'), p('Документация Bitrix для CModule::IncludeModule прямо описывает проверку установки и подключения модуля. Это не декоративная строка перед вызовом. Если модуль не подключён, сообщение об ошибке может появиться далеко от места, где принято решение использовать API. В адаптере проверка должна быть близко к границе и возвращать понятный результат, который можно показать в диагностике.'), p('Для D7 аналогично нужен конкретный namespace и набор методов. Нельзя заменить CUser на Bitrix\\Main\\UserTable, не проверив mapping полей, типы значений и обработку исключений. Хорошее сравнение описывает не названия классов, а операции: создать, обновить, найти, получить ID, обработать ошибку. Именно операции должны попасть в тестовую матрицу.'), figure('/assets/editorial/2027/bitrix-lessons-2027-version-boundary-matrix.svg', 'Матрица версионной границы Bitrix: подключённый модуль, доступная поверхность, контракт операции и результат проверки.', 'Имя метода занимает только первый слой. До изменения нужно пройти до фактической поверхности установленной версии и сопоставить поля.'), table('Уровни проверки Bitrix API', ['Уровень', 'Входной факт', 'Проверка', 'Риск пропуска'], [ ['Модуль', 'iblock или main подключён', 'IncludeModule возвращает true', 'класс не загружен'], ['Поверхность', 'метод или таблица существуют', 'проверить установленный API', 'вызов неизвестного метода'], ['Поля', 'названия и типы совпадают', 'сопоставить mapping', 'тихая потеря значения'], ['Семантика', 'ошибка и результат понятны', 'characterization/regression test', 'новая форма ведёт себя иначе'], ]), h2('Локальный инспектор поверхности'), p('Чтобы сделать проверку повторяемой, можно сначала работать с маленьким manifest, полученным из конкретного окружения: версия, признак подключения и список доступных операций. Ниже функция принимает такой manifest и выбирает следующий технический шаг. Она не делает вид, что знает реальный сервер: данные нужно собрать командой проверки в самой среде, а функция только не даёт перепутать отсутствие модуля с отсутствием метода.'), code(`import { inspectBitrixSurface } from './upgrade-2027-02.mjs'; const surfaces = [ { version: '20.0', moduleLoaded: false, methods: [] }, { version: '20.0', moduleLoaded: true, methods: ['CUser::GetByID', 'CUser::Update'] }, { version: '23.0', moduleLoaded: true, methods: ['Bitrix\\Main\\UserTable'] }, ]; for (const surface of surfaces) console.log(inspectBitrixSurface(surface)); // module-not-loaded // legacy-surface-present // d7-surface-present`), p('Здесь важен порядок. Первый manifest не доходит до анализа методов: отсутствует базовое условие. Второй разрешает говорить только о наличии legacy-поверхности и требует явного адаптера. Третий показывает D7-поверхность, но не объявляет mapping полей готовым. Такой результат проще проверять в CI или в диагностической команде, чем свободный текст в issue.'), h2('Поле важнее красивого имени'), p('Самая тихая ошибка миграции — значение сохранилось, но стало другим. Телефон мог быть строкой с пробелами и плюсами, email — сохранён с исходным регистром, пустое поле — означать «очистить», а отсутствие поля — «не менять». Если новый API получает обычный объект без различия этих состояний, wrapper стирает смысл запроса. Поэтому contract table должна перечислять хотя бы value, empty и missing.'), p('Version boundary нужно держать рядом с этой таблицей. Если в старой версии поле принимает строку, а новая модель отдаёт массив значений, название свойства не спасёт. Правильная проверка — пройти create/read/update на фиксированных данных и сравнить смысловой результат. Одинаковый ID после update ещё не доказывает, что события и индексы получили тот же input.'), h2('Действия по порядку'), ol([ 'Зафиксировать версию ядра, подключаемый модуль и источник документации, на который опирается вызов.', 'Собрать manifest доступных методов или таблиц из той среды, где будет выполняться изменение.', 'Разложить операцию на поля, пустое значение, отсутствие поля, ошибку и побочный event.', 'Проверить legacy и D7 на одном наборе входов, не сравнивая только имена классов или финальный ID.', 'Оставить adapter boundary до тех пор, пока regression не подтвердит одинаковый смысл результата.', ]), h2('Ограничения и следующий шаг'), p('Инспектор не заменяет запуск в Bitrix: он не видит автозагрузку, права, события, local overrides и SQL-ограничения. Номер версии может быть установлен, но отдельный модуль — отсутствовать. Semantic Versioning тоже не заставляет внутренний API платформы соблюдать обещания внешнего пакета. Любое утверждение о совместимости должно опираться на конкретный набор окружений и операций.'), p('Следующий шаг — добавить в проект диагностическую команду, которая печатает только безопасный manifest: версия, подключённый модуль и названия операций без данных пользователей. Затем используйте его перед миграцией одного метода. Если surface различается, адаптер должен остановить изменение с понятной причиной, а не подобрать метод по совпадению имени.'), ], [ { key: 'loader', use: 'Официальная проверка подключения модуля используется как первый слой version boundary.', boundary: 'Документация не сообщает состояние конкретной установки и не описывает mapping полей.' }, { key: 'cuser', use: 'CUser и его D7-аналог используются для различения поверхности API и операций пользователя.', boundary: 'Страница не обещает, что два класса равны по событиям, типам и ошибкам.' }, { key: 'semver', use: 'Правило совместимости версий используется как оговорка о границах обещаний поставщика.', boundary: 'Спецификация не делает Bitrix API Semantic Versioning-совместимым автоматически.' }, ]); const field = revision({ slug: 'editorial-2027-02-field-bitrix-lessons', title: 'Миграция Bitrix-поля: проверить сохранение, чтение и обратимость', categories: ['Bitrix', 'Данные'], cover: '/assets/editorial/2027/bitrix-lessons-2027-migration-evidence-loop.svg', excerpt: 'Полевой чек-лист переноса пользовательского поля: mapping, пустые значения, повторный запуск и проверяемый результат.', readingMinutes: 15, }, [ p('Проблема миграции Bitrix-поля проявляется после успешного ответа API. Запись получила ID, но телефон оказался пустым, email изменил регистр, а повторный запуск создал второе значение. Цена ошибки — не только испорченная строка. Дальше ломается поиск, уведомление или связь с внешней системой, а восстановить исходное состояние трудно, потому что команда сохранила только факт «update вернул успех».'), p('Полевой разбор должен проверять три операции: сохранить mapping, прочитать результат тем же смыслом и повторить вход без дубля. Для каждого поля нужно различить отсутствующее значение и явную очистку. Это особенно важно при переходе от массивов Bitrix к canonical объекту: старое имя можно удалить из кода, но нельзя удалить смысл значения до окончания проверки.'), h2('Сначала таблица mapping'), p('Документация CUser перечисляет поля пользователя, включая PERSONAL_PHONE, EMAIL, идентификатор и время изменения. Для проекта это отправная точка, а не готовая схема: рядом могут быть пользовательские поля, обработчики события и внешний XML_ID. Запишите для каждого значения источник, формат, пустое состояние и обратное представление. Если поле не переносится, причина должна быть явной.'), p('Нормализация должна быть идемпотентной: одинаковый вход при повторном запуске даёт одинаковый canonical результат. Для телефона это может быть trim без изменения номера, для email — lowercase, если бизнес-правило считает регистр незначимым. Нельзя применять общую нормализацию ко всем полям: комментарий пользователя, парольный хэш и XML_ID имеют разные правила.'), figure('/assets/editorial/2027/bitrix-lessons-2027-migration-evidence-loop.svg', 'Цикл проверки миграции Bitrix-поля: mapping входа, нормализация, запись, повторное чтение и контроль повторного запуска.', 'Схема показывает, что успешный вызов записи — только середина проверки. Нужны read-back и повторяемость результата.'), table('Полевой контракт переноса поля', ['Поле', 'Legacy-значение', 'Canonical-значение', 'Проверка'], [ ['Телефон', 'PERSONAL_PHONE, пробелы допустимы', 'phone, trimmed string', 'read-back и формат'], ['Email', 'EMAIL, исходный регистр', 'email, lower-case', 'валидность и отсутствие дубля'], ['ID', 'ID пользователя', 'externalId', 'одинаковая запись при retry'], ['Пустота', 'нет ключа или пустая строка', 'missing или clear', 'два разных теста'], ['Связь', 'XML_ID', 'externalId', 'двусторонний mapping'], ]), h2('Учебная нормализация без Bitrix-сервера'), p('Ниже запускается локальная функция преобразования одного объекта. Она сохраняет legacy-копию, создаёт canonical поля и возвращает признак обратимости. Это не подмена миграционного запуска: результат показывает, как тестировать mapping до подключения API. В интеграционном коде следующая проверка должна сравнить canonical объект с read-back из Bitrix.'), code(`import { migrateUserFields } from './upgrade-2027-02.mjs'; const input = { ID: 17, PERSONAL_PHONE: ' +7 900 000-00-00 ', EMAIL: 'User@Example.TEST', XML_ID: 'crm-17', }; console.log(migrateUserFields(input)); // canonical.phone === '+7 900 000-00-00' // canonical.email === 'user@example.test' // reversible === true`), p('Ожидаемый результат показывает две отдельные операции: пробелы убраны у телефона, регистр email нормализован, а исходные поля остаются в legacy snapshot. Snapshot нужен для теста и отката преобразования, но не должен случайно отправляться обратно в новый API. В production-модуле его заменит журнал безопасного mapping без персональных значений.'), h2('Пустое поле и отсутствующее поле'), p('Разница между {} и { PERSONAL_PHONE: "" } часто теряется в универсальном merge. Первый объект может означать «не менять телефон», второй — «очистить телефон». Если миграция смешивает эти случаи, повторный запуск удалит данные, которые не должны были меняться. В тестовой матрице должны быть оба входа и ожидаемое действие на стороне Bitrix.'), p('Внешняя валидация тоже не должна менять значение молча. PHP filter_var может помочь проверить email, но решение о допустимости адреса принадлежит контракту приложения. Неверный email лучше остановить до update, чем сохранить пустую строку и потом считать ответ API доказательством успеха. Для телефона нужны отдельные правила: длина, допустимые символы и локальный формат.'), h2('Действия по порядку'), ol([ 'Составить mapping table и отдельно назвать missing, empty, invalid и unchanged для каждого поля.', 'Запустить нормализацию на локальном наборе с пробелами, разным регистром, пустым и неверным значением.', 'В тестовой среде записать одну запись по стабильному ID или XML_ID, затем выполнить read-back.', 'Сравнить смысловые поля, время изменения, события и внешний идентификатор; не ограничиваться HTTP/API success.', 'Повторить тот же запуск и убедиться, что новая запись не появилась и canonical результат не изменился.', ]), h2('Ограничения и следующий шаг'), p('Локальная функция не знает о правах, событиях и особенностях конкретной версии Bitrix. Read-back может вернуть представление, отличное от входа: формат телефона, timezone или пустое значение иногда нормализуются сервером. Восстановление должно учитывать транзакцию и сохранённую связь, а не просто повторно отправлять старый объект.'), p('Следующий шаг — взять одно поле, для которого есть внешний XML_ID, и прогнать полный цикл на небольшой выборке: mapping, запись, read-back, повтор и отчёт по расхождениям. Пока расхождения не классифицированы, расширять миграцию на весь набор рискованно. Проверяемость одного поля ценнее широкого запуска с неясным результатом.'), ], [ { key: 'cuser', use: 'Официальный список полей CUser используется для примера PERSONAL_PHONE, EMAIL, ID и XML_ID.', boundary: 'Документация не знает пользовательские поля, события и фактические данные проекта.' }, { key: 'filter', use: 'PHP Manual используется для проверки допустимости входного email перед записью.', boundary: 'filter_var не определяет бизнес-правила, Bitrix-формат и гарантию сохранения поля.' }, { key: 'loader', use: 'Подключение модуля упомянуто как проверка интеграционной границы до записи.', boundary: 'Страница не подтверждает права и настройки конкретной среды.' }, ]); export const revisions = Object.freeze([practice, mechanism, field]); export function verifyRevisionsAgainstFixture() { const checks = revisions.map((item) => { const body = bodyText(item.contentHtml); return body.length >= 5000 && body.length <= 15000 && //.test(item.contentHtml) && /
/.test(item.contentHtml) && /
/.test(item.contentHtml) && /
    /.test(item.contentHtml) && !/(synthetic-plan-hand-off|productionEffect|future-only|plan\/scenario|source cutoff|not-collected|not-attempted|future owner|развитие автора)/i.test(body); }); const sample = inspectBitrixSurface({ version: '20.0', moduleLoaded: true, methods: ['CUser::GetByID', 'CUser::Update'] }); const mapping = migrateUserFields({ PERSONAL_PHONE: ' 123 ', EMAIL: 'A@B.C' }); const fixtureOk = sample.status === 'legacy-surface-present' && mapping.canonical.email === 'a@b.c'; return Object.freeze({ passed: checks.filter(Boolean).length + (fixtureOk ? 1 : 0), total: checks.length + 1, accepted: checks.every(Boolean) && fixtureOk, characters: Object.fromEntries(revisions.map((item) => [item.slug, bodyText(item.contentHtml).length])) }); } if (process.argv.includes('--verify-fixture')) { const result = verifyRevisionsAgainstFixture(); process.stdout.write(JSON.stringify(result, null, 2) + '\n'); if (!result.accepted) process.exitCode = 1; } if (process.argv.includes('--print-revisions')) process.stdout.write(JSON.stringify(revisions) + '\n');