From fa6d1c3eac86da97a44e3cf97b21dcbddda09519 Mon Sep 17 00:00:00 2001 From: "E.Gavrilov" Date: Fri, 31 Jul 2026 17:48:51 +0300 Subject: [PATCH] revise May 2025 engineering automation articles --- editorial/production/README.md | 2 +- editorial/reviews/2025-05-draft.md | 163 ++++ web/data/editorial-revisions.mjs | 2 + ...ering-automation-2025-authority-matrix.svg | 47 + ...-automation-2025-dry-run-evidence-loop.svg | 60 ++ ...omation-2025-preview-approval-rollback.svg | 67 ++ web/scripts/upgrade-2025-05.mjs | 915 ++++++++++++++++++ 7 files changed, 1255 insertions(+), 1 deletion(-) create mode 100644 editorial/reviews/2025-05-draft.md create mode 100644 web/public/assets/editorial/2025/engineering-automation-2025-authority-matrix.svg create mode 100644 web/public/assets/editorial/2025/engineering-automation-2025-dry-run-evidence-loop.svg create mode 100644 web/public/assets/editorial/2025/engineering-automation-2025-preview-approval-rollback.svg create mode 100644 web/scripts/upgrade-2025-05.mjs 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 @@ + + Матрица authority и границ автоматизации + Таблица показывает, что authority level допускает ограниченный selector и число targets, но не заменяет preview, approval binding, audit trail и verification. + + Authority ограничивает scope + Она не делает change верным и не заменяет verification + + + Проверка + Разрешает? + Не доказывает + + selector + max targets + bounded A+B + ДА + смысл change + observed result + + selector + 3 targets + limit = 2 + STOP + нужно новое + authority решение + + approval digest ≠ preview + targets A+C вместо A+B + STOP + review нельзя + переносить + + bound preview + approval + preconditions re-checked + EXECUTE + не равен + verified state + + audit event exists + links preserved + TRACE + observed state + на target + + + Правило fail-closed + unknown / mismatch / limit → no execute + Authority — gate scope, а не универсальная гарантия. + 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 @@ + + Петля evidence для инженерной автоматизации + Вертикальная петля показывает, как preview и dry-run создают evidence, authority и approval проверяют scope, execute и audit trail фиксируют попытку, а verification наблюдает target. Красная ветка создаёт только rollback plan. + + Evidence loop, а не «зелёный job» + Каждая карточка отвечает на другой вопрос + + + + PREVIEW / DRY-RUN + что предлагается; какой источник + и какая side-effect boundary + + + + + + SCOPE + AUTHORITY + exact ids, digest, selector, max count + scope wider than limit → STOP + + + + + + APPROVAL BINDING + operation + preview + target + authority + digest mismatch → STOP + + + + + + EXECUTE + AUDIT EVENT + attempt recorded with links and result + receipt is not target state + + + + + + INDEPENDENT VERIFY + authoritative reader compares expected + and observed state on exact scope + + + + + + UNKNOWN / MISMATCH → STOP + preserve reason and observed subset + new inventory → new bounded rollback plan + new authority + preview + approval before write + + + + + Пунктир: новый plan начинается с нового preview и approval. + Audit = trace; verification = observed state. Они не равны. + 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 @@ + + Цепочка защиты массовой инженерной операции + Вертикальный маршрут: preview, ограниченный scope, authority, approval, simulated execute, audit trail, verification и красная ветка безопасной остановки с отдельным rollback plan. + + Один change — семь разных доказательств + Каждый переход имеет свой contract и stop path + + + + 1 · PREVIEW / DRY-RUN + proposal, selector, targets, digest + не равен будущему execution + + + + + + 2 · BOUNDED SCOPE + allowed selector + max target count + + + + + + 3 · AUTHORITY + level и scope limit, не оценка смысла + + + + + + 4 · APPROVAL + binds preview digest, target digest, authority + + + + + + 5 · EXECUTE + re-check preconditions before any write + + + + + + 6 · AUDIT TRAIL + event и links; не observed state + + + + + + 7 · VERIFICATION + independent reader: expected vs observed + + + + + + STOP → NEW ROLLBACK PLAN + mismatch / unknown / stale evidence + new scope → new authority → new preview + rollback plan ≠ permission to execute it + + + Один барьер не снимает весь риск: он защищает свой переход. + 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) => '

' + text + '

'; +const code = (text) => '
' + escapeHtml(text) + '
'; +const ol = (items) => '
    ' + items.map((item) => '
  1. ' + item + '
  2. ').join('') + '
