Files
progcode/editorial/agent-rewrites/094.json
T

8 lines
23 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 94,
"slug": "editorial-2025-05-field-engineering-automation",
"title": "Как ограничить batch-автоматизацию до безопасного изменения",
"excerpt": "Разбираем учебный кейс с массовым скриптом: как связать preview, точный scope, approval, проверку результата и отдельный план восстановления, чтобы не принять успешный exit code за доказательство правильных данных.",
"contentHtml": "<p>Скрипт выбирает заявки по фильтру и закрывает их за секунды. Затем владелец формы замечает пропавшее поле: фильтр совпал с архивными записями, список targets устарел, а часть объектов уже исправил другой процесс. Ошибка не заканчивается ненулевым exit code. Она оставляет частичный change, теряет исходные значения и провоцирует второй массовый запуск для исправления первого.</p>\n<p>Рассмотрим этот сюжет как учебный кейс, а не как отчёт о конкретной production-системе. Главный вопрос такой: какие границы должен пройти batch-runner, прежде чем ему разрешат запись? Ответ — не «добавить dry-run», а разделить preview, scope, полномочия, approval, execute, журнал и независимую проверку. Каждый этап должен доказывать только свою часть результата.</p>\n<h2>Начните с наблюдаемого контракта</h2>\n<p>До запуска зафиксируйте карточку операции. Она превращает расплывчатое «закрыть старые заявки» в проверяемый набор условий: <code>operationId</code>, версия правила, selector, exclusions, точный список targets, ожидаемое изменение, допустимый лимит, срок действия preview и способ чтения результата.</p>\n<p>Selector отвечает на вопрос «как искать», а targets — «что именно разрешено менять». На execute нельзя заново выполнить широкий selector и надеяться получить тот же набор. Между двумя чтениями другой процесс может добавить запись, изменить статус или удалить объект. Поэтому preview должен содержать список и digest этого списка, а исполнитель — сверить их непосредственно перед записью.</p>\n<table><caption>Минимальные поля operation card</caption><thead><tr><th>Поле</th><th>Пример</th><th>Что проверяем</th><th>Стоп-условие</th></tr></thead><tbody><tr><td>operationId</td><td>close-requests-2025-05-25-01</td><td>один идентификатор проходит через все события</td><td>идентификатор повторно использован или пуст</td></tr><tr><td>selector</td><td>status=open, age&gt;30d</td><td>правило поиска и его версия известны reviewer</td><td>в запросе есть неявное «все»</td></tr><tr><td>targets</td><td>req-17, req-21</td><td>каждый ID перечислен и принадлежит ожидаемому типу</td><td>список пуст, содержит дубли или неожиданный тип</td></tr><tr><td>targetDigest</td><td>sha256:…</td><td>digest вычислен по зафиксированному представлению списка</td><td>digest нельзя пересчитать из показанных данных</td></tr><tr><td>authority</td><td>requests.close, максимум 2</td><td>право и количественный лимит покрывают точный scope</td><td>scope шире полномочия</td></tr><tr><td>expectation</td><td>status=closed у каждого target</td><td>есть authoritative reader для проверки</td><td>успехом считается только ответ команды</td></tr></tbody></table>\n<p>Digest — это связка между данными и решением, а не секретный пропуск. Для простого списка уникальных ASCII-ID достаточно заранее описать канонизацию, например сортировку и разделитель. Для вложенного JSON нельзя полагаться на случайный порядок свойств: RFC 8785 описывает детерминированное JSON-представление для повторяемого hashing. В любом варианте правила канонизации должны быть частью контракта и одинаково реализованы producer и executor.</p>\n<h2>Preview показывает намерение, но не даёт разрешение</h2>\n<p>Preview должен выполнять чтение и расчёт, но не внешнюю запись. Он показывает, какие изменения процесс собирается выполнить на момент чтения. Это полезное доказательство намерения, однако оно не обещает, что target останется прежним к моменту execute. Terraform прямо разделяет построение плана и применение, а также предупреждает, что более ранний speculative plan может устареть после изменений в целевой системе. Kubernetes аналогично называет <code>--dry-run=client</code> предварительным объектом, который не отправляется в кластер.</p>\n<p>Для reusable-скрипта сохраните не только красивый diff, но и машинно читаемую карточку: время получения, версию схемы, selector, targets, digest, identity создателя и <code>expiresAt</code>. Секреты и полные чувствительные значения в preview не включайте. В Terraform сохранённый plan-файл может содержать чувствительные данные, поэтому аналогичный артефакт нужно считать защищаемым.</p>\n<pre><code>node --input-type=module &lt;&lt;'NODE'\nconst preview = {\n operationId: 'close-requests-2025-05-25-01',\n selector: 'status=open,age&gt;30d',\n targets: ['req-17', 'req-21'],\n targetDigest: 'sha256:demo-list',\n expiresAt: '2025-05-25T12:00:00Z'\n};\n\nconst authority = {\n action: 'requests.close',\n maxTargets: 2\n};\n\nfunction checkPreview(candidate, permission, now) {\n if (candidate.expiresAt &amp;&amp; new Date(candidate.expiresAt) &lt;= now) {\n return { allowed: false, reason: 'preview-expired' };\n }\n if (candidate.targets.length === 0) {\n return { allowed: false, reason: 'empty-scope' };\n }\n if (candidate.targets.length &gt; permission.maxTargets) {\n return { allowed: false, reason: 'scope-exceeds-authority-limit' };\n }\n if (permission.action !== 'requests.close') {\n return { allowed: false, reason: 'wrong-authority' };\n }\n return { allowed: true, operationId: candidate.operationId };\n}\n\nconsole.log(checkPreview(\n preview,\n authority,\n new Date('2025-05-25T11:00:00Z'),\n));\n// { allowed: true, operationId: 'close-requests-2025-05-25-01' }\nNODE</code></pre>\n<p>Этот фрагмент воспроизводим в Unix-подобной оболочке и намеренно не обращается к сети или данным. Замените фиктивные targets только после того, как определите источник списка. Если лимит уменьшить до <code>1</code>, функция вернёт <code>scope-exceeds-authority-limit</code> и не должна передавать операцию дальше. Отрицательный путь важнее зелёного: ограничение обязано быть отказом, а не предупреждением.</p>\n<figure><img src=\"/assets/editorial/2025/engineering-automation-2025-preview-approval-rollback.svg\" alt=\"Поток batch-операции: preview и digest проходят проверку scope и approval, затем execute и audit; mismatch отправляет процесс к новому плану восстановления\"><figcaption>У каждой внешней записи есть предшествующий план и последующая проверка. Rollback начинается с нового scope, а не с автоматического inverse-вызова.</figcaption></figure>\n<h2>Свяжите approval с тем, что видел reviewer</h2>\n<p>Согласование по названию задачи слишком слабое. Reviewer должен видеть operation card, список targets, ожидаемые поля до и после, лимит и срок действия. В approval сохраните как минимум <code>operationId</code>, <code>targetDigest</code>, <code>authorityId</code>, identity reviewer, время решения и срок истечения.</p>\n<p>Непосредственно перед execute исполнитель заново проверяет identity, срок approval, действие, лимит и digest. Если вместо <code>req-17, req-21</code> появились <code>req-17, req-29</code>, старое решение не переносится. Сервис должен остановиться, создать новый preview и запросить новое approval. Нельзя молча взять пересечение списков: это меняет предмет решения и скрывает ошибку инвентаризации.</p>\n<p>Платформенный gate не заменяет эту связку, но показывает полезный паттерн. GitHub Environments позволяют требовать protection rules до запуска job и до доступа к secrets; среди правил есть required reviewers, запрет self-review и запрет обхода. Это граница workflow, а не доказательство того, что ваш selector выбрал правильные объекты. Содержимое approval и предмет изменения всё равно должны быть связаны в вашей системе.</p>\n<h2>Разделите execute, журнал и чтение результата</h2>\n<p>Execute отвечает на узкий вопрос: принял ли исполнитель запрос на изменение и что произошло с каждым target. Сохраните результат по объектам, а не только итоговое число. Для каждого элемента полезны <code>beforeVersion</code>, действие, ответ внешнего API, время, retry count и итоговый статус.</p>\n<p>Журнал — это evidence о ходе операции, но не текущее состояние данных. Он должен связывать operation id, target id, digest, identity, версию кода и correlation id. Не записывайте туда секреты и не называйте лог неизменяемым, если хранилище допускает редактирование. Kubernetes описывает аудит как хронологические записи о действиях пользователей, приложений и control plane, а policy определяет, что именно попадёт в backend. Эта модель полезна, но её полноту и срок хранения нужно проверить для конкретной платформы.</p>\n<table><caption>Как читать результаты batch-runner</caption><thead><tr><th>Наблюдение</th><th>Что оно доказывает</th><th>Чего не доказывает</th><th>Следующий шаг</th></tr></thead><tbody><tr><td>Preview создан</td><td>В момент чтения найден такой scope</td><td>Scope не изменился</td><td>Сохранить digest и срок действия</td></tr><tr><td>Approval получен</td><td>Reviewer принял конкретную карточку</td><td>Другой scope разрешён</td><td>Сверить digest перед execute</td></tr><tr><td>Команда завершилась с кодом 0</td><td>Процесс не сообщил об ошибке</td><td>Данные соответствуют expectation</td><td>Запустить независимый reader</td></tr><tr><td>Audit event записан</td><td>Событие попало в доступный журнал</td><td>Запись во внешнем источнике успешна</td><td>Проверить authoritative source</td></tr><tr><td>Один target matched</td><td>Только этот target наблюдается в ожидаемом состоянии</td><td>Остальные targets исправны</td><td>Вернуть subset и общий статус partial</td></tr></tbody></table>\n<h2>Verification должна иметь состояние unknown</h2>\n<p>После execute отдельный reader читает authoritative source — тот источник, которому вы доверяете для статуса заявки, поля формы или версии объекта. Reader сравнивает фактическое состояние с expectation из карточки. Минимальный результат — <code>matched</code>, <code>mismatched</code> или <code>unknown</code>.</p>\n<p><code>Unknown</code> нужен для таймаута, недоступного API, задержки репликации и ответа, в котором отсутствует нужное поле. Превращать его в успех опасно: batch-runner тогда закрывает операцию без доказательства. При partial execution выдайте точный subset: какие targets matched, какие mismatched, какие неизвестны. Общий статус в таком случае остаётся остановленным, даже если большинство элементов совпало.</p>\n<p>Проверка должна быть независимой хотя бы по одному важному измерению. Если execute и verification используют один кэш, одну ошибочную трансформацию или тот же промежуточный ответ, они могут согласованно подтвердить неверное состояние. Независимость не обязана означать другую команду или другой кластер, но источник и критерий чтения должны быть явно названы.</p>\n<h2>Rollback — это новый change</h2>\n<p>После mismatch хочется вызвать обратную команду в <code>finally</code>. Для массовых данных такой inverse может увеличить ущерб: объект уже изменили вручную, старое значение устарело, а selector стал шире. Поэтому сначала создайте recovery plan с subset, наблюдаемыми версиями, неизвестными значениями, владельцем восстановления и допустимым лимитом.</p>\n<p>Затем recovery проходит тот же маршрут: новый scope, новый authority check, новый preview, новое approval, запись результата и новая verification. Старое согласование не даёт права на обратную запись. Если предыдущая операция удаляла данные или перезаписывала поле без сохранения версии, честный результат может быть «автоматически восстановить нельзя». Это повод остановиться и привлечь владельца данных, а не запускать «вернуть всё назад».</p>\n<h2>Пошаговый маршрут и проверяемый результат</h2>\n<ol><li>Опишите intent: какое поле или состояние меняется и почему.</li><li>Зафиксируйте selector, exclusions, версию правила, точные targets и канонический targetDigest.</li><li>Сформируйте preview без внешней записи. Добавьте время получения, срок действия и expectation.</li><li>Проверьте действие, identity и лимит полномочия. При превышении верните явный stop code.</li><li>Передайте reviewer ровно эту карточку и сохраните approval, связанный с digest.</li><li>Непосредственно перед execute повторите freshness, digest, authority и срок approval.</li><li>Выполните ограниченный change с результатом по каждому target; retries не должны расширять scope.</li><li>Запишите audit event без секретов и с correlation id.</li><li>Прочитайте authoritative source. Закройте операцию только при полном matched.</li><li>При partial, mismatched или unknown создайте recovery plan, а не автоматический широкий rollback.</li></ol>\n<p>На тестовых данных воспроизведите три сценария. Нормальный scope должен пройти до matched. Scope больше authority должен остановиться до approval или execute. Замена одного target после approval должна остановиться на повторной проверке digest. Для каждого сценария сохраните operation id, причину остановки или результаты verification и подтвердите отсутствие запрещённой записи. Такой тест доказывает работу границ, но не гарантирует безопасность production без проверки гонок, прав и хранилища.</p>\n<h2>Ограничения применимости</h2>\n<p>Эта схема не делает любую batch-операцию безопасной автоматически. Она не доказывает идемпотентность, транзакционность, полноту inventory, свежесть реплики, корректность identity provider, неизменяемость журнала или возможность восстановить старые значения. Если API допускает partial success, правило остановки и повторного запуска нужно определить заранее.</p>\n<p>Учебный JavaScript использует память процесса, фиксированную дату и строковый digest. Он не permission system, не durable audit storage и не клиент реального API. В production нужно определить формат канонизации, защиту preview, максимальный возраст данных, version check или optimistic concurrency, политику retry, владельца unknown и способ ручного recovery. Неизвестное условие уменьшает допустимый scope; оно не является основанием расширить полномочия.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://developer.hashicorp.com/terraform/cli/commands/plan\" target=\"_blank\" rel=\"noopener noreferrer\">Terraform: terraform plan</a> — официальная документация разделяет preview и применение, предупреждает о расхождении старого speculative plan с изменившейся целевой системой и описывает риск чувствительных данных в сохранённом plan-файле.</li><li><a href=\"https://kubernetes.io/docs/reference/kubectl/conventions/\" target=\"_blank\" rel=\"noopener noreferrer\">Kubernetes: kubectl Usage Conventions</a> — официальная документация описывает <code>--dry-run=client</code> как preview объекта без отправки в кластер и рекомендует стабильный машинный вывод для скриптов.</li><li><a href=\"https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub Docs: Managing environments for deployment</a> — официальное описание protection rules, required reviewers, prevent self-review и доступа к secrets после прохождения правил.</li><li><a href=\"https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/\" target=\"_blank\" rel=\"noopener noreferrer\">Kubernetes: Auditing</a> — официальное описание хронологических audit records, policy, уровней детализации и log/webhook backends; документация также отмечает влияние аудита на память API server и необходимость настраивать хранение.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc8785\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 8785: JSON Canonicalization Scheme</a> — спецификация описывает детерминированное представление JSON для повторяемых hash/signature операций и отдельно подчёркивает, что это informational RFC, а не универсальная гарантия корректности данных.</li></ul>"
}