diff --git a/editorial/production/README.md b/editorial/production/README.md index 6fa0de8..b980ed3 100644 --- a/editorial/production/README.md +++ b/editorial/production/README.md @@ -1,6 +1,6 @@ # Производство редакционных партий -На 31 июля 2026 года строгий аудит проходит 310 из 358 созданных материалов. Остальные 48 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить. +На 31 июля 2026 года строгий аудит проходит 313 из 358 созданных материалов. Остальные 45 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить. ## Одна партия diff --git a/editorial/reviews/2026-09-draft.md b/editorial/reviews/2026-09-draft.md new file mode 100644 index 0000000..43b434f --- /dev/null +++ b/editorial/reviews/2026-09-draft.md @@ -0,0 +1,49 @@ +# P103 — сентябрь 2026: граница frontend и backend + +Статус: изолированный **план/сценарий на сентябрь 2026**, не field report. Исторический cutoff источников: **31.07.2026**. Пакет намеренно не утверждает, что сентябрьская работа была проведена, опубликована, измерена, внедрена или одобрена. Все модели, названия, поля, значения и positive outputs — named fixed synthetic JavaScript literals в памяти; UI, API, пользователь, сеть, trace, файл, секрет, endpoint и production не читаются и не представляются фактом. + +## Проход 1 — проблема, плотность, голос и независимость + +- Practice начинает с присвоения UI доменного факта и цены несовместимых версий между входами. Его вопрос: как в сентябрьском плане разложить local state, context и screen read model по владельцам. +- Mechanism начинает с handler, который трактует отправку command как подтверждение. Его предмет: какое право на вывод дают query, command, read model и problem envelope, а не карта владельцев как таковая. +- Field начинает с подмены evidence правдоподобным mock и цены неповторимого решения. Его предмет: как передать только synthetic review card без имитации UI-network trace. +- Первые два абзаца каждой статьи явно называют проблему и цену. В каждой есть отдельная таблица, SVG, выполняемый snippet, упорядоченный план, ограничения и следующий шаг. Общего вступительного блока нет. +- Размер тел без списка источников: practice — 8 953, mechanism — 8 977, field — 9 006 знаков. Это в диапазоне 5 000–15 000 и близко к целевому объёму 9 тыс. без наполнения повторами. +- Строгий попарный scan исключил source lists, но включил code: 0 совпадающих абзацев от 160 символов и 0 общих 12-словных фрагментов для всех трёх пар. + +## Проход 2 — источники, future-fact gate и literal execution + +1. [OpenAPI Specification 3.1.1](https://spec.openapis.org/oas/v3.1.1.html), версия 3.1.1 от 24.10.2024 — первичная версионированная спецификация. Использована только для узкого утверждения, что HTTP API имеет независимое от языка interface description. Она не доказывает существование endpoint, screen model или выбранного boundary. +2. [RFC 9110: HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110.html), июнь 2022 — неизменяемый RFC. Использован только для различения HTTP semantics и application-level problem policy. Он не задаёт retry, не подтверждает response и не описывает product flow. +3. [JSON Schema Core, draft 2020-12](https://json-schema.org/draft/2020-12/json-schema-core.html), Internet-Draft (work in progress), 16.06.2022. Использован только для идеи явной структуры/vocabulary contract-карточки. Валидация схемы не доказывает доменную корректность, UI-поведение или достоверность fixed literals. + +Ссылки открыты локальной проверкой до написания. Ни один источник не позже cutoff 31.07.2026. Свойства модели P103 — ownership map, состав read model, stop reasons и hand-off — отделены от источников и не приписываются стандартам. + +Public exports выполнены буквально: + +- `reviewFixedStateOwnership(createFixedBoundaryCase('ui-claims-domain-state-v1'))` → `stop-ui-claims-domain-state`. +- `explainFixedBoundaryInference(createFixedBoundaryCase('unactionable-error-v1'))` → `stop-error-not-actionable`. +- `explainFixedBoundaryInference(createFixedBoundaryCase('named-screen-boundary-v1'))` → `synthetic-inference-boundary-explained`, `productionEffect: not-attempted`. + +Fixture: 5/5. Вместе с тремя article checks — 8/8. Положительная ветка заканчивается только `synthetic-boundary-review-hand-off` или объяснением его границы; в ней нет approval, release, deployment, публикации, доступа к production или факта trace. + +## Проход 3 — выпускное качество, mobile SVG и уникальность + +- `node --check scripts/upgrade-2026-09.mjs` — pass. +- `node scripts/upgrade-2026-09.mjs --verify-fixture` — 8/8, pass. +- `npm run audit:draft -- scripts/upgrade-2026-09.mjs` — pass для трёх строгих slug. +- `xmllint --noout` — pass для трёх SVG. Safety scan — pass: отсутствуют `script`, `foreignObject`, `javascript:`, `data:image` и event handlers. +- Все SVG отрисованы Sharp до 375px и просмотрены визуально. В первой схеме различимы три владельца и стрелка command; во второй — четыре разных права на вывод; в третьей после редакции читается `review hand-off`, подпись не обрезается. Рабочий SVG-текст задан 21–28px при viewBox 720px; схемы содержат title/desc, осмысленный article alt и caption. +- В code нет обращения к сети, файловой системе, секретам, часам или UI. Input только JSON-clone + deep-freeze named fixtures, а неизвестный или неполный вход останавливается fail-closed. + +Draft готов для независимой production-приёмки. Registry, README, staging, commit и push находятся вне scope этого sidecar. + +## Независимая приёмка основного редактора — 31.07.2026 + +Принято только как **сентябрьский план/сценарий**. Во всех трёх текстах будущий месяц и cutoff `2026-07-31` названы прямо; positive branches заканчиваются `synthetic-boundary-review-hand-off` или `synthetic-inference-boundary-explained` с `productionEffect: not-attempted`. Пакет не выдаёт fixed model за UI/API/trace из production. + +Проверены первичные источники. Официальная OpenAPI Specification v3.1.1 действительно задаёт language-agnostic interface description для HTTP API и датирована **24.10.2024**; ошибочная дата 15.09.2023 исправлена до интеграции. RFC 9110 — June 2022 и описывает HTTP semantics; JSON Schema Core draft 2020-12 опубликован 16.06.2022 как Internet-Draft/work in progress. Поэтому последний источник используется только как vocabulary для явной структуры, а не как нормативное доказательство доменной схемы или поведения продукта. + +Повторно выполнены syntax и fixture `8/8`, draft audit с объёмами 8 953 / 8 977 / 9 006, XML и SVG safety-scan. Public exports выдают проверенные stops `stop-ui-claims-domain-state` и `stop-error-not-actionable`; accepted branch остаётся только synthetic и имеет `productionEffect: not-attempted`. Три SVG заново отрендерены Sharp на 375 px: текст, стрелки и подписи читаются без обрезания. Строгая pairwise-проверка, включающая code и исключающая source lists, вернула ноль совпадающих абзацев от 160 символов и ноль общих 12-словных фрагментов. + +В registry добавлены только три сентябрьские ревизии. Пользовательские application-файлы, `articles.json`, production queue и прочая незакоммиченная работа в приёмку не включены. diff --git a/web/data/editorial-revisions.mjs b/web/data/editorial-revisions.mjs index 408174c..97bd79c 100644 --- a/web/data/editorial-revisions.mjs +++ b/web/data/editorial-revisions.mjs @@ -99,6 +99,7 @@ import { revisions as may2026Revisions } from '../scripts/upgrade-2026-05.mjs'; import { revisions as june2026Revisions } from '../scripts/upgrade-2026-06.mjs'; import { revisions as july2026Revisions } from '../scripts/upgrade-2026-07.mjs'; import { revisions as august2026Revisions } from '../scripts/upgrade-2026-08.mjs'; +import { revisions as september2026Revisions } from '../scripts/upgrade-2026-09.mjs'; // This layer replaces archived source entries without losing their stable slug and date. export const editorialRevisions = [ @@ -203,4 +204,5 @@ export const editorialRevisions = [ ...june2026Revisions, ...july2026Revisions, ...august2026Revisions, + ...september2026Revisions, ]; diff --git a/web/public/assets/editorial/2026/frontend-backend-boundary-2026-contract-responsibility-matrix.svg b/web/public/assets/editorial/2026/frontend-backend-boundary-2026-contract-responsibility-matrix.svg new file mode 100644 index 0000000..ac94e60 --- /dev/null +++ b/web/public/assets/editorial/2026/frontend-backend-boundary-2026-contract-responsibility-matrix.svg @@ -0,0 +1,7 @@ + + Матрица обязанностей контрактаЧетыре строки различают query, command, read model и problem envelope. Красная нижняя строка запрещает доменный вывод из отправленной команды. + Boundary: право на вывод + ФормаДаёт UIНе даёт UI + queryвопрос к modelизменение domaincommandintentуспешный фактread modelstatus + actionsскрытую причинуproblemcode + retryдогадку о retry + Отправка command ≠ новый domain factМатрица для plan review; не описание реального API. + diff --git a/web/public/assets/editorial/2026/frontend-backend-boundary-2026-review-evidence-loop.svg b/web/public/assets/editorial/2026/frontend-backend-boundary-2026-review-evidence-loop.svg new file mode 100644 index 0000000..7632fc1 --- /dev/null +++ b/web/public/assets/editorial/2026/frontend-backend-boundary-2026-review-evidence-loop.svg @@ -0,0 +1,9 @@ + + Цикл evidence для synthetic hand-offПять шагов соединяют fixed literal, проверку ownership, stop или counterexample, уточнённый literal и ограниченный synthetic hand-off. Красная ветка возвращает domain claim к read model. + + Synthetic evidence loop + fixed literalownership checknamed resultreview hand-offnew literal + + domain claim → stop → read question + Positive output: только review hand-off.productionEffect: not-attemptedНет UI trace, сети, файлов или секретов. + diff --git a/web/public/assets/editorial/2026/frontend-backend-boundary-2026-state-ownership-map.svg b/web/public/assets/editorial/2026/frontend-backend-boundary-2026-state-ownership-map.svg new file mode 100644 index 0000000..b19df84 --- /dev/null +++ b/web/public/assets/editorial/2026/frontend-backend-boundary-2026-state-ownership-map.svg @@ -0,0 +1,9 @@ + + Карта владельцев состоянияТри колонки разделяют локальное состояние UI, контекст и API read model. Команда направлена к API, доменный факт не находится в UI. + + Карта владельцев: план на сентябрь + UIdraft-filteropen-panelне владеетdomain fact + Contextfixed-localepermissionусловиеинтерпретации + APIstatusallowedActionsmessageCoderead modelи problem + command intentFixed synthetic model; не trace и не факт системы. + diff --git a/web/scripts/upgrade-2026-09.mjs b/web/scripts/upgrade-2026-09.mjs new file mode 100644 index 0000000..877ba1d --- /dev/null +++ b/web/scripts/upgrade-2026-09.mjs @@ -0,0 +1,240 @@ +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) => '
' + alt + '
' + caption + '
'; +const table = (caption, headers, rows) => '
' + headers.map((item) => '').join('') + '' + rows.map((row) => '' + row.map((item) => '').join('') + '').join('') + '
' + caption + '
' + item + '
' + item + '
'; +function plainText(content) { return content.replace(/<[^>]+>/g, ' ').replace(/&(?:quot|amp|lt|gt|#039);/g, ' ').replace(/\s+/g, ' ').trim(); } +function bodyText(content) { return plainText(content.replace(/

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

