revise October 2026 code review plan articles
Build and deploy / deploy (push) Successful in 20s

This commit is contained in:
2026-07-31 19:54:32 +03:00
parent fcf82327b9
commit 3d879666ea
7 changed files with 402 additions and 1 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Производство редакционных партий # Производство редакционных партий
На 31 июля 2026 года строгий аудит проходит 313 из 358 созданных материалов. Остальные 45 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить. На 31 июля 2026 года строгий аудит проходит 316 из 358 созданных материалов. Остальные 42 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
## Одна партия ## Одна партия
+64
View File
@@ -0,0 +1,64 @@
# P104 — October 2026: «Стандарт code review»
## Статус и фактическая граница
Это изолированный draft-пакет. На дату подготовки — **31 июля 2026** — октябрь ещё не наступил. Все три статьи прямо оформлены как план/сценарий на октябрь 2026, а не как field report, история pull request, CI-прогон, merge, release, deploy, публикация или результат настоящего review.
Все входы — named fixed synthetic literals в памяти. Разрешённый положительный статус только `synthetic-review-hand-off`, в нём есть `productionEffect: not-attempted`. Код не читает repository, diff, сеть, файловую систему, часы, secrets, CI или production и не совершает внешних действий.
## Исследование источников до cutoff
1. [RFC 2119: Key words for use in RFCs to Indicate Requirement Levels](https://www.rfc-editor.org/rfc/rfc2119), BCP 14, March 1997, DOI `10.17487/RFC2119`. Проверены дата, определение `MUST`, `SHOULD`, `MAY` и ограничение: императивы надо применять бережно там, где они действительно нужны для совместимости или ограничения вредного поведения. Граница: RFC не является policy code review и не создаёт обязанностей этой команды.
2. [RFC 8174: Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words](https://www.rfc-editor.org/rfc/rfc8174), BCP 14, May 2017, DOI `10.17487/RFC8174`. Использован только для уточнения контекста прописных терминов. Граница: документ не делает synthetic validator стандартом проекта.
3. [NIST SP 800-218, SSDF Version 1.1](https://doi.org/10.6028/NIST.SP.800-218), February 2022, DOI `10.6028/NIST.SP.800-218`. Проверены месяц публикации, статус рекомендаций и назначение: высокоуровневые secure-development practices, интегрируемые в разные SDLC. Граница: NIST не подтверждает риск, уязвимость, CI или review конкретной организации.
Ни одно утверждение не зависит от факта после cutoff **2026-07-31**. Внешние документы отделены от моей проектной модели матрицы и fixed literals.
## Review pass 1 — проблема, плотность, голос, независимость
- **Practice** начинается с contract/migration change, который тонет в замечаниях о стиле; цена — не названные consumers и rollback. Угол: decision/evidence matrix.
- **Mechanism** начинает с вывода, который сильнее evidence; цена — ложная определённость о поведении системы. Угол: риск → evidence → допустимый вывод → fail-closed gate.
- **Field** начинается с hand-off, передающего вердикт вместо вопроса; цена — исчезновение consumer и rollback boundary. Угол: bounded synthetic hand-off без имитации истории.
- Тон M9: прагматичные короткие утверждения, ownership границ и различение факта, question, stop и вывода. Общего вводного блока нет; одинаковая терминология объяснена через разные задачи.
- Основной текст: 9 229 / 10 115 / 10 190 знаков (practice / mechanism / field). Все три тела в контракте 5 000–15 000 и в целевом диапазоне 9 000–13 000.
## Review pass 2 — факты, future boundary и literal execution
- Все fixed cases несут `planDate: 2026-10` и `sourceCutoff: 2026-07-31`; недатированный вход закрывается fail-closed status.
- Factory применяет JSON clone и deep freeze. Полный case не даёт approval: только `synthetic-review-hand-off` с `productionEffect: not-attempted`.
- Проверены четыре stop: неизвестный риск, недостаточный evidence, style вместо contract risk и positive conclusion `approve-and-merge`.
- Literal snippets выполнены через public exports. Получены статусы: `synthetic-review-hand-off`, `stop-insufficient-evidence`, `stop-disallowed-positive-conclusion`.
- В каждой статье есть независимые: механизм, HTML-таблица, оригинальный SVG с точными русскими alt/caption, runnable example, упорядоченные действия, границы и следующий шаг.
## Review pass 3 — выпуск, mobile SVG, уникальность
- XML валиден для трёх SVG; safety scan не нашёл `script`, `foreignObject`, `javascript:`, `data:image` или event handlers. Рабочий текст 20–27 px в viewBox 720×500.
- Все SVG отрендерены Sharp в 375 px и визуально проверены: заголовок и 4 колонки матрицы читаются, два stop gate не обрезаны, loop сохраняет стрелки и нижний stop. Фигуры несут данные о связи risk/evidence/conclusion, а не иллюстрируют тему декоративно.
- Строгая pairwise проверка исключила source lists, но включила code. Фактический результат каждой из трёх пар: `exactParagraphs160: 0`, `common12WordFragments: 0`.
## Команды локальной проверки
Запуск из `web/`:
```sh
node --check scripts/upgrade-2026-10.mjs
node scripts/upgrade-2026-10.mjs --verify-fixture
npm run audit:draft -- scripts/upgrade-2026-10.mjs
node --input-type=module -e "import { createFixedReviewCase, createFixedReviewHandOff, assessFixedDecisionEvidence } from './scripts/upgrade-2026-10.mjs'; const a=createFixedReviewHandOff(createFixedReviewCase('decision-evidence-ready-v1')); const b=assessFixedDecisionEvidence(createFixedReviewCase('missing-consumer-map-v1')); const c=createFixedReviewHandOff(createFixedReviewCase('approval-word-v1')); console.log(a.status, b.status, c.status);"
xmllint --noout public/assets/editorial/2026/code-review-standard-2026-decision-evidence-matrix.svg public/assets/editorial/2026/code-review-standard-2026-risk-escalation-gates.svg public/assets/editorial/2026/code-review-standard-2026-review-handoff-loop.svg
rg -n -i "<(script|foreignObject)\\b|javascript:|data:image|(?:^|[[:space:]])on[a-z]+=" public/assets/editorial/2026/code-review-standard-2026-decision-evidence-matrix.svg public/assets/editorial/2026/code-review-standard-2026-risk-escalation-gates.svg public/assets/editorial/2026/code-review-standard-2026-review-handoff-loop.svg
```
## Disposition
Локальные проверки прошли: syntax; fixture `7/7`; editorial audit для всех трёх slug; literal execution; XML; SVG safety; Sharp mobile inspection; strict duplicate scan. Draft ожидает отдельной независимой приёмки. Registry, README, application files, articles JSON, production queue и все Git-действия намеренно не затрагивались.
## Независимая приёмка основного редактора — 31.07.2026
Принято только как **октябрьский plan/scenario**. Месяц и cutoff `2026-07-31` явно присутствуют, а максимальный positive output — `synthetic-review-hand-off` с `productionEffect: not-attempted`; review не подменяется approval, merge, release или фактом о CI/production.
Проверены первичные источники: RFC 2119 — BCP 14 от March 1997 и действительно требует осторожности с императивами; RFC 8174 — BCP 14 от May 2017 и уточняет особый смысл только uppercase терминов. NIST SP 800-218 v1.1 подтверждает February 2022 и описывает высокоуровневые практики, интегрируемые в SDLC; исходное неподтверждённое уточнение «final, 3 February» удалено до интеграции. Из документов не выводятся локальные policy, реальный риск или состояние review.
Повторно выполнены syntax, fixture `7/7`, draft audit с объёмами 9 229 / 10 115 / 10 190, XML и SVG safety-scan. Public exports возвращают `synthetic-review-hand-off`, `stop-insufficient-evidence`, `stop-disallowed-positive-conclusion` и `stop-style-displaces-risk` на fixed inputs. PNG-рендеры трёх SVG визуально осмотрены на 375 px: таблица, stop-gates и loop читаются без обрезания. Строгий scan, включающий code и исключающий source lists, дал ноль совпадающих абзацев от 160 символов и ноль общих 12-словных фрагментов во всех трёх парах.
В registry добавлены только три октябрьские ревизии. Пользовательские application-файлы, `articles.json`, production queue и остальная незакоммиченная работа не вошли в приёмку.
+2
View File
@@ -100,6 +100,7 @@ import { revisions as june2026Revisions } from '../scripts/upgrade-2026-06.mjs';
import { revisions as july2026Revisions } from '../scripts/upgrade-2026-07.mjs'; import { revisions as july2026Revisions } from '../scripts/upgrade-2026-07.mjs';
import { revisions as august2026Revisions } from '../scripts/upgrade-2026-08.mjs'; import { revisions as august2026Revisions } from '../scripts/upgrade-2026-08.mjs';
import { revisions as september2026Revisions } from '../scripts/upgrade-2026-09.mjs'; import { revisions as september2026Revisions } from '../scripts/upgrade-2026-09.mjs';
import { revisions as october2026Revisions } from '../scripts/upgrade-2026-10.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 = [
@@ -205,4 +206,5 @@ export const editorialRevisions = [
...july2026Revisions, ...july2026Revisions,
...august2026Revisions, ...august2026Revisions,
...september2026Revisions, ...september2026Revisions,
...october2026Revisions,
]; ];
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 500" role="img" aria-labelledby="title desc">
<title id="title">Матрица решения и доказательств для планового code review</title>
<desc id="desc">Три строки рисков: контракт, эксплуатация и security. У каждой перечислены evidence, допустимый вывод и stop.</desc>
<rect width="720" height="500" fill="#f7f8fb"/>
<text x="36" y="45" font-family="Arial, sans-serif" font-size="27" font-weight="700" fill="#18212f">Решение начинается с evidence</text>
<text x="36" y="76" font-family="Arial, sans-serif" font-size="21" fill="#4b5a70">План на октябрь 2026 · не факт review</text>
<rect x="28" y="102" width="664" height="54" rx="8" fill="#213a63"/>
<g font-family="Arial, sans-serif" font-size="21" font-weight="700" fill="#fff"><text x="46" y="136">Риск</text><text x="194" y="136">Evidence</text><text x="430" y="136">Вывод</text><text x="584" y="136">Gate</text></g>
<g font-family="Arial, sans-serif" font-size="22" fill="#18212f">
<rect x="28" y="158" width="664" height="88" rx="8" fill="#e8f0ff"/><text x="46" y="191">Контракт</text><text x="194" y="189">schema · consumers</text><text x="194" y="218">rollback</text><text x="430" y="201">hand-off</text><text x="584" y="201" fill="#a3212a">STOP</text>
<rect x="28" y="258" width="664" height="88" rx="8" fill="#e7f7f3"/><text x="46" y="291">Эксплуатация</text><text x="194" y="289">state · failure</text><text x="194" y="318">наблюдение</text><text x="430" y="301">вопрос</text><text x="584" y="301" fill="#a3212a">STOP</text>
<rect x="28" y="358" width="664" height="88" rx="8" fill="#fff2e5"/><text x="46" y="391">Security</text><text x="194" y="389">trust · input</text><text x="194" y="418">abuse effect</text><text x="430" y="401">эскалация</text><text x="584" y="401" fill="#a3212a">STOP</text>
</g>
<text x="36" y="480" font-family="Arial, sans-serif" font-size="20" fill="#4b5a70">Нет нужной строки — не усиливать вывод</text>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 500" role="img" aria-labelledby="title desc">
<title id="title">Цикл bounded synthetic review hand-off</title>
<desc id="desc">Четыре шага: назвать учебный change, выбрать риск, приложить evidence и передать вопрос либо вернуть точный stop.</desc>
<rect width="720" height="500" fill="#f8fafc"/>
<text x="35" y="45" font-family="Arial, sans-serif" font-size="27" font-weight="700" fill="#18212f">Передать вопрос, не вердикт</text>
<text x="35" y="76" font-family="Arial, sans-serif" font-size="21" fill="#4b5a70">Synthetic loop на октябрь 2026</text>
<g font-family="Arial, sans-serif" font-size="23" font-weight="700" text-anchor="middle" fill="#18212f">
<rect x="65" y="135" width="235" height="70" rx="12" fill="#dce9ff"/><text x="182" y="179">1. Fixed change</text>
<rect x="420" y="135" width="235" height="70" rx="12" fill="#e7f7f3"/><text x="537" y="179">2. Риск</text>
<rect x="420" y="295" width="235" height="70" rx="12" fill="#fff2e5"/><text x="537" y="339">3. Evidence</text>
<rect x="65" y="295" width="235" height="70" rx="12" fill="#e8eef9"/><text x="182" y="339">4. Hand-off / stop</text>
</g>
<g stroke="#52647e" stroke-width="7" fill="none"><path d="M310 170H406"/><path d="M537 213V283"/><path d="M410 330H312"/><path d="M182 283V216"/></g>
<g fill="#52647e"><path d="M406 170l-13-9v18z"/><path d="M537 283l-9-13h18z"/><path d="M312 330l13-9v18z"/><path d="M182 216l-9 13h18z"/></g>
<rect x="150" y="412" width="420" height="46" rx="10" fill="#ffe1e2"/><text x="360" y="442" text-anchor="middle" font-family="Arial, sans-serif" font-size="21" font-weight="700" fill="#9f222b">Пробел evidence → точный STOP</text>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 500" role="img" aria-labelledby="title desc">
<title id="title">Escalation gates для риск-ориентированного review</title>
<desc id="desc">Последовательность от названного риска к evidence, затем либо точный stop, либо synthetic hand-off без approval.</desc>
<rect width="720" height="500" fill="#f8fafc"/>
<text x="35" y="45" font-family="Arial, sans-serif" font-size="27" font-weight="700" fill="#18212f">Gate не разрешает догадку</text>
<text x="35" y="76" font-family="Arial, sans-serif" font-size="21" fill="#4b5a70">Плановый сценарий · источник до 31.07.2026</text>
<g font-family="Arial, sans-serif" font-size="24" font-weight="700" text-anchor="middle">
<rect x="45" y="150" width="185" height="92" rx="12" fill="#dce9ff"/><text x="137" y="188" fill="#18212f">Назвать</text><text x="137" y="219" fill="#18212f">риск</text>
<rect x="268" y="150" width="185" height="92" rx="12" fill="#e5f5ee"/><text x="360" y="188" fill="#18212f">Проверить</text><text x="360" y="219" fill="#18212f">evidence</text>
<rect x="490" y="150" width="185" height="92" rx="12" fill="#e8eef9"/><text x="582" y="188" fill="#18212f">Только</text><text x="582" y="219" fill="#18212f">hand-off</text>
</g>
<g stroke="#52647e" stroke-width="7" fill="none"><path d="M230 196H258"/><path d="M453 196H480"/></g><g fill="#52647e"><path d="M258 196l-12-9v18z"/><path d="M480 196l-12-9v18z"/></g>
<g font-family="Arial, sans-serif" font-size="22" font-weight="700" text-anchor="middle"><rect x="45" y="320" width="185" height="78" rx="12" fill="#ffe1e2"/><text x="137" y="367" fill="#9f222b">STOP: риск?</text><rect x="268" y="320" width="185" height="78" rx="12" fill="#ffe1e2"/><text x="360" y="367" fill="#9f222b">STOP: пробел</text><rect x="490" y="320" width="185" height="78" rx="12" fill="#fff0d8"/><text x="582" y="367" fill="#86500b">Без approval</text></g>
<g stroke="#a3212a" stroke-width="5" fill="none"><path d="M137 242V307"/><path d="M360 242V307"/></g><g fill="#a3212a"><path d="M137 307l-9-13h18z"/><path d="M360 307l-9-13h18z"/></g>
<text x="35" y="466" font-family="Arial, sans-serif" font-size="20" fill="#4b5a70">Положительная ветка не означает merge, release или deploy</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+288
View File
@@ -0,0 +1,288 @@
function escapeHtml(value) {
return String(value).replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&#039;');
}
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((cell) => '<th scope="col">' + cell + '</th>').join('') + '</tr></thead><tbody>' + rows.map((row) => '<tr>' + row.map((cell) => '<td>' + cell + '</td>').join('') + '</tr>').join('') + '</tbody></table></div>';
function deepFreeze(value) {
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
Object.values(value).forEach(deepFreeze);
Object.freeze(value);
}
return value;
}
function cloneFixed(value) { return JSON.parse(JSON.stringify(value)); }
function plainText(html) { return html.replace(/<[^>]+>/g, ' ').replace(/&(?:quot|amp|lt|gt|#039);/g, ' ').replace(/\s+/g, ' ').trim(); }
function bodyText(html) { return plainText(html.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, '')); }
const REFERENCES = deepFreeze({
rfc2119: {
title: 'RFC 2119: Key words for use in RFCs to Indicate Requirement Levels',
url: 'https://www.rfc-editor.org/rfc/rfc2119',
version: 'BCP 14, March 1997, DOI 10.17487/RFC2119',
},
ssdf: {
title: 'NIST SP 800-218: Secure Software Development Framework (SSDF) Version 1.1',
url: 'https://doi.org/10.6028/NIST.SP.800-218',
version: 'February 2022, NIST SP 800-218, DOI 10.6028/NIST.SP.800-218',
},
rfc8174: {
title: 'RFC 8174: Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words',
url: 'https://www.rfc-editor.org/rfc/rfc8174',
version: 'BCP 14, May 2017, DOI 10.17487/RFC8174',
},
});
function sources(entries) {
return '<ul>' + entries.map(({ key, use, boundary }) => {
const ref = REFERENCES[key];
return '<li><a href="' + ref.url + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(ref.title) + '</a> — версия: ' + escapeHtml(ref.version) + '. ' + escapeHtml(use) + ' Граница: ' + escapeHtml(boundary) + '</li>';
}).join('') + '</ul>';
}
const FIXED_REVIEW_CASES = deepFreeze({
'decision-evidence-ready-v1': {
id: 'decision-evidence-ready-v1', planDate: '2026-10', sourceCutoff: '2026-07-31',
change: 'fixed-nullable-discount-contract', decision: 'request-contract-evidence',
evidence: ['fixed-schema-delta', 'fixed-consumer-map', 'fixed-rollback-note'],
risk: 'contract-migration', conclusion: 'synthetic-review-hand-off',
boundary: 'Fixed in-memory planning literal. It does not read a pull request, diff, repository, CI, clock, filesystem, network, secret, production system or customer data.',
},
'missing-consumer-map-v1': {
id: 'missing-consumer-map-v1', planDate: '2026-10', sourceCutoff: '2026-07-31',
change: 'fixed-nullable-discount-contract', decision: 'request-contract-evidence',
evidence: ['fixed-schema-delta', 'fixed-rollback-note'], risk: 'contract-migration', conclusion: 'synthetic-review-hand-off', boundary: 'fixed synthetic literal',
},
'style-only-for-contract-risk-v1': {
id: 'style-only-for-contract-risk-v1', planDate: '2026-10', sourceCutoff: '2026-07-31',
change: 'fixed-nullable-discount-contract', decision: 'style-note',
evidence: ['fixed-schema-delta', 'fixed-consumer-map', 'fixed-rollback-note'], risk: 'contract-migration', conclusion: 'synthetic-review-hand-off', boundary: 'fixed synthetic literal',
},
'approval-word-v1': {
id: 'approval-word-v1', planDate: '2026-10', sourceCutoff: '2026-07-31',
change: 'fixed-nullable-discount-contract', decision: 'request-contract-evidence',
evidence: ['fixed-schema-delta', 'fixed-consumer-map', 'fixed-rollback-note'], risk: 'contract-migration', conclusion: 'approve-and-merge', boundary: 'fixed synthetic literal',
},
'unknown-risk-v1': {
id: 'unknown-risk-v1', planDate: '2026-10', sourceCutoff: '2026-07-31',
change: 'fixed-nullable-discount-contract', decision: 'request-contract-evidence',
evidence: ['fixed-schema-delta', 'fixed-consumer-map', 'fixed-rollback-note'], risk: 'unknown', conclusion: 'synthetic-review-hand-off', boundary: 'fixed synthetic literal',
},
});
export function createFixedReviewCase(id) {
const value = FIXED_REVIEW_CASES[id];
return value ? deepFreeze(cloneFixed(value)) : undefined;
}
const requiredEvidence = deepFreeze({
'contract-migration': deepFreeze(['fixed-schema-delta', 'fixed-consumer-map', 'fixed-rollback-note']),
'operational-behavior': deepFreeze(['fixed-state-transition', 'fixed-failure-mode', 'fixed-observation-boundary']),
'security-boundary': deepFreeze(['fixed-trust-boundary', 'fixed-input-rule', 'fixed-abuse-consequence']),
});
export function assessFixedDecisionEvidence(reviewCase) {
if (!reviewCase || reviewCase.planDate !== '2026-10' || reviewCase.sourceCutoff !== '2026-07-31') return deepFreeze({ status: 'stop-undated-scenario', reason: 'october-plan-and-july-cutoff-required', nextAction: 'name-scenario-date-and-source-cutoff' });
const needed = requiredEvidence[reviewCase.risk];
if (!needed) return deepFreeze({ status: 'stop-unknown-risk-class', reason: 'risk-class-is-not-in-fixed-matrix', nextAction: 'name-one-bounded-risk-class' });
if (reviewCase.decision === 'style-note' && reviewCase.risk !== 'style-only') return deepFreeze({ status: 'stop-style-displaces-risk', reason: 'style-note-cannot-close-contract-or-behavior-risk', nextAction: 'replace-style-note-with-evidence-request' });
const missing = needed.filter((item) => !reviewCase.evidence.includes(item));
if (missing.length) return deepFreeze({ status: 'stop-insufficient-evidence', reason: 'fixed-evidence-set-is-incomplete', missing: deepFreeze(missing), nextAction: 'request-only-the-named-missing-evidence' });
return deepFreeze({ status: 'decision-evidence-map-ready', risk: reviewCase.risk, decision: reviewCase.decision, evidence: deepFreeze([...needed]), boundary: reviewCase.boundary });
}
export function createFixedReviewHandOff(reviewCase) {
const checked = assessFixedDecisionEvidence(reviewCase);
if (checked.status !== 'decision-evidence-map-ready') return checked;
if (reviewCase.conclusion !== 'synthetic-review-hand-off') return deepFreeze({ status: 'stop-disallowed-positive-conclusion', reason: 'positive-branch-is-hand-off-only', nextAction: 'use-synthetic-review-hand-off' });
return deepFreeze({ status: 'synthetic-review-hand-off', evidenceMap: checked, productionEffect: 'not-attempted', nextAction: 'synthetic-review-hand-off' });
}
export function runFixedCodeReviewFixture() {
const ready = createFixedReviewHandOff(createFixedReviewCase('decision-evidence-ready-v1'));
const missing = createFixedReviewHandOff(createFixedReviewCase('missing-consumer-map-v1'));
const style = createFixedReviewHandOff(createFixedReviewCase('style-only-for-contract-risk-v1'));
const approval = createFixedReviewHandOff(createFixedReviewCase('approval-word-v1'));
const unknown = createFixedReviewHandOff(createFixedReviewCase('unknown-risk-v1'));
return deepFreeze({ assertions: deepFreeze({
readyIsHandOffOnly: ready.status === 'synthetic-review-hand-off' && ready.productionEffect === 'not-attempted',
readyRetainsThreeEvidenceItems: ready.evidenceMap.evidence.length === 3,
missingConsumerStops: missing.status === 'stop-insufficient-evidence' && missing.missing.includes('fixed-consumer-map'),
styleCannotCloseRisk: style.status === 'stop-style-displaces-risk',
approvalWordStops: approval.status === 'stop-disallowed-positive-conclusion',
unknownRiskStops: unknown.status === 'stop-unknown-risk-class',
factoryReturnsFrozenClone: Object.isFrozen(createFixedReviewCase('decision-evidence-ready-v1')) && createFixedReviewCase('decision-evidence-ready-v1') !== FIXED_REVIEW_CASES['decision-evidence-ready-v1'],
}) });
}
function revision(meta, parts, sourceEntries) {
const contentHtml = parts.join('\n') + '\n' + h2('Проверяемые источники') + sources(sourceEntries);
const proseLength = bodyText(contentHtml).length;
if (proseLength < 5000 || proseLength > 15000) throw new Error(meta.slug + ': body length ' + proseLength);
return deepFreeze({ ...meta, contentHtml, proseLength });
}
const practice = revision({
slug: 'editorial-2026-10-practice-code-review-standard',
title: 'Октябрьский стандарт code review: матрица решения и доказательства вместо замечаний о стиле',
categories: ['Инженерные практики', 'Архитектура'],
cover: '/assets/editorial/2026/code-review-standard-2026-decision-evidence-matrix.svg',
excerpt: 'Сценарий на октябрь 2026: как превратить review из списка вкусовых замечаний в проверяемую матрицу риска, evidence и stop-the-line условий.',
readingMinutes: 18,
}, [
p('В октябре 2026 я бы начал стандарт review с простой поломки процесса: change меняет nullable поле, порядок миграции или обработку ошибки, а обсуждение застревает на названии переменной и длине функции. Контрактный риск остаётся без вопроса. Цена не эстетическая: потребитель получает новое значение раньше адаптера, rollback не назван, а команда позже спорит не о факте, а о том, «кто должен был заметить».'),
p('Вторая цена — ложный положительный исход. Фраза «выглядит хорошо» может звучать как решение, хотя у reviewer нет карты consumers, границы изменения и допустимого вывода. Это не отчёт об октябрьских pull request или CI. Ни один review не проводился. Ниже — плановый сценарий на октябрь 2026, источники ограничены 31.07.2026, а максимум результата fixed literal — <code>synthetic-review-hand-off</code>.'),
h2('Матрица сначала ограничивает вопрос'),
p('Матрица нужна не для оценки человека и не для механического чеклиста. Она вынуждает назвать четыре вещи до комментария: что именно меняется, какой риск следует из этой границы, какое доказательство способно сузить риск и какой вывод пока запрещён. Если строка не помещается в эти четыре колонки, reviewer не обязан сочинять диагноз. Его корректное действие — stop: изменение недостаточно описано для технического вывода.'),
p('У решения есть масштаб. Для style-only change достаточно указать локальную читаемость и предложить отдельный cleanup. Для contract migration этого недостаточно: нужны schema delta, карта consumers и заметка о возврате на старую форму. Для operational behavior нужны состояние, failure mode и граница наблюдения; для security boundary — доверенная сторона, правило входа и последствие злоупотребления. Таблица не утверждает, что эти категории покрывают любой проект. Она не позволяет заменить отсутствующее evidence уверенным тоном.'),
figure('/assets/editorial/2026/code-review-standard-2026-decision-evidence-matrix.svg', 'Матрица решения и доказательств: для риска контракта, эксплуатации и security перечислены входные evidence, допустимый вывод и красный stop при пробеле.', 'Схема — плановая карта разговора на октябрь 2026. Она не изображает выполненный review и не присваивает изменениям статус.'),
table('Плановая матрица решения', ['Риск', 'Входное evidence', 'Допустимый вывод', 'Stop-the-line'], [
['contract migration', 'schema delta; consumer map; rollback note', 'передать synthetic hand-off', 'consumer или возврат не назван'],
['operational behavior', 'state transition; failure mode; observation boundary', 'запросить точное уточнение', 'следующая ветка состояния неизвестна'],
['security boundary', 'trust boundary; input rule; abuse consequence', 'эскалировать вопрос владельцу риска', 'нет модели нарушителя или последствия'],
['style-only', 'локальный фрагмент и причина читаемости', 'оставить необязательную заметку', 'стилем закрывают иной риск'],
]),
h2('Evidence — не ссылка на ощущение'),
p('Полезное evidence можно проверить в пределах обсуждаемой модели. <code>fixed-schema-delta</code> отвечает, какое поле меняет форму. <code>fixed-consumer-map</code> отвечает, кто предполагается получателем. <code>fixed-rollback-note</code> отвечает, какая старая форма должна пережить отмену сценария. Это не артефакты настоящего репозитория и не доказательство работоспособности. Это именованные минимумы, без которых нельзя даже сформулировать вопрос о совместимости.'),
p('Важно отделить evidence от вывода. «Вижу три файла» — наблюдение, но не карта consumers. «Есть тест» — факт о названии, но не объяснение failure mode. «Автор уверен» — не технический аргумент. В октябрьском сценарии reviewer записывает связь в явном виде: риск <em>contract migration</em> требует три конкретные позиции; отсутствие хотя бы одной переводит ответ в stop. Так ожидание не прячется в личном опыте самого громкого участника.'),
h2('Нормативные слова не заменяют контекст'),
p('RFC 2119 определяет MUST, SHOULD и MAY для документов, где заранее оговорён уровень требования. RFC 8174 дополнительно уточняет проблему регистров. Из этого не следует, что любой комментарий к коду получает силу стандарта. В плановой матрице слова «обязательно» появляются только у локально названного stop: например, нельзя выводить совместимость, если нет consumer map. Вне этой рамки лучше написать причину и вариант, а не изображать универсальное правило.'),
p('Это разграничение защищает и автора change, и reviewer. Автор видит, что ему требуется не «докажи качество», а три именованные границы. Reviewer не обязан расширять задачу до аудита всего продукта. Когда evidence есть, положительная ветка всё равно не означает approval, merge, release или deployment: она лишь разрешает передать фиксированную карточку следующему владельцу будущего планирования.'),
h2('Буквально исполнимый validator матрицы'),
code("import { createFixedReviewCase, createFixedReviewHandOff } from './upgrade-2026-10.mjs';\n\nconst input = createFixedReviewCase('decision-evidence-ready-v1');\nconst result = createFixedReviewHandOff(input);\nconsole.log({ status: result.status, effect: result.productionEffect, risk: result.evidenceMap.risk });\n// { status: 'synthetic-review-hand-off', effect: 'not-attempted', risk: 'contract-migration' }"),
p('Snippet читает только JSON-cloned и deeply frozen named literal. Он не открывает pull request, не читает diff, CI, сеть, файловую систему, секреты или часы. Его positive result предельно узок: все три строки synthetic matrix присутствуют, поэтому можно передать сценарий как <code>synthetic-review-hand-off</code>. Он не оценивает реальную реализацию и не даёт разрешения изменять состояние где-либо вне памяти процесса.'),
h2('Цена evidence зависит от того, когда его запросили'),
p('У каждого доказательства есть стоимость подготовки и стоимость чтения. Schema delta удобно просить в начале: автор ещё помнит, что меняет, и может показать форму без исторического рассказа. Consumer map дороже, потому что она требует назвать предел поиска, а не перечислить все сервисы компании. Rollback note ценна не как обещание отмены, а как проверка направленности изменения: можно ли вообще говорить о возврате, если форма уже ушла за необратимую границу. Матрица делает эту стоимость видимой до того, как обсуждение превратится в длинную очередь мелких замечаний.'),
p('Не всякое evidence следует требовать в каждом случае. Если изменение действительно style-only и это явно ограничено, запросы про consumers создадут ритуал без пользы. Но симметричная ошибка опаснее: назвать contract migration косметикой, чтобы не собирать карту. Поэтому scope тоже должен быть evidence. Reviewer не обязан принимать «это только рефакторинг» как факт; он может попросить назвать invariant, который allegedly не меняется. Пока invariant не назван, классификация остаётся гипотезой и не открывает positive branch.'),
p('Матрица помогает и с асинхронным review. Вместо пяти параллельных вопросов автор получает один список missing items, каждый с назначением. Вместо ответа «добавил тест» reviewer может спросить, какую конкретно неопределённость тест должен был бы убрать. Это не обесценивает тестирование: оно предотвращает ситуацию, где один знакомый артефакт подставляют на место иной модели. Когда связь между evidence и вопросом ясна, комментарии короче, а stop меньше похож на личное недоверие.'),
h2('Отделить решение о риске от решения о форме'),
p('Внутри одного change обычно смешаны два класса решений. Первое — предметное: можно ли считать контрактный переход описанным. Второе — редакторское: читается ли код, уместно ли имя, нужна ли декомпозиция. Оба класса полезны, но у них разная цена ошибки. Редакторская рекомендация может остаться необязательной. Предметная граница требует evidence или stop. Если их свести в один список без меток, сильная проблема легко утонет среди десяти мелких улучшений и получит такой же приоритет, как запятая в сообщении.'),
p('Практическое правило плана: один комментарий несёт один тип действия. <code>request-contract-evidence</code> не маскируется под пожелание «может быть, добавить документацию». <code>style-note</code> не получает формулировку, будто без него система сломается. Это не жесткий формат интерфейса review tool; это дисциплина языка. У получателя остаётся возможность не согласиться с классификацией, зато разногласие становится конкретным: спорим о risk class или о completeness, а не о психологическом подтексте.'),
h2('Как провести плановую сессию'),
ol(['Назвать один будущий change и одну границу: contract, behavior, security или style-only.', 'Записать цену ошибки одним наблюдаемым следствием, не оценкой автора.', 'Выбрать строку матрицы и перечислить только required evidence для неё.', 'Для каждой позиции указать, какой вопрос она снимает, а какой оставляет открытым.', 'Если хотя бы один required input отсутствует, вернуть precise stop без догадки о причине.', 'Если fixed literal полный, сформировать только synthetic hand-off для следующего обсуждения.']),
h2('Границы стандарта и следующий шаг'),
p('Матрица не заменяет дизайн-документ, threat model, тестирование, эксплуатационное наблюдение или полномочия владельца продукта. Она также не назначает единственный набор risks для любой архитектуры. Её ограниченная цель — не позволить style discussion имитировать проверку контракта, миграции и эксплуатации. Если change действительно широк, честный результат — разбить вопрос или остановить его до появления минимальной модели, а не расширять список замечаний.'),
p('Следующий шаг для октябрьского плана — выбрать один нейтральный учебный contract scenario, заполнить матрицу без ссылок на настоящие системы и проверить, что каждый positive result остаётся hand-off only. Лишь после отдельного решения команды можно обсуждать реальные инструменты и policy. Пока такого решения нет, отсутствие evidence должно оставаться видимым и не компенсироваться доброжелательной формулировкой.'),
], [
{ key: 'rfc2119', use: 'Источник задаёт узкое значение терминов MUST, SHOULD и MAY в документе с оговорёнными правилами интерпретации.', boundary: 'RFC не является политикой review, не создаёт организационную обязанность и не описывает этот сценарий.' },
{ key: 'rfc8174', use: 'Источник уточняет трактовку прописных ключевых слов BCP 14.', boundary: 'Он не превращает комментарий или fixed literal в нормативное требование проекта.' },
{ key: 'ssdf', use: 'NIST SSDF используется как внешний vocabulary практик secure software development и рисков уязвимостей.', boundary: 'Документ не подтверждает проверку кода, CI или состояние конкретной организации.' },
]);
const mechanism = revision({
slug: 'editorial-2026-10-mechanism-code-review-standard',
title: 'Октябрьский сценарий code review: риск, доказательство, допустимый вывод и escalation gate',
categories: ['Инженерные практики', 'Надёжность'],
cover: '/assets/editorial/2026/code-review-standard-2026-risk-escalation-gates.svg',
excerpt: 'Сценарий на октябрь 2026: как построить fail-closed escalation gate, чтобы комментарий не выдавал предположение за технический вывод.',
readingMinutes: 19,
}, [
p('В октябрьском сценарии самая дорогая ошибка review возникает не там, где reviewer не нашёл все дефекты, а там, где он сделал вывод сильнее evidence. Например, видит обработку ошибки, но не знает состояние до неё, кому виден результат и что считается обратимым. Комментарий звучит завершённо, однако риск эксплуатации или контракта остаётся без владельца. Цена — ложная определённость: будущая миграция получает не вопрос, а чужую догадку в роли решения.'),
p('Стиль сам по себе не опасен, но он удобен: его можно обсудить без модели риска. Поэтому в октябре 2026 я бы спроектировал mechanism не как рейтинг reviewer, а как цепочку <em>риск → нужное доказательство → допустимый вывод → gate</em>. Это план, не отчёт о проведённом review. Источники зафиксированы на 31.07.2026; named literals не обращаются к PR, CI, сети или production, а их positive branch заканчивается только <code>synthetic-review-hand-off</code>.'),
h2('Риск — это граница, на которой меняется смысл'),
p('Для review полезно классифицировать не «сложность diff», а тип границы. Contract risk возникает, когда меняются форма данных, optionality, порядок полей или ожидание потребителя. Operational risk возникает, когда значение имеет переход состояния, повтор, timeout, отказ или наблюдаемость. Security risk возникает, когда решение зависит от того, кто доверен, какие входы допускаются и к чему приводит злоупотребление. Один change может затрагивать несколько границ; тогда нельзя закрыть все одной общей репликой.'),
p('Класс риска не равен severity и не предсказывает ущерб. Он лишь выбирает минимальный вопрос. Если риск неизвестен, gate должен остановить разговор раньше, чем появится «скорее всего безопасно». Такой stop не обвиняет автора. Он честно сообщает: текущий input недостаточен, чтобы выбрать набор evidence. Это дешевле, чем требовать от reviewer угадать историю системы по нескольким строкам.'),
figure('/assets/editorial/2026/code-review-standard-2026-risk-escalation-gates.svg', 'Три gate: неизвестный риск останавливает сценарий, неполное evidence возвращает запрос, полный named набор допускает только synthetic review hand-off; ветки contract, operational и security различаются.', 'Диаграмма фиксирует допустимую силу вывода. Она не показывает реальный PR, pipeline или результат проверки.'),
table('Связь риска, доказательства и вывода', ['Если известно', 'Ещё нужно назвать', 'Тогда допустимо', 'Запрещено утверждать'], [
['форма поля меняется', 'consumers и возврат формы', 'запросить contract evidence', 'что все клиенты совместимы'],
['есть ветка ошибки', 'state transition и наблюдаемое последствие', 'сформулировать operational question', 'что отказ обработан'],
['есть проверка входа', 'trust boundary и abuse consequence', 'эскалировать security question', 'что граница защищена'],
['есть только стиль', 'отсутствие иных рисков', 'оставить optional note', 'что изменение безопасно'],
]),
h2('Evidence должно уменьшать конкретную неопределённость'),
p('Хорошее evidence не обязано быть большим. Для contract migration <code>fixed-schema-delta</code> фиксирует старую и будущую форму в учебной модели; <code>fixed-consumer-map</code> перечисляет named categories потребителей; <code>fixed-rollback-note</code> удерживает вопрос о возврате. Эти три слова не доказывают выполнение операции. Они уменьшают три разные неопределённости: изменение формы, охват получателей и обратимость. Поэтому один screenshot, один тест или один комментарий автора не подменяет их набор.'),
p('У operational risk другая геометрия. Нужна не карта consumers, а state transition: что было до ветки, что происходит после неё, возможно ли повторение. Failure mode называет, какой отказ рассматривается, но не превращает его в статистику. Observation boundary показывает, что в будущем вообще можно было бы наблюдать, но не заявляет, что измерение уже существует. Такой разбор оставляет результат скромным: вопрос может быть подготовлен, но система не объявлена устойчивой.'),
h2('Gate должен быть fail-closed'),
p('Fail-closed здесь означает ровно одно: неизвестная или неполная модель не получает более сильный вывод. Неизвестный risk class возвращает <code>stop-unknown-risk-class</code>. Неполный набор возвращает <code>stop-insufficient-evidence</code> с точным списком missing. Попытка закрыть contract risk style comment возвращает <code>stop-style-displaces-risk</code>. Механизм не пытается восстановить намерение автора, не ищет нужные данные в мире и не предлагает обходное «можно потом проверить».'),
p('У такого stop есть социальная польза. Он делает спор проверяемым: можно добавить missing consumer map или переименовать риск, а не выигрывать дискуссию авторитетом. Одновременно он защищает от ложной эскалации. Нельзя назвать security problem лишь потому, что diff непривычен; требуются trust boundary, input rule и abuse consequence. Gate направляет к владельцу вопроса, но не изображает найденную уязвимость и не создаёт инцидент.'),
h2('Исполнимый пример ветки stop'),
code("import { createFixedReviewCase, assessFixedDecisionEvidence } from './upgrade-2026-10.mjs';\n\nconst incomplete = createFixedReviewCase('missing-consumer-map-v1');\nconst result = assessFixedDecisionEvidence(incomplete);\nconsole.log({ status: result.status, missing: result.missing, next: result.nextAction });\n// { status: 'stop-insufficient-evidence', missing: ['fixed-consumer-map'], next: 'request-only-the-named-missing-evidence' }"),
p('Этот public export работает над замороженным JSON clone и ничего не читает извне. <code>fixed-consumer-map</code> отсутствует в заранее заданном input, поэтому результат не «проверяет клиентов» и не создаёт задачу. Он просто запрещает переход к более сильной фразе. Такой literal полезен для будущего стандарта: условие stop можно обсудить до появления реального change, когда ни один участник не должен защищать конкретный implementation.'),
h2('Допустимый вывод должен быть проверяемо слабым'),
p('Хорошая формулировка результата содержит глагол, который соответствует входу. При наличии schema delta и consumer map можно сказать: «в плановой модели сформирован вопрос о совместимости». Нельзя сказать: «совместимость доказана». При наличии state transition можно спросить о повторе; нельзя утверждать, что retry безопасен. При наличии trust boundary можно эскалировать к владельцу security; нельзя сообщить, что уязвимость найдена. Эта разница не стилистическая. Она определяет, какие последующие действия читатель сочтёт уже разрешёнными.'),
p('Низкая сила вывода полезна ещё и тем, что переживает изменение вводных. Завтра окажется, что consumer category была неполной или failure mode шире. Если hand-off обещал только уточнение, его не нужно ретроспективно переписывать как ошибочный вердикт. Если он уже заявил safety, команда вынуждена объяснять, почему уверенность появилась раньше evidence. Поэтому механизм сознательно выбирает неброские статусы. Они дают следующему владельцу маршрут, но не забирают у него право принять реальное решение.'),
h2('Граница между stop и эскалацией'),
p('Stop и escalation часто смешивают, хотя у них разные причины. Stop означает: модель не содержит условие, без которого нельзя вывести даже плановый следующий шаг. Escalation означает: условие названо, но его владелец или трактовка лежат за пределом роли reviewer. Например, отсутствие trust boundary — stop; названная trust boundary с неясным abuse consequence — вопрос владельцу security. В обоих случаях нельзя добавить температуру фразой «критично», пока severity не является отдельным доказанным измерением.'),
p('Такое разделение уменьшает обратную связь с шумом. Автор future change не получает длинный список людей, которых нужно позвать «на всякий случай». Он сначала видит missing input. Если input полон, карточка формулирует, кому и зачем передаётся вопрос. В октябрьском сценарии это только учебная маршрутизация; она не открывает доступ к реальным владельцам и не создаёт процессное обязательство. Но она позволяет проверить, что путь эскалации не подменяет пробел evidence.'),
h2('Контрпример: знакомое слово не является моделью'),
p('Слова <code>rollback</code>, <code>retry</code> и <code>validation</code> часто вызывают ощущение, что система уже описана. В matrix это лишь ярлыки. Rollback note без старой формы не объясняет обратимость. Retry без state transition не говорит, повторяется ли побочный эффект. Validation без input rule не указывает, что именно отвергается. Reviewer не должен атаковать знакомое слово; достаточно попросить связать его с минимальным входом. Если связи нет, термин остаётся намерением, не evidence.'),
h2('Escalation — передача вопроса, не передача вины'),
p('Эскалация нужна, когда reviewer понимает тип риска, но не владеет контекстом решения. Для контракта вопрос может уйти владельцу schema; для операции — владельцу state machine или поддержки; для security — тому, кто определяет trust boundary. Handoff должен содержать риск, недостающее evidence, точный вопрос и ограничение вывода. В нём не должно быть «срочно», если нет названной причины, и не должно быть решения за получателя.'),
p('NIST SSDF описывает высокоуровневые практики secure software development, которые интегрируются в разные SDLC. Это полезная внешняя рамка для того, чтобы не вырывать security из жизненного цикла. Но она не диктует состав нашей матрицы и не заменяет локальные роли. Поэтому в сценарии источник поддерживает vocabulary риска, а decision gate остаётся явно проектным и ограниченным named literals.'),
h2('Последовательность планирования gate'),
ol(['Записать один будущий scenario и отделить изменение формы от предположения о его последствиях.', 'Назвать ровно один risk class или вернуть stop unknown-risk-class.', 'Сопоставить классу минимальный набор evidence, не добавляя нерелевантные проверки.', 'Проверить, что каждое evidence снимает отдельную неопределённость.', 'Сформулировать допустимый вывод слабее, чем факт работоспособности, approval или release.', 'Передать только synthetic hand-off либо вернуть missing evidence без выдуманного диагноза.']),
h2('Границы mechanism и следующий шаг'),
p('Эта модель не измеряет severity, не назначает SLA и не делает triage реального потока комментариев. Она не может доказать совместимость, отсутствие уязвимости или корректность migration. Её сила намеренно меньше: создать одинаковый язык для остановки вывода в момент, когда input ещё слаб. Если команде нужен количественный риск, это отдельная модель с данными, владельцем и правом на их использование.'),
p('Следующий шаг в плане на октябрь — прогнать четыре named fixed cases: полный contract input, отсутствующий consumer map, style вместо риска и неизвестный класс. Обсуждать нужно не то, «хорош ли reviewer», а то, дают ли статусы ожидаемые границы. После этого можно решить, нужны ли отдельные строки для API versioning, data retention или rollout; их нельзя молча добавить в existing gate.'),
], [
{ key: 'ssdf', use: 'NIST SSDF задаёт высокоуровневую рамку интегрируемых практик secure software development и общий словарь рисков.', boundary: 'Источник не подтверждает риск, уязвимость, review или процесс конкретной команды.' },
{ key: 'rfc2119', use: 'RFC используется для осторожного объяснения силы обязательных формулировок в спецификациях.', boundary: 'Он не назначает severity, владельца или escalation path данного плана.' },
{ key: 'rfc8174', use: 'RFC уточняет условие интерпретации ключевых слов BCP 14.', boundary: 'Документ не превращает статус fixed validator в approval или требование к реальному change.' },
]);
const field = revision({
slug: 'editorial-2026-10-field-code-review-standard',
title: 'Октябрьский сценарий hand-off в code review: передать ограниченный вопрос, а не выдать вердикт',
categories: ['Инженерные практики', 'Командная работа'],
cover: '/assets/editorial/2026/code-review-standard-2026-review-handoff-loop.svg',
excerpt: 'Сценарий на октябрь 2026: bounded synthetic hand-off для contract risk без имитации pull request, CI, approval или production-проверки.',
readingMinutes: 18,
}, [
p('Плановый hand-off ломается, когда в него передают итог вместо вопроса: «изменение безопасно», «ошибка обработана», «можно продолжать». Получатель не видит, на чём держится фраза, и вынужден либо поверить, либо начинать разбор заново. Для contract migration цена особенно заметна: missing consumer или rollback note исчезает в дружелюбном summary, а будущая работа получает иллюзию закрытой границы.'),
p('Ниже не полевой отчёт и не реконструкция настоящего review. На 31.07.2026 октябрь ещё впереди, поэтому это bounded synthetic scenario: fixed literals, заранее названные stops и один положительный исход — <code>synthetic-review-hand-off</code>. Он не читает PR, CI, diff, repository, сеть, файлы или secrets; не означает approval, merge, release, deploy, publication и не утверждает, что какая-либо команда уже изменила код.'),
h2('Карточка hand-off должна сохранять неопределённость'),
p('Минимальная карточка содержит ID учебного change, risk class, decision, список evidence, текущий статус, следующий вопрос и явную границу. В нашем literal ID <code>fixed-nullable-discount-contract</code> — не имя продукта и не ссылка на задачу. Он нужен, чтобы в одном сценарии не перепутать форму изменения с другим примером. Такие искусственные имена полезнее реалистичных псевдо-артефактов: они не создают впечатление, будто перед нами настоящий pull request или incident.'),
p('Статус говорит не «плохо» и не «хорошо», а какая операция доступна для модели. <code>stop-insufficient-evidence</code> означает: конкретный input не содержит named позиции, поэтому сильный вывод запрещён. <code>synthetic-review-hand-off</code> означает: минимальная карточка полна по собственному fixed rule и может быть передана дальше как плановый вопрос. Ни один статус не подтверждает реализацию, тесты, доступность сервиса или результат будущего обсуждения.'),
figure('/assets/editorial/2026/code-review-standard-2026-review-handoff-loop.svg', 'Цикл synthetic review hand-off: назвать change и риск, приложить named evidence, получить точный stop или hand-off; затем уточнить вопрос без решения за следующего владельца.', 'Цикл показывает ограниченный плановый обмен evidence. Он не изображает настоящий review flow, CI или публикацию.'),
table('Содержимое bounded hand-off', ['Поле', 'Пример fixed literal', 'Зачем нужно', 'Чего не доказывает'], [
['change', 'fixed-nullable-discount-contract', 'удерживает один учебный предмет', 'наличие кода или diff'],
['risk', 'contract-migration', 'выбирает набор вопросов', 'severity или ущерб'],
['evidence', 'schema delta; consumer map; rollback note', 'делает пробел видимым', 'совместимость consumers'],
['status', 'stop / synthetic hand-off', 'ограничивает следующий шаг', 'approval или merge'],
['boundary', 'in-memory fixed literal', 'исключает внешний эффект', 'результат production проверки'],
]),
h2('Один bounded scenario без имитации истории'),
p('Представим только учебную конструкцию: поле скидки становится nullable, и мы хотим подготовить вопрос о contract migration. Это не сообщение о том, что такая миграция существует или запланирована. <code>fixed-schema-delta</code> называет форму вопроса; <code>fixed-consumer-map</code> и <code>fixed-rollback-note</code> удерживают два обязательных пробела. Литерал не содержит автора, дату PR, ссылку на CI, номер задачи, клиента, длительность или результат — именно чтобы не спутать модель с историческим фактом.'),
p('Если consumer map отсутствует, hand-off не должен дорисовывать потребителя «по опыту». Правильный ответ — назвать missing item и вернуть его владельцу будущего обсуждения. Если есть карта, но conclusion содержит approval word, validator тоже останавливается. Это важно: полнота учебной карточки даёт право сформулировать следующий вопрос, но не право принимать решение за организацию. Положительный статус намеренно менее впечатляющий, чем «готово», зато его граница воспроизводима.'),
h2('Literal, который допускает только hand-off'),
code("import { createFixedReviewCase, createFixedReviewHandOff } from './upgrade-2026-10.mjs';\n\nconst planned = createFixedReviewCase('approval-word-v1');\nconst result = createFixedReviewHandOff(planned);\nconsole.log({ status: result.status, reason: result.reason, next: result.nextAction });\n// { status: 'stop-disallowed-positive-conclusion', reason: 'positive-branch-is-hand-off-only', next: 'use-synthetic-review-hand-off' }"),
p('Этот пример специально берёт literal с недопустимой фразой <code>approve-and-merge</code>. Он не проверяет существование merge и не пытается что-либо отменить. JSON clone создаётся в памяти, рекурсивный freeze не даёт сменить input через shared reference, а validator возвращает deterministic stop. Так текст можно проверить буквально: даже при полном наборе evidence код не допускает глагол, который притворяется решением о будущем change.'),
h2('Почему карточка короче review-резюме'),
p('Резюме часто становится складом контекста: туда попадают ссылки, даты, фрагменты обсуждения и пересказ решений. Для bounded hand-off это вредно. Чем больше неразмеченного материала, тем проще принять его за evidence, хотя он не отвечает ни на один точный вопрос. Карточка намеренно хранит только минимальную связку: named change, risk, required evidence, current status, next question и boundary. Остальное либо относится к будущей реальной работе, либо должно получить собственный тип и владельца.'),
p('Краткость не означает, что ответ можно написать без причин. Вместо «нужна карта consumers» hand-off должен сказать, что без неё нельзя определить охват изменения формы. Вместо «не approve» — что положительный исход сценария ограничен передачей вопроса. Так получатель получает не длинную запись журнала, а воспроизводимую причинную цепочку. Если он не согласен, можно оспорить одно звено: risk class, evidence requirement или границу вывода. Это лучше, чем спорить с общим ощущением «review был строгим».'),
h2('Контроль формулировок до передачи'),
p('Полезно отдельно искать глаголы, которые повышают силу вывода: approve, merge, release, deploy, publish, verified, safe. В настоящем процессе отдельное слово может быть допустимо при наличии полномочий и фактов. В synthetic сценарии оно запрещено, потому что создаёт ложную историю будущего. Validator показывает это на fixed input: даже полный набор contract evidence останавливается, если conclusion пытается назвать approval. Такая проверка не редактирует текст автоматически; она удерживает semantic boundary заметной.'),
p('Тем же способом надо проверять слова о внешнем наблюдении. «Лог показал», «CI прошёл», «клиент получил» выглядят как конкретика, но в октябрьской модели они были бы выдуманными production facts. Вместо них корректны конструкции «сценарий потребовал бы evidence», «будущий владелец уточнит», «fixed literal возвращает stop». Читатель не теряет практичность: он получает способ сформулировать вопрос, не делая вид, что доступ к данным уже существовал.'),
h2('Что останется после hand-off'),
p('Даже успешный synthetic hand-off оставляет работу. Нужно определить, какой реальный артефакт мог бы стать evidence, кто имеет право его читать, как он хранится и что делает команда при противоречивых данных. Нужно также решить, какие consumers существенны, а какой rollback возможен. Эти вопросы не дефект сценария. Они и есть граница между обучающей моделью и будущим процессом. Попытка заполнить её правдоподобными деталями сделала бы текст похожим на report, но менее честным.'),
h2('Передача вопроса другому владельцу'),
p('Хороший hand-off короток, но не телеграфен. Он говорит: «Рассматривается fixed contract scenario; риск — совместимость формы; присутствуют эти named evidence; вывод ограничен synthetic hand-off; нужен владелец, который уточнит consumer boundary». Он не говорит: «исправьте срочно», потому что urgency не следует из fixed literal. Он не говорит: «всё проверено», потому что проверка мира не выполнялась. Он не скрывает недостаток за ссылкой на внутренний процесс, которого читатель не видит.'),
p('Разделение ролей особенно важно в зрелой команде. Reviewer удерживает связь между риском и evidence. Автор будущего change уточняет намерение и границы. Владелец контракта решает, какие потребители существенны. Владелец эксплуатации отвечает на наблюдаемость поведения, а не на смысл schema. Такая декомпозиция не отменяет сотрудничество; она снижает цену ситуации, где каждый считает, что другой уже сделал необходимый вывод.'),
h2('Почему источник не превращается в policy'),
p('NIST SSDF описывает рекомендации для снижения риска уязвимостей в жизненном цикле разработки и подчёркивает интеграцию практик в разные SDLC. Для этого сценария его роль ограничена: он оправдывает разговор о системном risk vocabulary, а не конкретный состав карточки. RFC 2119 объясняет, почему слова MUST и SHOULD требуют объявленного контекста. Ни один из документов не выдает наш fixed hand-off за обязательную корпоративную процедуру и не подтверждает качество будущего code review.'),
p('Эта граница нужна и для обучения. Когда external source используют как печать «правильно», команда перестаёт видеть собственные допущения: какие contracts у неё есть, кто владеет rollback и что вообще может быть evidence. Более честная практика — назвать, какую терминологию источник даёт, а затем отдельно зафиксировать проектное правило. Если правило меняется, меняется сценарий и его fixture, а не смысл старого документа.'),
h2('Порядок для октябрьского synthetic hand-off'),
ol(['Выбрать один anonymized fixed scenario без реального ID, diff или истории.', 'Сформулировать риск как границу смысла, а не как оценку автора.', 'Приложить минимальные named evidence и назвать отсутствующие позиции.', 'Пропустить literal через fail-closed validator; не исправлять результат вручную.', 'Отправить следующий вопрос владельцу границы, не включив слова approval, merge, release или deploy.', 'Оставить boundary в карточке, чтобы читатель не принял модель за факт работы системы.']),
h2('Границы field-сценария и следующий шаг'),
p('Этот сценарий не заменяет review tool, policy, audit trail, change management или security assessment. Он не отвечает, сколько reviewers нужно, какой срок реакции разумен и когда допустима публикация. Не следует применять его как шаблон для настоящего потока без отдельного решения о данных, доступах и владельцах. Его единственная практическая проверка — может ли формулировка пережить отсутствие реального контекста, не начав выдавать предположение за доказательство.'),
p('Следующий шаг — в октябрьском плане обсудить карточку с командой на полностью synthetic case и попытаться намеренно сломать её: убрать consumer map, подменить risk style note, добавить approval word. Если каждый случай останавливается предсказуемо, можно отдельно решить, какие реальные артефакты вообще допустимо включать в будущий процесс. До такого решения остаёмся в границе планирования, а не имитируем field report.'),
], [
{ key: 'ssdf', use: 'NIST SSDF используется только как источник терминов о практиках secure software development в жизненном цикле.', boundary: 'Он не является policy этой команды и не подтверждает ни один hand-off или review result.' },
{ key: 'rfc2119', use: 'RFC объясняет смысл нормативных слов в спецификационном контексте.', boundary: 'Он не делает synthetic status фактом approval, merge или публикации.' },
{ key: 'rfc8174', use: 'RFC фиксирует уточнение BCP 14 для прописных ключевых слов.', boundary: 'Источник не определяет реальный процесс code review или полномочия его участников.' },
]);
export const revisions = deepFreeze([practice, mechanism, field]);
if (process.argv.includes('--verify-fixture')) {
const result = runFixedCodeReviewFixture();
const failed = Object.entries(result.assertions).filter(([, ok]) => ok !== true).map(([name]) => name);
if (failed.length) { process.stderr.write('FAIL fixture: ' + failed.join(', ') + '\n'); process.exitCode = 1; }
else { const count = Object.keys(result.assertions).length; process.stdout.write('PASS fixture: ' + count + '/' + count + ' assertions\n'); }
}
if (process.argv.includes('--print-revisions')) process.stdout.write(JSON.stringify(revisions) + '\n');