'; +const figure = (src, alt, caption) => '
' + alt + '
' + caption + '
'; +const table = (caption, headers, rows) => '
' + headers.map((item) => '').join('') + '' + rows.map((row) => '' + row.map((item) => '').join('') + '').join('') + '
' + caption + '
' + item + '
' + item + '
'; + +function deepFreeze(value, seen = new Set()) { + if (!value || typeof value !== 'object' || seen.has(value)) return value; + seen.add(value); + for (const child of Object.values(value)) deepFreeze(child, seen); + return Object.freeze(value); +} + +function plainText(content) { + return content + .replace(/<[^>]+>/g, ' ') + .replaceAll(' ', ' ') + .replaceAll('"', '"') + .replaceAll(''', "'") + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('&', '&') + .replace(/\s+/g, ' ') + .trim(); +} + +function bodyText(content) { + return plainText(content.replace(/

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

|$)/, '')); +} + +const sources = deepFreeze([ + { + title: 'HashiCorp Terraform plan, immutable commit 898e397 for v1.10.5 (22 January 2025)', + url: 'https://raw.githubusercontent.com/hashicorp/terraform/898e397ebaefb4d0c363b7332b3ba184b8852162/website/docs/cli/commands/plan.mdx', + version: 'Terraform v1.10.5 tag points to commit 898e397ebaefb4d0c363b7332b3ba184b8852162, dated 2025-01-22', + claim: 'Документ говорит, что plan сам не выполняет proposed changes; speculative plan описывает эффект без намерения применить его, а изменения target system между plan и apply могут изменить final effect, поэтому final non-speculative plan надо перепроверять.', + boundary: 'Документация Terraform не задаёт универсальный protocol approval, состав audit trail или rollback для произвольной автоматизации и не доказывает корректность конкретного change.', + }, + { + title: 'GitHub Docs: managing environments for deployment, immutable commit 6a92295 (31 January 2025)', + url: 'https://raw.githubusercontent.com/github/docs/6a92295d73cd86b37a810a87f45e7deb21d10a0f/content/actions/managing-workflow-runs-and-deployments/managing-deployments/managing-environments-for-deployment.md', + version: 'github/docs immutable commit 6a92295d73cd86b37a810a87f45e7deb21d10a0f, dated 2025-01-31', + claim: 'GitHub описывает, что job, ссылающийся на environment, не стартует и не получает environment secrets до прохождения protection rules; required reviewer может пропустить job, а bypass зависит от отдельной настройки environment.', + boundary: 'Это поведение GitHub Actions environments в описанной версии. Оно не подтверждает семантическую верность diff, полноту preview, независимость reviewer или безопасность другого CI и production процесса.', + }, + { + title: 'Kubernetes kubectl apply reference, immutable website commit c889d9b (14 March 2024)', + url: 'https://raw.githubusercontent.com/kubernetes/website/c889d9b2510bba78dbb527a79b8e2099b16f3d67/content/en/docs/reference/kubectl/generated/kubectl_apply/_index.md', + version: 'kubernetes/website immutable commit c889d9b2510bba78dbb527a79b8e2099b16f3d67, dated 2024-03-14', + claim: 'Справочник kubectl apply различает dry-run=client, который печатает object без отправки, и dry-run=server, который отправляет server-side request без persistence ресурса.', + boundary: 'Этот справочник не обещает, что последующий реальный apply увидит то же состояние, пройдёт те же policy или не затронет другие объекты. Он описывает только режим dry-run конкретной команды.', + }, +]); + +export const sourceReport = sources.map((source) => deepFreeze({ ...source })); + +function sourceList() { + return ''; +} + +function revision(meta, parts) { + const contentHtml = parts.join('\n') + '\n' + h2('Проверяемые источники') + '\n' + sourceList(); + const proseLength = bodyText(contentHtml).length; + if (proseLength < 5000 || proseLength > 15000) { + throw new Error(meta.slug + ': основной текст вне диапазона 5 000–15 000 знаков: ' + proseLength); + } + return deepFreeze({ ...meta, contentHtml, proseLength }); +} + +const MODEL_VERSION = 'fixed-synthetic-engineering-automation-2025-05-v1'; +const OPERATION_KIND = 'fixed-synthetic-operation-card-v1'; +const AUTHORITY_KIND = 'fixed-synthetic-authority-card-v1'; +const APPROVAL_KIND = 'fixed-synthetic-approval-card-v1'; +const PREVIEW_KIND = 'fixed-synthetic-preview-report-v1'; +const APPROVAL_REQUEST_KIND = 'fixed-synthetic-approval-request-v1'; +const EXECUTION_KIND = 'fixed-synthetic-execution-receipt-v1'; +const VERIFICATION_KIND = 'fixed-synthetic-verification-report-v1'; +const STOP_KIND = 'fixed-synthetic-stop-and-rollback-plan-v1'; +const SYNTHETIC_SCOPE = 'p87-engineering-automation-2025-05'; +const MODEL_LIMIT = 'fixed-synthetic-in-memory-only-no-files-no-git-no-network-no-ci-no-clock-no-telemetry-no-production-no-cloud-no-real-api-no-real-change'; + +const EFFECT_BOUNDARY = deepFreeze({ + model: MODEL_LIMIT, + files: 'not-read-or-written', + git: 'not-read-or-written', + network: 'not-used', + ci: 'not-run', + clock: 'not-read', + telemetry: 'not-read', + production: 'not-contacted', + cloud: 'not-contacted', + realApi: 'not-called', + realChange: 'not-attempted', + auditDurability: 'in-memory-object-freeze-only-not-a-durable-or-tamper-proof-log', + rollback: 'plan-only-not-executed', +}); + +function makeCard(caseId, operationId, targetIds, targetDigest, requestedAuthority, changeDigest) { + return { + kind: OPERATION_KIND, + version: MODEL_VERSION, + caseId, + operationId, + requestedAuthority, + targetSelection: { + kind: 'fixed-synthetic-target-selection-v1', + selectorId: 'synthetic-doc-labels-scope-v1', + targetIds, + targetDigest, + }, + proposedChange: { + kind: 'fixed-synthetic-change-v1', + changeType: 'normalize-synthetic-document-label', + expectedFrom: 'legacy', + desiredTo: 'stable', + changeDigest, + }, + sequence: ['preview', 'bounded-scope', 'approval', 'simulate-execute', 'audit-record', 'verify', 'stop-or-plan-rollback'], + }; +} + +function makeAuthority(caseId, authorityId, level, maxTargets, selectorId) { + return { + kind: AUTHORITY_KIND, + version: MODEL_VERSION, + caseId, + authorityId, + level, + maxTargets, + selectorId, + grantBoundary: 'fixed-synthetic-example-only-not-an-identity-or-production-permission', + }; +} + +function makeApproval(caseId, approvalId, operationId, targetDigest, authorityId, decision) { + return { + kind: APPROVAL_KIND, + version: MODEL_VERSION, + caseId, + approvalId, + operationId, + targetDigest, + authorityId, + decision, + approvedAt: '2025-05-20T10:00:00Z', + approvalBoundary: 'fixed-synthetic-review-marker-only-not-a-human-or-production-approval', + }; +} + +const FIXED_CASES = deepFreeze({ + 'fixed-bounded-approved-v1': { + card: makeCard( + 'fixed-bounded-approved-v1', + 'synthetic-normalize-doc-labels-bounded-v1', + ['synthetic-doc-a', 'synthetic-doc-b'], + 'synthetic-targets-a-b-v1', + 'synthetic-authority-l2', + 'synthetic-change-legacy-to-stable-v1', + ), + authority: makeAuthority( + 'fixed-bounded-approved-v1', + 'synthetic-authority-l2', + 'bounded-two-targets', + 2, + 'synthetic-doc-labels-scope-v1', + ), + approval: makeApproval( + 'fixed-bounded-approved-v1', + 'synthetic-approval-bounded-v1', + 'synthetic-normalize-doc-labels-bounded-v1', + 'synthetic-targets-a-b-v1', + 'synthetic-authority-l2', + 'approve-bounded-simulation', + ), + }, + 'fixed-over-scope-v1': { + card: makeCard( + 'fixed-over-scope-v1', + 'synthetic-normalize-doc-labels-over-scope-v1', + ['synthetic-doc-a', 'synthetic-doc-b', 'synthetic-doc-c'], + 'synthetic-targets-a-b-c-v1', + 'synthetic-authority-l2', + 'synthetic-change-legacy-to-stable-v1', + ), + authority: makeAuthority( + 'fixed-over-scope-v1', + 'synthetic-authority-l2', + 'bounded-two-targets', + 2, + 'synthetic-doc-labels-scope-v1', + ), + approval: null, + }, + 'fixed-target-mismatch-v1': { + card: makeCard( + 'fixed-target-mismatch-v1', + 'synthetic-normalize-doc-labels-mismatch-v1', + ['synthetic-doc-a', 'synthetic-doc-b'], + 'synthetic-targets-a-b-v1', + 'synthetic-authority-l2', + 'synthetic-change-legacy-to-stable-v1', + ), + authority: makeAuthority( + 'fixed-target-mismatch-v1', + 'synthetic-authority-l2', + 'bounded-two-targets', + 2, + 'synthetic-doc-labels-scope-v1', + ), + approval: makeApproval( + 'fixed-target-mismatch-v1', + 'synthetic-approval-mismatched-target-v1', + 'synthetic-normalize-doc-labels-mismatch-v1', + 'synthetic-targets-a-c-v1', + 'synthetic-authority-l2', + 'approve-bounded-simulation', + ), + }, + 'fixed-lower-authority-v1': { + card: makeCard( + 'fixed-lower-authority-v1', + 'synthetic-normalize-doc-labels-lower-authority-v1', + ['synthetic-doc-a', 'synthetic-doc-b'], + 'synthetic-targets-a-b-v1', + 'synthetic-authority-l1', + 'synthetic-change-legacy-to-stable-v1', + ), + authority: makeAuthority( + 'fixed-lower-authority-v1', + 'synthetic-authority-l1', + 'single-target-only', + 1, + 'synthetic-doc-labels-scope-v1', + ), + approval: null, + }, +}); + +function isPlainObject(value) { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype; +} + +function hasExactKeys(value, expected) { + if (!isPlainObject(value) || Object.getOwnPropertySymbols(value).length > 0) return false; + const actual = Object.keys(value).sort(); + const wanted = [...expected].sort(); + return actual.length === wanted.length && actual.every((item, index) => item === wanted[index]); +} + +function canonicalJson(value, stack = new Set()) { + if (value === null) return 'null'; + if (typeof value === 'string' || typeof value === 'boolean') return JSON.stringify(value); + if (typeof value === 'number') return Number.isFinite(value) ? JSON.stringify(value) : null; + if (typeof value !== 'object' || Object.getOwnPropertySymbols(value).length > 0 || stack.has(value)) return null; + stack.add(value); + let result = null; + if (Array.isArray(value)) { + const keys = Object.keys(value); + if (keys.length === value.length && keys.every((key, index) => key === String(index))) { + const entries = []; + for (const item of value) { + const encoded = canonicalJson(item, stack); + if (encoded === null) { + stack.delete(value); + return null; + } + entries.push(encoded); + } + result = '[' + entries.join(',') + ']'; + } + } else if (isPlainObject(value)) { + const entries = []; + for (const key of Object.keys(value).sort()) { + const encoded = canonicalJson(value[key], stack); + if (encoded === null) { + stack.delete(value); + return null; + } + entries.push(JSON.stringify(key) + ':' + encoded); + } + result = '{' + entries.join(',') + '}'; + } + stack.delete(value); + return result; +} + +function hasSameCanonicalJson(actual, expected) { + const actualJson = canonicalJson(actual); + const expectedJson = canonicalJson(expected); + return actualJson !== null && expectedJson !== null && actualJson === expectedJson; +} + +function cloneFixed(value) { + const encoded = canonicalJson(value); + if (encoded === null) throw new Error('fixed synthetic value must be canonical'); + return deepFreeze(JSON.parse(encoded)); +} + +function rejected(kind, reason, extra = {}) { + return deepFreeze({ + kind, + version: MODEL_VERSION, + scope: SYNTHETIC_SCOPE, + accepted: false, + stopped: true, + reason, + productionEffect: 'not-attempted', + boundary: EFFECT_BOUNDARY, + ...extra, + }); +} + +function caseByCard(card) { + return Object.entries(FIXED_CASES).find(([, fixed]) => hasSameCanonicalJson(card, fixed.card)); +} + +function previewFor(caseId) { + const fixed = FIXED_CASES[caseId]; + if (!fixed) return null; + const card = fixed.card; + return deepFreeze({ + kind: PREVIEW_KIND, + version: MODEL_VERSION, + scope: SYNTHETIC_SCOPE, + caseId, + operationId: card.operationId, + targetSelection: cloneFixed(card.targetSelection), + proposedChange: cloneFixed(card.proposedChange), + requestedAuthority: card.requestedAuthority, + previewDigest: 'synthetic-preview-' + caseId + '-v1', + status: 'preview-only-requires-bounded-authority-and-approval', + previewMeaning: 'fixed synthetic proposal, not a real diff, query, dry-run against a live system, or execution guarantee', + productionEffect: 'not-attempted', + boundary: EFFECT_BOUNDARY, + }); +} + +function approvalRequestFor(caseId) { + const preview = previewFor(caseId); + if (!preview) return null; + return deepFreeze({ + kind: APPROVAL_REQUEST_KIND, + version: MODEL_VERSION, + scope: SYNTHETIC_SCOPE, + caseId, + operationId: preview.operationId, + targetSelection: cloneFixed(preview.targetSelection), + requestedAuthority: preview.requestedAuthority, + previewDigest: preview.previewDigest, + status: 'awaiting-fixed-synthetic-approval', + authorityMeaning: 'bounded synthetic gate, not a human identity, legal authority, production permission, or correctness proof', + productionEffect: 'not-attempted', + boundary: EFFECT_BOUNDARY, + }); +} + +function approvedFor(caseId) { + const fixed = FIXED_CASES[caseId]; + const request = approvalRequestFor(caseId); + if (!fixed || !request || !fixed.approval || caseId !== 'fixed-bounded-approved-v1') return null; + return deepFreeze({ + kind: 'fixed-synthetic-approved-operation-v1', + version: MODEL_VERSION, + scope: SYNTHETIC_SCOPE, + caseId, + operationId: request.operationId, + targetSelection: cloneFixed(request.targetSelection), + previewDigest: request.previewDigest, + authorityId: fixed.authority.authorityId, + approvalId: fixed.approval.approvalId, + approvalDecision: fixed.approval.decision, + status: 'approved-for-simulation-only', + productionEffect: 'not-attempted', + boundary: EFFECT_BOUNDARY, + }); +} + +function receiptFor(caseId) { + const approved = approvedFor(caseId); + if (!approved) return null; + const auditRecord = deepFreeze({ + kind: 'fixed-synthetic-immutable-audit-record-v1', + version: MODEL_VERSION, + scope: SYNTHETIC_SCOPE, + recordId: 'synthetic-audit-bounded-v1', + operationId: approved.operationId, + targetDigest: approved.targetSelection.targetDigest, + previewDigest: approved.previewDigest, + authorityId: approved.authorityId, + approvalId: approved.approvalId, + simulatedAt: '2025-05-20T10:00:00Z', + event: 'simulation-recorded-no-real-change', + immutabilityBoundary: 'Object.freeze in this fixed in-memory model; not durable storage, signature, append-only infrastructure, or tamper-proof evidence', + }); + return deepFreeze({ + kind: EXECUTION_KIND, + version: MODEL_VERSION, + scope: SYNTHETIC_SCOPE, + caseId, + operationId: approved.operationId, + targetSelection: cloneFixed(approved.targetSelection), + status: 'simulated-execution-recorded-no-real-change', + auditRecord, + productionEffect: 'not-attempted', + boundary: EFFECT_BOUNDARY, + }); +} + +function verificationFor(caseId) { + const receipt = receiptFor(caseId); + if (!receipt) return null; + return deepFreeze({ + kind: VERIFICATION_KIND, + version: MODEL_VERSION, + scope: SYNTHETIC_SCOPE, + caseId, + operationId: receipt.operationId, + auditRecordId: receipt.auditRecord.recordId, + targetDigest: receipt.targetSelection.targetDigest, + status: 'requires-external-verification-not-performed', + verificationMeaning: 'the model checks only the exact synthetic receipt; it does not read a target, compare real state, or attest that a change happened', + productionEffect: 'not-attempted', + boundary: EFFECT_BOUNDARY, + }); +} + +export function createFixedSyntheticOperationCard(caseId) { + const fixed = FIXED_CASES[caseId]; + return fixed ? cloneFixed(fixed.card) : null; +} + +export function createFixedSyntheticAuthority(caseId) { + const fixed = FIXED_CASES[caseId]; + return fixed ? cloneFixed(fixed.authority) : null; +} + +export function createFixedSyntheticApproval(caseId) { + const fixed = FIXED_CASES[caseId]; + return fixed && fixed.approval ? cloneFixed(fixed.approval) : null; +} + +export function previewSyntheticAutomation(card) { + const expectedCardKeys = ['caseId', 'kind', 'operationId', 'proposedChange', 'requestedAuthority', 'sequence', 'targetSelection', 'version']; + if (!hasExactKeys(card, expectedCardKeys)) { + return rejected(PREVIEW_KIND, 'invalid-operation-card-shape'); + } + if (!hasExactKeys(card.targetSelection, ['kind', 'selectorId', 'targetDigest', 'targetIds']) || !hasExactKeys(card.proposedChange, ['changeDigest', 'changeType', 'desiredTo', 'expectedFrom', 'kind'])) { + return rejected(PREVIEW_KIND, 'invalid-operation-subcontract'); + } + const entry = caseByCard(card); + if (!entry) return rejected(PREVIEW_KIND, 'untrusted-or-unknown-operation-card'); + return previewFor(entry[0]); +} + +export function requestSyntheticApproval(preview, authority) { + if (!isPlainObject(preview) || !isPlainObject(authority) || !Object.hasOwn(FIXED_CASES, preview.caseId)) { + return rejected(APPROVAL_REQUEST_KIND, 'missing-or-unknown-preview-or-authority'); + } + const fixed = FIXED_CASES[preview.caseId]; + if (!hasSameCanonicalJson(preview, previewFor(preview.caseId))) { + return rejected(APPROVAL_REQUEST_KIND, 'untrusted-preview-report'); + } + if (!hasSameCanonicalJson(authority, fixed.authority)) { + return rejected(APPROVAL_REQUEST_KIND, 'forged-or-wrong-authority-card'); + } + if (authority.authorityId !== preview.requestedAuthority) { + return rejected(APPROVAL_REQUEST_KIND, 'authority-id-does-not-match-request'); + } + if (authority.selectorId !== preview.targetSelection.selectorId) { + return rejected(APPROVAL_REQUEST_KIND, 'authority-selector-does-not-match-target-scope'); + } + if (preview.targetSelection.targetIds.length > authority.maxTargets) { + return rejected(APPROVAL_REQUEST_KIND, 'stop-scope-exceeds-authority-limit', { + targetCount: preview.targetSelection.targetIds.length, + authorityMaxTargets: authority.maxTargets, + }); + } + return approvalRequestFor(preview.caseId); +} + +export function approveSyntheticOperation(request, approval) { + if (!isPlainObject(request) || !isPlainObject(approval) || !Object.hasOwn(FIXED_CASES, request.caseId)) { + return rejected('fixed-synthetic-approved-operation-v1', 'missing-or-unknown-approval-input'); + } + const fixed = FIXED_CASES[request.caseId]; + if (!hasSameCanonicalJson(request, approvalRequestFor(request.caseId))) { + return rejected('fixed-synthetic-approved-operation-v1', 'untrusted-approval-request'); + } + if (!fixed.approval || !hasSameCanonicalJson(approval, fixed.approval)) { + return rejected('fixed-synthetic-approved-operation-v1', 'forged-or-missing-approval-card'); + } + if (approval.operationId !== request.operationId || approval.targetDigest !== request.targetSelection.targetDigest || approval.authorityId !== request.requestedAuthority) { + return rejected('fixed-synthetic-approved-operation-v1', 'stop-approval-does-not-bind-preview-target-and-authority'); + } + return approvedFor(request.caseId); +} + +export function simulateSyntheticExecution(approved) { + if (!isPlainObject(approved) || approved.caseId !== 'fixed-bounded-approved-v1' || !hasSameCanonicalJson(approved, approvedFor('fixed-bounded-approved-v1'))) { + return rejected(EXECUTION_KIND, 'stop-no-exact-approved-synthetic-operation'); + } + return receiptFor('fixed-bounded-approved-v1'); +} + +export function verifySyntheticExecution(receipt) { + if (!isPlainObject(receipt) || receipt.caseId !== 'fixed-bounded-approved-v1' || !hasSameCanonicalJson(receipt, receiptFor('fixed-bounded-approved-v1'))) { + return rejected(VERIFICATION_KIND, 'untrusted-or-forged-synthetic-audit-receipt'); + } + return verificationFor('fixed-bounded-approved-v1'); +} + +export function stopAndPlanSyntheticRollback(value) { + const validVerification = hasSameCanonicalJson(value, verificationFor('fixed-bounded-approved-v1')); + const validReceipt = hasSameCanonicalJson(value, receiptFor('fixed-bounded-approved-v1')); + if (!validVerification && !validReceipt) { + return rejected(STOP_KIND, 'stop-untrusted-input-before-rollback-plan'); + } + return deepFreeze({ + kind: STOP_KIND, + version: MODEL_VERSION, + scope: SYNTHETIC_SCOPE, + stopped: true, + reason: validVerification ? 'verification-not-performed-outside-fixed-model' : 'simulation-boundary-before-external-verification', + rollbackPlan: { + kind: 'fixed-synthetic-rollback-plan-v1', + status: 'planned-only-not-executed', + preconditions: ['identify-exact-scope', 'confirm-observed-state-outside-model', 'obtain-new-authority', 'create-new-preview', 'approve-separate-rollback-operation'], + boundary: 'this is a fixed in-memory plan, not a rollback command or permission to change anything', + }, + productionEffect: 'not-attempted', + boundary: EFFECT_BOUNDARY, + }); +} + +export function runEngineeringAutomationFixture() { + const boundedCard = createFixedSyntheticOperationCard('fixed-bounded-approved-v1'); + const boundedAuthority = createFixedSyntheticAuthority('fixed-bounded-approved-v1'); + const boundedApproval = createFixedSyntheticApproval('fixed-bounded-approved-v1'); + const boundedPreview = previewSyntheticAutomation(boundedCard); + const boundedRequest = requestSyntheticApproval(boundedPreview, boundedAuthority); + const boundedApproved = approveSyntheticOperation(boundedRequest, boundedApproval); + const boundedReceipt = simulateSyntheticExecution(boundedApproved); + const boundedVerification = verifySyntheticExecution(boundedReceipt); + const boundedStop = stopAndPlanSyntheticRollback(boundedVerification); + + const overScopeCard = createFixedSyntheticOperationCard('fixed-over-scope-v1'); + const overScopePreview = previewSyntheticAutomation(overScopeCard); + const overScopeRequest = requestSyntheticApproval(overScopePreview, createFixedSyntheticAuthority('fixed-over-scope-v1')); + + const mismatchCard = createFixedSyntheticOperationCard('fixed-target-mismatch-v1'); + const mismatchPreview = previewSyntheticAutomation(mismatchCard); + const mismatchRequest = requestSyntheticApproval(mismatchPreview, createFixedSyntheticAuthority('fixed-target-mismatch-v1')); + const mismatchApproval = approveSyntheticOperation(mismatchRequest, createFixedSyntheticApproval('fixed-target-mismatch-v1')); + + const lowerCard = createFixedSyntheticOperationCard('fixed-lower-authority-v1'); + const lowerPreview = previewSyntheticAutomation(lowerCard); + const lowerAuthorityRequest = requestSyntheticApproval(lowerPreview, createFixedSyntheticAuthority('fixed-lower-authority-v1')); + + const unknown = previewSyntheticAutomation({ ...boundedCard, caseId: 'fixed-unknown-v1' }); + const extraField = previewSyntheticAutomation({ ...boundedCard, telemetry: 'forbidden' }); + const missingField = previewSyntheticAutomation({ kind: OPERATION_KIND, version: MODEL_VERSION }); + const forgedTarget = previewSyntheticAutomation({ + ...boundedCard, + targetSelection: { ...boundedCard.targetSelection, targetDigest: 'synthetic-targets-forged-v1' }, + }); + const sparseCard = cloneFixed(boundedCard); + const sparseTargets = []; + sparseTargets[1] = sparseCard.targetSelection.targetIds[1]; + const sparseResult = previewSyntheticAutomation({ + ...sparseCard, + targetSelection: { ...sparseCard.targetSelection, targetIds: sparseTargets }, + }); + const cyclicCard = { ...boundedCard }; + cyclicCard.self = cyclicCard; + const cyclicResult = previewSyntheticAutomation(cyclicCard); + const forgedAuthority = requestSyntheticApproval(boundedPreview, { ...boundedAuthority, maxTargets: 3 }); + const forgedRequest = requestSyntheticApproval({ ...boundedPreview, previewDigest: 'forged-preview-v1' }, boundedAuthority); + const forgedApproval = approveSyntheticOperation(boundedRequest, { ...boundedApproval, authorityId: 'forged-authority-v1' }); + const missingApproval = approveSyntheticOperation(boundedRequest, null); + const forgedApproved = simulateSyntheticExecution({ ...boundedApproved, approvalId: 'forged-approval-v1' }); + const forgedReceipt = verifySyntheticExecution({ + ...boundedReceipt, + auditRecord: { ...boundedReceipt.auditRecord, recordId: 'forged-audit-v1' }, + }); + const sparseReceipt = { ...boundedReceipt, targetSelection: { ...boundedReceipt.targetSelection, targetIds: [] } }; + sparseReceipt.targetSelection.targetIds[1] = 'synthetic-doc-b'; + const sparseReceiptResult = verifySyntheticExecution(sparseReceipt); + const cyclicReceipt = { ...boundedReceipt }; + cyclicReceipt.loop = cyclicReceipt; + const cyclicReceiptResult = verifySyntheticExecution(cyclicReceipt); + const forgedStop = stopAndPlanSyntheticRollback({ ...boundedVerification, status: 'verified-production-change' }); + const fixedBefore = canonicalJson(FIXED_CASES['fixed-bounded-approved-v1']); + void previewSyntheticAutomation(boundedCard); + const fixedAfter = canonicalJson(FIXED_CASES['fixed-bounded-approved-v1']); + + return deepFreeze({ + kind: 'fixed-synthetic-engineering-automation-fixture-v1', + version: MODEL_VERSION, + scope: SYNTHETIC_SCOPE, + assertions: { + boundedPreviewIsOnlyPreview: boundedPreview.status === 'preview-only-requires-bounded-authority-and-approval' && boundedPreview.productionEffect === 'not-attempted', + boundedPreviewKeepsExactTargets: boundedPreview.targetSelection.targetIds.join(',') === 'synthetic-doc-a,synthetic-doc-b' && boundedPreview.targetSelection.targetDigest === 'synthetic-targets-a-b-v1', + authorityCreatesSeparateGate: boundedRequest.status === 'awaiting-fixed-synthetic-approval' && boundedRequest.accepted === undefined, + boundedApprovalBindsOperationAndTarget: boundedApproved.status === 'approved-for-simulation-only' && boundedApproved.approvalId === 'synthetic-approval-bounded-v1', + simulationNeverClaimsRealExecution: boundedReceipt.status === 'simulated-execution-recorded-no-real-change' && boundedReceipt.productionEffect === 'not-attempted', + auditRecordIsFrozenInMemoryOnly: Object.isFrozen(boundedReceipt.auditRecord) && boundedReceipt.auditRecord.immutabilityBoundary.includes('not durable storage'), + verificationDoesNotClaimObservedState: boundedVerification.status === 'requires-external-verification-not-performed' && boundedVerification.productionEffect === 'not-attempted', + safeStopCreatesPlanOnly: boundedStop.stopped === true && boundedStop.rollbackPlan.status === 'planned-only-not-executed', + safeStopNamesNewAuthorityForRollback: boundedStop.rollbackPlan.preconditions.includes('obtain-new-authority'), + overScopeStopsBeforeApproval: overScopeRequest.accepted === false && overScopeRequest.reason === 'stop-scope-exceeds-authority-limit', + overScopeNamesCounts: overScopeRequest.targetCount === 3 && overScopeRequest.authorityMaxTargets === 2, + targetMismatchStopsAtApproval: mismatchApproval.accepted === false && mismatchApproval.reason === 'stop-approval-does-not-bind-preview-target-and-authority', + lowerAuthorityStopsBeforeApproval: lowerAuthorityRequest.accepted === false && lowerAuthorityRequest.reason === 'stop-scope-exceeds-authority-limit', + unknownCaseFailsClosed: unknown.accepted === false && unknown.reason === 'untrusted-or-unknown-operation-card', + unexpectedFieldFailsClosed: extraField.accepted === false && extraField.reason === 'invalid-operation-card-shape', + missingFieldsFailClosed: missingField.accepted === false && missingField.reason === 'invalid-operation-card-shape', + forgedTargetFailsClosed: forgedTarget.accepted === false && forgedTarget.reason === 'untrusted-or-unknown-operation-card', + sparseTargetsDoNotThrow: sparseResult.accepted === false && sparseResult.reason === 'untrusted-or-unknown-operation-card', + cyclicCardDoesNotThrow: cyclicResult.accepted === false && cyclicResult.reason === 'invalid-operation-card-shape', + forgedAuthorityFailsClosed: forgedAuthority.accepted === false && forgedAuthority.reason === 'forged-or-wrong-authority-card', + forgedPreviewFailsClosed: forgedRequest.accepted === false && forgedRequest.reason === 'untrusted-preview-report', + forgedApprovalFailsClosed: forgedApproval.accepted === false && forgedApproval.reason === 'forged-or-missing-approval-card', + missingApprovalFailsClosed: missingApproval.accepted === false && missingApproval.reason === 'missing-or-unknown-approval-input', + forgedApprovedOperationCannotSimulate: forgedApproved.accepted === false && forgedApproved.reason === 'stop-no-exact-approved-synthetic-operation', + forgedAuditRecordCannotVerify: forgedReceipt.accepted === false && forgedReceipt.reason === 'untrusted-or-forged-synthetic-audit-receipt', + sparseReceiptCannotVerify: sparseReceiptResult.accepted === false && sparseReceiptResult.reason === 'untrusted-or-forged-synthetic-audit-receipt', + cyclicReceiptDoesNotThrow: cyclicReceiptResult.accepted === false && cyclicReceiptResult.reason === 'untrusted-or-forged-synthetic-audit-receipt', + forgedStopCannotPlanRollback: forgedStop.accepted === false && forgedStop.reason === 'stop-untrusted-input-before-rollback-plan', + fixedCasesStayFrozenAndUnchanged: fixedBefore === fixedAfter && Object.isFrozen(FIXED_CASES['fixed-bounded-approved-v1']) && Object.isFrozen(FIXED_CASES['fixed-bounded-approved-v1'].card.targetSelection.targetIds), + effectBoundaryForbidsAllExternalActions: boundedPreview.boundary.files === 'not-read-or-written' && boundedReceipt.boundary.network === 'not-used' && boundedStop.boundary.realChange === 'not-attempted', + canonicalComparisonRejectsCycle: hasSameCanonicalJson(cyclicCard, boundedCard) === false, + }, + samples: { + boundedPreview, + boundedRequest, + boundedApproved, + boundedReceipt, + boundedVerification, + boundedStop, + overScopeRequest, + mismatchApproval, + lowerAuthorityRequest, + }, + }); +} + +const fixtureExample = [ + "import {", + " createFixedSyntheticOperationCard,", + " createFixedSyntheticAuthority,", + " createFixedSyntheticApproval,", + " previewSyntheticAutomation,", + " requestSyntheticApproval,", + " approveSyntheticOperation,", + " simulateSyntheticExecution,", + " verifySyntheticExecution,", + " stopAndPlanSyntheticRollback,", + " runEngineeringAutomationFixture,", + "} from './upgrade-2025-05.mjs';", + '', + "const caseId = 'fixed-bounded-approved-v1';", + 'const preview = previewSyntheticAutomation(createFixedSyntheticOperationCard(caseId));', + 'const request = requestSyntheticApproval(preview, createFixedSyntheticAuthority(caseId));', + 'const approved = approveSyntheticOperation(request, createFixedSyntheticApproval(caseId));', + 'const receipt = simulateSyntheticExecution(approved);', + 'const verification = verifySyntheticExecution(receipt);', + 'const stop = stopAndPlanSyntheticRollback(verification);', + '', + 'if (!Object.values(runEngineeringAutomationFixture().assertions).every(Boolean)) {', + " throw new Error('fixed synthetic fixture failed');", + '}', + '', + 'console.log({ preview: preview.status, execute: receipt.status, verify: verification.status, rollback: stop.rollbackPlan.status });', + '', + '// Fixed synthetic values in memory only.', + '// No files, Git, network, CI, clock, telemetry, production, cloud, real API or real change is used.', + '// simulateSyntheticExecution records a model receipt; it never executes an external operation.', +].join('\n'); + +const practice = revision({ + slug: 'editorial-2025-05-practice-engineering-automation', + title: 'Автоматизация инженерной рутины без массовой ошибки', + categories: ['Инфраструктура', 'Процессы'], + cover: '/assets/editorial/2025/engineering-automation-2025-preview-approval-rollback.svg', + excerpt: 'Как разбить массовую инженерную операцию на preview, ограниченный scope, отдельное approval, проверку результата и заранее спланированный rollback.', + readingMinutes: 13, +}, [ + p('Рутина становится опасной в момент, когда один корректный шаг превращают в массовый. Скрипт умеет переименовать label, обновить правило или закрыть устаревший флаг на одном объекте — и тем же циклом затрагивает тысячу. Симптом обычно спокойный: список targets собран, команда выглядит идемпотентной, а change уже нельзя глазами просмотреть. Цена ошибки конкретна: массово неверное состояние, длинное восстановление, потерянное время reviewers и спор, какой именно запуск изменил объект.'), + p('Причина не в том, что автоматизация сама по себе плоха. Ошибка появляется, когда preview считают доказательством будущего результата, approval — доказательством правильности, а audit log — страховкой от последствий. Это три разных вопроса: что предлагается, кто разрешил ограниченную операцию, и что зафиксировал процесс. Между ними остаются selection, момент запуска, реальные права, состояние цели и проверка после change. Поэтому процесс нужно строить как цепочку доказательств, а не как одну зелёную кнопку.'), + h2('Симптом → причина → проверка → действие'), + ol([ + 'Симптом. Один job готов обработать широкий selector, но reviewer видит только краткое «N объектов» и общий статус success.', + 'Причина. Команда не связала target list, preview, authority, approval и audit record одним неизменяемым идентификатором операции.', + 'Проверка. До запуска сравнить exact target digest и limit authority; после запуска запросить независимый observed-state check, а не только exit code job.', + 'Действие. Разделить путь на preview → bounded scope → approval → execute → audit trail → verification; rollback оформить отдельной операцией с новым preview и approval.', + ]), + h2('Preview показывает намерение, а не будущую реальность'), + p('Preview полезен, когда отвечает на короткий вопрос: «какие именно объекты и какое изменение мы сейчас предлагаем?» В Terraform документ для версии 1.10.5 прямо отделяет 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');