|$)/, '')); } +function clone(value) { return JSON.parse(JSON.stringify(value)); } +function deepFreeze(value) { if (value && typeof value === 'object' && !Object.isFrozen(value)) { Object.values(value).forEach(deepFreeze); Object.freeze(value); } return value; } + +const REFERENCES = deepFreeze({ + openapi: { title: 'OpenAPI Specification 3.1.1', url: 'https://spec.openapis.org/oas/v3.1.1.html', version: 'версия 3.1.1, 24 октября 2024, версионированная спецификация' }, + http: { title: 'RFC 9110: HTTP Semantics', url: 'https://www.rfc-editor.org/rfc/rfc9110.html', version: 'RFC 9110, июнь 2022, неизменяемый текст RFC' }, + schema: { title: 'JSON Schema Core: draft 2020-12', url: 'https://json-schema.org/draft/2020-12/json-schema-core.html', version: 'draft 2020-12, Internet-Draft (work in progress), 16 июня 2022' }, +}); +function sourceList(entries) { return ''; } + +const FIXED_BOUNDARY_CASES = deepFreeze({ + 'named-screen-boundary-v1': { + id: 'named-screen-boundary-v1', family: 'fixed-frontend-backend-boundary-v1', + ui: { localState: ['draft-filter', 'open-panel'], context: ['fixed-locale', 'fixed-view-permission'], forbiddenDomainClaims: [] }, + api: { command: { name: 'fixed-submit-choice', intent: 'set-choice', idempotency: 'named-key-v1' }, readModel: { name: 'fixed-choice-screen-v1', fields: ['status', 'allowedActions', 'messageCode'] }, error: { kind: 'named-problem', code: 'none', retry: 'not-requested' } }, + boundary: 'Named fixed synthetic JavaScript literal in memory. It describes no UI, user, HTTP request, network trace, file, secret, service, deployment or production state.', + }, + 'ui-claims-domain-state-v1': { + id: 'ui-claims-domain-state-v1', family: 'fixed-frontend-backend-boundary-v1', + ui: { localState: ['draft-filter'], context: ['fixed-locale'], forbiddenDomainClaims: ['choice-confirmed'] }, + api: { command: { name: 'fixed-submit-choice', intent: 'set-choice', idempotency: 'named-key-v1' }, readModel: { name: 'fixed-choice-screen-v1', fields: ['status', 'allowedActions', 'messageCode'] }, error: { kind: 'named-problem', code: 'none', retry: 'not-requested' } }, boundary: 'Negative fixed synthetic literal only.', + }, + 'unnamed-command-v1': { + id: 'unnamed-command-v1', family: 'fixed-frontend-backend-boundary-v1', + ui: { localState: ['draft-filter'], context: ['fixed-locale'], forbiddenDomainClaims: [] }, + api: { command: { name: '', intent: '', idempotency: '' }, readModel: { name: 'fixed-choice-screen-v1', fields: ['status'] }, error: { kind: 'named-problem', code: 'none', retry: 'not-requested' } }, boundary: 'Negative fixed synthetic literal only.', + }, + 'unactionable-error-v1': { + id: 'unactionable-error-v1', family: 'fixed-frontend-backend-boundary-v1', + ui: { localState: ['draft-filter'], context: ['fixed-locale'], forbiddenDomainClaims: [] }, + api: { command: { name: 'fixed-submit-choice', intent: 'set-choice', idempotency: 'named-key-v1' }, readModel: { name: 'fixed-choice-screen-v1', fields: ['status', 'allowedActions', 'messageCode'] }, error: { kind: 'thrown-value', code: '', retry: '' } }, boundary: 'Negative fixed synthetic literal only.', + }, +}); + +export function createFixedBoundaryCase(id = 'named-screen-boundary-v1') { const value = FIXED_BOUNDARY_CASES[id]; return value ? deepFreeze(clone(value)) : undefined; } +function stop(status, reason, nextAction) { return deepFreeze({ accepted: false, status, reasons: deepFreeze([reason]), nextAction, handoff: 'not-eligible-for-synthetic-boundary-review', productionEffect: 'not-attempted' }); } +export function reviewFixedStateOwnership(input) { + if (!Object.values(FIXED_BOUNDARY_CASES).some((known) => JSON.stringify(known) === JSON.stringify(input))) return stop('stop-unknown-fixed-input', 'input-is-not-a-known-named-fixed-literal', 'select-a-named-fixed-case'); + if (!input.ui?.localState?.length || !input.ui?.context?.length) return stop('stop-ui-boundary-incomplete', 'local-state-or-context-is-not-named', 'name-local-state-and-context-separately'); + if (input.ui.forbiddenDomainClaims?.length) return stop('stop-ui-claims-domain-state', 'ui-literal-claims-domain-state', 'remove-domain-claim-and-request-a-read-model'); + const command = input.api?.command; + if (!command?.name || !command.intent || !command.idempotency) return stop('stop-command-not-named', 'command-intent-or-idempotency-key-is-missing', 'name-command-intent-and-idempotency-boundary'); + const readModel = input.api?.readModel; + if (!readModel?.name || !['status', 'allowedActions', 'messageCode'].every((field) => readModel.fields?.includes(field))) return stop('stop-read-model-incomplete', 'screen-read-model-is-not-decidable', 'name-status-actions-and-message-code'); + const error = input.api?.error; + if (error?.kind !== 'named-problem' || !Object.hasOwn(error, 'code') || !Object.hasOwn(error, 'retry')) return stop('stop-error-not-actionable', 'error-is-not-a-named-problem-envelope', 'name-problem-code-and-retry-boundary'); + return deepFreeze({ accepted: true, status: 'synthetic-boundary-review-hand-off', caseId: input.id, uiOwns: deepFreeze([...input.ui.localState, ...input.ui.context]), apiOwns: deepFreeze([command.intent, readModel.name, 'named-problem-envelope']), boundary: input.boundary, productionEffect: 'not-attempted' }); +} +export function explainFixedBoundaryInference(input) { + const review = reviewFixedStateOwnership(input); + if (!review.accepted) return review; + return deepFreeze({ status: 'synthetic-inference-boundary-explained', allowedConclusion: 'named-fields-support-only-a-synthetic-review-hand-off', forbiddenConclusion: 'no-real-ui-api-or-domain-state-is-observed', nextAction: 'hand-off-fixed-synthetic-boundary-review', productionEffect: 'not-attempted' }); +} +export function runFixedBoundaryFixture() { + const cases = [ + ['named-screen-boundary-v1', 'synthetic-boundary-review-hand-off'], ['ui-claims-domain-state-v1', 'stop-ui-claims-domain-state'], ['unnamed-command-v1', 'stop-command-not-named'], ['unactionable-error-v1', 'stop-error-not-actionable'], + ]; + const checks = cases.map(([id, status]) => ({ id, expected: status, actual: reviewFixedStateOwnership(createFixedBoundaryCase(id)).status })); + const inference = explainFixedBoundaryInference(createFixedBoundaryCase('named-screen-boundary-v1')); + checks.push({ id: 'inference', expected: 'synthetic-inference-boundary-explained', actual: inference.status }); + const accepted = checks.every((check) => check.expected === check.actual) && inference.productionEffect === 'not-attempted'; + return deepFreeze({ passed: checks.filter((check) => check.expected === check.actual).length, total: checks.length, accepted, checks: deepFreeze(checks) }); +} + +const commonMeta = { readingMinutes: 10, tags: ['frontend', 'backend', 'contracts', 'architecture'] }; +const practice = { + ...commonMeta, slug: 'editorial-2026-09-practice-frontend-backend-boundary', + title: 'План на сентябрь: карта границы между экраном и доменом', + excerpt: 'Как до начала работ отделить локальное состояние UI от контекста и screen read model, не приписывая клиенту доменный факт.', + contentHtml: [ + p('План на сентябрь 2026, составленный по источникам на 31 июля 2026: команда часто называет «состоянием экрана» всё, что лежит в store. В результате UI присваивает себе доменный факт — например, «выбор подтверждён» — до того, как этот факт вообще определён контрактом. Цена ошибки конкретна: спор о кнопке превращается в спор о правде системы, а повторный вход или иной клиент получает несовместимую версию того же решения.'), + p('Задача этого сценария не в том, чтобы заменить API красивой схемой. Нужно до реализации разложить один экран на владельцев: что живёт только в UI, что является контекстом запроса и что backend обязан вернуть как read model. Если не назвать владельца, локальная оптимизация станет скрытым протоколом. Её стоимость проявится при ошибке, повторной команде или добавлении второго интерфейса: договорённость придётся восстанавливать по веткам компонентов.'), + h2('Три слоя, которые нельзя называть одним словом'), + p('Локальное состояние отвечает на вопрос «как пользователь сейчас управляет представлением»: открыт ли panel, что набрано в фильтре, какая вкладка выбрана до отправки. Оно может исчезнуть при закрытии view без изменения домена. Контекст не является результатом операции: locale, явно выданное permission или feature boundary задают, как интерпретировать запрос. Их нельзя подменять полем ответа и нельзя вычислять из CSS или текста кнопки.'), + p('Доменное состояние отвечает на другой вопрос: что система считает действительным для предмета. У экрана нет оснований владеть им только потому, что он его показывает. В сентябрьском плане API возвращает не «всю сущность», а named screen read model: status, allowedActions и messageCode. Это достаточная форма для решения UI, но не лицензия выдумать новые переходы или причины отказа.'), + figure('/assets/editorial/2026/frontend-backend-boundary-2026-state-ownership-map.svg', 'Схема из трёх колонок: UI владеет draft и открытой панелью, context содержит locale и permission, API возвращает status, allowed actions и message code. Стрелка команды идёт к API, а не к доменному факту внутри UI.', 'Карта владельцев не описывает настоящую систему. Это шаблон вопросов для сентябрьского design review.'), + table('Карта ответственности для одного synthetic screen scenario', ['Объект', 'Владелец', 'Допустимый вопрос', 'Недопустимая подмена'], [ + ['draft-filter', 'UI', 'что введено до команды?', 'назвать draft подтверждённым выбором'], + ['fixed-locale', 'context', 'в каком формате показать код?', 'вывести locale из результата операции'], + ['status', 'API read model', 'что разрешено показать?', 'превратить status в локальный флаг успеха'], + ['fixed-submit-choice', 'API command', 'какое намерение отправлено?', 'описать команду как прямую мутацию store'], + ['named-problem', 'contract', 'как UI может объяснить stop?', 'ловить произвольную thrown value'], + ]), + h2('Карта начинается не с полей, а с права утверждать'), + p('У каждого поля полезно спросить: кто имеет право изменить его смысл и кто должен увидеть изменение после повторного чтения? Если ответ «только текущий компонент», это кандидат в local state. Если ответ зависит от входной среды, это context с явным источником. Если ответ должен быть одинаково интерпретирован разными клиентами, это доменный факт или его проекция, и экран лишь получает его через контракт.'), + p('Такое правило не требует тотального backend-for-frontend. Иногда read model собирается отдельным adapter, иногда один ресурс уже годится для нескольких view. Важно не место кода, а обязанность: читатель ответа понимает, какие action разрешены, без копирования rules в UI. Если для этого приходится держать в компоненте таблицу переходов, граница не названа; перенос HTTP-вызова сам по себе её не исправит.'), + h2('Как распознать присвоение доменного состояния'), + p('Первый маркер — optimistic flag с именем в прошедшем времени: isConfirmed, wasPaid, hasAccess. Сам по себе optimistic interaction не запрещён, но его следует назвать именно interaction: «команда отправлена», «локальная форма заблокирована», «ожидается новый read model». Когда такой флаг влияет на список разрешённых действий, он уже конкурирует с domain rule. План не требует убрать весь optimistic UI; он требует не выдавать временную реакцию за знание о предмете.'), + p('Второй маркер — context, который появляется только после разбора ответа. Locale и permission обычно задают способ показать или запросить информацию; status описывает то, что вернул contract. Если перемешать их, UI начинает зависеть от исторического порядка: сначала пришёл ответ, потом вычислился context, потом поменялся текст. В сентябрьской карте контекст именуется отдельно именно чтобы reviewer мог спросить, откуда он взят, не разбирая payload и не строя гипотезу о session.'), + p('Третий маркер — boolean, который скрывает причину. canSubmit полезен как derived view state, если он получен из allowedActions или локальной валидности формы. Он опасен как самостоятельный доменный флаг, потому что другой client уже не узнает, почему действие недоступно. Вместо одной универсальной причины model держит status и message code. Это не усложнение ради полноты: следующий экран получает явный вопрос, а не обязанность повторить условие.'), + p('Наконец, карта не требует, чтобы API знал каждую анимацию или фокус. Такие детали остаются у UI и не обязаны переживать новый read. Плохая граница получается не тогда, когда клиент что-то хранит, а когда он хранит факт, который должен быть согласован между потребителями. Этот критерий помогает не уйти в архитектурную религию: локальное остаётся локальным, общее имеет контракт, а context не маскируется под оба случая.'), + h2('Literal-проверка карты, а не имитация trace'), + code("import { createFixedBoundaryCase, reviewFixedStateOwnership } from './upgrade-2026-09.mjs';\n\nconst input = createFixedBoundaryCase('ui-claims-domain-state-v1');\nconst result = reviewFixedStateOwnership(input);\nconsole.log({ status: result.status, reason: result.reasons[0], effect: result.productionEffect });\n// { status: 'stop-ui-claims-domain-state', reason: 'ui-literal-claims-domain-state', effect: 'not-attempted' }"), + p('Этот snippet выполняется над named fixed synthetic literal, клонирует его через JSON и freeze-ит результат. Он не открывает browser, не читает сеть и не изображает настоящий экран. Полезный output здесь — stop с конкретной причиной. Положительная ветка функции даёт лишь synthetic-boundary-review-hand-off; она не означает approval, release или готовность к реализации.'), + p('Для автора M9 эта проверка важна ещё и как способ писать короче. Вместо абзаца о том, «кто за что отвечает», он может показать поле, владельца и stop reason. Но краткость не должна скрывать цену переноса: если status становится локальным, UI берёт на себя обязанность поддерживать его после другой команды, другой вкладки и другого клиента. В плановой карте эта обязанность не делегируется компоненту. Она остаётся открытым вопросом к контракту, пока отдельный read model не даст основание для вывода. Нельзя закрыть этот вопрос именем store или выбором фреймворка: оба решения определяют хранение, но не источник domain truth.'), + h2('Почему screen model не равна DTO «как есть»'), + p('DTO транспорта может быть удобной исходной формой, но его наличие не отвечает на вопрос view. Screen model должна иметь ясное назначение: что показать, что разрешить, как обозначить проблемное состояние. OAS 3.1.1 полезна именно как описание интерфейса, независимое от языка; она не предписывает слой или форму state management. Поэтому в плане мы используем спецификацию, чтобы закрепить поля контракта, а не чтобы объявить каждый JSON API готовым для UI.'), + p('В error path особенно легко смешать роли. HTTP status описывает результат обмена на уровне протокола, но UI всё ещё нуждается в именованной проблеме с code и retry. Нельзя вывести retry из «любого 5xx» без правила конкретного контракта. RFC 9110 задаёт семантику HTTP, а наша модель добавляет ограниченный project-level вопрос: может ли view показать следующий шаг, не угадывая смысл исключения.'), + h2('Сентябрьская последовательность'), + ol([ + 'Выбрать один будущий screen scenario и прямо записать: это план, а не описание уже существующего UI.', + 'Выписать local state отдельно от context; у каждого элемента назвать допустимое время жизни.', + 'Для доменного факта сформулировать один read question и один command intent без слов «обновить store».', + 'Согласовать минимальный screen read model: status, allowed actions, message code и named problem envelope.', + 'Прогнать fixed literal с domain claim; при stop удалить claim, а не добавлять ещё один optimistic flag.', + 'Передать только synthetic review card с открытыми вопросами владельцу следующего design review.', + ]), + h2('Границы плана и следующий шаг'), + p('Карта не доказывает, что конкретный endpoint хорошо спроектирован, и не сообщает о latency, правах реальных пользователей или поведении браузера. Она также не решает offline, cache invalidation и composition нескольких aggregates. JSON Schema Core объясняет, как фиксировать структуру документа, но не превращает validation в доменную корректность. Все перечисленные вопросы потребуют отдельного входа и собственных критериев.'), + p('Следующий шаг на сентябрь — завести второй named literal, в котором у read model нет allowedActions, и убедиться, что review останавливается как на неполной модели. Так команда проверит не только удачную карту, но и язык отказа. До этого нельзя делать вывод о реальном frontend, backend или состоянии выбора: в пакете есть только изолированная модель и план её обсуждения.'), + h2('Проверяемые источники'), sourceList([ + { key: 'openapi', use: 'Использован узко: OAS определяет язык-независимое описание HTTP API, поэтому contract можно обсуждать отдельно от реализации.', boundary: 'Не доказывает, что какой-либо endpoint или screen model существует.' }, + { key: 'http', use: 'Использован только для различения HTTP semantics и project-level problem envelope.', boundary: 'Не задаёт retry policy и не подтверждает исход конкретного запроса.' }, + { key: 'schema', use: 'Использован для мысли о явной структуре документа и vocabulary.', boundary: 'Schema validation не доказывает доменную корректность или UI-поведение.' }, + ]), + ].join(''), +}; + +const mechanism = { + ...commonMeta, slug: 'editorial-2026-09-mechanism-frontend-backend-boundary', + title: 'План на сентябрь: на что boundary даёт право сделать вывод', + excerpt: 'Как отличить query, command, read model и error envelope, чтобы UI не превращал транспортный ответ в неподтверждённый доменный вывод.', + contentHtml: [ + p('Сентябрьский механизм начинается с неприятного симптома: один handler отправляет запрос, меняет локальный флаг и затем трактует любой ответ как подтверждение. Проблема не в количестве hooks. Проблема в незаконном выводе: UI делает доменное утверждение из факта отправки команды. Цена — не только неверная кнопка. При повторе, параллельном клиенте или ошибке команда перестаёт быть различимой, а support получает сообщение без понятной причины и следующего действия.'), + p('Это план для источников, известных на 31 июля 2026, а не отчёт о сентябрьской переделке. Он разделяет четыре формы: query запрашивает именованное представление, command выражает намерение, read model даёт экрану основания для показа, error envelope ограничивает реакцию. Ни одна форма не заменяет другую. Особенно опасно называть command «query с side effect» или считать status code готовой инструкцией для UI.'), + h2('Command не является правом изменить экран'), + p('Command полезен, когда его имя описывает намерение, а не способ работы с данными. В fixed literal это fixed-submit-choice с intent set-choice и named idempotency key. Имя не говорит, что выбор уже принят; оно лишь делает повторное намерение различимым в границе модели. Если name, intent или idempotency отсутствуют, review возвращает stop. Он не генерирует fallback и не пытается угадать безопасное значение.'), + p('Query в этом сценарии — вопрос о view, а не скрытая команда. Его ответом становится screen read model fixed-choice-screen-v1. Поля status, allowedActions и messageCode образуют достаточное основание для отображения; каждое имеет читателя в UI. Если добавить поле «для будущего», но не назвать, какой вывод оно поддерживает, оно не улучшает contract. Оно расширяет поверхность совместимости без проверяемой задачи.'), + figure('/assets/editorial/2026/frontend-backend-boundary-2026-contract-responsibility-matrix.svg', 'Матрица из четырёх строк: query читает screen model, command передаёт intent, read model даёт status и allowed actions, error envelope даёт code и retry. Внизу красная строка запрещает UI делать доменный вывод из отправки команды.', 'Матрица показывает разные права на вывод: отправка command не равна новому domain fact.'), + table('Четыре формы на boundary', ['Форма', 'Кто формулирует', 'Что может заключить UI', 'Чего заключать нельзя'], [ + ['query', 'view contract', 'какой named model нужен сейчас', 'что домен изменился'], + ['command', 'intent contract', 'какое намерение выражено', 'что намерение принято'], + ['read model', 'API boundary', 'что показать и какие action названы', 'внутреннюю причину без поля'], + ['problem envelope', 'error contract', 'какой message code и retry boundary есть', 'что ошибка transient без правила'], + ]), + h2('Право на вывод — более точный тест, чем «где код»'), + p('В проекте можно разместить adapter в frontend repo и всё равно держать доменную границу корректной. И наоборот, endpoint на backend не спасает, если component сам вычисляет разрешённый переход. Поэтому на сентябрьской встрече полезно не спорить об именах слоёв, а взять каждую ветку и спросить: каким полем contract она обоснована? Если такого поля нет, ветка должна стать stop, запросом на новую модель или явным local interaction — но не скрытым business rule.'), + p('Read model не обязана копировать aggregate и не обязана быть вечной. Она обязана быть решаемой для своего consumer. В примере отсутствие любого из трёх полей останавливает review с stop-read-model-incomplete. Это сознательно строгая учебная форма: она не заявляет, что три поля достаточны для всех экранов. Она фиксирует минимум для одного named synthetic scenario и оставляет расширение отдельным решением.'), + h2('Error — контракт действия, а не украшение ответа'), + p('Thrown value удобна внутри языка, но на boundary она не даёт потребителю стабильного вопроса. В плановой модели допустим только named-problem с присутствующими ключами code и retry. Значение none или not-requested — fixed literal, не наблюдение. Оно не сообщает, что в сентябре ошибок не будет и не моделирует retry сети. Оно заставляет автора назвать семантику до того, как UI начнёт угадывать её по тексту.'), + p('HTTP status остаётся значимым, но не должен молча стать domain language. RFC 9110 описывает status codes и семантику сообщений; из него не следует, что конкретный product flow обязан показывать кнопку retry. Такая кнопка появляется только при named rule на boundary. Этот разрыв полезен: transport failures, validation problems и запрещённые действия перестают сливаться в один catch, а каждое расширение контракта имеет место для review.'), + h2('Где механизм намеренно останавливает вывод'), + p('Первый stop связан с неизвестным input. Функция сравнивает вход с finite набором named fixtures, а не пытается принять похожий object. Это ограничение похоже на избыточную осторожность, но оно защищает учебный пример от тихой эволюции: добавили поле, поменяли смысл retry, а старый reviewer всё ещё читает accepted status. Новый input должен получить имя и пройти тот же review. Так автор видит изменение контракта до того, как его назовут обратной совместимостью.'), + p('Второй stop касается команды без idempotency boundary. Мы не утверждаем, что любой production command обязан иметь определённый ключ: это зависит от предмета и реализации. Однако для выбранного synthetic scenario повтор намерения является значимой частью разговора, поэтому пустое поле запрещает hand-off. Такой gate не проектирует storage и не выбирает HTTP header. Он лишь не позволяет тексту говорить «повтор безопасен», пока в модели не названо, что именно различает повтор.'), + p('Третий stop возникает, когда read model не даёт decision fields. Это не требование возвращать все возможные данные. Скорее, это отрицание ложной экономии: нельзя заставить consumer догадаться о permitted action по отсутствию поля или по message text. Если action зависит от сложного правила, API может вернуть более точную проекцию; если правило локальное, его следует явно вынести в UI. В обоих случаях boundary становится предметом change, а не неявным соглашением между двумя ветками.'), + p('Именно поэтому позитивный ответ механизма маленький. Он перечисляет, что named fields позволяют только synthetic-boundary-review-hand-off. У него нет слова success, нет timestamp и нет результата команды. Чем меньше такой output похож на status системы, тем труднее использовать его как декоративное доказательство. Для статьи о будущем это важнее эффекта «работающий пример»: пример должен проверять формулировку, а не имитировать эксплуатацию.'), + h2('Буквальная проверка fail-closed'), + code("import { createFixedBoundaryCase, explainFixedBoundaryInference } from './upgrade-2026-09.mjs';\n\nconst input = createFixedBoundaryCase('unactionable-error-v1');\nconst result = explainFixedBoundaryInference(input);\nconsole.log({ status: result.status, reason: result.reasons[0], next: result.nextAction });\n// { status: 'stop-error-not-actionable', reason: 'error-is-not-a-named-problem-envelope', next: 'name-problem-code-and-retry-boundary' }"), + p('Код исполняется только над зафиксированным literal в памяти и возвращает deterministic stop. Он не делает HTTP-вызов, не читает trace и не создаёт реальное исключение. Даже accepted branch выдаёт лишь synthetic hand-off. Это важная дисциплина для текста о будущем: «модель допускает передачу review» и «команда внедрила boundary» — разные предложения; второе у этого пакета не имеет источника и поэтому запрещено.'), + p('Полезно отдельно проверить, не подменяет ли модель failure mode термином transport. Например, named-problem не обязан быть сериализацией RFC status и не обязан нести stack. Его задача в выбранном сценарии — сохранить код и границу retry, достаточные для следующей UI-ветки. Если consumer требуется причина для журнала, это новый contract question с собственным доступом и privacy boundary. Добавлять её «на всякий случай» в screen model нельзя: поля без читателя быстро становятся неявным интерфейсом для чужой логики. В частности, message code не должен превращаться в место, куда UI складывает технический текст, если contract не назвал его читателя и срок жизни.'), + h2('Как проводить design review в сентябре'), + ol([ + 'Назвать один сценарий и дату: это план на сентябрь, состояние источников ограничено 31 июля.', + 'Записать command как intent с key для различимости повторов, не как обещание успешной мутации.', + 'Сформулировать query и screen read model через выводы, которые реально нужны view.', + 'Для каждой UI-ветки указать поле contract; ветку без поля заменить stop question.', + 'Описать problem envelope отдельно от HTTP-статуса и назвать retry boundary.', + 'Прогнать negative fixed literal и передать только synthetic hand-off с open question.', + ]), + h2('Граница источников и граница механизма'), + p('OpenAPI 3.1.1 поддерживает идею явного описания операции и сообщений, но не вводит наше разделение local/context/domain state. JSON Schema Core поддерживает vocabulary и ссылки в schema, но не решает, какие fields нужны человеку на экране. Эти решения — проектная модель P103. Review отмечает это отдельно, чтобы ссылкой на стандарт не прикрыть архитектурный вкус как доказанный факт.'), + p('Механизм не покрывает streaming updates, eventual consistency, cache coherence, авторизацию и финансовую идемпотентность. Также он не измеряет удобство API и не обещает уменьшение числа ошибок. Для таких утверждений нужны реальные наблюдения, которых fixed literal не содержит. Следующий шаг — создать named case с корректным command, но без messageCode, и проверить точность stop reason; только потом обсуждать, нужен ли новый contract field.'), + h2('Проверяемые источники'), sourceList([ + { key: 'openapi', use: 'Использован для узкой роли: операция и сообщения могут быть описаны как интерфейс HTTP API.', boundary: 'Не устанавливает наши правила command/query или состав screen model.' }, + { key: 'http', use: 'Использован для разграничения HTTP semantics и application-level problem policy.', boundary: 'Не подтверждает retry, error rate или итог конкретного обмена.' }, + { key: 'schema', use: 'Использован как пример явной vocabulary contract-документа.', boundary: 'Не выводит business meaning из валидности JSON.' }, + ]), + ].join(''), +}; + +const field = { + ...commonMeta, slug: 'editorial-2026-09-field-frontend-backend-boundary', + title: 'Сценарий на сентябрь: synthetic hand-off для границы UI и API', + excerpt: 'Как подготовить безопасную карточку review без изображения production trace, чтобы несогласованный ownership останавливал обсуждение точной причиной.', + contentHtml: [ + p('В сентябрьском сценарии проблема review часто не в отсутствии диаграммы, а в подмене evidence. Автор показывает «типичный ответ» и произносит, что экран уже согласован с API, хотя пример не имеет владельца, даты и отрицательной ветки. Цена такой уверенности — решение принимают по иллюстрации, которую нельзя повторить: при первом расхождении UI и backend спорят, была ли это договорённость или просто локальный mock.'), + p('Этот материал — план безопасного synthetic hand-off на сентябрь 2026 при состоянии источников на 31 июля. Он не описывает проведённый review и не имитирует UI-network trace. Вместо него используется named fixed literal с заранее известными полями. Положительный исход ограничен передачей карточки следующему reviewer; он не утверждает approval, deployment, публикацию или доступ к production.'), + h2('Карточка evidence должна содержать то, что может опровергнуть вывод'), + p('Поле ui.localState показывает, что interaction принадлежит экрану; ui.context фиксирует условия интерпретации; api.command называет намерение; api.readModel задаёт достаточное представление; api.error ограничивает реакцию. Эти поля не являются дампом данных. Они составлены так, чтобы reviewer мог отвергнуть карточку по одному отсутствующему условию, не достраивая его опытом или знанием настоящей системы.'), + p('Контрпример важнее красивой positive ветки. Если UI уже содержит choice-confirmed в forbiddenDomainClaims, функция не говорит «почти готово». Она возвращает stop-ui-claims-domain-state. Это не обвинение frontend и не факт о реальном приложении. Это свойство формального literal: boundary не позволяет выводу, пока claim не заменён read question или не закреплён соответствующим контрактом.'), + figure('/assets/editorial/2026/frontend-backend-boundary-2026-review-evidence-loop.svg', 'Цикл из пяти шагов: fixed literal, проверка ownership, stop или counterexample, уточнённый literal и synthetic hand-off. Красная ветка от domain claim возвращает к вопросу о read model.', 'Evidence loop останавливает недоказанный вывод и возвращает его к именованному входу, а не к реальному окружению.'), + table('Поля безопасной hand-off карточки', ['Поле', 'Зачем оно нужно', 'Пример fixed value', 'Что оно не доказывает'], [ + ['localState', 'отделить interaction от domain', 'draft-filter', 'что пользователь действительно вводил фильтр'], + ['context', 'назвать условие интерпретации', 'fixed-locale', 'реальное permission или session'], + ['command', 'зафиксировать intent', 'fixed-submit-choice', 'успешное изменение'], + ['readModel', 'дать основание UI-ветке', 'allowedActions', 'состояние настоящего экрана'], + ['problem', 'сохранить stop semantics', 'named-problem', 'наличие сетевой ошибки'], + ]), + h2('Что именно является evidence в synthetic работе'), + p('Evidence здесь — не лог браузера и не screenshot. Это связь между одним известным входом и результатом pure function. Вход создаётся JSON clone и deep freeze, поэтому caller не может тихо изменить fixture между проверкой и hand-off. Determinism полезен ровно в своей области: reviewer может запустить public export и получить тот же named stop или тот же limited hand-off. Он не может из этого узнать задержку сети, порядок событий или поведение реального кода.'), + p('Отдельно полезно писать границу прямо в output: productionEffect: not-attempted. Обычно такую строку считают избыточной, пока positive status не начинают читать как разрешение. Здесь статус synthetic-boundary-review-hand-off означает только полноту заранее заданной структуры. Он не утверждает, что ownership выбран верно для продукта, не меняет конфигурацию и не разрешает кому-либо отправлять command в настоящий сервис.'), + h2('Почему fixed literal лучше «правдоподобного» примера'), + p('Правдоподобный mock соблазняет деталями: URL, имя клиента, задержка, fragment response. Но чем точнее он похож на среду, тем проще читателю принять его за наблюдение. Для будущего месяца это особенно опасно: вымышленная trace визуально выглядит как выполненная работа. Named fixed literal отказывается от этой достоверности нарочно. Его values звучат искусственно — fixed-locale, named-key-v1 — и этим не дают перепутать модель с фактом.'), + p('JSON clone перед freeze нужен не для демонстрации приёма JavaScript, а для границы владения fixture. Проверяемая функция не получает ссылку на скрыто изменяемый внутренний объект. Она получает новый JSON-compatible input, после чего вложенные поля заморожены. Это не защищает реальную память процесса и не является security control. Это минимальное свойство учебного примера: один reviewer не может случайно изменить allowedActions перед передачей карточки другому.'), + p('Отсутствие clock — ещё одна сознательная граница. В пакете нельзя сказать «после двух секунд» или «сначала пришёл response», потому что в literal нет времени и событий. Вместо таймлайна есть порядок review: вход, проверка, reason, next action. Такой порядок не претендует на causal trace. Он удобен, когда обсуждается качество контракта: причинность должна быть подтверждена отдельным наблюдением, а не дорисована последовательностью стрелок.'), + p('Наконец, synthetic hand-off не является документом согласования между командами. Он только делает disagreement видимым. Реальный agreement потребовал бы участников, версий, полномочий и, вероятно, изменения описания API; ни одного такого факта P103 не содержит. Поэтому карточка завершает свою работу словом hand-off, а не «решение принято». Это сохраняет полезную дистанцию между подготовкой разговора и внешним действием.'), + p('В поле особенно соблазнительно заменить отсутствие evidence словами «этот путь типичен». Для P103 это запрещённая экономия. Типичным может быть только named fixture, а не пользователь, экран, endpoint или последовательность пакетов. Если следующий reviewer хочет проверить реальный boundary, он должен создать отдельную задачу с разрешёнными inputs и получить доступ к ним в своём процессе. Синтетическая карточка не переносит ему ни данных, ни полномочий, зато точно фиксирует, какую неизвестность нельзя прятать за правдоподобным примером. Это сохраняет применимость материала: он учит форме вопроса, а не создаёт ложное знание о системе, которую не наблюдали.'), + h2('Буквальный пример передачи, а не одобрения'), + code("import { createFixedBoundaryCase, explainFixedBoundaryInference } from './upgrade-2026-09.mjs';\n\nconst input = createFixedBoundaryCase('named-screen-boundary-v1');\nconst result = explainFixedBoundaryInference(input);\nconsole.log({ status: result.status, next: result.nextAction, effect: result.productionEffect });\n// { status: 'synthetic-inference-boundary-explained', next: 'hand-off-fixed-synthetic-boundary-review', effect: 'not-attempted' }"), + p('Это максимальный положительный выход P103. Snippet использует только named fixed literals; он не запускает UI, не читает файл, не обращается к секрету и не делает запрос. Слово hand-off здесь означает передачу формулировки для дальнейшего review, а не передачу контроля над средой. Такой узкий output делает полезной и negative ветку: при дефекте она сообщает ровно, какого поля не хватает, вместо декоративного «проверить интеграцию». '), + h2('Как reviewer читает карточку'), + p('Сначала он ищет ownership conflict: доменное утверждение в UI, контекст, спрятанный в read model, или command без intent. Затем он читает constraints у error envelope: есть ли code и retry boundary, а не просто строка ошибки. После этого он проверяет вывод: соответствует ли status одному из явно названных состояний функции. Если карточка просит сделать шаг в окружении, которого literal не касается, review обязан остановиться — даже если описание кажется правдоподобным.'), + p('Эта процедура нарочно скупа на «истории успеха». В ней нет synthetic метрики, потому что числовой эффект легко спутать с наблюдением. В ней нет названия настоящей команды или endpoint, потому что их нельзя проверить из изолированного пакета. Вместо этого сохраняется контрпример с неназванной command и контрпример с неоперабельной ошибкой. Они учат лучше, чем happy path: показывают момент, когда автор обязан сменить вопрос.'), + h2('Порядок сентябрьского hand-off'), + ol([ + 'Пометить карточку как план/сценарий на сентябрь и записать cutoff источников 31.07.2026.', + 'Собрать один fixed literal с local state, context, command, read model и error envelope.', + 'Запустить positive literal и проверить, что output ограничен synthetic hand-off и not-attempted.', + 'Запустить literal с UI domain claim и сохранить exact stop reason.', + 'Запустить literal с пустым command или error envelope и не заменять stop догадкой.', + 'Передать reviewer только вход, output, границы и открытый вопрос; не добавлять claims о real UI/API.', + ]), + h2('Ограничения и следующий вопрос'), + p('Synthetic hand-off не заменяет контрактное тестирование, доступность, security review и проверку authorization. Он не определяет, где хранить cache, как кодировать проблему на wire и как версии читать старым клиентам. OAS и JSON Schema помогают описывать форму, RFC 9110 — смысл HTTP, но ни один из этих источников не подтверждает, что предлагаемая форма полезна конкретному человеку. P103 сознательно не делает такого заявления.'), + p('Следующий шаг — на отдельном design review создать ещё один fixed literal с известным несовпадением: command named, но read model без allowedActions. Если reason останется точным и не даст «продолжить на глаз», сценарий готов к обсуждению следующего контракта. Если нет, следует уточнить форму карточки. До независимой работы с настоящими inputs нельзя говорить, что сентябрьский boundary уже реализован, измерен или опубликован.'), + h2('Проверяемые источники'), sourceList([ + { key: 'openapi', use: 'Использован только как первичный источник для понятия описываемого HTTP API interface.', boundary: 'Не является evidence настоящего hand-off или release.' }, + { key: 'http', use: 'Использован для границы между protocol semantics и application-level decision.', boundary: 'Не сообщает о реальном response, user или сети.' }, + { key: 'schema', use: 'Использован для идеи явно структурированной проверяемой карточки.', boundary: 'Не подтверждает достоверность значения внутри fixed literal.' }, + ]), + ].join(''), +}; + +export const revisions = [practice, mechanism, field]; +export function verifyRevisionsAgainstFixture() { + const fixture = runFixedBoundaryFixture(); + const articleChecks = revisions.map((revision) => { const text = bodyText(revision.contentHtml); return text.length >= 5000 && text.length <= 15000 && /(цен[аы]|стоимост|издержк)/i.test(text.slice(0, 900)) && //.test(revision.contentHtml) && /
/.test(revision.contentHtml) && /
/.test(revision.contentHtml) && /
    /.test(revision.contentHtml); }); + return deepFreeze({ passed: fixture.passed + articleChecks.filter(Boolean).length, total: fixture.total + articleChecks.length, accepted: fixture.accepted && articleChecks.every(Boolean), fixture, articleChecks, characters: Object.fromEntries(revisions.map((revision) => [revision.slug, bodyText(revision.contentHtml).length])) }); +} +if (process.argv.includes('--verify-fixture')) { const report = verifyRevisionsAgainstFixture(); console.log(JSON.stringify(report, null, 2)); if (!report.accepted) process.exitCode = 1; } +if (process.argv.includes('--print-revisions')) console.log(JSON.stringify(revisions));