На 31 июля 2026 года строгий аудит проходит 127 из 358 созданных материалов. Остальные 231 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
На 31 июля 2026 года строгий аудит проходит 130 из 358 созданных материалов. Остальные 228 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
Revision-модуль экспортирует только изменяемые редакционные поля. В нём нет
<code>date</code>, <code>author</code>, registry import или изменения
<code>articles.json</code>. Fixture использует только Array, Map и objects в
одном Node-процессе; не запускает БД, broker, реальные файлы, сеть, provider,
миграционный job или сервис.
## Проход 1. Структура, тон и объём — пройдено
| Revision | Ситуация и цена в первых двух абзацах | M4 и практическая граница | Объём body |
| --- | --- | --- | --- |
| Практика | Новый <code>timezone</code> смешивается с absent/null; цена — запись, которую старый reader трактует иначе | owner, поддержка старых pair и reader-before-writer rollout | **10 574** знака body |
| Механизм | Reader принял форму, но не смог объяснить значение; цена — закреплённая догадка вместо договора | type, presence, narrowing и semantic break разделены | **11 259** знаков body |
| Полевой разбор | Reader упал после записи; цена — потеря evidence при поспешном rollback | evidence, stop criterion и rollback-safe действия | **10 349** знаков body |
- У всех трёх текстов есть ситуация и стоимость ошибки в первых двух абзацах,
таблица, привязанный SVG с содержательными <code>alt</code>/<code>figcaption</code>,
рабочие JS-фрагменты, нумерованный маршрут, ограничения и отдельный раздел
источников.
- Голос М4 августа 2021 года: короткая инженерная речь «симптом → причина →
проверка → действие», явный owner и граница между reader, writer и contract.
Текст не изображает автора владельцем platform/SLA или реальной migration
program.
- Удалены универсальные формулы о «современном подходе»: каждое обобщение
знаков и наличие sections, table, figure, code, route, sources и локального
asset у каждого revision.
## Проход 2. Техника, source boundaries и fixture — пройдено
| Граница | Что утверждает пакет | Как это проверяется | Чего пакет не утверждает |
| --- | --- | --- | --- |
| JSON object | reader читает именованные key, а не их позицию | reordered v2 record даёт тот же нормализованный result | одинаковый порядок во всех parser или storage |
| Optional field | absent, <code>null</code> и строка — разные states | v1 → v2 даёт absent; v2 clear даёт explicit-null | absent всегда должен означать clear |
| Additive change | v2 добавляет only optional <code>timezone</code> | v2 writer → v1 reader и v1 writer → v2 reader зелёные в matrix | любой новый key безопасен для любого consumer |
| Breaking changes | narrowing, semantic change и required timezone отвергаются | три отдельные negative matrix assertions | generic JSON сам даёт эволюционную совместимость |
Первичные и официальные источники ограничивают историческую рамку:
- [RFC 8259](https://www.rfc-editor.org/rfc/rfc8259.html) для JSON object,
literal <code>null</code> и отсутствия переносимого договора о порядке
object members;
- [JSON Schema draft 2019-09 Validation](https://json-schema.org/draft/2019-09/draft-handrews-json-schema-validation-02), официальный опубликованный Draft от 17 сентября 2019 года (не IETF RFC),
для vocabulary structural assertions и типа <code>null</code>;
Проверки выполнены 31 июля 2026 года после всех правок:
| Проверка | Реальный результат |
| --- | --- |
| <code>node --check</code> | PASS, code 0 |
| <code>npm run audit:draft</code> | PASS: **10 574 / 11 259 / 10 349** знаков body; у всех трёх slug есть table, figure, code, route, sources и локальный SVG |
| In-memory fixture | PASS: **13/13** assertions истинны; зелёные v1/v2 направления и три отрицательные matrix-пары проверены |
| <code>xmllint --noout</code> | PASS, три SVG — корректный XML |
| SVG safety scan | PASS: не найдены active tags, <code>foreignObject</code>, внешние assets или raster data URI |
| Sharp mobile preflight | PASS: три финальных PNG шириной 375 px вручную просмотрены после правки; нет clipping, overlap или horizontal overflow |
| Scope/self-review | PASS: П42 создала только пять разрешённых файлов; registry, README, <code>articles.json</code>, audit scripts, общие правила/очередь и Git не менялись; mascot PNG не затрагивались |
<code>npm run audit:draft</code> вывела старые предупреждения пользовательской
конфигурации npm о <code>store-dir</code>, <code>cache-dir</code> и
<code>public-hoist-pattern</code>. Они не относятся к П42 и не изменялись.
Integration registry audit, production build, browser, реальное storage,
migration, CI, commit и push намеренно не запускались: они находятся за
границей автономной П42.
Выпусковой вердикт автономной партии: **готова к независимой интеграции**.
## Независимая редактура и интеграция · 31 июля 2026
Основной редактор провёл ещё один технический и исторический проход.
<textx="108"y="970"font-family="Arial, sans-serif"font-size="34"font-weight="700"fill="#fff5f7">Красная граница: тест обязан остановить изменение</text>
<circlecx="130"cy="1038"r="12"fill="#ff8fa4"/>
<textx="164"y="1048"font-family="Arial, sans-serif"font-size="27"fill="#ffe0e6">narrowing: новый reader отвергает legacy значение daily</text>
<circlecx="130"cy="1100"r="12"fill="#ff8fa4"/>
<textx="164"y="1110"font-family="Arial, sans-serif"font-size="27"fill="#ffe0e6">semantic change: weekly остаётся строкой, но меняет смысл</text>
<circlecx="130"cy="1162"r="12"fill="#ff8fa4"/>
<textx="164"y="1172"font-family="Arial, sans-serif"font-size="27"fill="#ffe0e6">presence break: старому writer нельзя внезапно потребовать timezone</text>
<titleid="title">Диагностика падения reader после записи</title>
<descid="desc">Дерево диагностики: зафиксировать безопасное evidence, различить неверный тип, absent и null, сужение старых значений и смену смысла. При несовместимой активной паре остановить producer, сохранить sample и добавить проверку в compatibility matrix.</desc>
<textx="178"y="1150"font-family="Arial, sans-serif"font-size="25"fill="#d7fff0">Сохранить sample и добавить accept/reject в compatibility matrix.</text>
<textx="178"y="1184"font-family="Arial, sans-serif"font-size="22"fill="#aee8d4">Не переписывать record наугад: сначала проверить active writer → reader pair.</text>
<descid="desc">Вертикальная последовательность: зафиксировать version one и samples, добавить reader version two с различением absent и null, затем добавить writer version two с optional timezone и проверить матрицу. Внизу показан запрет на narrowing старых значений и смену семантики.</desc>
note:'официально опубликованный Draft 2019-09 (17 сентября 2019, не IETF RFC) разделяет assertions для структуры и допускает тип <code>null</code>; конкретный валидатор и режим проверки остаются выбором приложения',
note:'официальная спецификация формата описывает resolution writer и reader schema; это пример форматно-зависимого правила, а не свойство произвольного JSON-объекта',
excerpt:'Контракт записи — это не только JSON-поля. Разбираю владельца, границу absent/null, поддержку старых reader и writer, а также маленькую compatibility matrix до безопасного rollout.',
readingMinutes:14,
},
[
paragraph('Проблема начинается не в момент большой миграции, а после небольшой записи. Producer добавил поле <code>timezone</code> в <code>profile/settings</code>, reader увидел незнакомое состояние или решил, что отсутствующее поле равно <code>null</code>. Цена такой догадки — не красивый exception, а уже записанная версия профиля, которую старый код трактует иначе. Затем команда спорит о формате, хотя причина лежит в неописанном договоре между тем, кто пишет, и тем, кто читает.'),
paragraph('Для августа 2021 года я бы не называл JSON контрактом. JSON даёт синтаксис объекта, но не владельца поля, не смысл строки, не срок поддержки старого reader и не порядок отката. В этой заметке договор строится вокруг одного нейтрального record <code>profile.settings</code>. Пример локальный: он работает только в памяти и не моделирует БД, файл, broker, репликацию или реальную миграцию.'),
heading('Сначала записываем границу договора'),
paragraph('У договора есть объект и владелец. Объект отвечает на вопрос, что именно меняется: в нашем случае настройки одного профиля, а не «пользовательские данные вообще». Владелец отвечает на другой вопрос: кто разрешает новый смысл поля, совместимость старого reader и момент, когда старый writer можно выключить. Если это не записано рядом со схемой, любое изменение выглядит локальным до первого consumer, который был собран раньше.'),
paragraph('Минимум полезных частей: стабильный идентификатор, обязательные поля, необязательные поля, состояние <code>absent</code>, значение <code>null</code>, допустимые значения и человеческий смысл. Для <code>emailDigest</code> недостаточно написать «строка». В нашем договоре это режим частоты сводки: <code>off</code>, <code>weekly</code> или <code>daily</code>. Если завтра тем же словом начинают помечать маркетинговый сегмент, тип остаётся строкой, а смысл уже сломан.'),
codeBlock(contractSketchCode),
dataTable(
'Минимальная карточка договора profile.settings',
['Часть','Кто задаёт правило','Что проверяем до записи','Что не обещает правило'],
[
['<code>id</code>','owner record','непустая строка и связь с одним профилем','что профиль существует в выбранном хранилище'],
['<code>settings.emailDigest</code>','owner значения и их смысла','значение входит в записанный набор','что строка сама раскрывает бизнес-смысл'],
['<code>timezone</code>','owner presence semantics','absent, <code>null</code> или непустая строка различены','что absent можно бездумно заменить на <code>null</code>'],
['версия reader/writer','владелец rollout','compatibility matrix покрывает поддерживаемые пары','что любой старый consumer узнает новые поля'],
],
),
paragraph('Таблица намеренно не называет конкретный storage. Контракт живёт выше него: одна реализация может держать запись в документе, другая в строке или в сообщении. Смена места не отменяет правила чтения. И наоборот: выбранный сериализатор не делает смысл поля проверяемым. Поэтому owner должен хранить не только схему, но и список поддерживаемых направлений: старый writer → новый reader, новый writer → старый reader и новая пара.'),
heading('Absent и null — два разных входа'),
paragraph('Необязательное поле имеет минимум три состояния: ключ не пришёл, ключ пришёл с <code>null</code>, ключ пришёл со значением. Для старого writer отсутствие <code>timezone</code> означает только то, что он её не передал. Это не разрешение подставить «часовой пояс очищен». <code>null</code> в данной модели, наоборот, является явной командой очистки. Если приложение выбирает другую семантику, её нужно записать и проверить тем же способом.'),
codeBlock(presenceCode),
paragraph('В JavaScript важно проверять наличие собственного ключа, а не правдивость значения. Условие <code>if (record.timezone)</code> смешает пустую строку, <code>null</code> и отсутствие поля; в нашем договоре все три случая требуют разных решений. Пустая строка не допускается вовсе, потому что она не обозначена как отдельное состояние. Такой запрет полезнее умного fallback: reader либо видит известный случай, либо останавливает интерпретацию и оставляет evidence для исправления.'),
'Схема совместимости profile.settings: writer v1 и writer v2, reader v1 и reader v2; v2 добавляет необязательное timezone, старый reader игнорирует его, новый reader различает absent, null и строковое значение',
'Additive-поле безопасно только для явно проверенной пары reader и writer. Стрелки на схеме не являются гарантией выбранного хранилища.',
),
heading('Поддержка старых чтений и записей — это матрица, а не надежда'),
paragraph('Перед rollout стоит назвать пары, которые действительно будут жить одновременно. В fixture writer v1 пишет только core-поля. Writer v2 добавляет <code>timezone</code> как optional. Reader v1 читает именованные core-поля и игнорирует неизвестный optional ключ. Reader v2 умеет прочитать старую запись и вернуть состояние <code>absent</code>, не изображая его очищенным значением. Именно эти четыре направления становятся тестом, а не устным обещанием.'),
codeBlock(rolloutCode),
paragraph('Здесь есть важная граница: «игнорировать неизвестное» допустимо только для поля, которое не меняет старое обязательное поведение. Нельзя добавить новое поле, а затем сделать старый <code>emailDigest</code> зависимым от него без обновления reader. Тогда поле выглядит additive по форме, но становится semantic break. Так же нельзя сузить множество старых значений: если v1 писал <code>daily</code>, новый reader, который принимает только <code>off</code> и <code>weekly</code>, обязан быть отклонён migration test до релиза.'),
heading('Безопасный rollout идёт от reader к writer'),
paragraph('Порядок короткий. Сначала фиксируем старый договор и примеры старых записей. Затем добавляем reader, который понимает старый record и новое optional поле. Только после этого writer начинает посылать новое поле. Пока старый reader остаётся в поддерживаемой матрице, writer не должен превращать optional поле в обязательное и не должен менять смысл существующих значений. Удаление старого пути — отдельное изменение: для него нужны данные, что соответствующей пары больше нет, а не просто новая дата в схеме.'),
dataTable(
'Порядок изменения без предположения о платформе',
['Шаг','Изменение','Проверка','Стоп-сигнал'],
[
['1','зафиксировать v1 и samples','v1 reader читает каждый sample','непонятен смысл или owner старого поля'],
['2','добавить v2 reader','v1 record даёт <code>timezone: absent</code>','reader подставляет <code>null</code> без правила'],
['3','добавить v2 writer','v1 reader читает core, v2 reader читает значение и clear','новое поле стало обязательным для старого пути'],
['4','предложить удаление legacy','matrix не содержит поддерживаемую старую пару','есть record или consumer вне доказанной матрицы'],
],
),
paragraph('Rollback тоже должен знать границу. Если writer только добавил optional поле, можно остановить его выпуск и оставить reader совместимым с уже появившимися record. Если writer переиспользовал существующее значение с новым смыслом, простая отмена кода не возвращает старое значением прежний смысл. В таком случае сначала останавливают producer, сохраняют примеры и запускают отдельный контролируемый перевод данных. В нашем локальном примере такого перевода нет; он специально не выдаётся за готовую migration procedure.'),
heading('Fixture превращает правило в проверяемую границу'),
paragraph('Функция <code>runStorageContractFixture()</code> создаёт v1, additive v2 и v2 с явным <code>null</code>. Она читает их двумя consumer, переставляет ключи объекта, проверяет matrix и намеренно предлагает три плохих reader: с narrowed набором <code>emailDigest</code>, со сменой смысла и с обязательным <code>timezone</code>. У каждой гарантии есть assertion. Это не проверка сериализатора и не тест выбранной БД; это маленькое место, где изменение договора получает наблюдаемый ответ.'),
codeBlock(fixtureCode),
paragraph('Отдельный плюс fixture — она защищает от красивых, но пустых слов. Нельзя сказать, что generic JSON «эволюционно совместим»: код принимает или отклоняет только правила, которые мы сами описали. Нельзя сказать, что объект упорядочен как contract: reader обращается к именам полей, а перестановка ключей даёт тот же результат. Нельзя сказать, что <code>null</code> равен отсутствию: обе ветви возвращают разные состояния. Когда правило меняется, рядом меняется assertion и matrix.'),
heading('Маршрут: симптом → причина → проверка → действие'),
orderedList([
'Зафиксировать симптом: какой reader, какой record id, какая пара writer/reader и на каком поле разошлась интерпретация. Не начинать с массового переписывания данных.',
'Назвать owner и ожидаемый смысл поля. Для <code>timezone</code> отдельно ответить, что означают absent, <code>null</code> и строка.',
'Проверить текущую compatibility matrix на старых и новых samples. Особо проверить направление старый writer → новый reader.',
'Если изменение additive, сначала выпустить reader, затем writer. Если поле становится required или меняет смысл, остановить rollout и оформить отдельный договор перехода.',
'Добавить failing sample для narrowing, type break и semantic break. Без отрицательного примера тест проверяет только удачный путь.',
'Удалять старую поддержку только после явной границы поддержки. До этого сохранять reader, который понимает уже записанные record.',
]),
heading('Пределы модели и источники'),
paragraph('RFC 8259 описывает JSON-объект как набор пар имя/значение и предупреждает о различиях реализации вокруг порядка членов. Поэтому порядок ключей в этой статье не является договором. JSON Schema draft 2019-09 полезен как язык structural assertions, но выбор валидатора и семантика приложения остаются с владельцем record. Apache Avro показывает другой, форматно-зависимый случай: у него есть writer и reader schema resolution. Нельзя переносить это правило на любой JSON лишь потому, что оба примера выглядят как данные.'),
paragraph('В пакете не запускаются storage engine, реальные файлы, сети, schema registry, миграционный job, browser, CI или deployment. Нет SLA, production-метрик и заявления, что конкретный rollout уже происходил. Следующий практический шаг — взять один настоящий record без чувствительных значений, записать его owner и поддерживаемые направления чтения, затем перенести эти samples в локальный compatibility test. После этого можно обсуждать конкретное хранилище, а не наоборот.'),
excerpt:'Разделяю форму записи, presence, тип и смысл поля. На одной fixture показываю producer/consumer v1/v2, matrix совместимости и причины, по которым narrowing или semantic change нужно отклонить.',
readingMinutes:15,
},
[
paragraph('Сбой reader после обычной записи часто выглядит как ошибка хранения: объект прочитан, но поле пришло «не таким». Цена неверного диагноза выше одного exception. Разработчик подставляет default, вторая версия writer закрепляет эту догадку, а старые record начинают означать другое. Через несколько изменений уже невозможно ответить, была ли <code>timezone</code> очищена, не передана старым writer или испорчена при преобразовании.'),
paragraph('Механизм совместимости начинается с простого разделения. Формат отвечает, как передать значения. Schema описывает разрешённую форму. Контракт добавляет presence и смысл. Consumer отвечает за интерпретацию конкретной версии. В этой статье я не запускаю parser, storage или migration framework: есть только Array, Map и объекты в одной Node fixture. Она показывает, какие assertions нужны до того, как формат или хранилище выбраны как решение.'),
heading('Четыре слоя, которые нельзя смешивать'),
paragraph('Первый слой — синтаксис JSON: есть object, member и literal <code>null</code>. Второй — structural validation: обязательность ключа, допустимый тип, набор значений. Третий — compatibility rule между writer и reader. Четвёртый — semantic rule: что означает строка <code>weekly</code> и можно ли переиспользовать её как другой признак. Если вопрос попал не на свой слой, решение оказывается слишком сильным или слишком слабым: например, schema разрешила строку, а consumer уже не может сказать, что эта строка обозначает.'),
dataTable(
'Где живёт каждое правило profile.settings',
['Слой','Пример правила','Какой дефект ловит','Чего не ловит сам'],
[
['формат','объект содержит пары имя/значение','неразбираемый текст','смысл и поддерживаемые версии'],
['shape','<code>id</code> — непустая строка; <code>timezone</code> — absent, <code>null</code> или строка','type и presence break','переосмысление старого значения'],
['compatibility','v2 writer добавляет только unknown optional поле','разрыв пары writer/reader','фактический rollout вне теста'],
['semantic','<code>weekly</code> — cadence сводки','та же строка с новым значением','право owner принять бизнес-решение'],
],
),
paragraph('Такое разделение делает ошибку локализуемой. Если <code>timezone: 3</code>, это type break: reader обязан отвергнуть record. Если ключа нет, это presence case: reader v2 возвращает <code>absent</code>, но не «очищено». Если новый reader перестал принимать старое <code>daily</code>, это narrowing: у writer v1 было законное значение, а новый reader сузил договор. Если <code>weekly</code> сохранило тип, но стало обозначать маркетинговый флаг, это semantic break. Ни один JSON parser не может угадать последнее правило.'),
heading('Absent, null и значение должны пройти разными ветками'),
paragraph('Отсутствующий key и key со значением <code>null</code> различаются уже в JSON-представлении. Но их бизнес-граница выбирается приложением. В учебном договоре absent означает «writer этой версии не сообщил timezone», а <code>null</code> — «writer явно очистил timezone». Поэтому reader v2 не имеет права свести оба случая к одной переменной без состояния. Иначе переход от v1 к v2 будет выглядеть успешным, но потеряет информацию, которую позже нельзя восстановить.'),
codeBlock(presenceCode),
paragraph('Обратите внимание на проверку собственного свойства. У объекта могут быть прототип, вычисляемый default или неудачный merge; договор читает конкретный record, а не всё, что JavaScript способен вернуть по цепочке. Для этого примера пустая строка тоже отвергается: она не является ни старым absent, ни явным clear, ни полезным часовым поясом. В реальном проекте допустимые строки и их нормализацию задаёт owner отдельно; fixture не притворяется справочником временных зон.'),
'Последовательность эволюции profile.settings: зафиксированный v1, reader v2 с поддержкой absent и null, additive writer v2, compatibility matrix и красная граница для narrowing либо переиспользования семантики',
'У reader появляется способность понять старые и новые record раньше, чем writer начинает выпускать новый optional key.',
),
heading('Порядок ключей не является договором record'),
paragraph('RFC 8259 не даёт переносимого права использовать порядок членов object как смысл. Реальная библиотека может сохранить insertion order, другая — показать свои структуры иначе; reader, который зависит от позиции, перестаёт быть договором по именам. В fixture функция <code>reorderV2Record()</code> меняет порядок тех же key. Reader v2 получает тот же id, <code>emailDigest</code> и timezone value, потому что читает имя key, а не его место. Это assertion о нашем reader, не обещание одинакового поведения всех библиотек.'),
codeBlock([
'const reordered = {',
' settings: record.settings,',
' timezone: record.timezone,',
' displayName: record.displayName,',
' id: record.id,',
'};',
'',
'const before = readProfileByConsumerV2(record);',
'const after = readProfileByConsumerV2(reordered);',
'// before.id === after.id; reader обращается к именам полей',
].join('\n')),
paragraph('Это правило имеет исключение только там, где формат сам фиксирует порядок: например, позиционный массив или отдельный бинарный protocol. Тогда порядок должен быть назван в contract и иметь собственный тест. Для обычного object такой перенос смысла создаёт невидимый coupling: producer переставил поля ради читаемости, а consumer вдруг прочитал другой record. Вместо позиции используйте name, explicit discriminator или отдельный массив, если порядок действительно является данными.'),
heading('Compatibility matrix проверяет направление, а не слово «v2»'),
paragraph('Версия не гарантирует совместимость. Нужна направленная проверка: может ли конкретный reader прочитать record конкретного writer. В нашей matrix четыре ожидаемо зелёные пары: v1 → v1, v2 additive → v1, v1 → v2, v2 → v2. Пара v2 writer → v1 reader разрешена только потому, что <code>timezone</code> описано как optional, а v1 reader явно игнорирует unknown optional fields. Если бы v1 reader запрещал все незнакомые ключи, та же запись стала бы несовместимой.'),
codeBlock(compatibilityCode),
dataTable(
'Migration matrix и ожидаемый verdict fixture',
['Writer','Reader','Ожидание','Почему'],
[
['v1 без <code>timezone</code>','v1','accept','одинаковый core-договор'],
['v2 с optional <code>timezone</code>','v1','accept','reader игнорирует только неизвестный optional key'],
['v1 без <code>timezone</code>','v2','accept','v2 выдаёт состояние <code>absent</code>'],
['v1 с <code>daily</code>','proposed narrowed v2','reject','reader удалил прежнее допустимое значение'],
['v2 с прежней формой','proposed semantic v2','reject','строка сохраняется, но meaning изменился'],
['v1 без <code>timezone</code>','proposed required-timezone reader','reject','старый writer вправе не присылать key'],
],
),
paragraph('Матрица не должна быть составлена из одних «зелёных» examples. Плохие пары важнее: они доказывают, что тест способен остановить опасную правку. В нашем коде proposal с narrowed <code>emailDigest</code> отвергается, потому что legacy sample содержит <code>daily</code>. Proposal с другой semantic меткой также отвергается, хотя набор JavaScript-типов прежний. Это делает обсуждение точным: нужно не «аккуратно менять схему», а решить, поддерживается ли старое значение и старый смысл.'),
heading('Schema validation не заменяет migration test'),
paragraph('JSON Schema draft 2019-09 позволяет выражать structural assertions, включая типы и ограничения. Это полезно для входа, но schema не знает сама по себе, какие версии writer ещё существуют, кто может удалить old path и что означает <code>weekly</code>. Даже форматно-зависимый механизм вроде Avro schema resolution не переносится автоматически на JSON objects: его правила относятся к конкретной паре writer/reader schema и выбранному format. Поэтому migration test содержит samples и направленную matrix рядом с документом, а не прячется в названии версии.'),
paragraph('Для нашей модели есть ещё одна граница. Assertion <code>genericJsonDoesNotBypassTheContract</code> проверяет строку <code>emailDigest: "hourly"</code>: JSON-форма корректна, но значение отсутствует в договоре и reader fixture его отвергает. Он не доказывает свойства любой JSON-библиотеки, persistence layer или schema registry. Если выбранный инструмент валидирует вход иначе, его поведение нужно добавить отдельным integration test. Маленькая fixture полезна тем, что сначала фиксирует собственное правило и не выдаёт локальную проверку за системную гарантию.'),
heading('Рабочая fixture и её assertions'),
paragraph('Один запуск создаёт producer v1, producer v2, v2 с clear и legacy sample с <code>daily</code>. Затем он строит Map matrix, читает record двумя consumer и намеренно подаёт missing id, числовой timezone, narrowed reader, semantic reader и required timezone reader. Assertion не просто перечисляет слова: он проверяет конкретную пару, состояние или факт rejection. Если новый change проходит только потому, что fixture ничего о нём не знает, это не совместимость, а отсутствующая проверка.'),
codeBlock(fixtureCode),
paragraph('Управление изменением остаётся простым: добавили правило — добавили sample и assertion; изменили meaning — изменили semantic marker и решили, нужен ли отдельный field; сделали поле required — доказали, что старые writer больше не входят в матрицу. Такой ход не даёт универсального migration framework, зато сохраняет техническую границу рядом с кодом. В 2021 году это важнее громкого названия: следующий reader сможет объяснить, почему он принимает record, а не просто «как-то переживает v2». '),
heading('Маршрут: симптом → причина → проверка → действие'),
orderedList([
'Взять один record, на котором reader упал, и выписать key names, schema label, путь ошибки и ожидаемую пару writer/reader. Значения профиля в диагностический вывод не добавлять.',
'Классифицировать разрыв: type, presence, narrowing или semantic. Не называть отсутствие key ошибкой типа.',
'Проверить absent и <code>null</code> через наличие собственного key. Если оба состояния сейчас сливаются, остановить изменение до выбора семантики.',
'Переставить key в локальном object. Если result меняется, consumer читает порядок, который JSON object не обязан гарантировать.',
'Добавить пару в migration matrix и требовать ожидаемый accept либо reject. Negative case должен оставаться в fixture после исправления.',
'Только затем выбрать validator, storage или форматный механизм. Их integration test дополняет, но не заменяет contract test.',
]),
heading('Ограничения и проверяемые источники'),
paragraph('Эта модель не утверждает, что JSON Schema, Avro или любое хранилище автоматически сохраняют совместимость. RFC 8259 задаёт синтаксическую рамку и предостерегает от зависимости от порядка object members. JSON Schema draft 2019-09 задаёт vocabulary для structural validation, а не готовую политику rollout. Avro 1.10.2 показывает, что schema resolution требует конкретных writer и reader schema. В статье эти источники используются для границы терминов, а не для заявления о запущенной production-инфраструктуре.'),
paragraph('Fixture не запускает БД, broker, файлы, SDK, реальную миграцию, сеть, browser, CI или deployment. Она не проверяет rights, backfill, retention и скорость обработки. Следующий шаг для проекта — перенести ровно эту matrix на реальные samples выбранного формата, добавить integration test его validator и отдельно описать owner решения о старом reader. До этого «v2» остаётся названием, а не доказательством совместимости.'),
],
sources,
);
constfieldArticle=createRevision(
{
slug:'editorial-2021-08-field-storage-contracts',
title:'Разбор: reader упал после записи — как диагностировать контракт хранилища',
excerpt:'Полевой маршрут для случая, когда reader падает после новой записи: какие evidence собрать, когда остановить producer, какой rollback безопасен и как вернуть изменение в compatibility test.',
readingMinutes:15,
},
[
paragraph('Симптом выглядит коротко: reader упал сразу после записи <code>profile/settings</code>. В логе видно «unexpected value», ау producer уже есть новая версия. Цена поспешного исправления — не только повторный сбой. Если сейчас переписать record, подставить default или включить прежний код без проверки, можно стереть различие между отсутствующим key, явным <code>null</code> и новым смыслом старой строки. Тогда откат станет похож на исправление, но потеряет evidence.'),
paragraph('Ниже — полевой маршрут для учебного record, а не отчёт о production-инциденте. Он помогает разделить четыре причины: type break, presence break, narrowing и semantic change. Моя fixture работает в одном Node-процессе с Array, Map и objects. Она не читает реальное хранилище и не умеет останавливать service; слова «остановить producer» здесь означают безопасное действие, которое владелец конкретной системы должен выполнить своими средствами после проверки границы.'),
heading('Сначала сохраняем evidence, а не меняем запись'),
paragraph('Для первого диагноза нужны record id, label версии writer, имя reader, путь ошибки, перечень key и состояние спорного поля. Сами значения профиля не обязательны и часто не должны попадать в общий лог. Если ошибка на <code>timezone</code>, достаточно различить absent, <code>null</code>, строку и неверный type. Если ошибка на <code>emailDigest</code>, нужен старый список допустимых значений и смысл, который reader ожидал. Это даёт проверяемую гипотезу до rollback.'),
codeBlock(diagnosisCode),
paragraph('Перечень key лучше сортировать только для стабильного evidence, а не использовать как порядок contract. RFC 8259 не обещает переносимую семантику порядка object members. Мы фиксируем, что <code>timezone</code> был или не был передан, но не делаем вывод из того, шёл ли он до <code>settings</code>. Для защищённых или персональных record hash, redaction и политика доступа добавляются в конкретной системе; fixture таких механизмов не изображает.'),
dataTable(
'Первая классификация падения reader',
['Симптом','Evidence','Вероятная граница','Безопасное первое действие'],
[
['<code>timezone</code> имеет число','key есть, type <code>number</code>','type break','остановить новый writer для этого значения; не подставлять строку наугад'],
['старый record не содержит <code>timezone</code>','key отсутствует, v1 sample','presence break','вернуть reader ветку <code>absent</code>, не писать <code>null</code> поверх record'],
['legacy <code>daily</code> отвергнут','v1 writer и старое допустимое значение','narrowing','отменить новый reader или расширить его договор; не менять legacy record массово'],
['<code>weekly</code> прочитан, но эффект другой','shape совпадает, meaning расходится','semantic break','остановить producer, который переиспользует значение; оформить отдельный field или migration plan'],
],
),
heading('Различаем invalid value и неизвестное состояние'),
paragraph('Типовой соблазн — сделать всё optional: если reader не понял поле, он молча берёт default. Это допустимо лишь когда default уже является частью договора и не скрывает факт. В нашем примере <code>timezone: 3</code> — invalid value, поэтому reader v2 выбрасывает ошибку. Отсутствующая <code>timezone</code> — допустимый legacy state, поэтому reader возвращает <code>{ state: "absent" }</code>. <code>null</code> — отдельная явная команда clear. Три ветки нужны, чтобы stop/rollback был основан на причине, а не на удобстве кода.'),
paragraph('Если actual reader не показывает эту разницу, сначала правят reader или договор, а не record. Перезапись absent в <code>null</code> создаёт видимость, что пользователь явно очистил значение. Превращение ошибочного числа в произвольную строку создаёт ещё один semantic guess. Обе правки усложняют расследование: следующие reader уже увидят синтетическое значение и не смогут отличить его от того, что producer действительно отправил.'),
'Диагностическое дерево падения reader после записи profile.settings: собрать безопасное evidence, отличить type, absence/null, narrowing и semantic break; при рискованном изменении остановить producer, сохранить sample и вернуть изменение в compatibility matrix',
'Путь заканчивается действием только после классификации. Остановка producer не равна удалению record и не отменяет необходимость сохранить evidence.',
),
heading('Когда producer нужно остановить'),
paragraph('Останавливать producer разумно, когда он продолжает создавать record, которые reader не может безопасно интерпретировать, или когда он меняет значение существующего key с новым смыслом. В type break это ограничивает появление новых неправильных record. В semantic break это останавливает смешение старого и нового meaning под одним словом. При ordinary additive поле, которое старый reader доказанно игнорирует, остановка может не понадобиться; это показывает matrix, а не интуиция.'),
paragraph('Не нужно обещать универсальный ручной рубильник. В одних системах owner может отключить writer через release, в других — через конфигурацию, очередь или права. Статья не выбирает механизм. Её правило уже уже: если в compatibility test нет зелёной пары для активного reader, producer не должен увеличивать число спорных record. Сначала останавливают создание нового несовместимого значения, затем решают, можно ли безопасно восстановить reader или нужен отдельный перевод.'),
dataTable(
'Выбор rollback-safe действия',
['Условие','Что можно откатить','Что сохраняем','Чего не делаем'],
[
['новый optional key, v1 reader его игнорирует','writer можно остановить; reader оставляют tolerant','v2 sample и matrix','не удаляем key из уже записанных record без причины'],
['reader ошибочно сузил <code>emailDigest</code>','откатываем reader contract или возвращаем legacy value','sample с <code>daily</code> и verdict теста','не переписываем <code>daily</code> в другое значение массово'],
['writer сменил semantic existing value','сначала останавливаем producer','old/new meaning, record ids, owner decision','не называем простой code rollback восстановлением semantics'],
['value неверного type','блокируем путь writer и чинить validator','ошибочный sample и error path','не заменяем value fallback-строкой без правила'],
],
),
heading('Rollback кода не всегда откатывает значение'),
paragraph('Это самая опасная часть разборов. Если v2 writer добавил independent optional key, старый reader может продолжить читать core, а v2 reader — понимать уже появившийся key. Code rollback в такой ситуации обычно не должен стирать новые record. Но если writer использовал <code>weekly</code> в новом смысле, у уже записанного value нет метки, которая вернёт старую трактовку. Вернуть бинарник назад недостаточно: старый reader прочитает ту же строку и решит, что она означает по-старому. Здесь требуется отдельный, владеемый переход, а не скрытый cleanup.'),
paragraph('Эта разница объясняет, почему evidence собирают раньше action. Сначала подтверждаем writer version, expected contract и samples. Затем выбираем rollback-safe маршрут: вернуть reader capability, остановить producer или подготовить новый field с явной семантикой. Важно не смешивать отмену кода с отменой данных. Реальное хранилище может иметь транзакции, snapshots, реплики или свои retention policy, но их нельзя приписывать нейтральной fixture.'),
heading('Возвращаем дефект в compatibility test'),
paragraph('После локализации случая он должен стать sample. Для type break добавляем record с <code>timezone: 3</code> и ожидаем rejection. Для presence break сохраняем v1 record без key и ожидаем <code>absent</code>. Для narrowing сохраняем legacy <code>daily</code> и ожидаем, что proposed reader будет отклонён matrix. Для semantic break сохраняем semantic marker contract и ожидаем rejection, пока owner не введёт отдельное поле или не опишет контролируемый переход. Иначе следующий релиз снова увидит только «странный старый record». '),
codeBlock(fixtureCode),
paragraph('В fixture уже есть тринадцать assertions. Они проверяют не реальную доставку, а условия учебного договора: v1/v2 reads, additive writer, absent versus explicit null, type rejection, order independence, narrowing, semantic and presence break. Если добавляется новая гарантия, её нельзя оставить в prose: нужен отдельный assertion. Это простая дисциплина для 2021 года — не утверждать, что reader «стал устойчивым», пока не видно, на каких input он обязан остановиться.'),
heading('Маршрут: симптом → причина → проверка → действие'),
orderedList([
'Зафиксировать record id, writer label, reader label, field names, error path и state спорного key. Не выводить в общий лог весь profile.',
'Проверить, это type break, absence/null, narrowing или semantic break. Одинаковый JavaScript type не отменяет semantic разрыв.',
'Сверить активную пару с compatibility matrix. Если пары нет или она красная, остановить producer, который продолжает создавать спорный value.',
'Для additive changes вернуть reader способность понимать old/new record. Для type или narrowing исправить contract и validator, а не подставлять default.',
'Если старое значение получило новый смысл, считать code rollback недостаточным: сохранить evidence, назначить owner и проектировать отдельный transition.',
'Добавить sample и ожидаемый verdict в fixture. После этого повторить только локальную matrix, затем выполнить integration checks выбранного storage отдельно.',
]),
heading('Границы разбора и источники'),
paragraph('RFC 8259 нужен здесь как граница JSON syntax и порядка object members. JSON Schema draft 2019-09 полезен для разговоров о structural validation, но не принимает за команду business decision о meaning поля. Apache Avro 1.10.2 показывает, что reader/writer resolution бывает частью конкретного формата; это не делает любое JSON-хранилище совместимым без matrix. Эти источники существовали к августу 2021 года и не используются для неподтверждённых claims о конкретном сервисе.'),
paragraph('Пакет не выполняет rollback, не останавливает настоящий producer и не открывает storage. Нет реальных record, production-логов, SLA, метрик, схемы доступа, browser, CI или deployment. Следующий шаг — повторить этот маршрут на одном безопасно обезличенном sample выбранной системы, указать фактический owner и добавить форматно-зависимый integration test. До такой проверки безопаснее остановить изменение, чем превратить неясный contract в новые необратимые записи.'),
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.