diff --git a/editorial/production/README.md b/editorial/production/README.md
index 14ca585..2104f5f 100644
--- a/editorial/production/README.md
+++ b/editorial/production/README.md
@@ -1,6 +1,6 @@
# Производство редакционных партий
-На 31 июля 2026 года строгий аудит проходит 262 из 358 созданных материалов. Остальные 96 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
+На 31 июля 2026 года строгий аудит проходит 265 из 358 созданных материалов. Остальные 93 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
## Одна партия
diff --git a/editorial/reviews/2025-05-draft.md b/editorial/reviews/2025-05-draft.md
new file mode 100644
index 0000000..898fa6e
--- /dev/null
+++ b/editorial/reviews/2025-05-draft.md
@@ -0,0 +1,163 @@
+# P87 — май 2025: автоматизация инженерной рутины без массовой ошибки
+
+## Область draft-пакета
+
+Пакет заменяет только три overlay-статьи:
+
+- editorial-2025-05-practice-engineering-automation — «Автоматизация инженерной рутины без массовой ошибки».
+- editorial-2025-05-mechanism-engineering-automation — «Почему preview, approval и audit trail не доказывают безопасный change».
+- editorial-2025-05-field-engineering-automation — «Полевой маршрут: автоматизация без массового неверного change».
+
+Единственный исполняемый артефакт — web/scripts/upgrade-2025-05.mjs.
+Все operation cards, target ids, selector ids, digests, authority levels,
+approval cards, timestamps, execution receipts, audit records, verification
+reports, stop codes и rollback plans в нём — fixed synthetic in-memory
+значения. Скрипт не читает и не пишет files, Git, сеть, CI, clock, telemetry,
+production, cloud или real API и не выполняет реальный change.
+
+simulateSyntheticExecution записывает только model receipt с
+productionEffect: not-attempted. Его immutable audit record
+заморожен через Object.freeze внутри модуля; это не durable,
+append-only или tamper-proof storage. stopAndPlanSyntheticRollback
+создаёт только новый plan с planned-only-not-executed; он не
+наследует approval и не выполняет rollback.
+
+Registry, README, очередь, articles.json, app-страницы, full
+build, Git staging, commit и push намеренно не менялись.
+
+## Исследование и историческая граница
+
+Проверка URL выполнена 31.07.2026 обычными HTTPS-запросами
+curl -L --fail / curl -I -L --fail, без TLS bypass
+и без авторизации. HTTP 200 получены для всех трёх закреплённых источников.
+Текущая доступность не заменяет историческую границу: все ссылки ведут на
+immutable commit, существовавший к маю 2025.
+
+| Источник | Историческое закрепление | Узко подтверждаемое утверждение | Граница утверждения |
+| --- | --- | --- | --- |
+| [Terraform plan](https://raw.githubusercontent.com/hashicorp/terraform/898e397ebaefb4d0c363b7332b3ba184b8852162/website/docs/cli/commands/plan.mdx) | commit 898e397ebaefb4d0c363b7332b3ba184b8852162; tag v1.10.5; commit date 22.01.2025 | terraform plan сам не выполняет proposed changes; speculative plan не имеет намерения apply, а change target system между early plan и final apply способен изменить final effect. | Не задаёт универсальный protocol approval, audit storage или rollback и не подтверждает correct change в другой системе. |
+| [GitHub Docs: environments](https://raw.githubusercontent.com/github/docs/6a92295d73cd86b37a810a87f45e7deb21d10a0f/content/actions/managing-workflow-runs-and-deployments/managing-deployments/managing-environments-for-deployment.md) | immutable commit 6a92295d73cd86b37a810a87f45e7deb21d10a0f, 31.01.2025 | Job, который ссылается на environment, ждёт protection rules и не получает environment secrets до их прохождения; approval и bypass имеют отдельные правила. | Описывает GitHub Actions environment в этой версии, не семантику diff, независимость reviewer, полноту preview или безопасность другого runner. |
+| [Kubernetes kubectl apply](https://raw.githubusercontent.com/kubernetes/website/c889d9b2510bba78dbb527a79b8e2099b16f3d67/content/en/docs/reference/kubectl/generated/kubectl_apply/_index.md) | immutable commit c889d9b2510bba78dbb527a79b8e2099b16f3d67, 14.03.2024 | --dry-run=client печатает object без отправки; --dry-run=server отправляет server-side request без persistence ресурса. | Не обещает, что поздний persistent apply увидит такое же state или policy, и не описывает другой инструмент. |
+
+Вывод автора — «preview, approval и audit trail отвечают на разные вопросы» —
+не приписан как гарантия ни Terraform, ни GitHub Actions, ни Kubernetes. Он
+следует из разнесённых contracts учебной модели.
+
+## Проход 1 — факты, историческая граница и техника
+
+**Проверена модель.**
+
+- previewSyntheticAutomation принимает только exact fixed
+ operation card: operation, selector, dense targets, target digest,
+ proposed change и requested authority.
+- requestSyntheticApproval отдельно проверяет exact preview,
+ authority id, allowed selector и maximum targets.
+- approveSyntheticOperation связывает operation id, preview
+ digest, target digest и authority id. Approval с другим target digest
+ останавливается до simulation.
+- simulateSyntheticExecution создаёт только in-memory receipt и
+ audit record без real external effect.
+- verifySyntheticExecution проверяет только exact synthetic
+ receipt и возвращает requires-external-verification-not-performed;
+ он не читает target и не объявляет production state проверенным.
+- stopAndPlanSyntheticRollback требует trusted receipt/report и
+ возвращает новый rollback plan, а не command. Для реального rollback нужны
+ новый scope, authority, preview и approval.
+
+**Проверены отрицательные ветки.**
+
+| Случай | Где закрывается путь | Ожидаемый результат |
+| --- | --- | --- |
+| Unknown, extra или missing operation field | preview contract | fail-closed, productionEffect: not-attempted |
+| Forged target digest | exact fixed card comparison | untrusted-or-unknown-operation-card |
+| Three targets при authority limit два | authority gate | stop-scope-exceeds-authority-limit до approval |
+| Forged authority card | authority gate | forged-or-wrong-authority-card |
+| Approval A+C для preview A+B | approval binding | stop-approval-does-not-bind-preview-target-and-authority |
+| Sparse/cyclic card или receipt | canonical comparison | reject без exception и без execution |
+| Forged audit record | verification entry | untrusted-or-forged-synthetic-audit-receipt |
+| Forged verification report | stop/rollback boundary | нет rollback plan для недоверенного input |
+
+Fixture проверяет exact contracts и negative branches; она не проверяет
+identity, durable storage, external state, real permissions, live policy,
+network races или криптографические свойства digest.
+
+**Вердикт прохода 1:** пройдено. Источники ограничены своим содержанием и
+датированы до мая 2025; пример не выдаёт in-memory model за production runner.
+
+## Проход 2 — редактура, М8 и плотность
+
+Тон соответствует М8: автор ведёт от наблюдаемой цены ошибки к ownership,
+contracts и проверяемому следующему шагу. Техническая речь строится по схеме
+«симптом → причина → проверка → действие» без универсальных обещаний и без
+анахронизмов после мая 2025.
+
+| Статья | Проблема и цена ошибки в первых двух абзацах | Основной текст | Практический артефакт | Проверяемый финал |
+| --- | --- | ---: | --- | --- |
+| practice | Массовая рутина масштабирует неверный selector; цена — неверное state, восстановление и потеря истории запуска. | 10 982 знака | operation card, цепочка gates, 31-assertion fixture | Подменить target digest или добавить target и получить stop до write. |
+| mechanism | Green pipeline скрывает разрыв между proposal, authority, approval, audit и observed state; цена — неверный scope и рискованный rollback. | 10 983 знака | матрица доказательств, authority/approval contracts | Удалить binding field и показать, что старый approval не переносится на новый scope. |
+| field | Широкий selector и stale inventory создают массовый change; цена — частичный state и повторный опасный запуск. | 10 982 знака | checklist, fail-closed example, rollback boundary | Запустить three-case test: bounded, over-scope, mismatched approval. |
+
+Редакторская вычитка:
+
+- Каждая статья прямо называет проблему и цену ошибки в
+ первых двух <p>; это проверяет усиленный audit.
+- В каждой статье есть table, figure с meaningful alt и caption,
+ компактный reproduciable example, ограничения и следующий проверяемый шаг.
+- Preview/dry-run, authority, approval, audit trail, verification и rollback
+ не названы взаимозаменяемыми. Текст отдельно фиксирует ограничения каждого
+ барьера.
+- Все body лежат в диапазоне 5 000–15 000 и в целевом диапазоне 8–11 тыс.
+ знаков; после сокращения сняты повторы о review, audit и rollback без потери
+ negative branches.
+- Fixed synthetic labels явно отделены от фактов источников; immutable
+ не выдан за свойство внешнего хранилища.
+
+**Вердикт прохода 2:** пройдено. Статьи стали короче после саморевью, сохранив
+таблицы, examples, ограничения, owner и final action.
+
+## Проход 3 — SVG, fixture и усиленный audit draft
+
+Проверены три собственных SVG:
+
+| Asset | Смысл | Итог 375 px |
+| --- | --- | --- |
+| engineering-automation-2025-preview-approval-rollback.svg | Семь переходов и красная stop/rollback-plan ветка. | Footer сокращён после первого render; все карточки и подписи читаемы. |
+| engineering-automation-2025-authority-matrix.svg | Authority limit, target mismatch, execute receipt и boundary verification. | Правый столбец и fail-closed footer сокращены после первого render; таблица читаема. |
+| engineering-automation-2025-dry-run-evidence-loop.svg | Evidence loop от preview до observed verification и нового plan. | Footer сокращён после первого render; пунктирный возврат к новому plan виден. |
+
+Последовательность проверок:
+
+- node --check scripts/upgrade-2025-05.mjs — PASS.
+- node scripts/upgrade-2025-05.mjs --verify-fixture — PASS,
+ 31/31 assertions.
+- npm run audit:draft -- scripts/upgrade-2025-05.mjs — PASS для
+ всех трёх slugs, включая requirement о цене ошибки в первых двух paragraphs
+ и диапазон 5 000–15 000.
+- xmllint --noout — PASS для трёх SVG.
+- SVG safety scan — чисто: нет script,
+ foreignObject, javascript:,
+ data:image и event-handler attributes.
+- Sharp render на ширине 375 px и ручной visual review — PASS после
+ сокращения трёх footer/column labels.
+
+Full build и registry audit намеренно не запускались. Registry, README, queue,
+articles.json, app-files и Git остаются вне scope P87.
+
+**Вердикт прохода 3:** пройдено для самостоятельного draft-пакета.
+Публикация не выполнялась.
+
+## Выпуск после независимой приёмки
+
+### 1. Факты и техника
+
+Редактор повторно проверил первичные источники по immutable HTTPS-revisions. Terraform commit `898e397ebaefb4d0c363b7332b3ba184b8852162` от 22 января 2025 подтверждает различие speculative plan и final apply, включая риск изменения target system между ними. GitHub Docs commit `6a92295d73cd86b37a810a87f45e7deb21d10a0f` от 31 января 2025 подтверждает environment protection rules, недоступность secrets до их прохождения и отдельную настройку bypass. Kubernetes website commit `c889d9b2510bba78dbb527a79b8e2099b16f3d67` от 14 марта 2024 подтверждает границу client/server dry-run. Ни один из фактов не расширен в текстах до гарантии безопасности произвольного runner.
+
+Заново запущены `node --check`, fixture (31/31) и усиленный draft audit. Ручной запуск всех трёх опубликованных code examples вернул ожидаемые статусы: positive path остаётся simulated execution без внешнего change, over-scope и lower-authority cases останавливаются до approval. Три статьи содержат 10 982, 10 983 и 10 982 знака. Fixed synthetic fixture сознательно не моделирует identity, срок approval, durable audit store и observed production state; статьи называют эти поля требованиями реального процесса, а не свойствами модели.
+
+### 2. Редактура и голос автора
+
+Полностью прочитаны practice, mechanism и field case. Первые два абзаца каждой формулируют наблюдаемую проблему и цену массовой ошибки. Текст выдерживает прагматичный маршрут «симптом → причина → проверка → действие»: отдельно названы proposal, scope, authority, approval, audit trail, observed verification и rollback plan. В статьях нет вымышленных production-инцидентов, фактических прав или внешних операций; термины получают технический контекст, а следующие шаги воспроизводимы на одной ограниченной операции.
+
+### 3. Визуал и выпуск
+
+Все SVG прошли XML и safety scan. Независимый Sharp render на ширине 375 px показал читаемые карточки, строки матрицы и stop-ветки без обрезания; figure alt и captions объясняют, что именно доказывает диаграмма. После подключения overlay целевой audit подтвердил по одной figure, таблице и code example у practice, по одной figure, двум таблицам и одному примеру у mechanism и field. Registry содержит 256 уникальных ревизий, `git diff --check` чист, production build успешно сгенерировал 374 страницы.
diff --git a/web/data/editorial-revisions.mjs b/web/data/editorial-revisions.mjs
index 34192d7..36f68f1 100644
--- a/web/data/editorial-revisions.mjs
+++ b/web/data/editorial-revisions.mjs
@@ -83,6 +83,7 @@ import { revisions as january2025Revisions } from '../scripts/upgrade-2025-01.mj
import { revisions as february2025Revisions } from '../scripts/upgrade-2025-02.mjs';
import { revisions as march2025Revisions } from '../scripts/upgrade-2025-03.mjs';
import { revisions as april2025Revisions } from '../scripts/upgrade-2025-04.mjs';
+import { revisions as may2025Revisions } from '../scripts/upgrade-2025-05.mjs';
// This layer replaces archived source entries without losing their stable slug and date.
export const editorialRevisions = [
@@ -171,4 +172,5 @@ export const editorialRevisions = [
...february2025Revisions,
...march2025Revisions,
...april2025Revisions,
+ ...may2025Revisions,
];
diff --git a/web/public/assets/editorial/2025/engineering-automation-2025-authority-matrix.svg b/web/public/assets/editorial/2025/engineering-automation-2025-authority-matrix.svg
new file mode 100644
index 0000000..cc41ade
--- /dev/null
+++ b/web/public/assets/editorial/2025/engineering-automation-2025-authority-matrix.svg
@@ -0,0 +1,47 @@
+
diff --git a/web/public/assets/editorial/2025/engineering-automation-2025-dry-run-evidence-loop.svg b/web/public/assets/editorial/2025/engineering-automation-2025-dry-run-evidence-loop.svg
new file mode 100644
index 0000000..4752430
--- /dev/null
+++ b/web/public/assets/editorial/2025/engineering-automation-2025-dry-run-evidence-loop.svg
@@ -0,0 +1,60 @@
+
diff --git a/web/public/assets/editorial/2025/engineering-automation-2025-preview-approval-rollback.svg b/web/public/assets/editorial/2025/engineering-automation-2025-preview-approval-rollback.svg
new file mode 100644
index 0000000..58d53e7
--- /dev/null
+++ b/web/public/assets/editorial/2025/engineering-automation-2025-preview-approval-rollback.svg
@@ -0,0 +1,67 @@
+
diff --git a/web/scripts/upgrade-2025-05.mjs b/web/scripts/upgrade-2025-05.mjs
new file mode 100644
index 0000000..7dad988
--- /dev/null
+++ b/web/scripts/upgrade-2025-05.mjs
@@ -0,0 +1,915 @@
+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('') + '
plan от применения: plan сам не выполняет change. Speculative plan может разойтись с final effect, если target system изменится между расчётом и execute.'),
+ p('Для массовой операции preview должен иметь собственный contract. В нём не «обновить документацию», а operation id, selector id, dense list target ids, digest списка, желаемый переход и граница времени. Если target list строится из широкого query, сохраняют и query, и результат query. Иначе reviewer согласует название операции, а исполнитель потом получает уже другой набор. Preview без списка — это заметка; preview без digest — это заметка, которую нельзя связать с approval.'),
+ table('Что фиксирует каждый барьер', ['Барьер', 'Вопрос', 'Минимальный артефакт', 'Чего он не доказывает'], [
+ ['preview / dry-run', 'что предлагается сейчас?', 'operation id, selector, target ids, digest, proposed diff', 'что реальный change позже увидит то же состояние'],
+ ['bounded scope', 'сколько и какие targets разрешены?', 'max targets, allowed selector, exclusions', 'что изменение семантически корректно'],
+ ['approval', 'кто согласовал именно этот scope?', 'approval id, preview digest, authority id, decision', 'что reviewer прочитал каждый объект или что execution пройдёт'],
+ ['audit trail', 'какой transition зафиксирован?', 'append-only record или event с links', 'что event невозможно подделать без свойств конкретного хранилища'],
+ ['verification', 'какое состояние наблюдается после change?', 'independent read, expected/observed comparison, stop reason', 'что следующий change безопасен или rollback не нужен'],
+ ]),
+ figure('/assets/editorial/2025/engineering-automation-2025-preview-approval-rollback.svg', 'Схема из семи карточек: preview, ограниченный scope, authority, approval, execute, audit trail, verification. Красная ветка ведёт к безопасной остановке и отдельному rollback plan; стрелки показывают, что approval и audit record не заменяют проверку результата.', 'Цепочка не пытается сделать один барьер универсальным. Preview связывают с targets, approval — с preview, а verification остаётся отдельным действием после исполнения.'),
+ h2('Dry-run и preview различаются, но оба имеют границу'),
+ p('Термины часто смешивают. Preview может быть расчётом в доменной модели: список того, что процесс намерен сделать. Dry-run — режим конкретного инструмента. В справочнике Kubernetes зафиксированы два разных dry-run режима для kubectl apply: client печатает object без отправки, server отправляет request без persistence ресурса. Значит, даже внутри одной команды «проверили заранее» имеет разные источники данных и разные границы. Ни один режим не говорит, что поздний реальный request обязательно увидит тот же набор policy и состояние.'),
+ h2('Ограниченный scope должен быть машинно проверяемым'),
+ p('Ограничение scope не означает «попросили быть осторожнее». Это правило, которое сравнивает authority с exact selection. У authority есть level, допустимый selector и максимум targets. У preview есть target digest. Approval содержит оба значения. Перед execute исполнитель ещё раз проверяет, что approval привязан к тому же operation id, selector, target digest и authority id. Любое несовпадение закрывает путь. Лучше получить стоп из-за лишнего target, чем позволить динамическому selector незаметно расширить команду.'),
+ p('GitHub Actions environments дают знакомый, но узкий пример такой границы: job, ссылающийся на environment, ждёт прохождения protection rules и не получает environment secrets до этого. Required reviewer может пропустить job, а возможность bypass зависит от отдельной настройки. Из документации не следует, что reviewer оценил бизнес-смысл diff или что выбранный environment равен вашему. Но она хорошо показывает разделение обязанностей: контроль допуска к запуску существует отдельно от содержательной проверки change.'),
+ code(fixtureExample),
+ p('Пример выше не запускает command и не моделирует реальное окружение. Все cards, ids, digest, authority, approval, timestamps и outputs — fixed synthetic values в памяти. Функция с названием simulateSyntheticExecution создаёт только immutable-in-memory receipt с явным productionEffect: not-attempted. Это важнее красивого demo: читатель может повторить negative cases без риска получить частично изменённую систему.'),
+ h2('Approval — это binding, а не печать «безопасно»'),
+ p('Approval имеет техническую работу: связать согласие с конкретным preview. Для этого недостаточно поля approved: true. Нужны operation id, target digest, authority id, decision, version policy и понятный срок действия. Если approval не несёт digest, его можно случайно применить к другому списку targets. Если не несёт authority, непонятно, какому уровню риска соответствовало решение. Если не несёт identifier preview, reviewer не может доказать, что видел именно тот diff.'),
+ h2('Audit trail нужен для расследования, не для оправдания запуска'),
+ p('Audit record записывает связь между intent и событием: operation id, preview digest, target digest, authority, approval, time, actor class, execution result и verification result. Он нужен, чтобы после сбоя не восстанавливать историю из чата и stdout. Но наличие log не делает change обратимым. Лог может быть неполным, неверно коррелированным или лежать рядом с системой, которая отказала. Свойства «append-only», signature и retention нужно доказывать свойствами конкретного storage, а не словом immutable в JSON.'),
+ h2('Verification начинает новый вопрос'),
+ p('После execute не нужно спрашивать «job зелёный?». Нужно сформулировать expectation и observed evidence: какие fields на каком наборе targets должны измениться, кто читает их из authoritative source, как распознаётся partial change и сколько времени допустимо ждать. Если verification не получена или не совпала с expectation, процесс переходит в stop path. Он не делает второй автоматический write в надежде, что это станет rollback.'),
+ p('Rollback — отдельная операция, а не кнопка «вернуть». Перед обратным change нужно знать фактическое состояние, источник истины, новый scope и побочные зависимости. Поэтому безопасный stop может создать только rollback plan: зафиксировать reason, собрать observed state вне модели, получить новую authority, подготовить отдельный preview и approval. Эта задержка дешевле, чем обратный массовый change по старому предположению.'),
+ h2('Маршрут внедрения на одной рутине'),
+ ol([
+ 'Выберите одну обратимую операцию. Например, поменять synthetic label на двух учебных cards, а не «автоматизировать всё сопровождение».',
+ 'Опишите target contract. Dense ids, selector, exclusions, target digest, expected before/after и max scope.',
+ 'Разведите preview и execute. Preview не имеет capability писать; execute принимает только exact bound approval.',
+ 'Сделайте один refusal test. Подмените target digest или добавьте третий target. Ожидаемый результат — stop before approval.',
+ 'Добавьте observed verification. Заранее опишите, какую независимую read-проверку выполнит owner после реального запуска.',
+ 'Спланируйте rollback отдельно. Не выдавайте старый preview за разрешение на обратный change; создайте новый operation card.',
+ ]),
+ h2('Ограничения и следующий проверяемый шаг'),
+ p('Эта статья не даёт production runner, identity system, policy engine, durable audit storage или реальный rollback. Fixed model не читает и не пишет files, Git, сеть, CI, telemetry, cloud, production или real API. Она не подтверждает права пользователя, фактическое состояние targets, качество query или успешность внешнего execute. Её смысл уже: сделать несоответствие scope, authority или approval наблюдаемой причиной остановки.'),
+ p('Следующий проверяемый шаг: выберите один существующий batch-job и выпишите два списка — что он предлагает и что он на самом деле меняет. Затем добавьте в тест подмену target digest и отдельный assertion, что executor останавливается до write. Если пока нельзя назвать exact targets и owner verification, автоматизацию не расширяют: сначала строят contract preview и stop path.'),
+ h2('Историческая граница мая 2025'),
+ p('Terraform commit для v1.10.5 датирован 22 января 2025; GitHub Docs commit — 31 января 2025; Kubernetes website commit — 14 марта 2024. Все источники существовали к маю 2025 и закреплены immutable commits. Они используются только для узких фактов о plan, environments и dry-run. Цепочка cards, fixed fixture, правила binding и rollback plan — инженерная конструкция автора, не обещание поведения Terraform, GitHub Actions или Kubernetes вне их документации.'),
+]);
+
+const mechanism = revision({
+ slug: 'editorial-2025-05-mechanism-engineering-automation',
+ title: 'Почему preview, approval и audit trail не доказывают безопасный change',
+ categories: ['Надёжность', 'Процессы'],
+ cover: '/assets/editorial/2025/engineering-automation-2025-authority-matrix.svg',
+ excerpt: 'Разбираем разные доказательства в автоматизации: preview показывает намерение, authority ограничивает scope, approval связывает решение, audit trail фиксирует событие, verification наблюдает результат.',
+ readingMinutes: 14,
+}, [
+ p('У массовой автоматизации есть опасная иллюзия: если pipeline показал preview, получил approval и оставил audit log, то change якобы стал безопасным. Симптом заметен после incident: в log есть аккуратный record, reviewer помнит кнопку approve, а реальный scope оказался шире или состояние цели успело измениться. Цена ошибки — не только неверные объекты. Команда тратит часы на доказательство того, что именно произошло, и может повторить ошибку rollback-скриптом, которому доверили старый контекст.'),
+ p('Причина в смешении типов доказательств. Preview отвечает о предложении, authority — о допустимом пределе, approval — о согласии на связанный request, audit trail — о записанном событии, verification — о наблюдаемом результате. Ни один из них не выводится из другого автоматически. Технически зрелый процесс проверяет переход между карточками и умеет остановиться при любом разрыве. Он не называет «green pipeline» доказательством правильности, пока не названо, что конкретно было измерено.'),
+ h2('Пять утверждений, которые нельзя склеивать'),
+ table('Артефакт и его логическое значение', ['Артефакт', 'Что можно утверждать честно', 'Типовая ложная интерпретация', 'Нужное следующее доказательство'], [
+ ['preview', 'в фиксированный момент рассчитан proposed change для зафиксированного selection', 'реальный execute будет таким же', 'повторная проверка preconditions рядом с execute'],
+ ['authority', 'policy ограничила допустимый selector или размер scope', 'операция полезна и корректна', 'review содержимого preview и intent'],
+ ['approval', 'кто-то разрешил связанный request в своей роли', 'reviewer проверил каждую техническую деталь', 'binding id/digest и понятная review policy'],
+ ['audit trail', 'система зафиксировала событие по выбранному contract', 'событие необратимо доказано и state верный', 'свойства storage плюс independent verification'],
+ ['verification', 'авторитетный reader наблюдал заданное условие', 'все последствия change устранены', 'monitoring, residual-risk owner или новый rollback plan'],
+ ]),
+ ol([
+ 'Зафиксировать proposal. Сохранить operation id, selector, dense targets, digest и version, а не только красивый diff.',
+ 'Сравнить scope с authority. Проверить selector и limit до того, как появится approval request.',
+ 'Связать approval буквально. Operation, preview, target и authority ids должны совпасть без implicit default.',
+ 'Отделить receipt от evidence. После execute читать authoritative target и только затем решать, закрывать operation или готовить новый plan.',
+ ]),
+ figure('/assets/editorial/2025/engineering-automation-2025-authority-matrix.svg', 'Матрица из строк authority level и столбцов target scope, preview digest, approval binding, execution и verification. Зелёная клетка разрешает только bounded scope; красные клетки показывают, что превышение лимита, другой selector или чужой digest должны остановить цепочку.', 'Authority сравнивается со scope, но не с «качеством идеи». Поэтому она защищает от расширения операции, а не заменяет review или verification.'),
+ h2('Preview — снимок намерения с известным сроком годности'),
+ p('Terraform отделяет speculative plan от apply: изменения target system между ранним plan и final apply могут изменить результат. Это относится и к самописному orchestration: состояние меняют другой job, человек или controller. Preview — вход в review, а не билет на исполнение через час.'),
+ p('Чтобы превратить preview в contract, сохраняют не только human-readable diff. Нужны stable operation id, model/config version, selector, dense target list, target digest, expected precondition и proposal digest. Digest — не мистическая защита: он просто позволяет проверять равенство того, что видел reviewer, и того, что получил executor. Надёжность hash, подписи и collision resistance — отдельные свойства конкретной реализации. В fixed model digest — статическая строка именно поэтому; fixture проверяет связь, а не криптографию.'),
+ h2('Dry-run не обладает одной силой'),
+ p('Называть любой безопасный расчёт dry-run вредно. Kubernetes reference для kubectl apply различает client dry-run и server dry-run. Первый печатает object без отправки; второй отправляет server-side request, но не сохраняет ресурс. Это два разных наблюдения: один проверяет serialization и локальную обработку, другой может пройти часть server-side пути. Они полезны, но neither превращается в утверждение о будущем persistent change.'),
+ p('В инженерном process рядом с названием preview пишут source of truth и side-effect boundary. Например: «local model preview, no external read», «server admission dry-run, no persistence» или «read-only inventory with fixed snapshot». Тогда reviewer понимает, какие questions ещё открыты. Если source inventory старый, target list может быть уже неверен. Если server dry-run прошёл, policy может поменяться до execute. Если preview собран на client, он может не знать admission rule. Каждая граница добавляет самостоятельный check, а не больше уверенности в одном цветном статусе.'),
+ h2('Authority ограничивает мощность, не доказывает смысл'),
+ p('Authority card в хорошем дизайне мала и машинно проверяема: level, allowed selector, maximum count, time window и исключения. Она не хранит длинное объяснение change и не решает за owner, стоит ли менять поле. Её задача — не позволить approval от малой операции «переехать» на больший scope. Если preview содержит три targets, а authority допускает два, executor не округляет limit и не просит пользователя «подтвердить ещё раз» в том же запуске. Он возвращает stop-scope-exceeds-authority-limit.'),
+ p('GitHub Docs для environments показывает похожую, но не тождественную механику: job ожидает protection rules, а секреты environment недоступны до их прохождения. Это gate доступа к стадии запуска. В документе есть и bypass boundary: возможность администратора обходить правила зависит от настройки environment. Следствие для собственного runner простое: bypass — тоже операция с отдельным identity, reason и audit event. Его нельзя скрыть внутри общего approved, иначе расследование не отличит нормальный путь от исключения.'),
+ code([
+ "import {",
+ " createFixedSyntheticOperationCard,",
+ " createFixedSyntheticAuthority,",
+ " previewSyntheticAutomation,",
+ " requestSyntheticApproval,",
+ "} from './upgrade-2025-05.mjs';",
+ '',
+ "const preview = previewSyntheticAutomation(",
+ " createFixedSyntheticOperationCard('fixed-over-scope-v1'),",
+ ');',
+ 'const gate = requestSyntheticApproval(',
+ ' preview,',
+ " createFixedSyntheticAuthority('fixed-over-scope-v1'),",
+ ');',
+ '',
+ 'console.log({ accepted: gate.accepted, reason: gate.reason, targetCount: gate.targetCount });',
+ '// Expected: false, stop-scope-exceeds-authority-limit, 3.',
+ '// The values are fixed synthetic in memory; no target is read or changed.',
+ ].join('\n')),
+ p('Пример показывает не «проверку прав пользователя», а exact contract. Card с тремя synthetic targets и authority максимум на два закрывает путь до approval. Никакой тайный fallback не оставляет третью запись «на потом», никакой реальный job не создаётся. Для production нужны identity provider, policy owner, storage и real-time conditions; они намеренно не входят в fixture.'),
+ h2('Approval обязан связывать четыре значения'),
+ p('Минимальный approval связывает operation id, preview digest, target digest и authority id. Если хотя бы одно значение другое, согласие не относится к request. Отдельно полезны policy version, reviewer role, expiry и reason. Но поля не делают approval автоматическим доказательством: reviewer мог видеть неполное представление, policy могла быть слишком широкой, а time window — слишком длинным. Поэтому в high-impact operations approval сопровождают change owner, expected verification и stop owner, а не только именем reviewer.'),
+ p('Target mismatch — показательная авария. Preview может содержать synthetic-targets-a-b-v1, approval — synthetic-targets-a-c-v1, а оба выглядят похожими для человека. В fixed fixture это не «warning»: approval получает stop-approval-does-not-bind-preview-target-and-authority. Хороший runner использует такую же fail-closed семантику для отсутствующего, неизвестного и forged field. Иначе позитивный путь testируется, а ошибочный request случайно получает полезный default.'),
+ h2('Audit trail хранит причинную цепочку, но не наблюдает state'),
+ p('Audit trail нужен потому, что у команды несколько источников истины: queue, executor, policy engine и target system. Он связывает phase transition одним operation id и удобен для восстановления последовательности. Но audit record может сказать «simulation recorded» и не может сам прочитать target. Поэтому audit fields не заменяют verification fields. Если их смешать, dashboard будет показывать «verified» по факту наличия log, хотя реальные targets никто не открыл.'),
+ h2('Verification и rollback не должны быть скрытым retry'),
+ p('Verification начинается с измеримого expected state. Не «проверьте всё», а «reader X видит field Y со значением Z на scope digest D; отсутствующие targets перечислены; stale data имеет отдельный reason». Если read не прошёл, state не равен expectation или источник не авторитетен, status остаётся stopped. Green execution receipt не переписывают как verification.'),
+ p('Rollback нужен именно в этот момент, но не как автоматическая инверсия исходного payload. Что вернуть зависит от observed state: часть targets могла измениться вручную, исходное значение уже неактуально, а побочные объекты требуют другого порядка. Поэтому rollback plan содержит preconditions: собрать observed state, создать новый bounded scope, получить новую authority, подготовить новый preview и approve отдельную операцию. «Rollback planned» не равно «rollback executed» и не даёт права делать новый write.'),
+ table('Fail-closed ветки fixed fixture', ['Нарушение', 'Где обнаруживается', 'Результат', 'Почему это важно'], [
+ ['unknown или missing field', 'preview contract', 'stop без proposal', 'не возникает default operation'],
+ ['forged authority', 'authority gate', 'stop before approval', 'нельзя расширить limit подменой card'],
+ ['approval с другим target digest', 'approval binding', 'stop before simulation', 'review не переносится на другой selection'],
+ ['sparse или cyclic structure', 'canonical comparison', 'reject без exception', 'fixture не скрывает некорректный input'],
+ ['forged audit record', 'verification entry', 'stop, не verified', 'record не становится state evidence'],
+ ]),
+ h2('Ограничения и следующий проверяемый шаг'),
+ p('В model нет реального permission check, scheduler, external time, inventory, API, storage или production target. Все values fixed synthetic и лежат в памяти. Поэтому fixture не проверяет network race, cryptographic digest, identity, доступность audit store или честность reviewer. Она проверяет более узкую вещь: каждый переход принимает только exact card и закрывается на mismatch, missing, unknown, sparse или cyclic input.'),
+ p('Следующий проверяемый шаг: возьмите самый короткий batch automation и перечислите для него пять предикатов из первой таблицы. Для каждого напишите owner и data source. Затем удалите один binding field — например target digest — и попробуйте доказать, что executor всё ещё не сможет применить старый approval к новому scope. Если доказательство строится на дисциплине оператора, а не на contract, добавьте gate до расширения автоматизации.'),
+ h2('Историческая граница мая 2025'),
+ p('В этой статье Terraform v1.10.5 используется только для границы speculative plan, GitHub Docs commit от 31 января 2025 — только для behaviour environment protection rules и bypass setting, Kubernetes reference commit от 14 марта 2024 — только для различия client/server dry-run. Все ссылки закреплены immutable commits и существовали к маю 2025. Матрица authority, stop codes и fixed cards — собственный учебный механизм автора; они не названы гарантией сторонних систем.'),
+]);
+
+const field = revision({
+ slug: 'editorial-2025-05-field-engineering-automation',
+ title: 'Полевой маршрут: автоматизация без массового неверного change',
+ categories: ['Инфраструктура', 'Надёжность'],
+ cover: '/assets/editorial/2025/engineering-automation-2025-dry-run-evidence-loop.svg',
+ excerpt: 'Практический маршрут для владельца batch-операции: построить preview, ограничить scope, привязать approval, сохранить audit trail, проверить observed state и остановиться до опасного rollback.',
+ readingMinutes: 14,
+}, [
+ p('Полевой риск автоматизации выглядит буднично: небольшая операция накопила исключения, кто-то добавил selector «для удобства», а запуск теперь меняет всё, что совпало с шаблоном. Сначала это экономит час ручной работы. Затем неверный filter или stale inventory создаёт массовый change, который трудно отмотать. Цена ошибки — частично изменённые targets, потерянные данные о прежнем состоянии, простой команды и ещё один рискованный запуск ради исправления первого.'),
+ p('В этот момент нельзя ограничиться просьбой «давайте внимательнее ревьюить скрипт». Причина системная: у процесса нет наблюдаемой границы между расчётом, согласием, исполнением и проверкой. Нужен короткий маршрут, который может остановить работу раньше внешнего write: preview/dry-run, bounded scope, authority, approval, execute, audit trail, independent verification и отдельно подготовленный rollback plan. Каждая остановка должна возвращать причину и владельца, а не расплывчатый failure.'),
+ h2('Сначала опишите operation card'),
+ p('Operation card — это не тикет с заголовком, а минимальный контракт запуска. В нём есть operation id, intent, selector id, dense target list, target digest, expected before/after, version change logic, requested authority и expected verification. Карточка должна уместиться в review без поиска по нескольким системам. Если один из fields нельзя назвать, это не повод проставить unknown и идти дальше. Это сигнал остановиться до того, как автоматизация выберет targets по умолчанию.'),
+ table('Карточка операции перед первым запуском', ['Поле', 'Пример в учебной модели', 'Проверка владельца', 'Стоп, если'], [
+ ['operationId', 'synthetic-normalize-doc-labels-bounded-v1', 'ID один и тот же в preview, approval, audit, verify', 'переиспользован или отсутствует'],
+ ['targetSelection', 'synthetic-doc-a + synthetic-doc-b', 'ids dense, selector и exclusions видны', 'list пустой, sparse или не совпадает с selector'],
+ ['targetDigest', 'synthetic-targets-a-b-v1', 'approval связывает именно этот list', 'digest другой или не зафиксирован'],
+ ['authority', 'synthetic-authority-l2, максимум два target', 'limit и selector подходят операции', 'scope шире authority'],
+ ['expected verification', 'external observed state после execute', 'есть reader и критерий success/failure', 'проверка только «job завершился»'],
+ ]),
+ h2('Preview и dry-run располагают данные, но не дают разрешение'),
+ p('Начните с preview, который можно показать reviewer без побочных эффектов. Он показывает proposed change и selection. Если инструмент предоставляет dry-run, подпишите его режим и источник. Kubernetes documentation различает client mode, который только печатает object, и server mode, который отправляет request без persistence. Но precondition всё равно проверяют перед execute. На реальном запуске другой controller может уже поменять target, а policy может принять другое решение.'),
+ p('Terraform описывает тот же интервал на другом языке: speculative plan не имеет намерения применяться, а изменения target system между ранним plan и final apply способны поменять effect. Поэтому не сохраняйте preview как статичный артефакт без срока. Укажите snapshot version, retrieval time, max age и condition, при котором runner обязан recompute preview. Даже если ваша система не похожа на Terraform, вопрос один: что делает старый calculated diff непригодным для нового execute?'),
+ figure('/assets/editorial/2025/engineering-automation-2025-dry-run-evidence-loop.svg', 'Петля доказательств: preview и dry-run создают proposal, authority ограничивает scope, approval связывает карточку, simulated execute оставляет audit record, независимая verification сравнивает observed state. Красный выход STOP ведёт к новому rollback plan, а не к автоматическому обратному write.', 'Проверка результата намеренно находится после execute и возвращает процесс к новому plan. Так receipt об исполнении не выдаётся за наблюдение состояния, а rollback не наследует полномочия исходной операции.'),
+ h2('Ограничьте scope до approval'),
+ p('Следующий шаг — проверить, может ли authority покрыть exact selection. Хороший authority level отвечает на три вопроса: какой selector допустим, сколько targets максимум разрешено и какие исключения требуют отдельного процесса. Это легче автоматизировать, чем оценку «change выглядит разумно». Если limit два, card с тремя targets должна остановиться до approval. Не уменьшайте список молча и не заменяйте limit на warning: оба решения делают следующее состояние непредсказуемым для reviewer.'),
+ p('Authority также не равна identity. В учебной модели synthetic-authority-l2 — строка и max targets, не пользователь и не реальная permission. В production policy владелец определяет identity source, expiry, delegated approval, break-glass и audit scope. GitHub environments дают понятный пример одного gate: job ждёт protection rules и не получает environment secrets до их прохождения. Это не общая модель полномочий, но полезное напоминание, что доступ к исполнению и смысловая правильность change — разные проверки.'),
+ h2('Approval связывает review с конкретной карточкой'),
+ p('В approval должен попасть не только комментарий reviewer. Минимальный набор: approval id, operation id, target digest, preview digest, authority id, decision и срок. Тогда executor проверяет связку перед execute. Если approval относится к synthetic-targets-a-c-v1, а preview — к synthetic-targets-a-b-v1, путь закрывается. В нормальном UX это может быть короткое объяснение «target mismatch, создай новый preview», а не попытка догадаться, какой target имел в виду reviewer.'),
+ code([
+ "import {",
+ " createFixedSyntheticOperationCard,",
+ " createFixedSyntheticAuthority,",
+ " previewSyntheticAutomation,",
+ " requestSyntheticApproval,",
+ "} from './upgrade-2025-05.mjs';",
+ '',
+ "const card = createFixedSyntheticOperationCard('fixed-lower-authority-v1');",
+ 'const preview = previewSyntheticAutomation(card);',
+ "const authority = createFixedSyntheticAuthority('fixed-lower-authority-v1');",
+ 'const result = requestSyntheticApproval(preview, authority);',
+ '',
+ 'console.log({ stopped: result.stopped, reason: result.reason, limit: result.authorityMaxTargets });',
+ '// Expected: true, stop-scope-exceeds-authority-limit, 1.',
+ '// This is fixed synthetic in-memory data, not a real approval or execution.',
+ ].join('\n')),
+ p('Этот компактный example проверяет failure path, а не счастливый сценарий. Card содержит два synthetic target, authority допускает один, поэтому approval request даже не формируется. Fixture дополнительно подаёт unknown/missing fields, forged authority и approval, mismatch targets, sparse arrays и cyclic structures. Все такие inputs fail closed: report содержит reason и productionEffect: not-attempted, а не best-effort default.'),
+ h2('Execute, audit trail и verification — три разные фазы'),
+ p('Только после exact approval executor может начать реальный change. До него полезно повторить preconditions: target ещё существует, version не устарела, scope не расширился, authorization не истекла. Если проверка не проходит, результат — stop, а не частичное применение «того, что осталось». Для high-impact операций стоит заранее решить, допустима ли partial execution, и кто владеет её recovery. Это domain decision, не побочный эффект loop.'),
+ p('Audit trail получает event после попытки execution, но event не подтверждает observed state. В нём полезно хранить operation id, input digests, authority/approval links, executor version, timestamps, per-target result и correlation id. Но свойства сохранности зависит от storage. Fixed script создаёт frozen record в памяти только для того, чтобы fixture могла проверить exact transition. Он прямо называет границу: это не durable append-only storage, не подпись и не доказательство, что target изменился.'),
+ p('Verification читает authoritative source вне executor и сравнивает observed state с explicit expectation. У неё три исхода: matched, mismatched или unknown. Только первый позволяет закрыть операцию с ограничениями; второй и третий идут в stop path. Убедительная фраза «apply completed» не должна менять status verification. Если reader недоступен, result не «успех с warning», а недостающая проверка с владельцем и сроком.'),
+ table('Реакция на результат после execute', ['Наблюдение', 'Статус процесса', 'Разрешённое действие', 'Что нельзя делать'], [
+ ['observed state совпадает с expectation', 'verified с named boundary', 'закрыть card и сохранить evidence', 'объявлять будущие runs безопасными'],
+ ['часть targets не совпала', 'stop / investigate', 'сохранить exact subset и owner recovery', 'запустить широкую обратную операцию'],
+ ['reader недоступен или evidence stale', 'unknown / stop', 'вернуть verification owner', 'подменить наблюдение exit code executor'],
+ ['scope неизвестен после partial failure', 'stop before rollback', 'собрать inventory и новый preview', 'использовать исходный approval для rollback'],
+ ]),
+ h2('Rollback plan — новый change, а не команда в finally'),
+ p('Когда verification не пройдена, хочется сразу выполнить inverse. Это особенно рискованно после массовой операции: часть targets может быть исправлена руками, прежнее значение могло стать опасным, а новая policy может запрещать обратный переход. Поэтому rollback plan сначала фиксирует boundary: какой subset наблюдался, что именно неизвестно, кому принадлежит recovery, какие data нужны до следующего decision. План не выполняет write и не наследует approval исходной операции.'),
+ p('Rollback повторяет весь маршрут с новым scope, authority, preview, approval и verification. В fixed model stopAndPlanSyntheticRollback возвращает planned-only-not-executed: stop event не разрешает новый write.'),
+ h2('Минимальный выпускной checklist'),
+ ol([
+ 'Проверьте target contract. Selector, ids, digest, exclusions и max count можно показать в одной карточке.',
+ 'Проверьте freshness preview. Явно задайте, когда preview перестаёт быть применимым и должен быть пересчитан.',
+ 'Проверьте binding approval. Operation, preview, target и authority ids совпадают буквально, а не «примерно».',
+ 'Проверьте audit boundary. Log хранит links, но команда не называет его observed state и не заявляет свойства storage без доказательства.',
+ 'Проверьте observed verification. Назначены reader, expectation, timeout и владелец mismatch.',
+ 'Проверьте stop path. Target mismatch, отсутствующий field и unknown authority останавливают process до write; rollback остаётся отдельным plan.',
+ ]),
+ h2('Ограничения и следующий проверяемый шаг'),
+ p('Полевой маршрут не заменяет policy и review конкретной организации. Все data, timestamps, approval и audit records в fixture synthetic и fixed; никаких файлов, Git, CI, сети, telemetry, production, cloud или real API script не касается. Он не доказывает, что текущий batch-job идемпотентен, что audit storage неизменяем или что rollback возможен. Он даёт строго проверяемую форму остановки до внешнего change.'),
+ p('Следующий проверяемый шаг: выберите одну задачу с batch selector и создайте рядом с ней учебный test из трёх cases — valid bounded selection, over-scope selection и approval с другим target digest. Ожидаемый результат во втором и третьем case должен быть один: no execute, явный stop code, сохранённый preview. Только после этого подключайте реальный reader для verification; если этот reader ещё не определён, у операции нет доказательства результата.'),
+ h2('Историческая граница мая 2025'),
+ p('Источники закреплены до мая 2025: Terraform v1.10.5 commit от 22 января 2025, GitHub Docs commit от 31 января 2025 и Kubernetes website commit от 14 марта 2024. Terraform подтверждает ограничение speculative plan, GitHub Docs — gate environment protection rules и configurable bypass, Kubernetes — различие режимов dry-run. Они не обещают единый безопасный процесс. Все stop codes, contracts и fixture этой статьи принадлежат fixed synthetic модели автора.'),
+]);
+
+export const revisions = [practice, mechanism, field].map(({ proseLength, ...item }) => item);
+
+function verifyFixture() {
+ const report = runEngineeringAutomationFixture();
+ const failed = Object.entries(report.assertions).filter(([, value]) => value !== true).map(([key]) => key);
+ if (failed.length > 0) {
+ process.stderr.write('FAIL fixture: ' + failed.join(', ') + '\n');
+ process.exitCode = 1;
+ return;
+ }
+ const count = Object.keys(report.assertions).length;
+ process.stdout.write('PASS fixture: ' + count + '/' + count + ' assertions\n');
+}
+
+if (process.argv.includes('--verify-fixture')) verifyFixture();
+if (process.argv.includes('--print-revisions')) process.stdout.write(JSON.stringify(revisions) + '\n');