diff --git a/editorial/production/README.md b/editorial/production/README.md
index 11fa8d8..0dc1d91 100644
--- a/editorial/production/README.md
+++ b/editorial/production/README.md
@@ -1,6 +1,6 @@
# Производство редакционных партий
-На 31 июля 2026 года строгий аудит проходит 253 из 358 созданных материалов. Остальные 105 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
+На 31 июля 2026 года строгий аудит проходит 256 из 358 созданных материалов. Остальные 102 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
## Одна партия
diff --git a/editorial/reviews/2025-02-draft.md b/editorial/reviews/2025-02-draft.md
new file mode 100644
index 0000000..cc6d0f5
--- /dev/null
+++ b/editorial/reviews/2025-02-draft.md
@@ -0,0 +1,122 @@
+# P84 · 2025-02 · Проверка сгенерированного кода — три прохода ревью
+
+## Граница пакета
+
+- Переписываются только `editorial-2025-02-practice-ai-code-verification`, `editorial-2025-02-mechanism-ai-code-verification` и `editorial-2025-02-field-ai-code-verification` через самостоятельный overlay `web/scripts/upgrade-2025-02.mjs`.
+- Все prompts, diff fragments, contracts, tests, roles, verdicts и «стоимость» — versioned fixed synthetic JavaScript literals в памяти. Пакет не читает и не пишет source repository, user code, secrets, Git, CI, сеть, telemetry, production, clock или API и не вызывает модель.
+- Скрипт не меняет `articles.json`; registry, README, очередь, staging, commit, push и полная production-сборка не входят в эту самостоятельную draft-партию.
+
+## База исследования
+
+Все ссылки ниже открыты обычным HTTPS без авторизации и без TLS bypass: `curl -I -L --fail` вернул HTTP 200 для каждой. Один и тот же набор из трёх первичных/официальных источников указан в каждой статье; это больше минимальных двух источников и сохраняет один исторический срез на февраль 2025.
+
+| Источник | Версия / дата | Узкое утверждение в статье | Ограничение источника |
+| --- | --- | --- | --- |
+| [GitHub Docs: Responsible use of GitHub Copilot code review](https://github.com/github/docs/blob/7b3918e77baf865d1f16bd60e570acea874ee9eb/content/copilot/responsible-use-of-github-copilot-features/responsible-use-of-github-copilot-code-review.md) | immutable commit `7b3918e77baf865d1f16bd60e570acea874ee9eb`, 12.12.2024 | AI review и suggestions дополняют, а не заменяют человеческую проверку; предложения могут быть неверными, небезопасными, давать false positive или пропускать проблемы. | Документация одного preview-продукта. Она не измеряет качество других ассистентов, не задаёт merge policy и не доказывает конкретный synthetic case. |
+| [NIST SP 800-218, SSDF Version 1.1](https://csrc.nist.gov/pubs/sp/800/218/final) | final, 03.02.2022 | PW.7 связывает code review и analysis с правилами организации, фиксирует findings и triage; результаты analysis/testing могут быть входом peer review. | Это высокоуровневая рамка: не выбирает инструмент, не даёт порог покрытия и не определяет риск конкретного diff. |
+| [OWASP Code Review Guide 2.0](https://owasp.org/www-project-code-review-guide/assets/OWASP_Code_Review_Guide_v2.pdf) | release PDF, July 2017 | Code review проверяет security и logical controls; tools масштабируют поиск, но не заменяют контекст и человеческое подтверждение. | Руководство не даёт языковых правил, не заменяет threat model и не обещает найти все уязвимости. |
+
+Историческая оговорка: в текст не перенесены сведения о моделях, агентах, benchmark, policy или возможностях инструментов после февраля 2025.
+
+## Проход 1 — факты и техника
+
+Проверил тезисы с узкими утверждениями источников и убрал обещания correctness/security. В статьях сказано, что contract check, static check, focused test, review и manual reproduction дают evidence с ограниченной областью действия; ни один из них и их комбинация не названы гарантией.
+
+Практическая модель закреплена в `synthetic-ai-code-verification-2025-02-v1` и принимает только три case: `contract-mismatch`, `hidden-side-effect`, `incomplete-test`. Она возвращает только in-memory report/plan/stop object и на невалидных данных работает fail-closed. Отрицательная fixture проверяет:
+
+- exact keys у input, report, plan и stop contracts;
+- dense arrays для evidence и actions;
+- unknown keys;
+- cyclic input/report/plan без выброса исключения;
+- canonical comparison fixed input, evidence и actions;
+- подменённые finding/action;
+- запрет merge без human approval и отсутствие production effect.
+
+Результат:
+
+```text
+node --check scripts/upgrade-2025-02.mjs
+PASS
+
+node scripts/upgrade-2025-02.mjs --verify-fixture
+PASS fixture: 25/25 assertions
+```
+
+Ограничение этого прохода: fixture подтверждает форму синтетического механизма, не поведение реального линтера, теста, ревьюера, модели или системы доступа.
+
+## Проход 2 — редактура и голос M8
+
+Проверены первые два абзаца каждой статьи: в них есть симптом, причина и стоимость ошибки без вымышленных денег, инцидентов или метрик. Автор февраля 2025 сравнивает стоимость способов проверки и оставляет повторяемый процесс, но не делает вид, что владеет универсальной AI-стратегией.
+
+| Статья | Основной текст | Проверка голоса и содержания |
+| --- | ---: | --- |
+| Practice | 8 647 знаков | Один зелёный test не равен contract proof; дан verification plan, evidence table, fixed example и чёткий stop. |
+| Mechanism | 10 031 знак | Раскрыты matrix risk → evidence, overlap, independence, false confidence, scope и stop condition. |
+| Field | 10 695 знаков | Разобраны три diff case, evidence chain, human approval, next action и граница stop/revert/rollback. |
+
+Редакторская вычитка отдельно проверила: короткую связку «симптом → причина → проверка → действие», отсутствие шаблонных оборотов из аудита, отсутствие анхронизмов 2026–2027 и отсутствие ложной формулы «зелёный статус = безопасность». Термины привязаны к фиксированному контракту, field, input ownership или роли; финалы дают конкретное действие, а не лозунг.
+
+Результат автоматического редакторского аудита:
+
+```text
+npm run audit:draft -- scripts/upgrade-2025-02.mjs
+PASS editorial-2025-02-practice-ai-code-verification: 8647 body chars
+PASS editorial-2025-02-mechanism-ai-code-verification: 10031 body chars
+PASS editorial-2025-02-field-ai-code-verification: 10695 body chars
+```
+
+Ограничение этого прохода: объём и стиль подтверждают плотность текста, но не переносят fixed verdict на чужой исходный код.
+
+## Проход 3 — визуал и выпуск
+
+Для каждой статьи создан отдельный безопасный SVG с содержательным `alt` и подписью: воронка verification, matrix risk → evidence и evidence chain. SVG проверены как XML и safety scan не нашёл `script`, `foreignObject`, `javascript:`, `data:image` или event handlers. Каждый визуал отрендерен Sharp и вручную просмотрен на ширине 375 px.
+
+| Asset | Рендер 375 px | Что проверено вручную |
+| --- | --- | --- |
+| `ai-code-verification-2025-verification-funnel.svg` | 375 × 238 | Порядок пяти gates, красная stop-ветка и подписи читаются. |
+| `ai-code-verification-2025-risk-detection-matrix.svg` | 375 × 250 | Колонки, маркеры `✓`/`?` и легенда читаются без горизонтального обрезания. |
+| `ai-code-verification-2025-evidence-chain.svg` | 375 × 250 | Видны разница между observation/decision и раздельные ветки stop/human approval. |
+
+После первого узкого render-прохода сокращена подпись первого шага воронки и разбита длинная нижняя легенда матрицы на две строки; затем XML и рендер повторно проверены.
+
+```text
+xmllint --noout <3 SVG>
+PASS
+
+SVG safety scan
+PASS
+
+Sharp 375 px + ручная проверка
+PASS
+```
+
+Полная build, registry update, staging, commit и push сознательно не запускались: это запрещено границей самостоятельного P84 draft-пакета. Перед будущей публикацией нужен отдельный выпускной проход с подключением overlay к registry и полной сборкой.
+
+## Выпуск после независимой приёмки
+
+Основной редактор выполнил отдельные три прохода после подключения overlay:
+
+1. **Факты и техника.** Raw-снимок GitHub Docs по commit
+ 7b3918e77baf865d1f16bd60e570acea874ee9eb подтверждает дату
+ 12 декабря 2024 и предупреждает, что Copilot code review дополняет, а не
+ заменяет человеческий review; возможны пропуски, false positive и
+ правдоподобный, но небезопасный код. В финальном PDF NIST SP 800-218
+ пункты PW.7.1–PW.7.2 требуют выбирать review/analysis по правилам
+ организации, фиксировать и triage найденные вопросы. OWASP Guide 2.0
+ подтверждает, что tools не заменяют человеческий context и verification.
+ Утверждения статей не выходят за эти границы; fixture повторно прошёл
+ **25/25**.
+2. **Редактура и голос.** Полные тексты перечитаны. Они различают стоимость
+ проверки для маленького diff, ложную уверенность нескольких похожих зелёных
+ сигналов и решение владельца границы. В начале каждой статьи есть симптом и
+ цена ошибки, что повторно подтвердил усиленный `audit:draft`: 8 647 / 10 031
+ / 10 695 знаков. Термины contract, evidence, stop, revert и rollback
+ раскрыты в контексте и не выданы за синонимы.
+3. **Визуал и выпуск.** Три SVG отрисованы Sharp на 375 px и просмотрены:
+ воронка 375×238, матрица 375×250, evidence chain 375×250. Карточки,
+ стрелки, легенды и stop-ветви остаются читаемыми; XML и safety scan чисты.
+ Целевой аудит подтверждает figure, code и таблицу в каждой статье. Реестр
+ содержит **247 уникальных** ревизий, `git diff --check` чист, production
+ build успешно сгенерировал **374** страницы.
+
+Вердикт: февральская партия готова к отдельному публикационному коммиту.
diff --git a/web/data/editorial-revisions.mjs b/web/data/editorial-revisions.mjs
index 1cd6aa3..03394c5 100644
--- a/web/data/editorial-revisions.mjs
+++ b/web/data/editorial-revisions.mjs
@@ -80,6 +80,7 @@ import { revisions as october2024Revisions } from '../scripts/upgrade-2024-10.mj
import { revisions as november2024Revisions } from '../scripts/upgrade-2024-11.mjs';
import { revisions as december2024Revisions } from '../scripts/upgrade-2024-12.mjs';
import { revisions as january2025Revisions } from '../scripts/upgrade-2025-01.mjs';
+import { revisions as february2025Revisions } from '../scripts/upgrade-2025-02.mjs';
// This layer replaces archived source entries without losing their stable slug and date.
export const editorialRevisions = [
@@ -165,4 +166,5 @@ export const editorialRevisions = [
...november2024Revisions,
...december2024Revisions,
...january2025Revisions,
+ ...february2025Revisions,
];
diff --git a/web/public/assets/editorial/2025/ai-code-verification-2025-evidence-chain.svg b/web/public/assets/editorial/2025/ai-code-verification-2025-evidence-chain.svg
new file mode 100644
index 0000000..52fd606
--- /dev/null
+++ b/web/public/assets/editorial/2025/ai-code-verification-2025-evidence-chain.svg
@@ -0,0 +1,59 @@
+
diff --git a/web/public/assets/editorial/2025/ai-code-verification-2025-risk-detection-matrix.svg b/web/public/assets/editorial/2025/ai-code-verification-2025-risk-detection-matrix.svg
new file mode 100644
index 0000000..e206c4d
--- /dev/null
+++ b/web/public/assets/editorial/2025/ai-code-verification-2025-risk-detection-matrix.svg
@@ -0,0 +1,66 @@
+
diff --git a/web/public/assets/editorial/2025/ai-code-verification-2025-verification-funnel.svg b/web/public/assets/editorial/2025/ai-code-verification-2025-verification-funnel.svg
new file mode 100644
index 0000000..1c628f0
--- /dev/null
+++ b/web/public/assets/editorial/2025/ai-code-verification-2025-verification-funnel.svg
@@ -0,0 +1,46 @@
+
diff --git a/web/scripts/upgrade-2025-02.mjs b/web/scripts/upgrade-2025-02.mjs
new file mode 100644
index 0000000..fa924ea
--- /dev/null
+++ b/web/scripts/upgrade-2025-02.mjs
@@ -0,0 +1,736 @@
+function escapeHtml(value) {
+ return String(value)
+ .replaceAll('&', '&')
+ .replaceAll('<', '<')
+ .replaceAll('>', '>')
+ .replaceAll('"', '"')
+ .replaceAll("'", ''');
+}
+
+const p = (text) => '
' + text + '
'; +const h2 = (text) => '' + escapeHtml(text) + '';
+const ol = (items) => '| ' + item + ' | ').join('') + '
|---|
| ' + item + ' | ').join('') + '
contract-mismatch generated mapper возвращает total, а fixed contract требует amountCents. Happy-path test проверяет, что получилось число, поэтому остаётся зелёным. Contract check, consumer-oriented test и review задают другой вопрос: доступно ли поле, на которое рассчитывает потребитель? Их расхождение — не повод подобрать тест до зелёного результата. Это повод оставить merge blocked и сначала решить, требуется ли совместимость или отдельный change decision.'),
+ h2('Пять шагов для одного небольшого изменения'),
+ ol([
+ 'Сузьте scope. Назовите один файл, один contract boundary и один ожидаемый эффект. Если это не получается, diff уже слишком широк для короткого review.',
+ 'Запишите negative path. Рядом с accept case добавьте отказ, отсутствие значения, старое поле или запретный side effect. Один happy path не выбирает отрицательную ветку за вас.',
+ 'Разделите output и state. Для preview и mapper сравните не только result, но и вход после вызова. Для guard отделите «вернул 200» от «правило доступа записано верно».',
+ 'Попросите review о решении, а не о красоте diff. Reviewer должен суметь назвать contract, residual risk и owner, который может принять исключение.',
+ 'Остановите спорный merge. Если contract, test и review говорят разное, следующая работа — объяснить расхождение, а не добрать ещё один зелёный запуск.',
+ ]),
+ h2('Почему линтер и тест не складываются в гарантию'),
+ p('Линтер полезен для правила, которое можно выразить как паттерн. Он может показать прямое присваивание аргументу или запрещённое имя. Он не знает, разрешена ли мутация именно в этом API и не видит договор с consumers. Focused test полезен, когда ожидание написано в форме входа и результата. Он не знает про путь, который тестировавший не назвал. Review полезен для контекста, но зависит от размера diff, ясности требования и времени человека. Поэтому формулировка «прошёл линтер и тест» должна означать только это, не «корректен» и тем более не «безопасен».'),
+ p('GitHub в зафиксированной документации на февраль 2025 прямо описывает AI review как дополнение к human review и предупреждает о ложных срабатываниях, пропусках и небезопасных suggestions. NIST SSDF PW.7 также не выбирает единственный инструмент: он связывает review, analysis, фиксацию findings и их triage с правилами организации. Эти источники поддерживают дисциплину нескольких доказательств, но не подтверждают наши synthetic cases и не дают универсальный threshold для merge.'),
+ h2('Stop condition и стоимость задержки'),
+ p('Практический stop condition короткий: остановить подготовку merge, когда заявленный contract, отрицательный тест и reviewer rationale не совпадают. Это не наказание за generated code. Это экономия на более дорогом цикле: после merge команда будет выяснять, является ли отсутствующее значение новым API, допустимой мутацией или пропущенной политикой доступа. Остановить маленький diff обычно дешевле, чем расследовать неявную границу после того, как его уже приняли.'),
+ p('Но stop не равен rollback. Revert отбрасывает ещё не принятый change или создаёт обратный change в конкретной VCS-политике. Rollback меняет уже доставленное состояние и требует подтверждённых условий восстановления. Этот пакет не делает ни того ни другого: он только возвращает teaching plan к fixed contract и просит human approval вне модели. В реальном проекте владелец должен решить, кто имеет право на merge, revert и rollback отдельно.'),
+ h2('Ограничения и следующий проверяемый шаг'),
+ p('Все diff, contracts, tests, verdicts, роли, поля и измерения в этой статье — versioned fixed synthetic literals. Они не являются наблюдением за моделью, репозиторием, пользователями, секретами, CI или production. Нельзя по ним заключать, что конкретный линтер, тест или reviewer обнаружит такой же риск в другом языке. Нельзя подменять ими threat model, policy доступа или анализ фактической зависимости.'),
+ p('Следующий шаг: возьмите один маленький generated diff и до запуска напишите таблицу из пяти строк: contract, static pattern, positive/negative test, reviewer question и manual scenario. У каждой строки назовите blind spot. Если хотя бы две строки спорят, не пытайтесь получить «среднее» verdict; вынесите вопрос владельцу границы. Ожидаемый результат — не больше тестов вообще, а доказательство, которое можно повторить и оспорить.'),
+ h2('Историческая граница февраля 2025'),
+ p('В тексте использованы GitHub Docs на immutable commit от 12 декабря 2024, NIST SP 800-218 Version 1.1 от 3 февраля 2022 и OWASP Code Review Guide 2.0 от июля 2017. Они были доступны к февралю 2025. Более поздние сведения о моделях, агентных режимах, benchmark или возможностях инструментов сюда не переносятся.'),
+]);
+
+const mechanism = revision({
+ slug: 'editorial-2025-02-mechanism-ai-code-verification',
+ title: 'Независимые доказательства ловят разные классы ошибок',
+ excerpt: 'Матрица «risk → detecting evidence»: где evidence пересекается, где создаёт ложную уверенность и в какой момент нужно остановить merge.',
+ readingMinutes: 10,
+}, [
+ p('У generated diff может быть одновременно зелёный линтер, зелёный unit test и неверное решение. Цена ошибки не в том, что один инструмент «не сработал». Цена в ложной уверенности: три сигнала повторяют один happy path, команда считает риск закрытым и замечает нарушение контракта только после следующего потребителя. В синтетическом примере это повторная проверка; в реальной системе размер ущерба нельзя вывести из числа зелёных индикаторов.'),
+ p('Причина — смешение классов ошибок. Static check хорошо ищет форму, тест — заранее названное поведение, review — намерение и контекст, manual reproduction — путь одного потребителя. Если все четыре свидетельства отвечают на вопрос «функция вернула число», ни одно не отвечает на вопрос «сохранилось ли поле публичного API». Механизм проверки должен явно связывать risk с evidence и оставлять пустые клетки видимыми.'),
+ h2('Матрица важнее списка инструментов'),
+ p('Я не начинаю с названий инструментов. Сначала в первой колонке записываю risk как наблюдаемый разрыв: поле переименовано, вход мутируется, неуказанная роль допускается, обработка ошибки пропущена. Во второй колонке — evidence, способное опровергнуть именно это утверждение. В третьей — scope: что этот evidence видит. В четвёртой — stop condition. Тогда команда сравнивает не бренды и не «уровни автоматизации», а стоимость закрытия конкретной неопределённости.'),
+ figure('/assets/editorial/2025/ai-code-verification-2025-risk-detection-matrix.svg', 'Матрица риска и доказательств: contract, static check, focused test, human review и manual reproduction подсвечивают разные классы ошибок и не образуют универсальную гарантию.', 'Матрица показывает пересечения и пробелы. Зелёная клетка означает «может дать релевантное свидетельство», а не «гарантирует отсутствие ошибки».'),
+ table('Risk → detecting evidence в fixed synthetic cases', ['Риск', 'Наиболее прямое evidence', 'Полезное пересечение', 'Ложная уверенность', 'Stop condition'], [
+ ['Переименование публичного поля', 'contract assertion с expected shape', 'consumer-oriented focused test и review', 'тест проверяет только число, а не имя поля', 'contract и test описывают разные result shape'],
+ ['Мутация caller-owned input', 'input before/after assertion', 'static assignment rule и review ownership', 'rendered output верный, поэтому side effect не смотрят', 'output green, input boundary не доказана'],
+ ['Missing role допускается', 'negative test для отсутствующего значения', 'allow-list review и static pattern', 'editor-only test трактуют как access proof', 'default-deny и условие расходятся'],
+ ['Возможная security weakness', 'threat-aware review и relevant test', 'analysis rule, если риск формализован', 'линтер или один test объявляют security proven', 'риск требует отсутствующего context или authority'],
+ ]),
+ h2('Независимость не означает «не похожи»'),
+ p('Два evidence независимы для решения не потому, что их сделали разные люди или они называются по-разному. Они независимы, когда могут опровергнуть разные предпосылки. Contract check и consumer test частично пересекаются: оба смотрят на форму результата. Но первый проверяет declared shape, а второй — использование конкретным потребителем. Static assignment rule и input-equality test тоже пересекаются, но один находит прямую запись в тексте, другой видит наблюдаемый эффект fixed call. Их полезно держать вместе, пока оба остаются короткими.'),
+ p('Не нужно притворяться, что overlap — дефект. Пересечение снижает риск того, что одна опечатка в test fixture останется незамеченной. Проблема начинается, когда overlap маскируют под независимость. Например, generated code и generated test могут повторить одно неправильное предположение: «роль отсутствует, значит это не viewer». Второй тест тогда лишь умножает доверие к той же ветке. Добавить human review полезно, если reviewer получает contract и может задать вопрос, которого нет в prompt или test name.'),
+ h2('Компактный fixed synthetic model'),
+ p('Модель ниже не классифицирует реальный код. Она принимает только три закреплённые карточки и на совпадении фиксирует disagreement. Если в report появилась лишняя колонка, sparse array, цикл или подменённое finding, следующий шаг возвращает closed plan. Это намеренно консервативное поведение: сомнительное evidence не становится «почти достаточным».'),
+ code([
+ "import {",
+ " createFixedSyntheticVerificationInput,",
+ " inspectSyntheticGeneratedDiff,",
+ " buildSyntheticVerificationPlan,",
+ "} from './upgrade-2025-02.mjs';",
+ '',
+ "const input = createFixedSyntheticVerificationInput('hidden-side-effect');",
+ 'const report = inspectSyntheticGeneratedDiff(input);',
+ 'const plan = buildSyntheticVerificationPlan(report);',
+ '',
+ "console.log(report.evidence.map(({ id }) => id));",
+ '// [ contract, static, focused-test, review, manual-reproduction ]',
+ "console.log(plan.humanApproval);",
+ '// required-after-evidence-agrees-and-outside-this-model',
+ ].join('\n')),
+ p('Здесь output preview может быть правильным, но mutable input нарушает ownership boundary. Static check находит присваивание, focused test должен сравнить input до и после, reviewer соотносит имя preview с поведением, manual reproduction показывает, что caller видит новое значение после вызова. Ни одно свидетельство не доказывает безопасность функции. Вместе они только делают конкретный риск наблюдаемым и объясняют, почему merge нельзя продолжить без решения.'),
+ h2('Где возникает false confidence'),
+ p('Первая ловушка — считать количество зелёных запусков мерой correctness. Три теста одного валидного значения всё ещё не проверяют отсутствующее значение, старый consumer или mutation. Вторая — считать отсутствие finding результатом анализа. Линтер без правила для контракта не говорит, что контракт сохранён. Третья — считать хороший review comment доказательством: comment может быть точным, но не иметь acceptance criterion и не сопровождаться test. Четвёртая — превращать manual reproduction в substitute for automated evidence, хотя ручной шаг трудно повторять без входа, ожидаемого результата и владельца.'),
+ p('Цена каждого способа разная. Static check быстро масштабируется после того, как риск стал формальным, но требует поддержки правила и даёт false positive. Focused test стоит времени на фикстуру, зато делает один expected path повторяемым. Human review медленнее и дороже на единицу diff, но может заметить скрытую границу или неопределённое решение. Manual reproduction полезна для короткого consumer scenario, но не масштабируется как постоянный gate. Поздний автор не выбирает «самый современный» способ: он выбирает самый дешёвый evidence, который способен опровергнуть текущую гипотезу.'),
+ table('Компромисс способов проверки', ['Способ', 'Стоимость для маленького diff', 'Сильная сторона', 'Нужно добавить, когда'], [
+ ['Static check', 'низкая после настройки правила', 'повторяемый явный паттерн', 'нужен intent или runtime path'],
+ ['Focused test', 'средняя: fixture и expected result', 'одна stated branch', 'есть другой consumer, state или boundary'],
+ ['Human review', 'средняя/высокая: внимание владельца', 'контекст, policy и незаданный вопрос', 'diff крупный или contract не записан'],
+ ['Manual reproduction', 'низкая для одного случая, высокая при масштабировании', 'видимый путь потребителя', 'сценарий должен стать regression test'],
+ ]),
+ h2('Scope и stop condition'),
+ p('Scope — это не название модуля. Для contract-mismatch scope — один fixed result shape. Для hidden-side-effect — ownership одного argument. Для incomplete-test — три значения fixed role. Как только review пытается сделать из них оценку production traffic, поведения модели или состояния доступа, он выходит за границу evidence. Правильное действие — записать unknown и открыть отдельное authorized investigation, а не продолжать merge по аналогии.'),
+ p('Stop condition должен быть машинально читаемым человеком: «contract, negative test и reviewer rationale расходятся», «input boundary не проверена», «default-deny не выражен в условии». Формулировка «не нравится diff» не годится, потому что не даёт следующего шага. Формулировка «получилось зелёное» тоже не годится, потому что не называет рассмотренный risk. Стоп не доказывает, что diff плох; он говорит, что текущий набор evidence не имеет права на решение.'),
+ h2('Короткий порядок работы с матрицей'),
+ ol([
+ 'Назовите риск наблюдаемым разрывом. Не «AI ошибся», а «публичное поле исчезло», «input изменился» или «отсутствующее значение получило доступ».',
+ 'Выберите прямое evidence. Оно должно уметь опровергнуть именно эту формулировку, а не просто добавить ещё один зелёный статус.',
+ 'Найдите повтор предпосылки. Если code и test исходят из одного неверного правила, добавьте contract assertion, reviewer question или negative case.',
+ 'Запишите stop. При расхождении сохраните known и unknown, затем передайте следующий вопрос владельцу границы.',
+ ]),
+ h2('Что говорят источники, а чего не говорят'),
+ p('NIST SP 800-218 в PW.7 связывает выбор review и analysis со стадией разработки, а findings — с triage в рабочем процессе. Это поддерживает мысль о scope и фиксации расхождений. OWASP Code Review Guide описывает сочетание инструментов и человеческой проверки, отмечая, что tools не понимают весь context. Документация GitHub Copilot code review предупреждает о missed problems, false positives и возможной небезопасности suggestions. Ни один источник не устанавливает волшебный набор из пяти gates и не подтверждает, что матрица автоматически покрывает security или correctness.'),
+ h2('Ограничения и следующий проверяемый шаг'),
+ p('В этой статье risk names, diff fragments, tests, verdicts, labels, роли и «стоимость» — только fixed synthetic учебные данные. Нет model inference, API, prompt execution, source search, Git, CI, telemetry, production, пользователей, секретов или измерения времени. Контроль, который дал evidence в карточке, может не существовать в вашем стеке; добавлять его стоит только после того, как владелец подтвердит contract и допустимый scope.'),
+ p('Следующий шаг: на одном diff сделайте две таблицы. В первой свяжите каждый риск с самым прямым evidence. Во второй напишите, какие два evidence повторяют одну предпосылку. Затем добавьте один negative case или reviewer question, который может опровергнуть эту предпосылку. Если его нельзя назвать без изучения реальной системы, остановите шаблонный merge и запросите контекст у владельца. Ожидаемый результат — не «больше контроля», а самостоятельное доказательство для каждой важной границы.'),
+ h2('Историческая граница февраля 2025'),
+ p('Source set закреплён состоянием до февраля 2025: GitHub Docs commit от 12 декабря 2024, NIST SSDF final 2022 и OWASP Guide 2.0 2017. В статье не используются сведения о версиях моделей, агентах, security scoring или инструментах после этой даты.'),
+]);
+
+const field = revision({
+ slug: 'editorial-2025-02-field-ai-code-verification',
+ title: 'Как остановить merge, когда evidence расходится',
+ excerpt: 'Три fixed synthetic случая: нарушение контракта, скрытый side effect и неполный тест. Где заканчивается stop, начинается human approval и чем revert отличается от rollback.',
+ readingMinutes: 10,
+}, [
+ p('Самый неприятный generated diff — не тот, который сразу падает. Опаснее diff, где один сигнал говорит «готово», а другой — «граница нарушена». Зелёный happy path закрывает тикет, reviewer видит аккуратный код, но consumer ждёт старое поле, caller получает изменённый объект или пустая роль проходит в protected branch. Цена ошибки — лишний цикл принятия и неясность, кто должен решить исключение; реальные деньги, пользователи и инциденты в этих карточках намеренно отсутствуют.'),
+ p('В такой момент merge нельзя останавливать фразой «что-то не так». Нужна воспроизводимая цепочка: какой contract заявлен, какой fixed diff его оспаривает, какое evidence расходится, что именно блокируется и какой человек имеет право принять риск. Ни линтер, ни test, ни review не дают сами по себе human approval. Они дают материал, на котором владелец может принять решение или отправить diff на доработку.'),
+ h2('Case 1. Contract mismatch: число верно, поле неверно'),
+ p('Первый fixed case выглядит безобидно. Mapper получает subtotalCents и taxCents, складывает их и возвращает число. Generated diff выбирает поле total. Happy-path test проверяет, что итог равен ожидаемому числу. Он зелёный, потому что арифметика не менялась. Но declared output требует объект { amountCents: number }, а synthetic consumer читает result.amountCents. Его результат — undefined.'),
+ p('Симптом: два доказательства смотрят на разные вещи. Причина: test name описывает value, но не public shape. Проверка: сравнить contract assertion, consumer-oriented test и reviewer question «было ли отдельно принято переименование?». Действие: оставить merge preparation blocked до одного из двух честных исходов — вернуть amountCents или оформить compatibility decision за границей этого пакета. Нельзя исправить дело тем, что тест начнёт проверять total: тогда он просто закрепит непроверенное решение.'),
+ h2('Case 2. Hidden side effect: preview меняет чужой объект'),
+ p('Во втором case generated helper называется preview. Он возвращает нужный rendered text, и проверка результата зелёная. Внутри helper делает draft.status = "normalized". Если input принадлежит caller, это side effect: после preview следующий код видит не исходный draft. Внешний вид правильный, поэтому проблема не находится проверкой, которая смотрит только на строку в ответе.'),
+ p('Симптом: output совпал, state неожиданно изменился. Причина: contract не разделил derived result и ownership input. Проверка: добавить fixed input before/after assertion, static rule на прямое присваивание аргументу и review вопрос «почему preview имеет право менять draft?». Действие: создать derived value, оставить аргумент неизменным или вынести изменение в отдельную named operation. До этого human approval не просится: решение ещё не имеет согласованного evidence.'),
+ h2('Case 3. Incomplete test: editor прошёл, отсутствующая роль тоже проходит'),
+ p('Третий case связан с доступом, но не следует называть его доказанной security vulnerability. Fixed contract задаёт небольшой мир: editor разрешён, viewer и отсутствующее значение отклоняются. Generated condition пишет actorRole !== "viewer". Editor действительно проходит; viewer действительно не проходит. Но отсутствие роли тоже достигает allow(). Если suite содержит только editor test, она сообщает ровно один факт: editor path работает. Она не сообщает, что default-deny сохранён.'),
+ p('Симптом: хороший accept case выдаётся за access decision. Причина: negative path не оформлен как контракт. Проверка: запустить fixed viewer и missing-role assertions, прочитать условие как allow-list, а не как «почти deny-list», и спросить reviewer о политике отсутствующего значения. Действие: записать allow-list и добавить оба отрицательных случая. Это не гарантирует безопасность реальной авторизации: здесь нет токенов, tenant boundary, identity provider и threat model. Но это устраняет конкретную дыру в заявленном small contract.'),
+ figure('/assets/editorial/2025/ai-code-verification-2025-evidence-chain.svg', 'Цепочка evidence для спорного generated diff: от фиксированного контракта через статический результат, тест, review и ручной сценарий к явному решению block или human approval.', 'Цепочка отделяет наблюдения от решения. Стрелка в stop не делает revert или rollback: она прекращает только подготовку merge для fixed synthetic карточки.'),
+ table('Три fixed synthetic diff cases', ['Case', 'Зелёный сигнал', 'Расходящееся evidence', 'Что блокируется', 'Следующее действие'], [
+ ['Contract mismatch', 'число рассчитано', 'shape и consumer access требуют amountCents', 'merge preparation', 'вернуть поле или вынести compatibility decision'],
+ ['Hidden side effect', 'preview output совпал', 'input ownership нарушен присваиванием', 'human approval request', 'добавить derived value и input equality check'],
+ ['Incomplete test', 'editor allowed', 'отсутствующая роль не проверена и проходит условие', 'acceptance of access rule', 'написать allow-list и negative cases'],
+ ]),
+ h2('Компактный прогон evidence chain'),
+ p('Этот пример проходит ровно ту же fixed in-memory цепочку, что описана в трёх cases. Он не читает PR, не запускает test runner, не вызывает модель и не отправляет сообщение человеку. Его цель — проверить форму решения: plan должен оставаться blocked, пока canonical evidence совпадает с зафиксированной карточкой и явно содержит disagreement.'),
+ code([
+ "import {",
+ " createFixedSyntheticVerificationInput,",
+ " inspectSyntheticGeneratedDiff,",
+ " buildSyntheticVerificationPlan,",
+ " stopSyntheticMerge,",
+ "} from './upgrade-2025-02.mjs';",
+ '',
+ "const report = inspectSyntheticGeneratedDiff(",
+ " createFixedSyntheticVerificationInput('incomplete-test'),",
+ ');',
+ 'const plan = buildSyntheticVerificationPlan(report);',
+ 'const stopped = stopSyntheticMerge(plan);',
+ '',
+ 'console.log(plan.mergeDecision); // blocked-until-independent-evidence-agrees',
+ 'console.log(stopped.stopped); // true',
+ 'console.log(stopped.evidenceChain); // fixed evidence identifiers only',
+ ].join('\n')),
+ p('Fixture у script проверяет именно границы формы: exact keys, dense arrays, unknown keys, cycles и canonical comparison. Если report получает лишнее поле, если action array становится sparse, если self-reference попадает в data или if finding меняют после создания report, next plan закрывается. Такая проверка не утверждает, что сериализация решает инженерную задачу. Она защищает учебный механизм от знакомой подмены: внешне похожее evidence уже содержит неподтверждённый факт или действие.'),
+ h2('Где проходит граница stop, revert и rollback'),
+ p('Stop в этом материале означает одно: не продолжать подготовку merge, пока evidence расходится. Он ничего не меняет в VCS и не отправляет команду в delivery pipeline. Revert — отдельное решение об отмене конкретного change, для него нужно знать, принят ли change, как устроена история и кто несёт ответственность за обратный diff. Rollback — отдельное решение о восстановлении уже доставленного состояния; ему нужны реальный scope, состояние данных, проверка восстановления и authority. Подменять stop словом rollback опасно: оно создаёт видимость, что путь восстановления уже доказан.'),
+ p('Human approval нужен после того, как команда может сформулировать выбор. Например: «мы сознательно переименовываем public field и публикуем compatibility path» или «мы сохраняем default-deny и покрыли fixed missing-role branch». Approval не должен закрывать пустоту evidence. Если reviewer не может назвать contract, user-visible boundary и residual risk, вопрос ещё не готов к одобрению. В реальном процессе назначение владельца и полномочия зависят от политики команды; synthetic role в этой статье их не заменяет.'),
+ h2('Как документировать next action'),
+ p('У хорошего next action пять частей: case, нарушение, evidence, owner question и stop condition. «Починить AI code» — плохой action: непонятно, что исправлять и чем закончить. «Для fixed guard записать allow-list; добавить viewer и missing-role tests; reviewer подтверждает default-deny; до этого merge blocked» — хороший. Он короткий, проверяемый и не обещает безопасность системы. После того как action выполнен в настоящем репозитории, команда всё равно должна заново собрать фактические evidence: учебная карточка не переносит verdict в production.'),
+ table('Шаблон записи спорного diff', ['Поле', 'Что записать', 'Что не писать'], [
+ ['Contract', 'одна форма, инвариант или owner boundary', 'общую фразу «код должен быть качественным»'],
+ ['Evidence', 'какой check нашёл или не нашёл риск', '«всё зелёное» без scope'],
+ ['Decision', 'block, revise или вынести на approval', 'rollback, если ничего не доставлялось'],
+ ['Owner question', 'кто принимает остаточный риск и на каких данных', 'имя человека без вопроса и полномочий'],
+ ['Stop', 'точное расхождение, блокирующее merge', 'оценку вкуса или доверие генератору'],
+ ]),
+ h2('Порядок перед передачей на approval'),
+ ol([
+ 'Соберите цепочку. Contract, static result, positive/negative test, review rationale и ручной сценарий должны ссылаться на один scope.',
+ 'Отделите observation от decision. Запишите, что каждый signal подтверждает, и не называйте его готовым merge verdict.',
+ 'Сработайте stop. При противоречии не создавайте revert или rollback автоматически; сохраните evidence и блокируйте только preparation.',
+ 'Сформулируйте вопрос владельцу. Он должен иметь выбор, границу полномочий и недостающее evidence, а не просьбу «одобрить AI».',
+ ]),
+ h2('Почему источники не дают готового verdict'),
+ p('GitHub Docs на фиксированном commit предупреждают, что generated suggestions и AI review могут пропускать ошибки, давать false positive и предлагать небезопасный код; это аргумент за проверку и human review, не за автоматическое отклонение каждого diff. NIST SSDF PW.7 рекомендует проводить review и analysis по правилам организации, фиксировать и triage findings; он не задаёт единственный список gates. OWASP Code Review Guide подчёркивает, что инструменты не заменяют context и человеческое подтверждение; он не обещает, что manual review найдёт всё. Поэтому final verdict принадлежит не источнику и не модели, а человеку с нужными полномочиями и фактическими evidence.'),
+ h2('Ограничения и следующий проверяемый шаг'),
+ p('Все три cases — fixed synthetic JavaScript literals: contract names, field names, role values, diff fragments, evidence, tests, owners и verdicts придуманы как учебный материал и хранятся в памяти. Пакет не получает prompt, не вызывает AI, не читает source code, Git, CI, logs, telemetry, секреты или production. Отсюда нельзя сделать вывод о реальной уязвимости, качестве модели, поведении пользователей или готовности релиза.'),
+ p('Следующий шаг: для первого diff, где test и review дают разные сигналы, выпишите evidence chain на одной странице. Не начинайте с решения. Сначала обозначьте contract, затем каждое наблюдение и его scope, потом stop. Только после этого сформулируйте вопрос владельцу: approve exception, revise diff или собрать недостающее evidence. Ожидаемый результат — спор о фактах и границах, а не спор о том, насколько убедительно выглядит generated code.'),
+ h2('Историческая граница февраля 2025'),
+ p('В выводах используются только материалы, доступные к февралю 2025: immutable GitHub Docs commit от 12 декабря 2024, NIST SSDF Version 1.1 final 2022 и OWASP Code Review Guide 2.0 2017. Никаких сведений о поздней эволюции моделей, агентов или автоматических approval-практик в эти cases не добавлено.'),
+]);
+
+export const revisions = [practice, mechanism, field].map(({ proseLength, ...item }) => Object.freeze(item));
+
+function verifyFixture() {
+ const report = runAiCodeVerificationFixture();
+ 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');