diff --git a/editorial/production/README.md b/editorial/production/README.md index 43226a5..1398c13 100644 --- a/editorial/production/README.md +++ b/editorial/production/README.md @@ -1,6 +1,6 @@ # Производство редакционных партий -На 31 июля 2026 года строгий аудит проходит 292 из 358 созданных материалов. Остальные 66 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить. +На 31 июля 2026 года строгий аудит проходит 295 из 358 созданных материалов. Остальные 63 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить. ## Одна партия diff --git a/editorial/reviews/2026-03-draft.md b/editorial/reviews/2026-03-draft.md new file mode 100644 index 0000000..023b4b9 --- /dev/null +++ b/editorial/reviews/2026-03-draft.md @@ -0,0 +1,111 @@ +# P97 — март 2026: Контракты данных + +## Область изолированного draft-пакета + +Пакет содержит только три overlay-статьи: + +- editorial-2026-03-practice-data-contracts; +- editorial-2026-03-mechanism-data-contracts; +- editorial-2026-03-field-data-contracts. + +Исполняемый модуль — web/scripts/upgrade-2026-03.mjs. Все schema, version, producer, consumer, manifest, compatibility report и boundary data — named fixed synthetic JavaScript literals в памяти. Модуль не обращается к schema registry, сети, файловой системе, Git, CI, часам, telemetry, production data, API или deployment. Положительный verdict ограничен synthetic compatibility-review hand-off; он не изменяет форму и не доказывает результат release, migration или rollout. + +Registry, README, app-файлы, articles.json, очередь, Git-state и любые чужие незавершённые изменения не менялись. Полный site build намеренно не запускался: это интеграционный шаг вне разрешённого scope. + +## Исследование и историческая граница + +Историческая граница статей — **31 марта 2026**. Все ссылки первичные или официальные. Изменяемая документация не используется: RFC неизменяем, IETF draft закреплён номером версии, а Avro привязан exact source commit. + +| Источник | Version / pin | Узкий подтверждённый факт | Явная граница | +| --- | --- | --- | --- | +| [JSON Schema Core, draft-bhutton-json-schema-01](https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-01) | draft-bhutton-json-schema-01, **10.06.2022**, immutable versioned IETF draft | Vocabulary описывает object properties и применение additionalProperties к именам, не обработанным другими keywords. | Не задаёт policy конкретного consumer, contract family, manifest или результат данного gate. | +| [RFC 8927: JSON Type Definition](https://www.rfc-editor.org/rfc/rfc8927.html) | RFC 8927, **November 2020**, immutable publication | JTD различает properties, optionalProperties и additionalProperties; по умолчанию дополнительные properties не разрешены. | Experimental RFC не определяет status review, registry policy или deployment decision. | +| [Apache Avro Specification 1.12.0](https://github.com/apache/avro/blob/8c27801dc8d42ccc00997f25c0b8f45f8d4a233e/doc/content/en/docs/%2B%2Bversion%2B%2B/Specification/_index.md#schema-resolution) | commit 8c27801dc8d42ccc00997f25c0b8f45f8d4a233e, release tag **05.08.2024** | Specification различает writer and reader schemas и описывает schema resolution. | Не является реализацией fixed JSON-like literal, не подтверждает compatibility этого gate и не заменяет consumer review. | + +Проверка источников выполнена точечными запросами: RFC подтвердил дату и vocabulary properties/optionalProperties/additionalProperties; IETF draft подтвердил версию и дату; raw Avro file по exact commit подтвердил формулировку writer schema, reader schema и Schema Resolution. Ни один источник не использован как evidence успешного synthetic hand-off. + +## Проход 1 — качество, problem/cost, голос и самостоятельность + +Прочитаны полные тела всех трёх статей без source list. Во всех первых двух абзацах есть наблюдаемый problem, стоимость ручной координации и короткое действие. Голос M9 держит последовательность symptom → mechanism → check → next action; текст не обещает эффект вне synthetic boundary. + +| Статья | Самостоятельный вопрос | Свой механизм и visual | Цена ошибки | +| --- | --- | --- | --- | +| practice | Как перевести ручное schema change в явную карточку? | baseline/candidate/manifest + эволюция контракта | позднее восстановление обещаний после change | +| mechanism | Что именно вычисляет backward compatibility? | direction, field maps, manifest и capability reader + gate loop | спор о слове compatible вместо проверяемого отношения | +| field | Как вести несколько consumer без ложного общего verdict? | pair matrix, inventory и адресный возврат stop | поздний поиск владельца и ручное исправление для разных readers | + +Таблица, ordered sequence, executable public-export snippet, ограничения и следующий шаг есть в каждой статье. У статей нет общего развёрнутого вступления, общего примера или общей таблицы. + +Во время первого uniqueness check найден один общий 12-словный фрагмент только в префиксе двух code snippets. Mechanism snippet переписан через локальные public-export aliases; поведение и output сохранены. Финальный cross-article check: 0 одинаковых абзацев от 160 знаков и 0 общих 12-словных фрагментов во всех трёх парах. + +**Вердикт прохода 1: PASS после целевой правки.** + +## Проход 2 — источники, историческая граница и буквальное выполнение кода + +Проверены ссылки, их version/date/pin и границы применимости. В sourceList каждой статьи версия или commit показывается рядом со ссылкой. Все даты находятся до 31.03.2026. Для Avro используется immutable GitHub commit, а не live documentation; RFC и versioned IETF draft также не опираются на текущую mutable страницу. Во время проверки дата IETF draft была уточнена с ошибочных 16.06.2022 на фактические 10.06.2022 и сразу исправлена в sourceList и этой таблице. + +Все visible snippets импортируют только public exports из upgrade-2026-03.mjs и выполнены буквально из директории web/scripts: + + practice: + { added: ['priority'], + status: 'synthetic-compatibility-review-hand-off', + effect: 'not-attempted' } + + mechanism: + { status: 'stop-backward-incompatible-schema', + removed: ['state'], + next: 'retain-required-baseline-field-or-name-a-separate-migration' } + + field: + { status: 'stop-incompatible-consumer', + reason: 'incompatible-consumer', + assertions: 15 } + +Fixture принимает только known fixed case. Он fail-closed на требуемых границах: + +- stop-backward-incompatible-schema при исчезновении required state; +- stop-undocumented-schema-field для routingHint вне manifest; +- stop-incompatible-consumer для strict reader; +- stop-implicit-comparison, когда relation не названа; +- stop-unknown-fixed-contract-case для произвольного object input. + +Единственный положительный путь заканчивается synthetic-compatibility-review-hand-off и productionEffect: not-attempted. Он не создаёт release, API change, registry update или deployment. + +**Вердикт прохода 2: PASS.** + +## Проход 3 — visual, аудит, объём и выпускное качество + +| Статья | Основной текст без source list | SVG | Mobile inspection 375 px | +| --- | ---: | --- | --- | +| practice | **9 210** знаков | data-contracts-2026-contract-evolution.svg | PASS: карточки v1.0/v1.1, gate и hidden field читаемы | +| mechanism | **9 426** знаков | data-contracts-2026-compatibility-gate-loop.svg | PASS после сокращения обрезанного заголовка | +| field | **9 748** знаков | data-contracts-2026-producer-consumer-matrix.svg | PASS: статусы матрицы и подписи различимы | + +Sharp render всех SVG на ширине 375 px просмотрен визуально. В первом visual-проходе у схемы gate loop заголовок выходил за правую границу; он сокращён до «У каждого stop есть своя причина» и повторно проверен. Final SVG не содержат script, foreignObject, javascript:, data:image или inline event handlers. + +Команды и результаты: + +- node --check web/scripts/upgrade-2026-03.mjs — PASS. +- node web/scripts/upgrade-2026-03.mjs --verify-fixture — PASS, **15/15 assertions**. +- npm run audit:draft -- scripts/upgrade-2026-03.mjs из web/ — PASS: 9 210 / 9 426 / 9 748; problem/cost в начале, table, figure с alt/caption, code, ordered sequence и source section присутствуют. +- Literal execution трёх visible snippets — PASS. +- xmllint --noout для трёх SVG — PASS. +- SVG safety scan через rg — PASS. +- Sharp render 375 px для всех трёх SVG и ручной visual inspection — PASS. +- Cross-article duplicate scan — PASS: paragraphs160=0 и fragments12=0 для каждой пары. +- git diff --check и no-index whitespace check для untracked draft-файлов — PASS. + +**Вердикт прохода 3: PASS после целевой visual-правки.** + +Пакет намеренно остаётся изолированным draft: registry и README не подключены; stage, commit и push отсутствуют. + +## Независимая приёмка основного редактора + +Проверено 2026-07-31 перед подключением. + +- Первичные источники перепроверены отдельно: RFC 8927 опубликована в November 2020 и содержит `optionalProperties`/`additionalProperties`; exact Avro commit `8c27801dc8d42ccc00997f25c0b8f45f8d4a233e` содержит Schema Resolution и различает writer/reader schema; versioned IETF draft закреплён как `draft-bhutton-json-schema-01`. Эти документы использованы только как терминологическая опора, не как доказательство совместимости fixture. +- `node --check`, fixture 15/15 и `audit:draft` прошли повторно. Дословно выполнены три public-export примера: additive case заканчивается только `synthetic-compatibility-review-hand-off` с `not-attempted`; отсутствие `state` и строгий reader получают разные fail-closed статусы. +- Три SVG прошли XML и safety scan. PNG на 375 px просмотрены повторно: labels, стрелки и матрица читаемы без обрезания. +- Строгий scan полного body, включая code и без source list, подтвердил для каждой пары `0` общих абзацев от 160 знаков и `0` общих 12-словных фрагментов. После подключения production audit и сборка должны быть обязательной частью релиза. + +Решение: принять P97 как учебный synthetic overlay. Он не подтверждает реальную схему, consumer, registry, migration или deployment. diff --git a/web/data/editorial-revisions.mjs b/web/data/editorial-revisions.mjs index 2204b9b..14cd68b 100644 --- a/web/data/editorial-revisions.mjs +++ b/web/data/editorial-revisions.mjs @@ -93,6 +93,7 @@ import { revisions as november2025Revisions } from '../scripts/upgrade-2025-11.m import { revisions as december2025Revisions } from '../scripts/upgrade-2025-12.mjs'; import { revisions as january2026Revisions } from '../scripts/upgrade-2026-01.mjs'; import { revisions as february2026Revisions } from '../scripts/upgrade-2026-02.mjs'; +import { revisions as march2026Revisions } from '../scripts/upgrade-2026-03.mjs'; // This layer replaces archived source entries without losing their stable slug and date. export const editorialRevisions = [ @@ -191,4 +192,5 @@ export const editorialRevisions = [ ...december2025Revisions, ...january2026Revisions, ...february2026Revisions, + ...march2026Revisions, ]; diff --git a/web/public/assets/editorial/2026/data-contracts-2026-compatibility-gate-loop.svg b/web/public/assets/editorial/2026/data-contracts-2026-compatibility-gate-loop.svg new file mode 100644 index 0000000..afec2b8 --- /dev/null +++ b/web/public/assets/editorial/2026/data-contracts-2026-compatibility-gate-loop.svg @@ -0,0 +1,82 @@ + diff --git a/web/public/assets/editorial/2026/data-contracts-2026-contract-evolution.svg b/web/public/assets/editorial/2026/data-contracts-2026-contract-evolution.svg new file mode 100644 index 0000000..680c700 --- /dev/null +++ b/web/public/assets/editorial/2026/data-contracts-2026-contract-evolution.svg @@ -0,0 +1,66 @@ + diff --git a/web/public/assets/editorial/2026/data-contracts-2026-producer-consumer-matrix.svg b/web/public/assets/editorial/2026/data-contracts-2026-producer-consumer-matrix.svg new file mode 100644 index 0000000..dfecc50 --- /dev/null +++ b/web/public/assets/editorial/2026/data-contracts-2026-producer-consumer-matrix.svg @@ -0,0 +1,73 @@ + diff --git a/web/scripts/upgrade-2026-03.mjs b/web/scripts/upgrade-2026-03.mjs new file mode 100644 index 0000000..6256728 --- /dev/null +++ b/web/scripts/upgrade-2026-03.mjs @@ -0,0 +1,684 @@ +function escapeHtml(value) { + return String(value) + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +const p = (text) => '
' + text + '
'; +const h2 = (text) => '' + escapeHtml(text) + '';
+const ol = (items) => '| ' + item + ' | ').join('') + '
|---|
| ' + item + ' | ').join('') + '
state, проблема в сохранении обязательной поверхности. Если появился routingHint, которого нет в manifest, проблема в документации change. Если reader строгий, проблема не в абстрактной версии, а в его заявленной границе дополнительных полей.'),
+ figure('/assets/editorial/2026/data-contracts-2026-contract-evolution.svg', 'Три карточки показывают эволюцию fixed схемы work item от версии 1.0 к 1.1 с необязательным полем priority. Между схемами расположен compatibility gate, который требует явное направление и manifest, а скрытое поле routingHint отправляет по красной ветке в stop.', 'Эволюция становится проверяемой, когда новая форма, заявленный diff и конкретный reader находятся на одной карточке review.'),
+ table('Карточка change перед compatibility gate', ['Часть', 'Что в ней назвать', 'Fixed пример', 'Что нельзя подразумевать'], [
+ ['baseline', 'какая форма была точкой отсчёта', 'fixed-work-item-v1', 'любую прежнюю схему из памяти'],
+ ['candidate', 'какая форма предлагается', 'fixed-work-item-v1-1', 'latest без версии'],
+ ['manifest', 'какие поля добавлены или удалены', 'added: priority', 'скрытый routingHint'],
+ ['направление', 'кто читает чей результат', 'backward producer → consumer', 'похожая форма значит compatible'],
+ ['consumer', 'какой reader проверяется', 'fixed-tolerant-reader-v1', 'все возможные читатели'],
+ ['граница', 'что даёт положительный output', 'synthetic review hand-off', 'deploy, migration или реестр'],
+ ]),
+ h2('Сначала назвать минимальный контракт, который нельзя потерять'),
+ p('В fixed baseline три поля: обязательные id и state, а также необязательный note. Такой маленький набор выбран не как модель реального домена, а как способ увидеть механизм. Когда candidate добавляет необязательный priority, gate может перечислить ровно одно новое поле. Когда candidate заменяет state на phase, разница уже не выглядит косметической: baseline required field исчез. Отдельное имя для change снимает ложную дискуссию о том, достаточно ли похожи слова state и phase.'),
+ p('Важна и граница закрытости. Не каждый reader обязан отвергать добавления, но его поведение нельзя угадывать по типу данных. В synthetic наборе tolerant reader прямо говорит, что принимает declared added fields. Strict reader прямо говорит обратное. Это не характеристика человека или сервиса, а поле учебной карточки. Gate не пытается «уговорить» strict reader. Он возвращает stop, потому что нам не разрешено превратить отдельную потребность в общее обещание без нового review.'),
+ h2('Исполняемая проверка additive change'),
+ code("import { createFixedDataContractCase, inspectFixedSchemaChange, reviewFixedDataContractCompatibility } from './upgrade-2026-03.mjs';\n\nconst item = createFixedDataContractCase('compatible-additive-v2');\nconst change = inspectFixedSchemaChange(item);\nconst report = reviewFixedDataContractCompatibility(item);\nconsole.log({ added: change.addedFields, status: report.status, effect: report.productionEffect });\n// { added: ['priority'], status: 'synthetic-compatibility-review-hand-off', effect: 'not-attempted' }"),
+ p('Фрагмент вызывает только public exports. Все схемы, версии, producer, consumer и boundary data уже лежат в named fixed literal; ни один объект не считывается извне. Output не утверждает, что новый формат развернут или что реальный читатель обработал данные. Он говорит значительно меньше и поэтому полезнее: один заранее названный synthetic pair прошёл правила этой карточки, а следующий шаг — hand-off независимому reviewer.'),
+ h2('Manifest делает незаметное поле видимым решением'),
+ p('Полезная дисциплина manifest очень проста: каждое новое поле candidate должно находиться в declaredAddedFields, а объявленное поле должно действительно присутствовать в candidate. Это не замена документации типа и не собственная спецификация формата. Это контроль связи между намерением и diff. В строке review можно увидеть, что priority добавили намеренно. Если в candidate есть routingHint, но manifest о нём молчит, gate завершает проверку stop-undocumented-schema-field.'),
+ p('Такой stop не доказывает, что routingHint вреден. Возможно, поле нужно отдельному процессу. Но сейчас у команды нет права подменить неизвестность словом optional. Поле может попасть в строгий parser, логику сравнения или новую схему потребителя; это уже другой вопрос. Сначала его нужно назвать, выбрать направление и указать, кто будет читать candidate. Лишь после этого обсуждается, является ли поле additive, отдельным контрактом или поводом перенести изменение в другую миграцию.'),
+ h2('Почему version не выполняет работу gate'),
+ p('Номер версии полезен как координата, но не как verdict. Он позволяет связать baseline, candidate и карточку потребителя во времени. Он не сообщает, удалено ли обязательное поле, может ли reader получить дополнительные значения или неявно ли вообще задано сравнение. Попытка заменить diff только строкой 1.1.0 создаёт ровно ту же ручную координацию, только с более аккуратным названием. Поэтому version в fixed module проверяется вместе с family, direction и consumerId, а не отдельно.'),
+ p('Это соответствует взрослому компромиссу: карточка чуть длиннее одного сообщения, зато повторяема. В ней нет требования описать каждый будущий интеграционный путь. Есть требование не называть текущий путь безопасным, пока объект сравнения не определён. Если в новом change нет named consumer, можно вернуть stop implicit comparison и поставить задачу на уточнение. Неприятный короткий ответ дешевле уверенного, но ненаблюдаемого разрешения.'),
+ h2('Последовательность перед synthetic hand-off'),
+ ol([
+ 'Выбрать baseline. Зафиксировать schema id, version и обязательные поля, от которых зависит рассматриваемый reader.',
+ 'Описать candidate. Добавить новую форму отдельной карточкой; не менять смысл baseline задним числом.',
+ 'Собрать manifest. Перечислить additions, removals и type changes; поле вне списка считать неоформленным.',
+ 'Назвать направление. Записать producer, consumer, contract family и relation backward producer to consumer.',
+ 'Проверить две границы. Сначала сохранение required surface, затем способность этого reader принять declared additions.',
+ 'Передать ограниченно. Сохранить status, reasons и next action; положительный результат остаётся synthetic compatibility-review hand-off.',
+ ]),
+ h2('Граничные данные проверяют не красивый объект, а ветку решения'),
+ p('Для такой карточки особенно ценны короткие boundary cases. В module есть удачное additive изменение, удаление state, неоформленный routingHint, строгий reader и неявное сравнение. Они не изображают production payload и не покрывают реальную систему. Их цель скромнее: доказать, что gate не принимает объект только потому, что он похож на удачный. Каждая ошибка получает собственный status и следующее действие.'),
+ p('Например, удаление state не надо прятать в общую ошибку consumer. Gate сначала видит backward incompatibility: required field baseline отсутствует в candidate. Это устраняет соблазн исправить только профиль reader и оставить сам разрыв схемы. Напротив, strict consumer останавливает уже полностью описанный additive candidate. Разные причины должны оставаться разными, иначе следующая встреча снова будет обсуждать симптомы вместо контракта.'),
+ h2('Ограничения и следующий шаг'),
+ p('Этот overlay не подключён к schema registry, не читает файлы, не вызывает сеть, не использует CI, Git, telemetry, clock или реальные данные. Он не умеет доказывать совместимость всех будущих readers и не создаёт migration plan. Источники ниже описывают vocabularies и reader-writer resolution, но не подтверждают успешность именно этого synthetic gate или какой-либо deployment. Нельзя переносить его output в production как сертификат.'),
+ p('Следующий разумный шаг — взять один будущий change и составить карточку без произвольной автоматизации: baseline, candidate, manifest, direction и одного конкретного consumer. Если до этой точки неизвестно, кто читает форму, зафиксируйте unknown как результат исследования, а не как пустой список рисков. Когда пара названа, её можно прогнать через обычный compatibility review и получить либо ограниченный hand-off, либо конкретную причину остановки.'),
+], [
+ { key: 'jsonSchema', use: 'Draft 2020-12 описывает применение properties и additionalProperties к object instance, поэтому помогает отделить известные и дополнительные поля в vocabulary статьи.', boundary: 'Документ не задаёт правила данного fixed gate, поведение любого consumer или результат deploy.' },
+ { key: 'jtd', use: 'RFC 8927 различает required properties, optionalProperties и режим additionalProperties, что подтверждает необходимость явно говорить о дополнительных полях.', boundary: 'RFC не определяет contract family, manifest или verdict synthetic review.' },
+ { key: 'avro', use: 'Pinned Avro specification описывает reader and writer schemas и schema resolution, поэтому подтверждает, что направление чтения является техническим вопросом, а не только номером версии.', boundary: 'Avro не доказывает совместимость fixed JSON-like literals и не заменяет named consumer comparison.' },
+]);
+
+const mechanism = revision({
+ slug: 'editorial-2026-03-mechanism-data-contracts',
+ title: 'Backward совместимость как направление: механизм compatibility gate для схемы данных',
+ categories: ['Архитектура', 'Данные'],
+ cover: '/assets/editorial/2026/data-contracts-2026-compatibility-gate-loop.svg',
+ excerpt: 'Разбор механизма compatibility gate: почему version не является verdict, как разделить schema diff, direction и consumer capability и где fail-closed остановить неявное сравнение.',
+ readingMinutes: 15,
+}, [
+ p('Поломка схемы часто маскируется под обновление версии: в карточке написано v2, поля похожи, а значит якобы можно двигаться дальше. Техническая ошибка в другом месте — не задано отношение между старой формой, новой формой и reader. Цена такой неопределённости высока: любое позднее несовпадение превращается в спор о трактовке слова compatible, а не в проверку конкретного условия. Compatibility gate нужен, чтобы вернуть сравнению направление и наблюдаемые причины отказа.'),
+ p('Механизм начинается с fail-closed правила: если direction, baseline, candidate или consumer не названы, сравнение не выполняется. Нельзя вычислять совместимость по пересечению имён или по удачному serialisation sample. Дальше gate разнимает три вопроса, которые обычно склеивают: сохранил ли candidate required surface baseline, оформлены ли все новые поля и может ли этот consumer принять declared additions. Только после этих ответов возможен synthetic hand-off.'),
+ h2('Backward — не направление стрелки в changelog'),
+ p('Слово backward звучит знакомо, но без субъекта оно пустое. В этом модуле оно значит: fixed producer создаёт candidate, а fixed consumer, чья точка отсчёта baseline, получает эту форму. Отношение несимметрично. Можно отдельно исследовать, способен ли новый reader разобрать старые данные; это будет другая карточка с другой семантикой. Склеить оба вопроса в один boolean удобно для отчёта, но опасно для решения: неизвестно, что именно можно сохранить при stop.'),
+ p('Поэтому comparison содержит пять значений: direction, contract family, baseline version, candidate version и consumerId. Family отсекает случайное сравнение одинаковых JSON-объектов, версии закрепляют две точки, consumerId запрещает заменить проверяемого участника во время обсуждения. Если хотя бы одно значение не совпадает с карточкой, status становится stop-implicit-comparison. Это не syntax error и не слабая форма false. Это признание, что механизм пока не знает, какое отношение он должен вычислять.'),
+ figure('/assets/editorial/2026/data-contracts-2026-compatibility-gate-loop.svg', 'Диаграмма цикла compatibility gate: fixed case проходит явное направление, diff обязательных полей, manifest additions и capability consumer. Зелёная ветка заканчивается synthetic hand-off, четыре красные ветки возвращают разные stop statuses к уточнению карточки.', 'Gate удерживает причины раздельно: сначала точность сравнения, затем схема, затем declared fields и только потом capability named consumer.'),
+ table('Слои механизма и их отдельные вердикты', ['Слой', 'Вопрос', 'Удачный fixed ответ', 'Fail-closed status'], [
+ ['отношение', 'что и в какую сторону сопоставляем?', 'backward producer → consumer', 'stop-implicit-comparison'],
+ ['required surface', 'сохранился ли baseline required field?', 'id и state на месте', 'stop-backward-incompatible-schema'],
+ ['manifest', 'названо ли каждое новое поле?', 'priority указан', 'stop-undocumented-schema-field'],
+ ['capability', 'принимает ли reader declared additions?', 'tolerant reader: да', 'stop-incompatible-consumer'],
+ ['выход', 'какое право даёт результат?', 'synthetic hand-off', 'не deploy и не migration'],
+ ]),
+ h2('Diff схемы должен сохранять тип и обязательность'),
+ p('Первый технический слой строит индексы полей baseline и candidate. Затем он ищет две опасные разницы: required field baseline исчез в candidate или остался с другим type. В fixed case state заменяют на phase. Человек может увидеть близкий смысл, но gate не интерпретирует семантику имён. Для reader, которому нужен state, поле отсутствует. Статус stop-backward-incompatible-schema даёт короткий следующий шаг: восстановить required field либо честно назвать отдельную migration, а не обновлять таблицу версий.'),
+ p('Почему проверять именно required baseline fields, а не всё подряд? Потому что цель этой карточки узкая. Необязательное поле может иметь собственный риск, но его отсутствие не должно автоматически приравниваться к нарушению обязательной поверхности. Если команде нужно защищать и optional semantic contracts, это надо добавить новым явным правилом и тестом. Механизм не становится надёжнее от безымянной строгости. Он становится надёжнее, когда каждое правило можно указать в report и воспроизвести на fixed case.'),
+ h2('Исполняемая остановка на разрушенной поверхности'),
+ code("import {\n reviewFixedDataContractCompatibility as review,\n createFixedDataContractCase as fixedCase,\n} from './upgrade-2026-03.mjs';\n\nconst item = fixedCase('backward-incompatible-v2');\nconst report = review(item);\nconsole.log({ status: report.status, removed: report.removedRequiredFields, next: report.nextAction });\n// { status: 'stop-backward-incompatible-schema', removed: ['state'], next: 'retain-required-baseline-field-or-name-a-separate-migration' }"),
+ p('Здесь нет сериализации, registry client или внешнего schema file. Literal содержит обе формы и named consumer, а exported function только сравнивает их по правилам module. Это намеренно ограничивает доказательство. Мы можем буквально выполнить ветку fail-closed и увидеть, что исчезновение state не проходит как простое rename. Мы не можем из этого вывода делать заявление о совместимости реального формата или о результате какого-либо release.'),
+ h2('Почему необязательное поле всё ещё требует consumer review'),
+ p('Следующий слой кажется парадоксальным. Candidate с необязательным priority не удаляет id и state, значит structural check проходит. Но producer может всё равно передать объект, где priority присутствует. Tolerant reader заранее объявил готовность к declared additions; strict reader объявил, что такие additions не принимает. Это не противоречие между двумя версиями schema. Это два разных требования к границе consumer, которые нельзя вывести из одного лишь слова optional.'),
+ p('Именно здесь появляется отдельный status stop-incompatible-consumer. Gate не изменяет candidate, не пытается удалить поле на лету и не предполагает адаптер. Он возвращает, что для данной named пары positive hand-off невозможен. Возможны разные инженерные ответы: изменить reader, разделить форму, задержать candidate или завести отдельную migration. Выбор остаётся за следующим решением. Качество gate в том, что он не маскирует эту развилку под зелёный version badge.'),
+ h2('Manifest связывает фактический diff с намерением'),
+ p('Поле можно добавить двумя способами: как declared element change и как побочный след реализации. Для формата это одинаковые байты или ключи. Для контракта это разные состояния знания. Manifest не пытается предсказать смысл priority; он лишь перечисляет, что команда сознательно добавила priority. При сравнении candidate с hidden routingHint обнаруживается поле, которого нет в manifest. Gate возвращает stop-undocumented-schema-field до проверки consumer capability.'),
+ p('Этот порядок важен. Если сначала спросить tolerant reader, он мог бы сказать, что дополнительные поля допустимы, и скрытое изменение получило бы ложный положительный знак. Но acceptability consumer не заменяет обязательство producer объяснить новую поверхность. Сначала field становится предметом change, затем мы спрашиваем, может ли конкретный reader его получить. Получается небольшая, но полезная последовательность ответственности: producer называет изменение; review проверяет diff; consumer задаёт границу принятия.'),
+ h2('Внутренний порядок gate'),
+ ol([
+ 'Проверить известность case. Модуль принимает только clone named fixed literal; неизвестный объект получает stop unknown fixed contract case.',
+ 'Проверить relation. Сверить direction, family, версии и consumerId с обеими schema cards и профилем reader.',
+ 'Построить field maps. Вычислить additions, отсутствующие required baseline fields и type changes без сетевых или файловых зависимостей.',
+ 'Сверить manifest. Остановить case, если candidate содержит addition вне declaredAddedFields или manifest указывает несуществующее новое поле.',
+ 'Проверить capability. Сопоставить required fields consumer, supported candidate version и policy declared additions.',
+ 'Вернуть строго ограниченный output. Хранить reasons и next action; accepted result означает только synthetic compatibility-review hand-off.',
+ ]),
+ h2('Почему gate не должен вычислять процент совместимости'),
+ p('Процент быстро сглаживает нужную информацию. В одной корзине оказываются неизвестное направление, удалённый required field, скрытый addition и strict reader. У каждого состояния другой владелец следующего шага и другой риск. Если сказать «совместимо на 75 процентов», никто не понимает, можно ли уточнить manifest, восстановить field, завести migration или просто изменить consumer policy. Число выглядит нейтрально, но фактически стирает причины.'),
+ p('Для M9-практики полезнее один небольшой status на один единичный review. Это не означает, что в реальном процессе нельзя агрегировать итоговые данные. Но агрегировать следует после того, как сохраняется исходная структура: family, direction, baseline, candidate, consumer, reason. Иначе дашборд успокаивает команду ровно в тот момент, когда ей нужна конкретная карточка работы. Synthetic module намеренно не имеет общего счётчика и не измеряет успех.'),
+ h2('Граница источников и следующий механизм'),
+ p('JSON Schema и JTD дают vocabulary для описания object fields и additional properties. Avro формулирует relation writer and reader schemas и правила resolution. Ни один из этих документов не определяет statuses этого overlay и не обещает, что конкретный parser перенесёт change. Поэтому код не объявляет себя реализацией стандарта. Он показывает минимальный механизм принятия решения: различить форму, намерение и capability reader, а неизвестность остановить раньше успешного hand-off.'),
+ p('Следующий шаг для команды — явно выбрать, какой второй direction требуется отдельно: новый consumer читает baseline или old consumer читает candidate. Не пытайтесь расширить текущую функцию без новой карточки и fixtures. Сначала назовите relation, затем добавьте один fixed boundary case, status и next action. Так compatibility gate растёт как контракт собственных решений, а не как накопление неявных if вокруг версий.'),
+], [
+ { key: 'avro', use: 'Exact Avro 1.12.0 source distinguishes writer schema from reader schema and describes schema resolution, supporting the article distinction between directions of comparison.', boundary: 'Pinned source does not define the overlay statuses, synthetic field map or a result for any external schema registry.' },
+ { key: 'jsonSchema', use: 'Draft 2020-12 defines vocabulary for object properties and additionalProperties, which is used only to explain why a field boundary must be explicit.', boundary: 'The dated document does not establish backward compatibility for this fixed comparison or a named reader policy.' },
+ { key: 'jtd', use: 'RFC 8927 describes properties, optionalProperties and additionalProperties as distinct schema concepts, supporting the separate treatment of required and added fields.', boundary: 'RFC 8927 does not supply a producer inventory, migration decision or deployment approval.' },
+]);
+
+const field = revision({
+ slug: 'editorial-2026-03-field-data-contracts',
+ title: 'Не один verdict на всех: полевой цикл producer и consumer для контракта данных',
+ categories: ['Платформы', 'Данные'],
+ cover: '/assets/editorial/2026/data-contracts-2026-producer-consumer-matrix.svg',
+ excerpt: 'Полевой цикл compatibility gate: вести матрицу named producer и consumer, отделять unknown comparison от incompatible reader и передавать только synthetic review hand-off.',
+ readingMinutes: 14,
+}, [
+ p('В полевой работе с контрактом данных самая дорогая ошибка — объявить один schema change совместимым «для всех», потому что один consumer прочитал sample. Остальные могут ожидать другой набор полей, запрещать additions или вообще относиться к другой contract family. Цена общего verdict — поздний поиск владельца и ручное исправление уже после того, как решение разошлось между командами. Нужна не длинная рассылка, а матрица, где каждая строка фиксирует одну пару producer и consumer.'),
+ p('Практический цикл строится вокруг простого правила: неизвестный consumer не получает зелёный статус по умолчанию. Сначала карточка называет family, baseline, candidate, direction и capability reader. Затем gate возвращает один из раздельных результатов: hand-off, incompatible consumer, undocumented field, backward break или implicit comparison. Действие для техлида — сохранить именно эту причину рядом с парой, не превращая stop в общий риск без адреса.'),
+ h2('Матрица начинается с единицы решения, а не со списка систем'),
+ p('Список интеграций обычно полезен для владения, но слишком широк для compatibility review. Здесь нужна минимальная единица: один fixed producer создаёт один candidate schema, а один fixed consumer принимает или не принимает конкретную границу этой формы. В карточке consumer достаточно нескольких свойств: family, required fields, supported candidate version и policy для declared additions. Все значения в overlay — учебные literals. Они не изображают реальных сервисов, пользователей, сообщений или наблюдаемость.'),
+ p('Такая узость снимает лишнюю претензию к gate. Он не строит полный граф компании и не обещает найти каждый hidden reader. Он даёт команде способ не потерять уже известную пару. Tolerant reader в matrix соглашается на described addition priority; strict reader останавливается на том же candidate; profile с другой family вообще не должен быть включён в это сравнение. Даже отсутствующая карточка лучше воспринимается как work item, а не как доказательство отсутствия риска.'),
+ figure('/assets/editorial/2026/data-contracts-2026-producer-consumer-matrix.svg', 'Матрица producer и consumer: одна fixed candidate schema с declared полем priority сравнивается с tolerant reader, strict reader и неизвестной парой. Зелёная ячейка ведёт к synthetic hand-off, красная — к incompatible consumer, серая — к отдельному уточнению relation.', 'Матрица показывает, что один schema diff не создаёт один общий verdict: результат зависит от явно названной capability reader и от того, существует ли сравнение.'),
+ table('Матрица полевого review до общего сообщения', ['Пара', 'Что известно', 'Вердикт gate', 'Куда вернуть работу'], [
+ ['producer 1.1 → tolerant reader', 'priority declared, reader принимает additions', 'synthetic compatibility-review hand-off', 'в независимый hand-off'],
+ ['producer 1.1 → strict reader', 'priority declared, reader запрещает additions', 'stop-incompatible-consumer', 'к границе reader или отдельной migration'],
+ ['producer 2.0 → tolerant reader', 'baseline state удалён', 'stop-backward-incompatible-schema', 'к candidate required surface'],
+ ['producer 1.1 → unnamed relation', 'нет direction или consumerId', 'stop-implicit-comparison', 'к карточке сравнения'],
+ ['producer 1.1 hidden field', 'routingHint отсутствует в manifest', 'stop-undocumented-schema-field', 'к change manifest'],
+ ]),
+ h2('Inventory consumer хранит условия чтения, а не репутацию команды'),
+ p('Профиль consumer не должен звучать как оценка: «старый», «сложный», «привередливый». Такие слова не помогают выполнить проверку. Вместо них нужны наблюдаемые условия. Required fields показывают минимальную поверхность, без которой reader не может принять решение. Policy additions показывает, допускает ли он именно описанные новые поля. Supported candidate version делает временную точку явной. Family не даёт сравнить read contract с другой операцией только потому, что обе стороны используют JSON.'),
+ p('В fixed cases strict reader не является ошибкой. Он говорит понятное правило: declared additions не принимаются. Gate не вправе объявить его плохим участником или изменить его policy. Он должен сохранить stop-incompatible-consumer и следующий шаг. Это полезно и для людей: вместо спора о скорости команды видно, какой контрактный выбор надо сделать. Возможно, producer удержит addition, возможно, consumer расширит границу, возможно, появится отдельная форма. Пока решение не принято, красный status честнее зелёной надежды.'),
+ h2('Исполняемый triage для strict consumer'),
+ code("import { createFixedDataContractCase, reviewFixedDataContractCompatibility, runFixedDataContractFixture } from './upgrade-2026-03.mjs';\n\nconst item = createFixedDataContractCase('incompatible-consumer-v2');\nconst report = reviewFixedDataContractCompatibility(item);\nconst fixture = runFixedDataContractFixture();\nconsole.log({ status: report.status, reason: report.reasons[0], assertions: Object.keys(fixture.assertions).length });\n// { status: 'stop-incompatible-consumer', reason: 'incompatible-consumer', assertions: 15 }"),
+ p('Это настоящий запуск public functions данного module. Он берёт named fixed case, а fixture проверяет пять независимых веток. При этом код не пишет в registry, не посылает sample, не читает environment и не получает данные о внешнем consumer. Положительный case в той же fixture заканчивается только hand-off. Такой предел защищает от подмены: успешное упражнение не становится основанием сообщить, что что-то уже опубликовано или работает за границей учебной модели.'),
+ h2('Почему gate loop должен возвращаться к карточке, а не к общей очереди'),
+ p('У хорошего stop есть адрес возврата. Backward break возвращается к candidate schema: пропал required baseline field. Undocumented field возвращается к manifest: новая поверхность не была названа. Incompatible consumer возвращается к capability named reader. Implicit comparison возвращается к relation: не задано, что и в какую сторону сравнивают. Если все четыре причины превратить в «нужно договориться», команда вернётся к исходной ручной координации, только с более формальным заголовком.'),
+ p('Поэтому loop в visual не имеет линии deploy. После зелёной ячейки он передаёт limited review hand-off, после красной — конкретное уточнение. Даже hand-off не означает, что gate распоряжается выпуском. Следующий участник может потребовать дополнительные основания, а реальная система может иметь условия вне этой модели. Роль compatibility gate ограничена: сделать вопрос о форме и reader проверяемым, удержать конкретную причину и не потерять границу полномочий.'),
+ h2('Один глобальный verdict скрывает разные владельцы решения'),
+ p('Когда у change есть пять consumer, хочется свернуть матрицу в один статус. Делать это можно только после определения цели агрегирования. Для release note достаточно перечислить пары и их состояния. Для приоритизации можно посчитать очереди stop по причинам. Но нельзя присвоить candidate строку compatible, если хотя бы один известный reader требует отдельного решения. Это не бюрократия. Это различие между «какая-то проверка прошла» и «все названные контракты покрыты утверждением».'),
+ p('Отдельно храните incomparable или implicit relation. Нулевая информация о consumer — не tolerance. Профиль другой family — не incompatible, пока не выяснено, существует ли связь. В данном module не создаётся отдельный profile другой family, потому что user story ограничена четырьмя обязательными stop. Но правило остаётся: прежде чем считать поля, нужно подтвердить ось сравнения. Это дешевле, чем строить огромную matrix, где половина ячеек имеет красиво окрашенный, но бессмысленный verdict.'),
+ h2('Полевой порядок работы с change'),
+ ol([
+ 'Завести одну строку. Взять один producer, baseline, candidate и одного consumer вместо массового статуса для схемы.',
+ 'Собрать capability. Записать contract family, required fields, policy declared additions и candidate version без догадки о будущих сценариях.',
+ 'Определить relation. Указать direction и обе точки schema; незаполненная связь должна остановить review.',
+ 'Прогнать structural слой. Проверить required baseline fields, типы и manifest additions раньше consumer policy.',
+ 'Сохранить раздельный verdict. Не заменять reason общей фразой; вернуть её в именно ту часть карточки, которая требует решения.',
+ 'Передать только область. При зелёном status отдать named pair в следующий review, не объявляя registry, CI или deploy завершёнными.',
+ ]),
+ h2('Граничные данные нужны для чужой проверки правил'),
+ p('Тестовый набор gate обычно соблазняются наполнить красивыми объектами. В этой задаче полезнее обратное: несколько коротких случаев, которые обязаны остановиться. Case с удалённым state не даёт перепутать rename и сохранение required surface. Case с routingHint не даёт tolerant consumer узаконить скрытое поле. Case со strict reader не даёт structural diff выдать за полную совместимость. Case с неявным direction не даёт sample превратить в отношение.'),
+ p('Эти данные фиксированы в памяти, поэтому reviewer может повторить результат без доступа к production. Они не заменяют реальные boundary payload и не дают сигнал о нагрузке, retention, access control или времени доставки. Но именно в их ограничении есть польза для документации: каждый выход виден, каждое поле имеет известное происхождение, а код можно буквально выполнить из article snippet. При расширении gate новый rule обязан принести свой fixed case и своё fail-closed ожидание.'),
+ h2('Ограничения и следующий шаг'),
+ p('Изолированный overlay не обслуживает реальный schema registry и не умеет обнаруживать неизвестных consumers. Он не подключается к network, filesystem, Git, CI, clock, telemetry, API или production data. Reference documents ниже объясняют schema vocabulary и reader-writer direction, но не подтверждают output этой матрицы, отсутствие инцидентов или успех какого-либо deployment. Разумно воспринимать её как форму инженерного review, а не как гарантию системы.'),
+ p('Следующий шаг — не масштабировать matrix сразу. Выберите одну известную связку, у которой сегодня есть ручное сообщение о change, и запишите capability reader в четырёх полях. Если relation ещё нельзя назвать, оставьте explicit stop и назначьте владельца уточнения. Если relation читается, добавьте fixed case в локальный набор. Так data contract перестаёт жить только в памяти producer и становится точкой, которую consumer может проверить до следующего deploy.'),
+], [
+ { key: 'jtd', use: 'RFC 8927 явно разделяет required properties, optionalProperties и additional properties, поэтому используется как первичный vocabulary для матрицы field boundaries.', boundary: 'Experimental RFC не устанавливает policy конкретного reader, ownership matrix или outcome synthetic gate.' },
+ { key: 'avro', use: 'Pinned Avro specification называет writer and reader schemas и описывает resolution, что подтверждает необходимость хранить направление relation рядом с участниками.', boundary: 'Avro source не сообщает о существовании named fixed consumers и не подтверждает release decision.' },
+ { key: 'jsonSchema', use: 'Dated Draft 2020-12 описывает object-property vocabulary, применяемую здесь только для ясного разговора о declared additions.', boundary: 'Specification не даёт единого compatibility verdict для произвольной группы consumer.' },
+]);
+
+export const revisions = deepFreeze([practice, mechanism, field]);
+
+if (process.argv.includes('--verify-fixture')) {
+ const result = runFixedDataContractFixture();
+ const failed = Object.entries(result.assertions)
+ .filter(([, value]) => value !== true)
+ .map(([key]) => key);
+ if (failed.length) {
+ process.stderr.write('FAIL fixture: ' + failed.join(', ') + '\n');
+ process.exitCode = 1;
+ } else {
+ const count = Object.keys(result.assertions).length;
+ process.stdout.write('PASS fixture: ' + count + '/' + count + ' assertions\n');
+ }
+}
+
+if (process.argv.includes('--print-revisions')) {
+ process.stdout.write(JSON.stringify(revisions) + '\n');
+}