This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Производство редакционных партий
|
# Производство редакционных партий
|
||||||
|
|
||||||
На 31 июля 2026 года строгий аудит проходит 262 из 358 созданных материалов. Остальные 96 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
|
На 31 июля 2026 года строгий аудит проходит 265 из 358 созданных материалов. Остальные 93 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
|
||||||
|
|
||||||
## Одна партия
|
## Одна партия
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
# P87 — май 2025: автоматизация инженерной рутины без массовой ошибки
|
||||||
|
|
||||||
|
## Область draft-пакета
|
||||||
|
|
||||||
|
Пакет заменяет только три overlay-статьи:
|
||||||
|
|
||||||
|
- <code>editorial-2025-05-practice-engineering-automation</code> — «Автоматизация инженерной рутины без массовой ошибки».
|
||||||
|
- <code>editorial-2025-05-mechanism-engineering-automation</code> — «Почему preview, approval и audit trail не доказывают безопасный change».
|
||||||
|
- <code>editorial-2025-05-field-engineering-automation</code> — «Полевой маршрут: автоматизация без массового неверного change».
|
||||||
|
|
||||||
|
Единственный исполняемый артефакт — <code>web/scripts/upgrade-2025-05.mjs</code>.
|
||||||
|
Все 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.
|
||||||
|
|
||||||
|
<code>simulateSyntheticExecution</code> записывает только model receipt с
|
||||||
|
<code>productionEffect: not-attempted</code>. Его immutable audit record
|
||||||
|
заморожен через <code>Object.freeze</code> внутри модуля; это не durable,
|
||||||
|
append-only или tamper-proof storage. <code>stopAndPlanSyntheticRollback</code>
|
||||||
|
создаёт только новый plan с <code>planned-only-not-executed</code>; он не
|
||||||
|
наследует approval и не выполняет rollback.
|
||||||
|
|
||||||
|
Registry, README, очередь, <code>articles.json</code>, app-страницы, full
|
||||||
|
build, Git staging, commit и push намеренно не менялись.
|
||||||
|
|
||||||
|
## Исследование и историческая граница
|
||||||
|
|
||||||
|
Проверка URL выполнена 31.07.2026 обычными HTTPS-запросами
|
||||||
|
<code>curl -L --fail</code> / <code>curl -I -L --fail</code>, без TLS bypass
|
||||||
|
и без авторизации. HTTP 200 получены для всех трёх закреплённых источников.
|
||||||
|
Текущая доступность не заменяет историческую границу: все ссылки ведут на
|
||||||
|
immutable commit, существовавший к маю 2025.
|
||||||
|
|
||||||
|
| Источник | Историческое закрепление | Узко подтверждаемое утверждение | Граница утверждения |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| [Terraform plan](https://raw.githubusercontent.com/hashicorp/terraform/898e397ebaefb4d0c363b7332b3ba184b8852162/website/docs/cli/commands/plan.mdx) | commit <code>898e397ebaefb4d0c363b7332b3ba184b8852162</code>; tag <code>v1.10.5</code>; commit date 22.01.2025 | <code>terraform plan</code> сам не выполняет 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 <code>6a92295d73cd86b37a810a87f45e7deb21d10a0f</code>, 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 <code>c889d9b2510bba78dbb527a79b8e2099b16f3d67</code>, 14.03.2024 | <code>--dry-run=client</code> печатает object без отправки; <code>--dry-run=server</code> отправляет server-side request без persistence ресурса. | Не обещает, что поздний persistent apply увидит такое же state или policy, и не описывает другой инструмент. |
|
||||||
|
|
||||||
|
Вывод автора — «preview, approval и audit trail отвечают на разные вопросы» —
|
||||||
|
не приписан как гарантия ни Terraform, ни GitHub Actions, ни Kubernetes. Он
|
||||||
|
следует из разнесённых contracts учебной модели.
|
||||||
|
|
||||||
|
## Проход 1 — факты, историческая граница и техника
|
||||||
|
|
||||||
|
**Проверена модель.**
|
||||||
|
|
||||||
|
- <code>previewSyntheticAutomation</code> принимает только exact fixed
|
||||||
|
operation card: operation, selector, dense targets, target digest,
|
||||||
|
proposed change и requested authority.
|
||||||
|
- <code>requestSyntheticApproval</code> отдельно проверяет exact preview,
|
||||||
|
authority id, allowed selector и maximum targets.
|
||||||
|
- <code>approveSyntheticOperation</code> связывает operation id, preview
|
||||||
|
digest, target digest и authority id. Approval с другим target digest
|
||||||
|
останавливается до simulation.
|
||||||
|
- <code>simulateSyntheticExecution</code> создаёт только in-memory receipt и
|
||||||
|
audit record без real external effect.
|
||||||
|
- <code>verifySyntheticExecution</code> проверяет только exact synthetic
|
||||||
|
receipt и возвращает <code>requires-external-verification-not-performed</code>;
|
||||||
|
он не читает target и не объявляет production state проверенным.
|
||||||
|
- <code>stopAndPlanSyntheticRollback</code> требует trusted receipt/report и
|
||||||
|
возвращает новый rollback plan, а не command. Для реального rollback нужны
|
||||||
|
новый scope, authority, preview и approval.
|
||||||
|
|
||||||
|
**Проверены отрицательные ветки.**
|
||||||
|
|
||||||
|
| Случай | Где закрывается путь | Ожидаемый результат |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Unknown, extra или missing operation field | preview contract | fail-closed, <code>productionEffect: not-attempted</code> |
|
||||||
|
| Forged target digest | exact fixed card comparison | <code>untrusted-or-unknown-operation-card</code> |
|
||||||
|
| Three targets при authority limit два | authority gate | <code>stop-scope-exceeds-authority-limit</code> до approval |
|
||||||
|
| Forged authority card | authority gate | <code>forged-or-wrong-authority-card</code> |
|
||||||
|
| Approval A+C для preview A+B | approval binding | <code>stop-approval-does-not-bind-preview-target-and-authority</code> |
|
||||||
|
| Sparse/cyclic card или receipt | canonical comparison | reject без exception и без execution |
|
||||||
|
| Forged audit record | verification entry | <code>untrusted-or-forged-synthetic-audit-receipt</code> |
|
||||||
|
| 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. |
|
||||||
|
|
||||||
|
Редакторская вычитка:
|
||||||
|
|
||||||
|
- Каждая статья прямо называет проблему и <strong>цену ошибки</strong> в
|
||||||
|
первых двух <code><p></code>; это проверяет усиленный audit.
|
||||||
|
- В каждой статье есть table, figure с meaningful <code>alt</code> и 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 явно отделены от фактов источников; <code>immutable</code>
|
||||||
|
не выдан за свойство внешнего хранилища.
|
||||||
|
|
||||||
|
**Вердикт прохода 2:** пройдено. Статьи стали короче после саморевью, сохранив
|
||||||
|
таблицы, examples, ограничения, owner и final action.
|
||||||
|
|
||||||
|
## Проход 3 — SVG, fixture и усиленный audit draft
|
||||||
|
|
||||||
|
Проверены три собственных SVG:
|
||||||
|
|
||||||
|
| Asset | Смысл | Итог 375 px |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| <code>engineering-automation-2025-preview-approval-rollback.svg</code> | Семь переходов и красная stop/rollback-plan ветка. | Footer сокращён после первого render; все карточки и подписи читаемы. |
|
||||||
|
| <code>engineering-automation-2025-authority-matrix.svg</code> | Authority limit, target mismatch, execute receipt и boundary verification. | Правый столбец и fail-closed footer сокращены после первого render; таблица читаема. |
|
||||||
|
| <code>engineering-automation-2025-dry-run-evidence-loop.svg</code> | Evidence loop от preview до observed verification и нового plan. | Footer сокращён после первого render; пунктирный возврат к новому plan виден. |
|
||||||
|
|
||||||
|
Последовательность проверок:
|
||||||
|
|
||||||
|
- <code>node --check scripts/upgrade-2025-05.mjs</code> — PASS.
|
||||||
|
- <code>node scripts/upgrade-2025-05.mjs --verify-fixture</code> — PASS,
|
||||||
|
<strong>31/31</strong> assertions.
|
||||||
|
- <code>npm run audit:draft -- scripts/upgrade-2025-05.mjs</code> — PASS для
|
||||||
|
всех трёх slugs, включая requirement о цене ошибки в первых двух paragraphs
|
||||||
|
и диапазон 5 000–15 000.
|
||||||
|
- <code>xmllint --noout</code> — PASS для трёх SVG.
|
||||||
|
- SVG safety scan — чисто: нет <code>script</code>,
|
||||||
|
<code>foreignObject</code>, <code>javascript:</code>,
|
||||||
|
<code>data:image</code> и event-handler attributes.
|
||||||
|
- Sharp render на ширине 375 px и ручной visual review — PASS после
|
||||||
|
сокращения трёх footer/column labels.
|
||||||
|
|
||||||
|
Full build и registry audit намеренно не запускались. Registry, README, queue,
|
||||||
|
<code>articles.json</code>, 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 страницы.
|
||||||
@@ -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 february2025Revisions } from '../scripts/upgrade-2025-02.mjs';
|
||||||
import { revisions as march2025Revisions } from '../scripts/upgrade-2025-03.mjs';
|
import { revisions as march2025Revisions } from '../scripts/upgrade-2025-03.mjs';
|
||||||
import { revisions as april2025Revisions } from '../scripts/upgrade-2025-04.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.
|
// This layer replaces archived source entries without losing their stable slug and date.
|
||||||
export const editorialRevisions = [
|
export const editorialRevisions = [
|
||||||
@@ -171,4 +172,5 @@ export const editorialRevisions = [
|
|||||||
...february2025Revisions,
|
...february2025Revisions,
|
||||||
...march2025Revisions,
|
...march2025Revisions,
|
||||||
...april2025Revisions,
|
...april2025Revisions,
|
||||||
|
...may2025Revisions,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 375 720" role="img" aria-labelledby="title desc">
|
||||||
|
<title id="title">Матрица authority и границ автоматизации</title>
|
||||||
|
<desc id="desc">Таблица показывает, что authority level допускает ограниченный selector и число targets, но не заменяет preview, approval binding, audit trail и verification.</desc>
|
||||||
|
<rect width="375" height="720" fill="#0b1220"/>
|
||||||
|
<text x="20" y="34" fill="#f8fafc" font-family="Arial, sans-serif" font-size="18" font-weight="700">Authority ограничивает scope</text>
|
||||||
|
<text x="20" y="55" fill="#a5b4fc" font-family="Arial, sans-serif" font-size="12">Она не делает change верным и не заменяет verification</text>
|
||||||
|
<g font-family="Arial, sans-serif" font-size="11">
|
||||||
|
<rect x="20" y="82" width="335" height="46" rx="8" fill="#1e293b"/>
|
||||||
|
<text x="32" y="104" fill="#cbd5e1" font-weight="700">Проверка</text>
|
||||||
|
<text x="172" y="104" fill="#cbd5e1" font-weight="700">Разрешает?</text>
|
||||||
|
<text x="264" y="104" fill="#cbd5e1" font-weight="700">Не доказывает</text>
|
||||||
|
<rect x="20" y="128" width="335" height="94" fill="#172554" stroke="#334155"/>
|
||||||
|
<text x="32" y="153" fill="#dbeafe" font-weight="700">selector + max targets</text>
|
||||||
|
<text x="32" y="174" fill="#bfdbfe">bounded A+B</text>
|
||||||
|
<text x="172" y="164" fill="#86efac">ДА</text>
|
||||||
|
<text x="264" y="153" fill="#bfdbfe">смысл change</text>
|
||||||
|
<text x="264" y="174" fill="#bfdbfe">observed result</text>
|
||||||
|
<rect x="20" y="222" width="335" height="94" fill="#3b2f0b" stroke="#334155"/>
|
||||||
|
<text x="32" y="247" fill="#fef3c7" font-weight="700">selector + 3 targets</text>
|
||||||
|
<text x="32" y="268" fill="#fde68a">limit = 2</text>
|
||||||
|
<text x="172" y="258" fill="#fda4af">STOP</text>
|
||||||
|
<text x="264" y="247" fill="#fde68a">нужно новое</text>
|
||||||
|
<text x="264" y="268" fill="#fde68a">authority решение</text>
|
||||||
|
<rect x="20" y="316" width="335" height="94" fill="#4c1020" stroke="#334155"/>
|
||||||
|
<text x="32" y="341" fill="#ffe4e6" font-weight="700">approval digest ≠ preview</text>
|
||||||
|
<text x="32" y="362" fill="#fecdd3">targets A+C вместо A+B</text>
|
||||||
|
<text x="172" y="352" fill="#fda4af">STOP</text>
|
||||||
|
<text x="264" y="341" fill="#fecdd3">review нельзя</text>
|
||||||
|
<text x="264" y="362" fill="#fecdd3">переносить</text>
|
||||||
|
<rect x="20" y="410" width="335" height="94" fill="#123524" stroke="#334155"/>
|
||||||
|
<text x="32" y="435" fill="#d1fae5" font-weight="700">bound preview + approval</text>
|
||||||
|
<text x="32" y="456" fill="#a7f3d0">preconditions re-checked</text>
|
||||||
|
<text x="172" y="446" fill="#86efac">EXECUTE</text>
|
||||||
|
<text x="264" y="435" fill="#a7f3d0">не равен</text>
|
||||||
|
<text x="264" y="456" fill="#a7f3d0">verified state</text>
|
||||||
|
<rect x="20" y="504" width="335" height="94" fill="#1e293b" stroke="#334155"/>
|
||||||
|
<text x="32" y="529" fill="#e2e8f0" font-weight="700">audit event exists</text>
|
||||||
|
<text x="32" y="550" fill="#cbd5e1">links preserved</text>
|
||||||
|
<text x="172" y="540" fill="#fbbf24">TRACE</text>
|
||||||
|
<text x="264" y="529" fill="#cbd5e1">observed state</text>
|
||||||
|
<text x="264" y="550" fill="#cbd5e1">на target</text>
|
||||||
|
</g>
|
||||||
|
<rect x="20" y="624" width="335" height="62" rx="10" fill="#4c1020" stroke="#fb7185"/>
|
||||||
|
<text x="34" y="649" fill="#ffe4e6" font-family="Arial, sans-serif" font-size="13" font-weight="700">Правило fail-closed</text>
|
||||||
|
<text x="34" y="670" fill="#fecdd3" font-family="Arial, sans-serif" font-size="12">unknown / mismatch / limit → no execute</text>
|
||||||
|
<text x="20" y="708" fill="#94a3b8" font-family="Arial, sans-serif" font-size="10">Authority — gate scope, а не универсальная гарантия.</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.8 KiB |
+60
@@ -0,0 +1,60 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 375 860" role="img" aria-labelledby="title desc">
|
||||||
|
<title id="title">Петля evidence для инженерной автоматизации</title>
|
||||||
|
<desc id="desc">Вертикальная петля показывает, как preview и dry-run создают evidence, authority и approval проверяют scope, execute и audit trail фиксируют попытку, а verification наблюдает target. Красная ветка создаёт только rollback plan.</desc>
|
||||||
|
<rect width="375" height="860" fill="#0b1220"/>
|
||||||
|
<text x="22" y="33" fill="#f8fafc" font-family="Arial, sans-serif" font-size="18" font-weight="700">Evidence loop, а не «зелёный job»</text>
|
||||||
|
<text x="22" y="54" fill="#a5b4fc" font-family="Arial, sans-serif" font-size="12">Каждая карточка отвечает на другой вопрос</text>
|
||||||
|
<g font-family="Arial, sans-serif">
|
||||||
|
<g>
|
||||||
|
<rect x="24" y="82" width="327" height="78" rx="12" fill="#172554" stroke="#60a5fa"/>
|
||||||
|
<text x="42" y="110" fill="#dbeafe" font-size="15" font-weight="700">PREVIEW / DRY-RUN</text>
|
||||||
|
<text x="42" y="132" fill="#bfdbfe" font-size="12">что предлагается; какой источник</text>
|
||||||
|
<text x="42" y="148" fill="#bfdbfe" font-size="12">и какая side-effect boundary</text>
|
||||||
|
</g>
|
||||||
|
<path d="M187 160v22" stroke="#60a5fa" stroke-width="2"/>
|
||||||
|
<path d="M182 177l5 6 5-6" fill="none" stroke="#60a5fa" stroke-width="2"/>
|
||||||
|
<g>
|
||||||
|
<rect x="24" y="190" width="327" height="78" rx="12" fill="#172554" stroke="#60a5fa"/>
|
||||||
|
<text x="42" y="218" fill="#dbeafe" font-size="15" font-weight="700">SCOPE + AUTHORITY</text>
|
||||||
|
<text x="42" y="240" fill="#bfdbfe" font-size="12">exact ids, digest, selector, max count</text>
|
||||||
|
<text x="42" y="256" fill="#bfdbfe" font-size="12">scope wider than limit → STOP</text>
|
||||||
|
</g>
|
||||||
|
<path d="M187 268v22" stroke="#60a5fa" stroke-width="2"/>
|
||||||
|
<path d="M182 285l5 6 5-6" fill="none" stroke="#60a5fa" stroke-width="2"/>
|
||||||
|
<g>
|
||||||
|
<rect x="24" y="298" width="327" height="78" rx="12" fill="#172554" stroke="#60a5fa"/>
|
||||||
|
<text x="42" y="326" fill="#dbeafe" font-size="15" font-weight="700">APPROVAL BINDING</text>
|
||||||
|
<text x="42" y="348" fill="#bfdbfe" font-size="12">operation + preview + target + authority</text>
|
||||||
|
<text x="42" y="364" fill="#bfdbfe" font-size="12">digest mismatch → STOP</text>
|
||||||
|
</g>
|
||||||
|
<path d="M187 376v22" stroke="#34d399" stroke-width="2"/>
|
||||||
|
<path d="M182 393l5 6 5-6" fill="none" stroke="#34d399" stroke-width="2"/>
|
||||||
|
<g>
|
||||||
|
<rect x="24" y="406" width="327" height="76" rx="12" fill="#123524" stroke="#34d399"/>
|
||||||
|
<text x="42" y="434" fill="#d1fae5" font-size="15" font-weight="700">EXECUTE + AUDIT EVENT</text>
|
||||||
|
<text x="42" y="456" fill="#a7f3d0" font-size="12">attempt recorded with links and result</text>
|
||||||
|
<text x="42" y="472" fill="#a7f3d0" font-size="12">receipt is not target state</text>
|
||||||
|
</g>
|
||||||
|
<path d="M187 482v22" stroke="#fbbf24" stroke-width="2"/>
|
||||||
|
<path d="M182 499l5 6 5-6" fill="none" stroke="#fbbf24" stroke-width="2"/>
|
||||||
|
<g>
|
||||||
|
<rect x="24" y="512" width="327" height="76" rx="12" fill="#3b2f0b" stroke="#fbbf24"/>
|
||||||
|
<text x="42" y="540" fill="#fef3c7" font-size="15" font-weight="700">INDEPENDENT VERIFY</text>
|
||||||
|
<text x="42" y="562" fill="#fde68a" font-size="12">authoritative reader compares expected</text>
|
||||||
|
<text x="42" y="578" fill="#fde68a" font-size="12">and observed state on exact scope</text>
|
||||||
|
</g>
|
||||||
|
<path d="M187 588v22" stroke="#fb7185" stroke-width="2"/>
|
||||||
|
<path d="M182 605l5 6 5-6" fill="none" stroke="#fb7185" stroke-width="2"/>
|
||||||
|
<g>
|
||||||
|
<rect x="24" y="618" width="327" height="120" rx="12" fill="#4c1020" stroke="#fb7185" stroke-width="2"/>
|
||||||
|
<text x="42" y="648" fill="#ffe4e6" font-size="16" font-weight="700">UNKNOWN / MISMATCH → STOP</text>
|
||||||
|
<text x="42" y="672" fill="#fecdd3" font-size="12">preserve reason and observed subset</text>
|
||||||
|
<text x="42" y="692" fill="#fecdd3" font-size="12">new inventory → new bounded rollback plan</text>
|
||||||
|
<text x="42" y="712" fill="#fecdd3" font-size="12">new authority + preview + approval before write</text>
|
||||||
|
</g>
|
||||||
|
<path d="M187 738v28c0 12 18 12 18 0v-626" fill="none" stroke="#818cf8" stroke-width="2" stroke-dasharray="5 5"/>
|
||||||
|
<path d="M200 144l5 -7 5 7" fill="none" stroke="#818cf8" stroke-width="2"/>
|
||||||
|
</g>
|
||||||
|
<text x="22" y="807" fill="#c4b5fd" font-family="Arial, sans-serif" font-size="12">Пунктир: новый plan начинается с нового preview и approval.</text>
|
||||||
|
<text x="22" y="834" fill="#94a3b8" font-family="Arial, sans-serif" font-size="10">Audit = trace; verification = observed state. Они не равны.</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.7 KiB |
+67
@@ -0,0 +1,67 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 375 940" role="img" aria-labelledby="title desc">
|
||||||
|
<title id="title">Цепочка защиты массовой инженерной операции</title>
|
||||||
|
<desc id="desc">Вертикальный маршрут: preview, ограниченный scope, authority, approval, simulated execute, audit trail, verification и красная ветка безопасной остановки с отдельным rollback plan.</desc>
|
||||||
|
<rect width="375" height="940" fill="#0b1220"/>
|
||||||
|
<text x="24" y="32" fill="#f8fafc" font-family="Arial, sans-serif" font-size="18" font-weight="700">Один change — семь разных доказательств</text>
|
||||||
|
<text x="24" y="54" fill="#a5b4fc" font-family="Arial, sans-serif" font-size="12">Каждый переход имеет свой contract и stop path</text>
|
||||||
|
<g font-family="Arial, sans-serif">
|
||||||
|
<g>
|
||||||
|
<rect x="24" y="80" width="327" height="74" rx="12" fill="#172554" stroke="#60a5fa"/>
|
||||||
|
<text x="42" y="108" fill="#dbeafe" font-size="15" font-weight="700">1 · PREVIEW / DRY-RUN</text>
|
||||||
|
<text x="42" y="130" fill="#bfdbfe" font-size="12">proposal, selector, targets, digest</text>
|
||||||
|
<text x="42" y="146" fill="#bfdbfe" font-size="12">не равен будущему execution</text>
|
||||||
|
</g>
|
||||||
|
<path d="M187 154v20" stroke="#60a5fa" stroke-width="2"/>
|
||||||
|
<path d="M182 169l5 6 5-6" fill="none" stroke="#60a5fa" stroke-width="2"/>
|
||||||
|
<g>
|
||||||
|
<rect x="24" y="182" width="327" height="68" rx="12" fill="#172554" stroke="#60a5fa"/>
|
||||||
|
<text x="42" y="209" fill="#dbeafe" font-size="15" font-weight="700">2 · BOUNDED SCOPE</text>
|
||||||
|
<text x="42" y="231" fill="#bfdbfe" font-size="12">allowed selector + max target count</text>
|
||||||
|
</g>
|
||||||
|
<path d="M187 250v20" stroke="#60a5fa" stroke-width="2"/>
|
||||||
|
<path d="M182 265l5 6 5-6" fill="none" stroke="#60a5fa" stroke-width="2"/>
|
||||||
|
<g>
|
||||||
|
<rect x="24" y="278" width="327" height="68" rx="12" fill="#172554" stroke="#60a5fa"/>
|
||||||
|
<text x="42" y="305" fill="#dbeafe" font-size="15" font-weight="700">3 · AUTHORITY</text>
|
||||||
|
<text x="42" y="327" fill="#bfdbfe" font-size="12">level и scope limit, не оценка смысла</text>
|
||||||
|
</g>
|
||||||
|
<path d="M187 346v20" stroke="#60a5fa" stroke-width="2"/>
|
||||||
|
<path d="M182 361l5 6 5-6" fill="none" stroke="#60a5fa" stroke-width="2"/>
|
||||||
|
<g>
|
||||||
|
<rect x="24" y="374" width="327" height="68" rx="12" fill="#172554" stroke="#60a5fa"/>
|
||||||
|
<text x="42" y="401" fill="#dbeafe" font-size="15" font-weight="700">4 · APPROVAL</text>
|
||||||
|
<text x="42" y="423" fill="#bfdbfe" font-size="12">binds preview digest, target digest, authority</text>
|
||||||
|
</g>
|
||||||
|
<path d="M187 442v20" stroke="#34d399" stroke-width="2"/>
|
||||||
|
<path d="M182 457l5 6 5-6" fill="none" stroke="#34d399" stroke-width="2"/>
|
||||||
|
<g>
|
||||||
|
<rect x="24" y="470" width="327" height="68" rx="12" fill="#123524" stroke="#34d399"/>
|
||||||
|
<text x="42" y="497" fill="#d1fae5" font-size="15" font-weight="700">5 · EXECUTE</text>
|
||||||
|
<text x="42" y="519" fill="#a7f3d0" font-size="12">re-check preconditions before any write</text>
|
||||||
|
</g>
|
||||||
|
<path d="M187 538v20" stroke="#34d399" stroke-width="2"/>
|
||||||
|
<path d="M182 553l5 6 5-6" fill="none" stroke="#34d399" stroke-width="2"/>
|
||||||
|
<g>
|
||||||
|
<rect x="24" y="566" width="327" height="68" rx="12" fill="#123524" stroke="#34d399"/>
|
||||||
|
<text x="42" y="593" fill="#d1fae5" font-size="15" font-weight="700">6 · AUDIT TRAIL</text>
|
||||||
|
<text x="42" y="615" fill="#a7f3d0" font-size="12">event и links; не observed state</text>
|
||||||
|
</g>
|
||||||
|
<path d="M187 634v20" stroke="#fbbf24" stroke-width="2"/>
|
||||||
|
<path d="M182 649l5 6 5-6" fill="none" stroke="#fbbf24" stroke-width="2"/>
|
||||||
|
<g>
|
||||||
|
<rect x="24" y="662" width="327" height="68" rx="12" fill="#3b2f0b" stroke="#fbbf24"/>
|
||||||
|
<text x="42" y="689" fill="#fef3c7" font-size="15" font-weight="700">7 · VERIFICATION</text>
|
||||||
|
<text x="42" y="711" fill="#fde68a" font-size="12">independent reader: expected vs observed</text>
|
||||||
|
</g>
|
||||||
|
<path d="M187 730v20" stroke="#fb7185" stroke-width="2"/>
|
||||||
|
<path d="M182 745l5 6 5-6" fill="none" stroke="#fb7185" stroke-width="2"/>
|
||||||
|
<g>
|
||||||
|
<rect x="24" y="758" width="327" height="128" rx="12" fill="#4c1020" stroke="#fb7185" stroke-width="2"/>
|
||||||
|
<text x="42" y="788" fill="#ffe4e6" font-size="16" font-weight="700">STOP → NEW ROLLBACK PLAN</text>
|
||||||
|
<text x="42" y="813" fill="#fecdd3" font-size="12">mismatch / unknown / stale evidence</text>
|
||||||
|
<text x="42" y="833" fill="#fecdd3" font-size="12">new scope → new authority → new preview</text>
|
||||||
|
<text x="42" y="853" fill="#fecdd3" font-size="12">rollback plan ≠ permission to execute it</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<text x="24" y="918" fill="#94a3b8" font-family="Arial, sans-serif" font-size="11">Один барьер не снимает весь риск: он защищает свой переход.</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -0,0 +1,915 @@
|
|||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value)
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll("'", ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
const p = (text) => '<p>' + text + '</p>';
|
||||||
|
const h2 = (text) => '<h2>' + text + '</h2>';
|
||||||
|
const code = (text) => '<pre><code>' + escapeHtml(text) + '</code></pre>';
|
||||||
|
const ol = (items) => '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
|
||||||
|
const figure = (src, alt, caption) => '<figure><img src="' + src + '" alt="' + alt + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
|
||||||
|
const table = (caption, headers, rows) => '<div class="table-scroll"><table><caption>' + caption + '</caption><thead><tr>' + headers.map((item) => '<th scope="col">' + item + '</th>').join('') + '</tr></thead><tbody>' + rows.map((row) => '<tr>' + row.map((item) => '<td>' + item + '</td>').join('') + '</tr>').join('') + '</tbody></table></div>';
|
||||||
|
|
||||||
|
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>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 '<ul>' + sources.map((source) => '<li><a href="' + source.url + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(source.title) + '</a> — ' + escapeHtml(source.claim) + ' Граница: ' + escapeHtml(source.boundary) + '</li>').join('') + '</ul>';
|
||||||
|
}
|
||||||
|
|
||||||
|
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 — страховкой от последствий. Это три разных вопроса: что <em>предлагается</em>, кто разрешил <em>ограниченную операцию</em>, и что <em>зафиксировал процесс</em>. Между ними остаются selection, момент запуска, реальные права, состояние цели и проверка после change. Поэтому процесс нужно строить как цепочку доказательств, а не как одну зелёную кнопку.'),
|
||||||
|
h2('Симптом → причина → проверка → действие'),
|
||||||
|
ol([
|
||||||
|
'<strong>Симптом.</strong> Один job готов обработать широкий selector, но reviewer видит только краткое «N объектов» и общий статус success.',
|
||||||
|
'<strong>Причина.</strong> Команда не связала target list, preview, authority, approval и audit record одним неизменяемым идентификатором операции.',
|
||||||
|
'<strong>Проверка.</strong> До запуска сравнить exact target digest и limit authority; после запуска запросить независимый observed-state check, а не только exit code job.',
|
||||||
|
'<strong>Действие.</strong> Разделить путь на preview → bounded scope → approval → execute → audit trail → verification; rollback оформить отдельной операцией с новым preview и approval.',
|
||||||
|
]),
|
||||||
|
h2('Preview показывает намерение, а не будущую реальность'),
|
||||||
|
p('Preview полезен, когда отвечает на короткий вопрос: «какие именно объекты и какое изменение мы сейчас предлагаем?» В Terraform документ для версии 1.10.5 прямо отделяет <code>plan</code> от применения: 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 режима для <code>kubectl apply</code>: 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 в памяти. Функция с названием <code>simulateSyntheticExecution</code> создаёт только immutable-in-memory receipt с явным <code>productionEffect: not-attempted</code>. Это важнее красивого demo: читатель может повторить negative cases без риска получить частично изменённую систему.'),
|
||||||
|
h2('Approval — это binding, а не печать «безопасно»'),
|
||||||
|
p('Approval имеет техническую работу: связать согласие с конкретным preview. Для этого недостаточно поля <code>approved: true</code>. Нужны 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, а не словом <em>immutable</em> в 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 <em>plan</em>: зафиксировать reason, собрать observed state вне модели, получить новую authority, подготовить отдельный preview и approval. Эта задержка дешевле, чем обратный массовый change по старому предположению.'),
|
||||||
|
h2('Маршрут внедрения на одной рутине'),
|
||||||
|
ol([
|
||||||
|
'<strong>Выберите одну обратимую операцию.</strong> Например, поменять synthetic label на двух учебных cards, а не «автоматизировать всё сопровождение».',
|
||||||
|
'<strong>Опишите target contract.</strong> Dense ids, selector, exclusions, target digest, expected before/after и max scope.',
|
||||||
|
'<strong>Разведите preview и execute.</strong> Preview не имеет capability писать; execute принимает только exact bound approval.',
|
||||||
|
'<strong>Сделайте один refusal test.</strong> Подмените target digest или добавьте третий target. Ожидаемый результат — stop before approval.',
|
||||||
|
'<strong>Добавьте observed verification.</strong> Заранее опишите, какую независимую read-проверку выполнит owner после реального запуска.',
|
||||||
|
'<strong>Спланируйте rollback отдельно.</strong> Не выдавайте старый 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 и выпишите два списка — что он <em>предлагает</em> и что он <em>на самом деле меняет</em>. Затем добавьте в тест подмену 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([
|
||||||
|
'<strong>Зафиксировать proposal.</strong> Сохранить operation id, selector, dense targets, digest и version, а не только красивый diff.',
|
||||||
|
'<strong>Сравнить scope с authority.</strong> Проверить selector и limit до того, как появится approval request.',
|
||||||
|
'<strong>Связать approval буквально.</strong> Operation, preview, target и authority ids должны совпасть без implicit default.',
|
||||||
|
'<strong>Отделить receipt от evidence.</strong> После 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 для <code>kubectl apply</code> различает 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 и не просит пользователя «подтвердить ещё раз» в том же запуске. Он возвращает <code>stop-scope-exceeds-authority-limit</code>.'),
|
||||||
|
p('GitHub Docs для environments показывает похожую, но не тождественную механику: job ожидает protection rules, а секреты environment недоступны до их прохождения. Это gate доступа к стадии запуска. В документе есть и bypass boundary: возможность администратора обходить правила зависит от настройки environment. Следствие для собственного runner простое: bypass — тоже операция с отдельным identity, reason и audit event. Его нельзя скрыть внутри общего <code>approved</code>, иначе расследование не отличит нормальный путь от исключения.'),
|
||||||
|
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 может содержать <code>synthetic-targets-a-b-v1</code>, approval — <code>synthetic-targets-a-c-v1</code>, а оба выглядят похожими для человека. В fixed fixture это не «warning»: approval получает <code>stop-approval-does-not-bind-preview-target-and-authority</code>. Хороший 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 нельзя назвать, это не повод проставить <code>unknown</code> и идти дальше. Это сигнал остановиться до того, как автоматизация выберет 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. В учебной модели <code>synthetic-authority-l2</code> — строка и 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 относится к <code>synthetic-targets-a-c-v1</code>, а preview — к <code>synthetic-targets-a-b-v1</code>, путь закрывается. В нормальном 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 и <code>productionEffect: not-attempted</code>, а не 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 <code>stopAndPlanSyntheticRollback</code> возвращает <code>planned-only-not-executed</code>: stop event не разрешает новый write.'),
|
||||||
|
h2('Минимальный выпускной checklist'),
|
||||||
|
ol([
|
||||||
|
'<strong>Проверьте target contract.</strong> Selector, ids, digest, exclusions и max count можно показать в одной карточке.',
|
||||||
|
'<strong>Проверьте freshness preview.</strong> Явно задайте, когда preview перестаёт быть применимым и должен быть пересчитан.',
|
||||||
|
'<strong>Проверьте binding approval.</strong> Operation, preview, target и authority ids совпадают буквально, а не «примерно».',
|
||||||
|
'<strong>Проверьте audit boundary.</strong> Log хранит links, но команда не называет его observed state и не заявляет свойства storage без доказательства.',
|
||||||
|
'<strong>Проверьте observed verification.</strong> Назначены reader, expectation, timeout и владелец mismatch.',
|
||||||
|
'<strong>Проверьте stop path.</strong> 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');
|
||||||
Reference in New Issue
Block a user