This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Производство редакционных партий
|
# Производство редакционных партий
|
||||||
|
|
||||||
На 31 июля 2026 года строгий аудит проходит 103 из 358 созданных материалов. Остальные 255 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
|
На 31 июля 2026 года строгий аудит проходит 106 из 358 созданных материалов. Остальные 252 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
|
||||||
|
|
||||||
## Одна партия
|
## Одна партия
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
# Автономное тройное ревью П34 · декабрь 2020 · «Разбор инцидента»
|
||||||
|
|
||||||
|
Статус: **принят независимым редактором в выпусковой набор**. Пакет содержит
|
||||||
|
ровно три revision для стабильных slug:
|
||||||
|
|
||||||
|
- <code>editorial-2020-12-practice-incident-review</code>;
|
||||||
|
- <code>editorial-2020-12-mechanism-incident-review</code>;
|
||||||
|
- <code>editorial-2020-12-field-incident-review</code>.
|
||||||
|
|
||||||
|
Созданы только пять разрешённых файлов П34:
|
||||||
|
|
||||||
|
- <code>web/scripts/upgrade-2020-12.mjs</code>;
|
||||||
|
- <code>web/public/assets/editorial/2020/incident-timeline-2020.svg</code>;
|
||||||
|
- <code>web/public/assets/editorial/2020/incident-decision-record-2020.svg</code>;
|
||||||
|
- <code>web/public/assets/editorial/2020/incident-learning-loop-2020.svg</code>;
|
||||||
|
- этот документ.
|
||||||
|
|
||||||
|
Revision-модуль экспортирует только изменяемые редакционные поля и не содержит
|
||||||
|
<code>date</code> или <code>author</code>. Он не подключает registry.
|
||||||
|
<code>articles.json</code>, registry, README, очередь, package config и Git
|
||||||
|
этой партией не менялись. Команда <code>--print-revisions</code> печатает
|
||||||
|
только JSON, а <code>--verify-fixture</code> запускает отдельную
|
||||||
|
детерминированную проверку в памяти.
|
||||||
|
|
||||||
|
## Проход 1. Факты и техническая рамка — пройдено
|
||||||
|
|
||||||
|
| Утверждение | Первичный или официальный источник | Проверенная граница |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Управление инцидентом выигрывает от живой записи состояния, разделения работы и явного восстановления | [Google SRE Book: Managing Incidents](https://sre.google/sre-book/managing-incidents/) | В текст перенесён только малый каркас «наблюдение → гипотеза → действие → проверка», без утверждения, что у автора есть сложный процесс управления инцидентами |
|
||||||
|
| Postmortem документирует влияние, действия, contributing causes и follow-up; разбор без обвинения ищет условия системы, а не персональную вину | [Google SRE Book: Postmortem Culture](https://sre.google/sre-book/postmortem-culture/) | Статьи говорят о component, поле, контракте и проверке; fixture не содержит имён или ролей людей |
|
||||||
|
| Диагностика строится на наблюдениях, проверяемых гипотезах и полезном отрицательном результате | [Google SRE Book: Effective Troubleshooting](https://sre.google/sre-book/effective-troubleshooting/) | H1 отделена от E2; непрохождение V1 описано как новый факт, а не как повод переписать историю |
|
||||||
|
| Для security-инцидентов NIST описывает цикл подготовки, обнаружения, анализа, сдерживания и post-incident activity | [NIST SP 800-61 Rev. 2](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-61r2.pdf) | Источник используется только как общий ориентир жизненного цикла; статья не называет учебный случай security-инцидентом и не выдаёт его за нормативную процедуру |
|
||||||
|
|
||||||
|
Весь технический случай создан в <code>runIncidentFixture()</code>. В нём пять
|
||||||
|
синтетических событий с последовательностью E1, E2, H1, A1, V1; один decision
|
||||||
|
record D1 и два профилактических пункта P1/P2 в состоянии
|
||||||
|
<code>planned</code>. Значения не взяты из журнала, сетевого запроса,
|
||||||
|
пользовательской истории или production-системы:
|
||||||
|
|
||||||
|
- E1 фиксирует только учебное состояние <code>blocked</code> на границе
|
||||||
|
<code>preview-order</code>;
|
||||||
|
- E2 фиксирует отсутствие <code>currency</code> после mapper;
|
||||||
|
- H1 — явно изменяемая гипотеза, основанная на E2;
|
||||||
|
- A1 — ограниченный выбор прежнего mapper в памяти fixture;
|
||||||
|
- V1 — отдельная проверка A1, а не заявление о здоровье системы;
|
||||||
|
- P1/P2 остаются planned и имеют будущие checks.
|
||||||
|
|
||||||
|
В fixture запрещены непустые или переставленные ссылки: hypothesis требует
|
||||||
|
раннее observation, action — раннюю hypothesis, verification — раннее action.
|
||||||
|
Также проверяются decision trigger/expected check, конкретность профилактики и
|
||||||
|
отсутствие атрибуции людям. Она намеренно не моделирует HTTP, proxy, базу,
|
||||||
|
очередь, browser, мониторинг, deployment, реальный трафик или логи. В статьях
|
||||||
|
нет claim о реальном инциденте, числовой норме доступности, времени
|
||||||
|
восстановления или зрелой платформе надёжности.
|
||||||
|
|
||||||
|
Вердикт прохода: **пройден**. Источники отделены от учебной модели, а учебная
|
||||||
|
модель — от настоящей операционной среды.
|
||||||
|
|
||||||
|
## Проход 2. Редактура, глубина и голос М3 — пройдено
|
||||||
|
|
||||||
|
| Revision | Симптом и цена в первых двух абзацах | Главный вопрос | Объём основного текста |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Практика | Видимый симптом исправляют до фиксации фактов; цена — возвращающийся случай и запись «починили» без следующей проверки | Как собрать минимальную временную шкалу до первой правки | **9 214** знаков body |
|
||||||
|
| Механизм | Один абзац смешивает факт, гипотезу, обход и профилактику; цена — потерянная граница и временное действие вместо вывода | Как связать observation, hypothesis, action, verification и prevention | **10 362** знака body |
|
||||||
|
| Полевой разбор | После восстановления обход принимают за завершённый разбор; цена — повторение старого действия вне исходной границы | Как прочитать синтетический случай без выдуманного production-отчёта | **9 752** знака body |
|
||||||
|
|
||||||
|
- Все три материала начинают с «симптом → цена → ограниченная учебная
|
||||||
|
граница», затем сохраняют последовательность «причина → проверка →
|
||||||
|
действие».
|
||||||
|
- У каждого revision больше пяти смысловых разделов, самостоятельный
|
||||||
|
<code>figure</code> с содержательным <code>alt</code> и
|
||||||
|
<code>figcaption</code>, таблица с <code>caption</code>/<code>thead</code>,
|
||||||
|
два технических примера, нумерованный маршрут и три или четыре официальные
|
||||||
|
ссылки.
|
||||||
|
- Практическая статья учит сначала сохранить E1/E2 и только потом менять
|
||||||
|
код. Механическая статья объясняет границы состояний. Полевой материал
|
||||||
|
показывает их на одном контролируемом входе, не расширяя вывод до всех
|
||||||
|
путей.
|
||||||
|
- Речь короткая и предметная: «факт → гипотеза → проверка → действие».
|
||||||
|
Она называет <code>mapper</code>, <code>currency</code>,
|
||||||
|
<code>pricing-adapter</code>, expected check и planned follow-up вместо
|
||||||
|
общего рассуждения о важности надёжности.
|
||||||
|
- Голос соответствует М3 конца 2020 года: автор уже видит границы модулей и
|
||||||
|
повторяемый порядок работы, но не приписывает себе опыт построения
|
||||||
|
платформы, числовых целей доступности, командного центра или большого
|
||||||
|
production-инцидента.
|
||||||
|
|
||||||
|
Второй проход отдельно проверил две опасные подмены. Первая: наблюдение
|
||||||
|
<code>currency === undefined</code> не названо причиной. Вторая: A1 и V1 не
|
||||||
|
называются профилактикой; P1/P2 остаются будущими, отдельно проверяемыми
|
||||||
|
изменениями. Разбор без обвинения не убирает техническую ответственность:
|
||||||
|
он требует назвать поле, границу, проверку и следующее изменение, но не
|
||||||
|
приписывает намерение отсутствующим в fixture людям.
|
||||||
|
|
||||||
|
Вердикт прохода: **пройден**. Длина в пределах 5 000–15 000 знаков, заголовки
|
||||||
|
не обещают универсальный процесс, а глубина достигается анализом границ и
|
||||||
|
контрпримеров, а не повтором одной формулы.
|
||||||
|
|
||||||
|
## Проход 3. Визуал, fixture и выпусковой preflight — пройдено в пределах пакета
|
||||||
|
|
||||||
|
- <code>incident-timeline-2020.svg</code> показывает вертикальную
|
||||||
|
зависимость «симптом → E1/E2 → H1 → A1 → V1» и отдельную плановую
|
||||||
|
профилактику. Он не соединяет проверку с симптомом напрямую.
|
||||||
|
- <code>incident-decision-record-2020.svg</code> делает видимыми E2,
|
||||||
|
H1, D1/A1, V1 и P1; у решения есть trigger и expected check.
|
||||||
|
- <code>incident-learning-loop-2020.svg</code> показывает, что цикл
|
||||||
|
закрывается следующей проверкой контракта, а не словом «готово».
|
||||||
|
- Каждый SVG имеет <code>title</code>, <code>desc</code>,
|
||||||
|
<code>role="img"</code>, вертикальный viewBox 720 px, контрастные карточки
|
||||||
|
и текст не мельче 21 px в исходнике. Во всех трёх отсутствуют JavaScript,
|
||||||
|
<code>foreignObject</code>, внешние URL и raster data URI.
|
||||||
|
- Mobile preflight отрендерил каждый SVG в PNG шириной 375 px через Sharp и
|
||||||
|
просмотрел результат вручную. Заголовки, карточки, стрелки и нижние
|
||||||
|
подписи читаются; clipping, наложение и горизонтальный overflow внутри
|
||||||
|
SVG не обнаружены. Это статическая визуальная проверка, а не browser-run
|
||||||
|
и не проверка screen reader.
|
||||||
|
|
||||||
|
### Фактически выполненные проверки
|
||||||
|
|
||||||
|
Финальное состояние пакета проверено 31 июля 2026 года:
|
||||||
|
|
||||||
|
<pre><code>cd web && node --check scripts/upgrade-2020-12.mjs
|
||||||
|
cd web && npm run audit:draft -- scripts/upgrade-2020-12.mjs
|
||||||
|
cd web && node scripts/upgrade-2020-12.mjs --verify-fixture
|
||||||
|
cd web && xmllint --noout \
|
||||||
|
public/assets/editorial/2020/incident-timeline-2020.svg \
|
||||||
|
public/assets/editorial/2020/incident-decision-record-2020.svg \
|
||||||
|
public/assets/editorial/2020/incident-learning-loop-2020.svg</code></pre>
|
||||||
|
|
||||||
|
| Проверка | Реальный результат |
|
||||||
|
| --- | --- |
|
||||||
|
| <code>node --check</code> | PASS, code 0 |
|
||||||
|
| Import-safe export и draft gate | PASS: **9 214 / 10 362 / 9 752** знака body; у трёх slug найдены sections, table, figure, code, route, sources и локальные assets |
|
||||||
|
| In-memory fixture | PASS: семь assertions истинны — временная шкала упорядочена; H1 основана на E2; A1 следует за H1; V1 проверяет A1; D1 содержит expected check; P1/P2 конкретны и planned; имена людей отсутствуют |
|
||||||
|
| <code>xmllint --noout</code> | PASS, все три SVG — корректный XML |
|
||||||
|
| Sharp mobile preflight | PASS: три PNG шириной 375 px просмотрены; нет clipping, overlap или horizontal overflow внутри схем |
|
||||||
|
| Scope/self-review | PASS: в revision нет <code>date</code>/<code>author</code>; пакет не подключает registry и не редактирует чужие незакоммиченные материалы |
|
||||||
|
|
||||||
|
<code>npm run audit:draft</code> завершилась с code 0; npm дополнительно вывел
|
||||||
|
предупреждения о старых пользовательских конфигурациях
|
||||||
|
<code>store-dir</code>, <code>cache-dir</code> и
|
||||||
|
<code>public-hoist-pattern</code>. Они не относятся к П34 и не изменялись
|
||||||
|
этим пакетом.
|
||||||
|
|
||||||
|
Не запускались: strict audit после подключения к registry, production build,
|
||||||
|
browser, screen reader, реальный HTTP/proxy, база, очередь, клиентские SDK,
|
||||||
|
мониторинг, deployment и публикация. Эти проверки принадлежат отдельной
|
||||||
|
интеграционной и выпусковой операции; данный документ не заявляет их
|
||||||
|
выполненными.
|
||||||
|
|
||||||
|
## Независимая интеграционная приёмка
|
||||||
|
|
||||||
|
Основной редактор 31 июля 2026 года подключил три revision к
|
||||||
|
<code>web/data/editorial-revisions.mjs</code>, не меняя базовый
|
||||||
|
<code>articles.json</code>, даты или автора архивных записей. В registry стало
|
||||||
|
97 revision. Официальные источники Google SRE и NIST сверены независимо: они
|
||||||
|
поддерживают более общий порядок управления инцидентом и postmortem, но не
|
||||||
|
превращают учебный пример в отчёт о реальном инциденте.
|
||||||
|
|
||||||
|
| Проверка после интеграции | Реальный результат |
|
||||||
|
| --- | --- |
|
||||||
|
| Строгий audit трёх slug | PASS: 9 214 / 10 362 / 9 752 знака; у каждой статьи есть figure, table и code examples |
|
||||||
|
| Production build | PASS: Next.js собрал 374 статические страницы |
|
||||||
|
| Независимый mobile visual review | PASS: основной редактор повторно просмотрел три SVG после Sharp-рендера в 375 px; clipping, overlap и overflow не обнаружены |
|
||||||
|
|
||||||
|
Ни этот отчёт, ни интеграция не утверждают запуск HTTP, proxy, базы, очереди,
|
||||||
|
monitoring, deployment, browser или assistive technology.
|
||||||
|
|
||||||
|
Выпусковой вердикт: **ACCEPT**. Commit и push выполняются отдельной
|
||||||
|
публикационной операцией; Git остаётся источником её фактической записи.
|
||||||
@@ -30,6 +30,7 @@ import { revisions as august2020Revisions } from '../scripts/upgrade-2020-08.mjs
|
|||||||
import { revisions as september2020Revisions } from '../scripts/upgrade-2020-09.mjs';
|
import { revisions as september2020Revisions } from '../scripts/upgrade-2020-09.mjs';
|
||||||
import { revisions as october2020Revisions } from '../scripts/upgrade-2020-10.mjs';
|
import { revisions as october2020Revisions } from '../scripts/upgrade-2020-10.mjs';
|
||||||
import { revisions as november2020Revisions } from '../scripts/upgrade-2020-11.mjs';
|
import { revisions as november2020Revisions } from '../scripts/upgrade-2020-11.mjs';
|
||||||
|
import { revisions as december2020Revisions } from '../scripts/upgrade-2020-12.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 = [
|
||||||
@@ -65,4 +66,5 @@ export const editorialRevisions = [
|
|||||||
...september2020Revisions,
|
...september2020Revisions,
|
||||||
...october2020Revisions,
|
...october2020Revisions,
|
||||||
...november2020Revisions,
|
...november2020Revisions,
|
||||||
|
...december2020Revisions,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 1270" role="img" aria-labelledby="title desc">
|
||||||
|
<title id="title">Учебная схема решения в разборе инцидента</title>
|
||||||
|
<desc id="desc">Вертикальная схема связывает записанное наблюдение с гипотезой, ограниченным действием, ожидаемой проверкой и отдельной плановой профилактикой.</desc>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="background" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#15172a"/>
|
||||||
|
<stop offset="100%" stop-color="#20264a"/>
|
||||||
|
</linearGradient>
|
||||||
|
<marker id="arrow" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto">
|
||||||
|
<path d="M0,0 L12,6 L0,12 Z" fill="#8da5ca"/>
|
||||||
|
</marker>
|
||||||
|
<filter id="shadow" x="-10%" y="-10%" width="120%" height="130%">
|
||||||
|
<feDropShadow dx="0" dy="8" stdDeviation="8" flood-color="#090a16" flood-opacity=".36"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<rect width="720" height="1270" rx="32" fill="url(#background)"/>
|
||||||
|
<text x="54" y="76" fill="#fbfcff" font-family="Arial, sans-serif" font-size="42" font-weight="700">Decision record</text>
|
||||||
|
<text x="54" y="114" fill="#c1c9e8" font-family="Arial, sans-serif" font-size="24">От факта к проверке без поиска виноватого</text>
|
||||||
|
|
||||||
|
<g filter="url(#shadow)">
|
||||||
|
<rect x="62" y="168" width="596" height="158" rx="22" fill="#293356" stroke="#62b8ff" stroke-width="3"/>
|
||||||
|
</g>
|
||||||
|
<text x="98" y="218" fill="#a8dcff" font-family="Arial, sans-serif" font-size="28" font-weight="700">E2 · записанное наблюдение</text>
|
||||||
|
<text x="98" y="257" fill="#f4f7ff" font-family="Arial, sans-serif" font-size="24">После mapper отсутствует currency</text>
|
||||||
|
<text x="98" y="292" fill="#c1c9e8" font-family="Arial, sans-serif" font-size="21">evidence: значение на contract boundary</text>
|
||||||
|
|
||||||
|
<line x1="360" y1="326" x2="360" y2="380" stroke="#8da5ca" stroke-width="6" marker-end="url(#arrow)"/>
|
||||||
|
<g filter="url(#shadow)">
|
||||||
|
<rect x="62" y="396" width="596" height="158" rx="22" fill="#342e5c" stroke="#bd91ff" stroke-width="3"/>
|
||||||
|
</g>
|
||||||
|
<text x="98" y="446" fill="#decaff" font-family="Arial, sans-serif" font-size="28" font-weight="700">H1 · гипотеза</text>
|
||||||
|
<text x="98" y="485" fill="#f4f7ff" font-family="Arial, sans-serif" font-size="24">Новый mapper мог потерять поле</text>
|
||||||
|
<text x="98" y="520" fill="#c1c9e8" font-family="Arial, sans-serif" font-size="21">не факт; ссылка остаётся на E2</text>
|
||||||
|
|
||||||
|
<line x1="360" y1="554" x2="360" y2="608" stroke="#8da5ca" stroke-width="6" marker-end="url(#arrow)"/>
|
||||||
|
<g filter="url(#shadow)">
|
||||||
|
<rect x="62" y="624" width="596" height="178" rx="22" fill="#214b48" stroke="#62d69d" stroke-width="3"/>
|
||||||
|
</g>
|
||||||
|
<text x="98" y="674" fill="#a9f0ca" font-family="Arial, sans-serif" font-size="28" font-weight="700">D1 / A1 · решение и действие</text>
|
||||||
|
<text x="98" y="713" fill="#f4f7ff" font-family="Arial, sans-serif" font-size="24">Оставить прежний mapper в fixture</text>
|
||||||
|
<text x="98" y="748" fill="#c5e8dc" font-family="Arial, sans-serif" font-size="21">trigger: H1 · граница: учебная ветка</text>
|
||||||
|
<text x="98" y="778" fill="#c5e8dc" font-family="Arial, sans-serif" font-size="21">не retry и не новый timeout без факта</text>
|
||||||
|
|
||||||
|
<line x1="360" y1="802" x2="360" y2="856" stroke="#8da5ca" stroke-width="6" marker-end="url(#arrow)"/>
|
||||||
|
<g filter="url(#shadow)">
|
||||||
|
<rect x="62" y="872" width="596" height="158" rx="22" fill="#214b48" stroke="#62d69d" stroke-width="3"/>
|
||||||
|
</g>
|
||||||
|
<text x="98" y="922" fill="#a9f0ca" font-family="Arial, sans-serif" font-size="28" font-weight="700">V1 · expected check</text>
|
||||||
|
<text x="98" y="961" fill="#f4f7ff" font-family="Arial, sans-serif" font-size="24">Контролируемый вход вернул allowed</text>
|
||||||
|
<text x="98" y="996" fill="#c5e8dc" font-family="Arial, sans-serif" font-size="21">и сохранил обязательное поле</text>
|
||||||
|
|
||||||
|
<line x1="360" y1="1030" x2="360" y2="1084" stroke="#8da5ca" stroke-width="6" marker-end="url(#arrow)"/>
|
||||||
|
<g filter="url(#shadow)">
|
||||||
|
<rect x="62" y="1100" width="596" height="112" rx="22" fill="#62441f" stroke="#f4b64a" stroke-width="3"/>
|
||||||
|
</g>
|
||||||
|
<text x="98" y="1148" fill="#ffd98f" font-family="Arial, sans-serif" font-size="28" font-weight="700">P1 · профилактика: planned</text>
|
||||||
|
<text x="98" y="1186" fill="#fff4dc" font-family="Arial, sans-serif" font-size="22">Contract fixture отклоняет mapper без currency</text>
|
||||||
|
<text x="360" y="1250" text-anchor="middle" fill="#c1c9e8" font-family="Arial, sans-serif" font-size="22">Идея не равна выполненной проверке</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.8 KiB |
@@ -0,0 +1,56 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 1280" role="img" aria-labelledby="title desc">
|
||||||
|
<title id="title">Цикл обучения после учебного инцидента</title>
|
||||||
|
<desc id="desc">Вертикальный цикл показывает сбор факта, ограниченное действие, проверку, плановую профилактику и запуск новой проверки при следующем изменении.</desc>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="background" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#14242a"/>
|
||||||
|
<stop offset="100%" stop-color="#182c36"/>
|
||||||
|
</linearGradient>
|
||||||
|
<marker id="arrow" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto">
|
||||||
|
<path d="M0,0 L12,6 L0,12 Z" fill="#93b7b7"/>
|
||||||
|
</marker>
|
||||||
|
<filter id="shadow" x="-10%" y="-10%" width="120%" height="130%">
|
||||||
|
<feDropShadow dx="0" dy="8" stdDeviation="8" flood-color="#071014" flood-opacity=".36"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<rect width="720" height="1280" rx="32" fill="url(#background)"/>
|
||||||
|
<text x="54" y="76" fill="#fbfffe" font-family="Arial, sans-serif" font-size="42" font-weight="700">Цикл после разбора</text>
|
||||||
|
<text x="54" y="114" fill="#b9d8d5" font-family="Arial, sans-serif" font-size="24">Профилактика закрывает не текст, а следующую проверку</text>
|
||||||
|
|
||||||
|
<g filter="url(#shadow)">
|
||||||
|
<rect x="94" y="174" width="532" height="136" rx="24" fill="#263f4d" stroke="#64c6e5" stroke-width="3"/>
|
||||||
|
</g>
|
||||||
|
<text x="132" y="224" fill="#a7e5f7" font-family="Arial, sans-serif" font-size="29" font-weight="700">1. Собрать факт</text>
|
||||||
|
<text x="132" y="263" fill="#f2ffff" font-family="Arial, sans-serif" font-size="23">Симптом, граница, evidence</text>
|
||||||
|
|
||||||
|
<line x1="360" y1="310" x2="360" y2="364" stroke="#93b7b7" stroke-width="6" marker-end="url(#arrow)"/>
|
||||||
|
<g filter="url(#shadow)">
|
||||||
|
<rect x="94" y="380" width="532" height="136" rx="24" fill="#354061" stroke="#c1a3ff" stroke-width="3"/>
|
||||||
|
</g>
|
||||||
|
<text x="132" y="430" fill="#e0d3ff" font-family="Arial, sans-serif" font-size="29" font-weight="700">2. Сузить гипотезу</text>
|
||||||
|
<text x="132" y="469" fill="#f2ffff" font-family="Arial, sans-serif" font-size="23">Одна связь со старым observation</text>
|
||||||
|
|
||||||
|
<line x1="360" y1="516" x2="360" y2="570" stroke="#93b7b7" stroke-width="6" marker-end="url(#arrow)"/>
|
||||||
|
<g filter="url(#shadow)">
|
||||||
|
<rect x="94" y="586" width="532" height="136" rx="24" fill="#24544d" stroke="#67dda3" stroke-width="3"/>
|
||||||
|
</g>
|
||||||
|
<text x="132" y="636" fill="#b4f4d2" font-family="Arial, sans-serif" font-size="29" font-weight="700">3. Ограничить действие</text>
|
||||||
|
<text x="132" y="675" fill="#f2ffff" font-family="Arial, sans-serif" font-size="23">Обратимый шаг с ясной границей</text>
|
||||||
|
|
||||||
|
<line x1="360" y1="722" x2="360" y2="776" stroke="#93b7b7" stroke-width="6" marker-end="url(#arrow)"/>
|
||||||
|
<g filter="url(#shadow)">
|
||||||
|
<rect x="94" y="792" width="532" height="136" rx="24" fill="#24544d" stroke="#67dda3" stroke-width="3"/>
|
||||||
|
</g>
|
||||||
|
<text x="132" y="842" fill="#b4f4d2" font-family="Arial, sans-serif" font-size="29" font-weight="700">4. Проверить эффект</text>
|
||||||
|
<text x="132" y="881" fill="#f2ffff" font-family="Arial, sans-serif" font-size="23">V1 относится к действию, не к легенде</text>
|
||||||
|
|
||||||
|
<line x1="360" y1="928" x2="360" y2="982" stroke="#93b7b7" stroke-width="6" marker-end="url(#arrow)"/>
|
||||||
|
<g filter="url(#shadow)">
|
||||||
|
<rect x="94" y="998" width="532" height="136" rx="24" fill="#654920" stroke="#f2bd58" stroke-width="3"/>
|
||||||
|
</g>
|
||||||
|
<text x="132" y="1048" fill="#ffe0a1" font-family="Arial, sans-serif" font-size="29" font-weight="700">5. Сделать профилактику</text>
|
||||||
|
<text x="132" y="1087" fill="#fff8e8" font-family="Arial, sans-serif" font-size="23">Тест или контракт со своим check</text>
|
||||||
|
|
||||||
|
<path d="M628 1066 C690 1066,690 1186,604 1186 L144 1186 C80 1186,80 1148,80 1118" fill="none" stroke="#93b7b7" stroke-width="6" marker-end="url(#arrow)"/>
|
||||||
|
<text x="360" y="1240" text-anchor="middle" fill="#b9d8d5" font-family="Arial, sans-serif" font-size="22">Следующее изменение снова проходит проверку</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.3 KiB |
@@ -0,0 +1,73 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 1290" role="img" aria-labelledby="title desc">
|
||||||
|
<title id="title">Учебная временная шкала разбора инцидента</title>
|
||||||
|
<desc id="desc">Вертикальная схема показывает порядок от симптома и наблюдений через гипотезу и ограниченное действие к проверке и плановой профилактике.</desc>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="background" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#101827"/>
|
||||||
|
<stop offset="100%" stop-color="#16243a"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="shadow" x="-10%" y="-10%" width="120%" height="130%">
|
||||||
|
<feDropShadow dx="0" dy="8" stdDeviation="8" flood-color="#050914" flood-opacity=".34"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<rect width="720" height="1290" rx="32" fill="url(#background)"/>
|
||||||
|
<text x="54" y="76" fill="#f8fbff" font-family="Arial, sans-serif" font-size="42" font-weight="700">Разбор: порядок фактов</text>
|
||||||
|
<text x="54" y="114" fill="#b7c8df" font-family="Arial, sans-serif" font-size="24">Учебная линия, а не отчёт о боевой системе</text>
|
||||||
|
<line x1="100" y1="184" x2="100" y2="1102" stroke="#607493" stroke-width="8" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<circle cx="100" cy="210" r="30" fill="#f4a340"/>
|
||||||
|
<text x="100" y="219" text-anchor="middle" fill="#172134" font-family="Arial, sans-serif" font-size="22" font-weight="700">T0</text>
|
||||||
|
<g filter="url(#shadow)">
|
||||||
|
<rect x="164" y="160" width="500" height="120" rx="20" fill="#253652" stroke="#f4a340" stroke-width="3"/>
|
||||||
|
</g>
|
||||||
|
<text x="198" y="205" fill="#ffd598" font-family="Arial, sans-serif" font-size="27" font-weight="700">Симптом</text>
|
||||||
|
<text x="198" y="242" fill="#eff5ff" font-family="Arial, sans-serif" font-size="23">Путь вернул неожиданное состояние</text>
|
||||||
|
|
||||||
|
<circle cx="100" cy="370" r="30" fill="#5eb8ff"/>
|
||||||
|
<text x="100" y="379" text-anchor="middle" fill="#172134" font-family="Arial, sans-serif" font-size="22" font-weight="700">E1</text>
|
||||||
|
<g filter="url(#shadow)">
|
||||||
|
<rect x="164" y="316" width="500" height="146" rx="20" fill="#253652" stroke="#5eb8ff" stroke-width="3"/>
|
||||||
|
</g>
|
||||||
|
<text x="198" y="361" fill="#a7dcff" font-family="Arial, sans-serif" font-size="27" font-weight="700">Наблюдение</text>
|
||||||
|
<text x="198" y="398" fill="#eff5ff" font-family="Arial, sans-serif" font-size="23">Факт и граница: preview-order</text>
|
||||||
|
<text x="198" y="431" fill="#b7c8df" font-family="Arial, sans-serif" font-size="21">без объяснения причины</text>
|
||||||
|
|
||||||
|
<circle cx="100" cy="550" r="30" fill="#5eb8ff"/>
|
||||||
|
<text x="100" y="559" text-anchor="middle" fill="#172134" font-family="Arial, sans-serif" font-size="22" font-weight="700">E2</text>
|
||||||
|
<g filter="url(#shadow)">
|
||||||
|
<rect x="164" y="496" width="500" height="146" rx="20" fill="#253652" stroke="#5eb8ff" stroke-width="3"/>
|
||||||
|
</g>
|
||||||
|
<text x="198" y="541" fill="#a7dcff" font-family="Arial, sans-serif" font-size="27" font-weight="700">Наблюдение</text>
|
||||||
|
<text x="198" y="578" fill="#eff5ff" font-family="Arial, sans-serif" font-size="23">После mapper нет currency</text>
|
||||||
|
<text x="198" y="611" fill="#b7c8df" font-family="Arial, sans-serif" font-size="21">evidence можно перепроверить</text>
|
||||||
|
|
||||||
|
<circle cx="100" cy="730" r="30" fill="#bd91ff"/>
|
||||||
|
<text x="100" y="739" text-anchor="middle" fill="#172134" font-family="Arial, sans-serif" font-size="22" font-weight="700">H1</text>
|
||||||
|
<g filter="url(#shadow)">
|
||||||
|
<rect x="164" y="676" width="500" height="146" rx="20" fill="#253652" stroke="#bd91ff" stroke-width="3"/>
|
||||||
|
</g>
|
||||||
|
<text x="198" y="721" fill="#d9c2ff" font-family="Arial, sans-serif" font-size="27" font-weight="700">Гипотеза</text>
|
||||||
|
<text x="198" y="758" fill="#eff5ff" font-family="Arial, sans-serif" font-size="23">Новый mapper мог не перенести поле</text>
|
||||||
|
<text x="198" y="791" fill="#b7c8df" font-family="Arial, sans-serif" font-size="21">ссылка ведёт к E2</text>
|
||||||
|
|
||||||
|
<circle cx="100" cy="910" r="30" fill="#62d69d"/>
|
||||||
|
<text x="100" y="919" text-anchor="middle" fill="#172134" font-family="Arial, sans-serif" font-size="22" font-weight="700">A1</text>
|
||||||
|
<g filter="url(#shadow)">
|
||||||
|
<rect x="164" y="856" width="500" height="146" rx="20" fill="#253652" stroke="#62d69d" stroke-width="3"/>
|
||||||
|
</g>
|
||||||
|
<text x="198" y="901" fill="#a9f0ca" font-family="Arial, sans-serif" font-size="27" font-weight="700">Ограниченное действие</text>
|
||||||
|
<text x="198" y="938" fill="#eff5ff" font-family="Arial, sans-serif" font-size="23">В fixture выбран прежний mapper</text>
|
||||||
|
<text x="198" y="971" fill="#b7c8df" font-family="Arial, sans-serif" font-size="21">это обход, не доказанная причина</text>
|
||||||
|
|
||||||
|
<circle cx="100" cy="1090" r="30" fill="#62d69d"/>
|
||||||
|
<text x="100" y="1099" text-anchor="middle" fill="#172134" font-family="Arial, sans-serif" font-size="22" font-weight="700">V1</text>
|
||||||
|
<g filter="url(#shadow)">
|
||||||
|
<rect x="164" y="1036" width="500" height="146" rx="20" fill="#253652" stroke="#62d69d" stroke-width="3"/>
|
||||||
|
</g>
|
||||||
|
<text x="198" y="1081" fill="#a9f0ca" font-family="Arial, sans-serif" font-size="27" font-weight="700">Проверка</text>
|
||||||
|
<text x="198" y="1118" fill="#eff5ff" font-family="Arial, sans-serif" font-size="23">Контролируемый вход сохранил поле</text>
|
||||||
|
<text x="198" y="1151" fill="#b7c8df" font-family="Arial, sans-serif" font-size="21">после действия, а не до него</text>
|
||||||
|
|
||||||
|
<rect x="54" y="1218" width="612" height="42" rx="20" fill="#f4a340"/>
|
||||||
|
<text x="360" y="1248" text-anchor="middle" fill="#172134" font-family="Arial, sans-serif" font-size="22" font-weight="700">Профилактика — отдельная плановая проверка</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.9 KiB |
@@ -0,0 +1,582 @@
|
|||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value)
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll("'", ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function paragraph(text) {
|
||||||
|
return '<p>' + text + '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function heading(text) {
|
||||||
|
return '<h2>' + text + '</h2>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function codeBlock(lines) {
|
||||||
|
return '<pre><code>' + escapeHtml(lines.join('\n')) + '</code></pre>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function figure(src, alt, caption) {
|
||||||
|
return '<figure><img src="' + src + '" alt="' + alt + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function orderedList(items) {
|
||||||
|
return '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function dataTable(caption, headers, rows) {
|
||||||
|
const captionHtml = '<caption>' + caption + '</caption>';
|
||||||
|
const head = '<thead><tr>' + headers.map((header) => '<th scope="col">' + header + '</th>').join('') + '</tr></thead>';
|
||||||
|
const body = '<tbody>' + rows.map((row) => '<tr>' + row.map((cell) => '<td>' + cell + '</td>').join('') + '</tr>').join('') + '</tbody>';
|
||||||
|
return '<div class="table-scroll"><table>' + captionHtml + head + body + '</table></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceList(items) {
|
||||||
|
return '<ul>' + items.map((item) => '<li><a href="' + item.url + '" target="_blank" rel="noopener noreferrer">' + item.title + '</a> — ' + item.note + '</li>').join('') + '</ul>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleText(html) {
|
||||||
|
return html
|
||||||
|
.replace(/<[^>]*>/g, ' ')
|
||||||
|
.replaceAll(' ', ' ')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll(''', "'")
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function proseText(html) {
|
||||||
|
return visibleText(
|
||||||
|
html
|
||||||
|
.replace(/<pre><code>[\s\S]*?<\/code><\/pre>/g, '')
|
||||||
|
.replace(/<figure>[\s\S]*?<\/figure>/g, '')
|
||||||
|
.replace(/<div class="table-scroll">[\s\S]*?<\/div>/g, ''),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRevision(meta, bodyParts, sources) {
|
||||||
|
const bodyHtml = bodyParts.join('\n');
|
||||||
|
const proseLength = proseText(bodyHtml).length;
|
||||||
|
|
||||||
|
if (proseLength < 5000 || proseLength > 15000) {
|
||||||
|
throw new Error(meta.slug + ': prose length must be 5000–15000, got ' + proseLength);
|
||||||
|
}
|
||||||
|
if (sources.length < 2) {
|
||||||
|
throw new Error(meta.slug + ': at least two primary or official sources are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...meta,
|
||||||
|
contentHtml: [bodyHtml, heading('Проверяемые источники'), sourceList(sources)].join('\n'),
|
||||||
|
proseLength,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const googleIncidentManagement = {
|
||||||
|
title: 'Google SRE Book: Managing Incidents',
|
||||||
|
url: 'https://sre.google/sre-book/managing-incidents/',
|
||||||
|
note: 'официальная глава 2017 года о фиксации живого состояния инцидента, разделении работ и восстановлении; это источник принципов, а не описание данного учебного случая',
|
||||||
|
};
|
||||||
|
|
||||||
|
const googlePostmortemCulture = {
|
||||||
|
title: 'Google SRE Book: Postmortem Culture',
|
||||||
|
url: 'https://sre.google/sre-book/postmortem-culture/',
|
||||||
|
note: 'официальное описание postmortem как записи влияния, действий, причин и follow-up; особенно полезно различение системных причин и персонального обвинения',
|
||||||
|
};
|
||||||
|
|
||||||
|
const googleTroubleshooting = {
|
||||||
|
title: 'Google SRE Book: Effective Troubleshooting',
|
||||||
|
url: 'https://sre.google/sre-book/effective-troubleshooting/',
|
||||||
|
note: 'официальный материал о проверяемых гипотезах, наблюдениях и ценности отрицательного результата при поиске причины',
|
||||||
|
};
|
||||||
|
|
||||||
|
const nistIncidentHandling = {
|
||||||
|
title: 'NIST SP 800-61 Rev. 2: Computer Security Incident Handling Guide',
|
||||||
|
url: 'https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-61r2.pdf',
|
||||||
|
note: 'первичный нормативный источник для security-инцидентов; здесь используется только его общий цикл подготовки, обнаружения, анализа, сдерживания и извлечения уроков',
|
||||||
|
};
|
||||||
|
|
||||||
|
const knownEventKinds = new Set(['observation', 'hypothesis', 'action', 'verification']);
|
||||||
|
|
||||||
|
function requireString(value, name) {
|
||||||
|
if (typeof value !== 'string' || value.trim().length === 0) {
|
||||||
|
throw new Error(name + ' must be a non-empty string');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertChronologicalTimeline(timeline) {
|
||||||
|
if (!Array.isArray(timeline) || timeline.length < 4) {
|
||||||
|
throw new Error('Controlled incident fixture needs at least four timeline events');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = new Set();
|
||||||
|
let previousTime = -Infinity;
|
||||||
|
|
||||||
|
for (let index = 0; index < timeline.length; index += 1) {
|
||||||
|
const event = timeline[index];
|
||||||
|
requireString(event.id, 'timeline event id');
|
||||||
|
requireString(event.at, 'timeline event at');
|
||||||
|
requireString(event.kind, 'timeline event kind');
|
||||||
|
requireString(event.statement, 'timeline event statement');
|
||||||
|
if (!knownEventKinds.has(event.kind)) {
|
||||||
|
throw new Error('Unsupported fixture event kind: ' + event.kind);
|
||||||
|
}
|
||||||
|
if (ids.has(event.id)) {
|
||||||
|
throw new Error('Duplicate fixture event id: ' + event.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = Date.parse(event.at);
|
||||||
|
if (!Number.isFinite(timestamp) || timestamp <= previousTime) {
|
||||||
|
throw new Error('Fixture timeline must be strictly chronological at ' + event.id);
|
||||||
|
}
|
||||||
|
previousTime = timestamp;
|
||||||
|
ids.add(event.id);
|
||||||
|
|
||||||
|
if (event.kind === 'observation') requireString(event.evidence, event.id + '.evidence');
|
||||||
|
if (event.kind === 'hypothesis') requireString(event.basedOn, event.id + '.basedOn');
|
||||||
|
if (event.kind === 'action') requireString(event.basedOn, event.id + '.basedOn');
|
||||||
|
if (event.kind === 'verification') requireString(event.checksAction, event.id + '.checksAction');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventById(timeline, id) {
|
||||||
|
const event = timeline.find((candidate) => candidate.id === id);
|
||||||
|
if (!event) throw new Error('Fixture reference points to a missing event: ' + id);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertIncidentLinks(timeline, decisions, prevention) {
|
||||||
|
const ids = assertChronologicalTimeline(timeline);
|
||||||
|
const position = new Map(timeline.map((event, index) => [event.id, index]));
|
||||||
|
|
||||||
|
for (const event of timeline) {
|
||||||
|
if (event.kind === 'hypothesis') {
|
||||||
|
const observation = eventById(timeline, event.basedOn);
|
||||||
|
if (observation.kind !== 'observation' || position.get(observation.id) >= position.get(event.id)) {
|
||||||
|
throw new Error(event.id + ' must be based on an earlier observation');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.kind === 'action') {
|
||||||
|
const hypothesis = eventById(timeline, event.basedOn);
|
||||||
|
if (hypothesis.kind !== 'hypothesis' || position.get(hypothesis.id) >= position.get(event.id)) {
|
||||||
|
throw new Error(event.id + ' must be based on an earlier hypothesis');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.kind === 'verification') {
|
||||||
|
const action = eventById(timeline, event.checksAction);
|
||||||
|
if (action.kind !== 'action' || position.get(action.id) >= position.get(event.id)) {
|
||||||
|
throw new Error(event.id + ' must check an earlier action');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(decisions) || decisions.length < 1) {
|
||||||
|
throw new Error('Fixture needs a decision record');
|
||||||
|
}
|
||||||
|
for (const decision of decisions) {
|
||||||
|
requireString(decision.id, 'decision id');
|
||||||
|
requireString(decision.trigger, decision.id + '.trigger');
|
||||||
|
requireString(decision.action, decision.id + '.action');
|
||||||
|
requireString(decision.expectedCheck, decision.id + '.expectedCheck');
|
||||||
|
if (!ids.has(decision.trigger) || !ids.has(decision.expectedCheck)) {
|
||||||
|
throw new Error(decision.id + ' must point to known timeline events');
|
||||||
|
}
|
||||||
|
if (eventById(timeline, decision.trigger).kind !== 'hypothesis') {
|
||||||
|
throw new Error(decision.id + '.trigger must point to a hypothesis');
|
||||||
|
}
|
||||||
|
if (eventById(timeline, decision.expectedCheck).kind !== 'verification') {
|
||||||
|
throw new Error(decision.id + '.expectedCheck must point to a verification');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(prevention) || prevention.length < 1) {
|
||||||
|
throw new Error('Fixture needs prevention work');
|
||||||
|
}
|
||||||
|
for (const item of prevention) {
|
||||||
|
requireString(item.id, 'prevention id');
|
||||||
|
requireString(item.change, item.id + '.change');
|
||||||
|
requireString(item.check, item.id + '.check');
|
||||||
|
requireString(item.state, item.id + '.state');
|
||||||
|
if (item.state !== 'planned') {
|
||||||
|
throw new Error(item.id + ' must remain planned in the controlled fixture');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSyntheticFixture() {
|
||||||
|
return {
|
||||||
|
timeline: [
|
||||||
|
{
|
||||||
|
id: 'E1',
|
||||||
|
at: '2020-12-03T10:00:00Z',
|
||||||
|
kind: 'observation',
|
||||||
|
boundary: 'preview-order',
|
||||||
|
statement: 'Учебный вызов preview-order завершился состоянием blocked.',
|
||||||
|
evidence: 'fixture.result.state === "blocked"',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'E2',
|
||||||
|
at: '2020-12-03T10:02:00Z',
|
||||||
|
kind: 'observation',
|
||||||
|
boundary: 'pricing-adapter contract',
|
||||||
|
statement: 'В учебном входе поле currency отсутствует после преобразования mapper.',
|
||||||
|
evidence: 'fixture.mappedInput.currency === undefined',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'H1',
|
||||||
|
at: '2020-12-03T10:05:00Z',
|
||||||
|
kind: 'hypothesis',
|
||||||
|
boundary: 'mapper',
|
||||||
|
statement: 'Новый путь mapper не переносит обязательное поле currency.',
|
||||||
|
basedOn: 'E2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'A1',
|
||||||
|
at: '2020-12-03T10:08:00Z',
|
||||||
|
kind: 'action',
|
||||||
|
boundary: 'controlled branch',
|
||||||
|
statement: 'В учебном сценарии выбран прежний mapper до проверки контракта.',
|
||||||
|
basedOn: 'H1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'V1',
|
||||||
|
at: '2020-12-03T10:10:00Z',
|
||||||
|
kind: 'verification',
|
||||||
|
boundary: 'fixture assertion',
|
||||||
|
statement: 'Контролируемый вызов с прежним mapper даёт состояние allowed и сохраняет currency.',
|
||||||
|
checksAction: 'A1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
decisions: [
|
||||||
|
{
|
||||||
|
id: 'D1',
|
||||||
|
trigger: 'H1',
|
||||||
|
action: 'Не расширять retry и не менять timeout; сначала оставить проверяемый старый mapper в учебной ветке.',
|
||||||
|
expectedCheck: 'V1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
prevention: [
|
||||||
|
{
|
||||||
|
id: 'P1',
|
||||||
|
change: 'Добавить contract fixture, который отклоняет mapper без currency.',
|
||||||
|
check: 'runIncidentFixture() сохраняет проверку отсутствующего поля.',
|
||||||
|
state: 'planned',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'P2',
|
||||||
|
change: 'Записать обязательные поля у границы pricing-adapter рядом с преобразованием.',
|
||||||
|
check: 'Код-ревью сверяет поле currency с маленьким входным примером.',
|
||||||
|
state: 'planned',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runIncidentFixture() {
|
||||||
|
const fixture = createSyntheticFixture();
|
||||||
|
assertIncidentLinks(fixture.timeline, fixture.decisions, fixture.prevention);
|
||||||
|
|
||||||
|
const byId = new Map(fixture.timeline.map((event) => [event.id, event]));
|
||||||
|
const observationIds = fixture.timeline.filter((event) => event.kind === 'observation').map((event) => event.id);
|
||||||
|
const action = byId.get('A1');
|
||||||
|
const verification = byId.get('V1');
|
||||||
|
const serialized = JSON.stringify(fixture);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...fixture,
|
||||||
|
assertions: {
|
||||||
|
timelineIsStrictlyOrdered: fixture.timeline.every((event, index) => index === 0 || event.at > fixture.timeline[index - 1].at),
|
||||||
|
hypothesisUsesRecordedObservation: byId.get('H1').basedOn === 'E2' && observationIds.includes('E2'),
|
||||||
|
actionFollowsHypothesis: action.basedOn === 'H1',
|
||||||
|
verificationFollowsAction: verification.checksAction === action.id,
|
||||||
|
decisionCarriesExpectedCheck: fixture.decisions[0].expectedCheck === verification.id,
|
||||||
|
preventionIsConcreteAndStillPlanned: fixture.prevention.every((item) => item.change && item.check && item.state === 'planned'),
|
||||||
|
fixtureDoesNotNamePeople: !serialized.includes('personName') && !serialized.includes('operatorName'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const syntheticTimelineCode = [
|
||||||
|
'// Детерминированная in-memory фикстура из этого revision-модуля.',
|
||||||
|
'// Это не журнал, не трафик и не отчёт о боевой системе.',
|
||||||
|
'const timeline = [',
|
||||||
|
" { id: 'E1', kind: 'observation', evidence: 'result.state === \"blocked\"' },",
|
||||||
|
" { id: 'E2', kind: 'observation', evidence: 'mappedInput.currency === undefined' },",
|
||||||
|
" { id: 'H1', kind: 'hypothesis', basedOn: 'E2' },",
|
||||||
|
" { id: 'A1', kind: 'action', basedOn: 'H1' },",
|
||||||
|
" { id: 'V1', kind: 'verification', checksAction: 'A1' },",
|
||||||
|
'];',
|
||||||
|
'',
|
||||||
|
'// Каждая ссылка проверяется до публикации revision.',
|
||||||
|
'assertIncidentLinks(timeline, decisions, prevention);',
|
||||||
|
];
|
||||||
|
|
||||||
|
const decisionRecordCode = [
|
||||||
|
'const decision = {',
|
||||||
|
" id: 'D1',",
|
||||||
|
" trigger: 'H1',",
|
||||||
|
" action: 'оставить старый mapper в учебной ветке',",
|
||||||
|
" expectedCheck: 'V1',",
|
||||||
|
'};',
|
||||||
|
'',
|
||||||
|
'// Запрещённый вывод: «mapper виноват».',
|
||||||
|
'// Проверяемый вывод: H1 основана на E2 и ожидает V1.',
|
||||||
|
];
|
||||||
|
|
||||||
|
const validationCode = [
|
||||||
|
'function assertIncidentLinks(timeline, decisions, prevention) {',
|
||||||
|
' // hypothesis ссылается на более раннее observation;',
|
||||||
|
' // action ссылается на hypothesis;',
|
||||||
|
' // verification проверяет конкретный action.',
|
||||||
|
" if (hypothesis.basedOn !== 'E2') throw new Error('missing evidence');",
|
||||||
|
" if (verification.checksAction !== 'A1') throw new Error('missing check');",
|
||||||
|
" if (prevention.state !== 'planned') throw new Error('premature completion');",
|
||||||
|
'}',
|
||||||
|
'',
|
||||||
|
'// Настоящий код модуля делает эти проверки для всех записей fixture.',
|
||||||
|
];
|
||||||
|
|
||||||
|
const preventionCode = [
|
||||||
|
'const prevention = [',
|
||||||
|
' {',
|
||||||
|
" id: 'P1',",
|
||||||
|
" change: 'fixture отклоняет mapper без currency',",
|
||||||
|
" check: 'runIncidentFixture() возвращает true',",
|
||||||
|
" state: 'planned',",
|
||||||
|
' },',
|
||||||
|
'];',
|
||||||
|
'',
|
||||||
|
'// Planned не означает «выполнено».',
|
||||||
|
'// У действия есть отдельная будущая проверка и место в очереди команды.',
|
||||||
|
];
|
||||||
|
|
||||||
|
const fieldWalkthroughCode = [
|
||||||
|
'// Синтетический сценарий: данные существуют только в памяти Node.',
|
||||||
|
'const mappedInput = { amount: 100 };',
|
||||||
|
'const result = mappedInput.currency',
|
||||||
|
" ? { state: 'allowed' }",
|
||||||
|
" : { state: 'blocked', reason: 'currency-missing' };",
|
||||||
|
'',
|
||||||
|
'// Это наблюдение E1/E2, не причина и не описание внешнего сервиса.',
|
||||||
|
'console.log(result);',
|
||||||
|
];
|
||||||
|
|
||||||
|
const fixtureRunCode = [
|
||||||
|
'const fixture = runIncidentFixture();',
|
||||||
|
'if (!Object.values(fixture.assertions).every(Boolean)) {',
|
||||||
|
" throw new Error('controlled incident contract failed');",
|
||||||
|
'}',
|
||||||
|
'',
|
||||||
|
'console.log(fixture.timeline.map((event) => event.id));',
|
||||||
|
"// ['E1', 'E2', 'H1', 'A1', 'V1']",
|
||||||
|
];
|
||||||
|
|
||||||
|
const practiceArticle = createRevision(
|
||||||
|
{
|
||||||
|
slug: 'editorial-2020-12-practice-incident-review',
|
||||||
|
title: 'Разбор инцидента: как собрать факты до первого исправления',
|
||||||
|
categories: ['Надёжность', 'Команда', 'Практика'],
|
||||||
|
cover: '/assets/editorial/2020/incident-timeline-2020.svg',
|
||||||
|
excerpt: 'Видимый симптом легко исправить отдельной правкой. Разбираем учебную временную шкалу, чтобы связать наблюдение, гипотезу, решение, проверку и профилактику без поиска виноватого.',
|
||||||
|
readingMinutes: 15,
|
||||||
|
},
|
||||||
|
[
|
||||||
|
paragraph('Симптом во время сбоя обычно короткий: один путь перестал завершаться ожидаемым состоянием. Цена поспешного исправления выше, чем кажется. Если сразу увеличить timeout, добавить повтор или переписать соседний модуль, команда получает новый код, но не получает цепочку доказательств. Через месяц похожий симптом возвращается, а в заметке остаётся только фраза «починили». Она не объясняет, что именно наблюдали, почему выбрали это действие и как следующий инженер проверит профилактику.'),
|
||||||
|
paragraph('В декабре 2020 года я бы начал не с большой процедуры и не с роли для каждого человека. Достаточно завести короткую запись разбора и научиться держать в ней пять разных вещей: симптом, наблюдение, гипотезу, действие и проверку. Ниже все значения созданы в памяти Node для учебной фикстуры. Это не журнал, не сетевой вызов, не история реального инцидента и не доказательство работы в боевой среде. Цель уже: не дать видимому симптому превратиться в уверенный, но непроверяемый диагноз.'),
|
||||||
|
heading('Сначала останавливаем историю до первой правки'),
|
||||||
|
paragraph('Первое полезное действие — зафиксировать наблюдение до изменения конфигурации или кода. Наблюдение отвечает на вопрос «что именно увидели на границе?». В учебном случае вызов <code>preview-order</code> получил состояние <code>blocked</code>, а затем вход для <code>pricing-adapter</code> оказался без <code>currency</code>. Здесь ещё нет причины. Поле могло исчезнуть в mapper, во входном сценарии или в самом описании контракта. Такая осторожность не замедляет разбор: она не даёт следующему шагу подменить факт удобной версией события.'),
|
||||||
|
paragraph('Временная шкала нужна не для красивого отчёта. Она сохраняет порядок. Сначала есть E1 и E2 — два наблюдения. Потом H1 — гипотеза, которая прямо ссылается на E2. Только после неё A1 — ограниченное действие в учебной ветке, и V1 — проверка этого действия. Если поменять порядок местами, запись станет похожа на объяснение задним числом: решение уже принято, а факт подбирается к нему позже. Для небольшого сервиса или модуля такая дисциплина особенно полезна, потому что один разработчик часто одновременно смотрит код, меняет конфигурацию и пишет комментарий к задаче.'),
|
||||||
|
dataTable(
|
||||||
|
'Минимальная запись разбора: каждый тип строки имеет свой вопрос',
|
||||||
|
['Элемент', 'Что в нём фиксируем', 'Что не пишем вместо него', 'Следующая граница'],
|
||||||
|
[
|
||||||
|
['Симптом', 'какой путь дал неожидаемое состояние и какова цена повторения', 'название предполагаемой причины', 'выбрать точку наблюдения'],
|
||||||
|
['Наблюдение', 'значение, место и способ его получения', 'объяснение, почему значение появилось', 'сформулировать одну гипотезу'],
|
||||||
|
['Гипотеза', 'проверяемое предположение со ссылкой на наблюдение', 'обвинение компонента или человека', 'поставить малую проверку'],
|
||||||
|
['Действие', 'что временно изменили и почему это ограничено', 'заявление, что причина устранена', 'ожидаемая проверка'],
|
||||||
|
['Профилактика', 'изменение контракта, теста или документа и его будущая проверка', 'слово «готово» без критерия', 'вернуться к очереди изменения'],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
paragraph('Цена смешения этих строк практическая. Фраза «mapper сломал заказ» одновременно содержит наблюдение, гипотезу и обвинение, но ни одну часть нельзя проверить. Короткая запись лучше: «E2: после преобразования нет <code>currency</code>; H1: новый mapper не переносит обязательное поле; A1: учебная ветка остаётся на предыдущем mapper; V1: контролируемый вызов снова получает <code>allowed</code>». В ней есть граница, связь и следующий вопрос. Она не обещает, что этого достаточно для любой системы.'),
|
||||||
|
heading('Факт должен пережить смену версии гипотезы'),
|
||||||
|
paragraph('Хорошее наблюдение можно перечитать после того, как гипотеза оказалась неверной. Поэтому рядом с ним полезно хранить не пересказ, а маленькое доказательство: вход, ожидаемое значение, фактическое значение и границу. В fixture эта роль у строк <code>evidence</code>. Они намеренно короткие: <code>result.state === "blocked"</code> и <code>mappedInput.currency === undefined</code>. Такой фрагмент не заменяет тест интеграции, но отделяет факт от подписи к нему. Если завтра окажется, что поле убрал не mapper, E2 не надо переписывать. Нужно заменить H1 и поставить новый путь проверки.'),
|
||||||
|
codeBlock(syntheticTimelineCode),
|
||||||
|
paragraph('Код выше не подключается к HTTP, базе или очереди. Он делает более скромную работу: проверяет, что ссылка из гипотезы ведёт к раннему наблюдению, действие следует за гипотезой, а проверка относится к действию. Это полезный тип fixture для текста о процессе. Когда запись меняют в будущем, модуль не позволит тихо переставить V1 перед A1 или добавить профилактику без критерия. Получается не имитация чужой инфраструктуры, а контракт на порядок рассуждения.'),
|
||||||
|
figure(
|
||||||
|
'/assets/editorial/2020/incident-timeline-2020.svg',
|
||||||
|
'Вертикальная схема учебного разбора: от симптома и двух наблюдений через одну гипотезу и ограниченное действие к проверке и плановой профилактике; стрелки показывают, что проверка не может появиться раньше действия',
|
||||||
|
'Временная шкала сохраняет зависимость: наблюдение не равно причине, а временный обход не равен профилактике.',
|
||||||
|
),
|
||||||
|
heading('Решение записываем как проверяемую ставку'),
|
||||||
|
paragraph('Действие в разборе не обязано быть окончательным исправлением. Иногда задача действия — остановить дальнейшее ухудшение или вернуть систему к известному варианту, пока причина ещё проверяется. Но у действия должен быть trigger и ожидаемый check. В D1 trigger — H1, а expected check — V1. Благодаря этому нельзя написать «вернули старый mapper, стало лучше» без пояснения, что именно проверялось. Если проверка не проходит, действие не превращается в истину: его отменяют или меняют гипотезу.'),
|
||||||
|
codeBlock(decisionRecordCode),
|
||||||
|
paragraph('В примере специально не выбираются retry и timeout. Повтор мог бы скрыть обязательное поле, а увеличение времени никак не проверяет контракт <code>pricing-adapter</code>. Это не запрет на такие меры. Это порядок: сначала назвать наблюдаемую границу, затем проверить, относится ли мера к ней. Если новое наблюдение покажет временный отказ зависимости, запись станет другой: появится другая гипотеза, другое действие и другой expected check. Технический разбор ценен именно тем, что допускает изменение решения без подмены уже записанного факта.'),
|
||||||
|
heading('Без поиска виноватого — не значит без технической ответственности'),
|
||||||
|
paragraph('Фактологичный разбор не пишет «кто допустил ошибку», потому что такая строка редко помогает следующему изменению. Вместо неё он спрашивает: какая граница не показала обязательное поле; какая проверка отсутствовала; почему временное решение было разумным с доступной информацией; какой контракт снизит шанс повторения. Это не попытка смягчить проблему. Наоборот, техническая запись становится точнее: она обязана назвать компонент, поле, действие и контрольную проверку, но не приписывает намерения или знания людям, которых в fixture вообще нет.'),
|
||||||
|
paragraph('Здесь важно не превратить принцип в набор мягких слов. Если mapper действительно отбрасывает <code>currency</code>, это надо написать как условие кода и закрепить тестом. Если документация не называет поле обязательным, это тоже надо зафиксировать. Если решение было временным, рядом остаётся его граница и срок следующей проверки. «Не искать виноватого» означает не пропускать причинные условия. Оно не означает скрывать неверный контракт или откладывать неудобную профилактику.'),
|
||||||
|
heading('Маршрут для следующего разбора'),
|
||||||
|
orderedList([
|
||||||
|
'Записать симптом без диагноза: какой путь дал неожидаемое состояние и что стоит повторение этой ошибки для читателя, пользователя или команды.',
|
||||||
|
'Снять одно или два наблюдения до правки. У каждого указать границу, вход или значение и способ получить его снова.',
|
||||||
|
'Сформулировать одну гипотезу с явной ссылкой на наблюдение. Не писать причину в форме факта, пока нет отдельной проверки.',
|
||||||
|
'Выбрать ограниченное действие. Записать, что оно меняет, чего не меняет и при каких условиях его нужно отменить.',
|
||||||
|
'Назвать expected check: какой контролируемый сценарий или автоматическая проверка подтвердит именно это действие.',
|
||||||
|
'Сформировать профилактику отдельно от обхода. В ней должен быть конкретный тест, контракт или документ и признак будущего выполнения.',
|
||||||
|
'Перед публикацией перечитать временную шкалу сверху вниз: любой вывод должен ссылаться на предыдущее наблюдение, а любое «готово» — на проверку.',
|
||||||
|
]),
|
||||||
|
heading('Граница учебного примера и источники метода'),
|
||||||
|
paragraph('Эта статья не строит процесс для крупной организации и не обещает, что пять строк заменят коммуникацию, резервные каналы или отдельные меры безопасности. Наша фикстура не запускает реальный запрос, proxy, хранилище, развёртывание или мониторинг. Она не измеряет время восстановления и не назначает числовую норму. Её можно взять как минимальный каркас для локального модуля, а затем проверить на реальном стенде и в правилах конкретной команды.'),
|
||||||
|
paragraph('Источники ниже важны именно как рамка. Глава Google о разборе инцидентов показывает ценность живого состояния и разделения работ; глава о postmortem — ценность записи причин и follow-up без обвинения. NIST описывает более широкий цикл обработки security-инцидентов, поэтому его нельзя механически переносить на любой баг. В учебном материале остаётся общий, проверяемый принцип: собрать наблюдения, ограничить действие, проверить эффект и оставить конкретную профилактику.'),
|
||||||
|
],
|
||||||
|
[googleIncidentManagement, googlePostmortemCulture, nistIncidentHandling],
|
||||||
|
);
|
||||||
|
|
||||||
|
const mechanismArticle = createRevision(
|
||||||
|
{
|
||||||
|
slug: 'editorial-2020-12-mechanism-incident-review',
|
||||||
|
title: 'Разбор инцидента: как связать наблюдение, решение и профилактику',
|
||||||
|
categories: ['Надёжность', 'Команда', 'Архитектура'],
|
||||||
|
cover: '/assets/editorial/2020/incident-decision-record-2020.svg',
|
||||||
|
excerpt: 'Разделяем состояние разбора на факты, гипотезы, действия, проверки и профилактику. Так временный обход не выдаётся за устранённую причину, а новая проверка не теряется в пересказе.',
|
||||||
|
readingMinutes: 16,
|
||||||
|
},
|
||||||
|
[
|
||||||
|
paragraph('Проблема разбора редко в отсутствии длинного документа. Чаще команда чинит видимый симптом, а затем одним абзацем называет это причиной, решением и профилактикой сразу. Цена такого текста — потерянная граница ответственности. В следующем сбое непонятно, что повторить: проверить вход, отключить тот же путь, сверить контракт или искать другую ветку. Временный обход живёт как окончательный вывод, а тест появляется только как пожелание.'),
|
||||||
|
paragraph('Для автора конца 2020 года здесь полезна не сложная платформа, а простая модель состояния. Наблюдение хранит то, что произошло в учебном сценарии. Гипотеза объясняет одно наблюдение, но остаётся изменяемой. Действие выбирает ограниченный ход. Проверка измеряет эффект действия. Профилактика меняет будущий путь и остаётся planned, пока её не проверили отдельно. Весь пример ниже синтетический и выполняется только в памяти; он не изображает трафик, журнал, людей или production как факт.'),
|
||||||
|
heading('Один абзац не должен играть пять ролей'),
|
||||||
|
paragraph('Возьмём фразу: «мы вернули старую настройку, потому что mapper отбросил currency, и добавили тест». В ней можно найти смысл, но нельзя восстановить ход работы. Неясно, было ли отсутствие поля наблюдением или догадкой. Неясно, какая настройка менялась, как её проверили и был ли тест действительно добавлен. После нескольких недель такая фраза превращается в легенду: звучит уверенно, но не даёт безопасного следующего действия. Модель с разными полями делает текст чуть длиннее, зато резко уменьшает число неявных предположений.'),
|
||||||
|
paragraph('В fixture E2 хранит наблюдение об отсутствии <code>currency</code>. H1 говорит только о возможной связи с mapper и ссылается на E2. A1 возвращает в учебной ветке известный вариант. V1 проверяет именно A1, а не вообще «здоровье системы». P1 и P2 остаются planned: это будущие изменения теста и описания контракта. Такой набор не пытается вычислить все корни проблемы. Он создаёт достаточную трассу от наблюдения к следующей проверяемой работе.'),
|
||||||
|
dataTable(
|
||||||
|
'Механика записи: каждое поле отвечает на собственный вопрос',
|
||||||
|
['Состояние', 'Минимальное содержимое', 'Допустимая связь', 'Ошибка при смешении'],
|
||||||
|
[
|
||||||
|
['Observation', 'граница, значение, evidence', 'может стать основанием H1', 'значение выдают за причину'],
|
||||||
|
['Hypothesis', 'предположение и ссылка на observation', 'ведёт к одному или нескольким действиям', 'гипотеза становится обвинением или окончательным фактом'],
|
||||||
|
['Action', 'ограниченное изменение и условие возврата', 'следует за hypothesis', 'обход называют устранением причины'],
|
||||||
|
['Verification', 'сценарий и ожидаемый результат', 'проверяет конкретный action', 'общий успех не связан с изменением'],
|
||||||
|
['Prevention', 'изменение и собственный check', 'остаётся planned до отдельной проверки', 'задача объявляется сделанной в момент идеи'],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
paragraph('Этот контракт удобен тем, что его можно проверять механически. Время событий должно возрастать. H1 не может ссылаться на будущую строку. V1 не должен возникать до A1. Decision D1 обязан назвать trigger и expected check. Prevention не должна менять состояние на completed только потому, что её записали в отчёт. Такие правила не доказывают техническую причину автоматически. Они защищают форму разбора, чтобы у людей осталось место для технического исследования, а не для восстановления истории по памяти.'),
|
||||||
|
heading('Почему observation и hypothesis разделяют даже в маленьком модуле'),
|
||||||
|
paragraph('Наблюдение переживает неудачную гипотезу; гипотеза — нет. В этом разница их стоимости. Если написать «mapper не передал currency» как observation, а позже выяснится, что поле отсутствовало ещё до mapper, придётся переписать прошлое. Если же observation говорит «после преобразования поле отсутствует», история остаётся верной. Меняется только H1 и следующая проверка. Это важно не только для аварий. Так же работают разборы странного кеша, ошибки миграции, зависимости от переменной окружения и любого условия, которое появляется на границе модулей.'),
|
||||||
|
paragraph('Проверяемая гипотеза обязана сузить пространство. H1 не говорит «что-то сломалось в заказе». Она говорит, что конкретный новый путь mapper мог не перенести обязательное поле. У неё есть E2, с которым можно спорить, и действие A1, которое можно отменить. Если гипотеза не позволяет придумать маленький эксперимент, она слишком общая. Нужно разделить её: одна строка о входе, другая о преобразовании, третья о контракте. Тогда отрицательный результат не разочаровывает, а сокращает список оставшихся причин.'),
|
||||||
|
codeBlock(validationCode),
|
||||||
|
paragraph('В исходном модуле проверка шире короткого фрагмента: она проходит по всем events, решениям и профилактическим пунктам. Но даже такой псевдокод показывает смысл. Контракт не проверяет «идеальную архитектуру». Он проверяет, что будущее не перепутано с прошлым и что готовность не подменена намерением. Когда кто-то меняет fixture, ошибка о missing evidence или premature completion полезнее красивой диаграммы: она показывает, какая связь исчезла.'),
|
||||||
|
heading('Решение — это ставка с ожидаемой проверкой'),
|
||||||
|
paragraph('Техническое действие во время разбора часто временное. Иногда его выбирают потому, что оно обратимо; иногда потому, что есть понятный прошлый вариант; иногда потому, что позволяет сохранить данные для следующего исследования. В любом случае оно должно быть описано как ставка: на какой hypothesis реагируем, какую границу меняем, что ожидаем увидеть и в каком случае откатываемся. Эта запись не тормозит восстановление. Она освобождает от спорного воспоминания через час, когда уже сделано несколько правок.'),
|
||||||
|
paragraph('D1 в учебном случае говорит: не расширять retry и не увеличивать timeout, оставить предыдущий mapper, затем ждать V1. Это не объявление, что retry плохой. Наоборот, оно оставляет дверь для другой ветки, если V1 не подтвердит действие. У retry должен быть свой trigger, свой риск дублирования и свой check. У timeout — своя причина медленного ответа. Когда все меры складывают в один ответ, команда одновременно меняет несколько переменных и теряет возможность понять, какая из них повлияла на результат.'),
|
||||||
|
figure(
|
||||||
|
'/assets/editorial/2020/incident-decision-record-2020.svg',
|
||||||
|
'Вертикальная схема decision record: записанное наблюдение E2 порождает гипотезу H1, она ограничивает действие A1, после него идёт проверка V1; профилактика P1 остаётся отдельной плановой работой с собственным критерием',
|
||||||
|
'Decision record не назначает виноватого. Он делает видимыми trigger, действие и ожидаемую проверку.',
|
||||||
|
),
|
||||||
|
heading('Профилактика не равна строке в конце отчёта'),
|
||||||
|
paragraph('После временного действия возникает соблазн закрыть разбор фразой «добавить тест». Это слишком мало. Нужно назвать изменение, границу и способ убедиться, что изменение существует. P1 добавляет contract fixture, который отклоняет mapper без <code>currency</code>. P2 закрепляет обязательное поле рядом с границей <code>pricing-adapter</code> и требует сверки на ревью с маленьким входным примером. Обе строки остаются planned. Это честнее, чем создаёт видимость завершённой работы.'),
|
||||||
|
codeBlock(preventionCode),
|
||||||
|
paragraph('Статус planned не обесценивает профилактику. Он показывает разницу между знанием и изменением. В разборе уже есть вывод: проверка отсутствующего поля нужна. Но ещё нет доказательства, что она попала в репозиторий, выполняется в нужном месте и ловит нужную регрессию. Когда такую разницу не фиксируют, follow-up становится эмоциональным обещанием. Когда фиксируют, его можно положить в обычную очередь задач и вернуться с отдельным результатом проверки.'),
|
||||||
|
heading('Что остаётся технической работой, а не языком отчёта'),
|
||||||
|
paragraph('Слова observation, hypothesis и prevention не должны заменить анализ кода. После E2 всё равно нужно открыть преобразование, проследить поле во входном объекте, проверить версию контракта и выбрать точку теста. Запись лишь задаёт порядок, в котором эта работа не уничтожит сама себя. Если обнаружится несколько contributing conditions, можно добавить H2 и A2, но у каждой пары должны остаться свои ссылки. Один большой «root cause» часто скрывает разные условия: пропущенное поле, отсутствие проверки и слишком широкий обход.'),
|
||||||
|
paragraph('Этот подход также не требует копировать процессы большой компании. Не нужно придумывать командный центр, десятки ролей или общую платформу, чтобы перестать смешивать факты с выводами. Для малого проекта достаточно отдельного документа или задачи, небольшой fixture и понятного владельца follow-up в привычной очереди. Важнее другое: текст не должен выдавать остановленный симптом за устранённую причину, а идея теста — за проведённую проверку.'),
|
||||||
|
heading('Маршрут механического ревью записи'),
|
||||||
|
orderedList([
|
||||||
|
'Разделить черновик на observation, hypothesis, action, verification и prevention. Если одна строка попала в две группы, переписать её на две короткие.',
|
||||||
|
'Проверить порядок: каждая hypothesis ссылается на более раннее observation, а каждое действие — на конкретную hypothesis.',
|
||||||
|
'Проверить decision record: у решения есть trigger, ограниченное действие и expected check, а не только итоговая формулировка.',
|
||||||
|
'Проверить verification: она должна измерять именно действие, а не произвольный положительный сигнал рядом.',
|
||||||
|
'Проверить prevention: у неё есть изменение и будущий check, а статус planned не перепутан с выполнением.',
|
||||||
|
'Отдельно прочитать, какие реальные технические границы ещё не проверены. Не добавлять имитированные журналы, клиентов или поведение среды вместо этого исследования.',
|
||||||
|
'Сохранить fixture рядом с текстом и запускать её после изменения записи, чтобы связи не разошлись незаметно.',
|
||||||
|
]),
|
||||||
|
heading('Граница модели и историческая рамка'),
|
||||||
|
paragraph('Модель не заменяет диагностику, резервирование, мониторинг или безопасную процедуру изменения. Она не делает из учебной ветки описание production-инцидента и не говорит, что один mapper был причиной любого отказа. В материале не запускались SDK, клиент, HTTP, база, очередь, dashboard или deployment. Синтетический вход нужен только для того, чтобы независимый читатель увидел связь E2 → H1 → A1 → V1 и мог проверить её локально.'),
|
||||||
|
paragraph('Официальные материалы ниже используют более широкий язык incident management и postmortem. Здесь от них взят ограниченный, доступный в 2020 году урок: состояние разбора должно быть живым, причины — отделены от обвинений, а follow-up — проверяемым. Это соответствует растущей инженерной практике автора, но не приписывает ему опыт зрелой платформы надёжности или универсальный процесс для всех команд.'),
|
||||||
|
],
|
||||||
|
[googleIncidentManagement, googlePostmortemCulture, googleTroubleshooting],
|
||||||
|
);
|
||||||
|
|
||||||
|
const fieldArticle = createRevision(
|
||||||
|
{
|
||||||
|
slug: 'editorial-2020-12-field-incident-review',
|
||||||
|
title: 'Учебный разбор инцидента: почему временный обход не становится выводом',
|
||||||
|
categories: ['Надёжность', 'Отладка', 'Практика'],
|
||||||
|
cover: '/assets/editorial/2020/incident-learning-loop-2020.svg',
|
||||||
|
excerpt: 'На синтетическом случае разбираем путь от blocked-состояния к проверяемому временному обходу и плановой профилактике. Главный критерий: ни один вывод не должен появиться раньше своего наблюдения.',
|
||||||
|
readingMinutes: 16,
|
||||||
|
},
|
||||||
|
[
|
||||||
|
paragraph('Полевая заметка о сбое часто ломается в одном месте: после восстановления команда считает, что разбор закончен. Симптом исчез, но причина, проверка и профилактика не связаны. Цена заметна при следующем изменении. Кто-то снова видит <code>blocked</code>, находит старую фразу «вернули настройку» и повторяет её вне исходной границы. В результате временный обход становится традицией, а обязательное поле или тест по-прежнему отсутствуют.'),
|
||||||
|
paragraph('Ниже не рассказ о существующей системе. Это детерминированный сценарий в памяти Node: объект без <code>currency</code> проходит через учебный mapper, <code>preview-order</code> возвращает <code>blocked</code>, а предыдущий mapper в той же fixture даёт <code>allowed</code>. В нём нет реального трафика, логов, пользователей, внешних сервисов или боевого развёртывания. Сценарий нужен для одного вопроса: как записать цепочку так, чтобы временное действие не выдали за доказанную причину.'),
|
||||||
|
heading('Учебная граница: один вход, один ожидаемый контракт'),
|
||||||
|
paragraph('У каждого разбора должна быть граница. Здесь это не «оформление заказа вообще», а переход от mapper к <code>pricing-adapter</code>. Вход содержит сумму, но не содержит обязательное поле валюты. Результат <code>blocked</code> — первый наблюдаемый эффект. Он не говорит, что зависимость неисправна и не утверждает, что mapper уже виноват. Второе наблюдение говорит только то, что после преобразования <code>currency</code> отсутствует. Два наблюдения вместе дают право сформулировать H1, но не закрывают расследование сами по себе.'),
|
||||||
|
paragraph('Это сужение может показаться слишком простым. Оно полезно именно потому, что не позволяет заполнить пробелы словами «иногда падает» или «сервис не отвечает». Чем шире симптом, тем больше случайных исправлений. Чем конкретнее граница, тем проще придумать проверку. В реальном проекте это может быть endpoint, очередь, миграция или сохранение файла. Но одна статья должна держать один контракт. Иначе временная шкала превращается в список разнородных событий, из которого нельзя сделать следующий безопасный шаг.'),
|
||||||
|
dataTable(
|
||||||
|
'Учебный случай: от видимого эффекта к ограниченному действию',
|
||||||
|
['Момент', 'Зафиксированный факт', 'Допустимый вывод', 'Что пока неизвестно'],
|
||||||
|
[
|
||||||
|
['E1', '<code>preview-order</code> вернул <code>blocked</code> в fixture', 'есть симптом на границе пути', 'какой компонент изменил вход'],
|
||||||
|
['E2', 'после mapper отсутствует <code>currency</code>', 'обязательное поле не дошло до contract boundary', 'где поле впервые пропало'],
|
||||||
|
['H1', 'новый mapper мог не переносить <code>currency</code>', 'можно проверить конкретную ветку mapper', 'что это единственная причина'],
|
||||||
|
['A1', 'учебная ветка использует прежний mapper', 'есть ограниченный обратимый обход', 'что причина устранена'],
|
||||||
|
['V1', 'fixture возвращает <code>allowed</code> и сохраняет поле', 'A1 прошёл заданную проверку', 'что будущий mapper не повторит ошибку'],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
paragraph('Таблица особенно важна в строке «что пока неизвестно». Она не позволяет V1 доказать больше, чем он умеет. Проверка старого mapper показывает, что выбранный обход соответствует учебному входу. Она не доказывает корректность всех валют, правил цен или окружений. Если подменить эту границу, next step станет опасным: временный обход распространится на другие пути, а отсутствующая профилактика останется незаметной.'),
|
||||||
|
heading('Сначала запускаем маленькую модель, а не сочиняем журнал'),
|
||||||
|
paragraph('Fixture должна быть читаемой без доступа к внутренним системам. В нашем случае она собирает обычный объект, получает <code>blocked</code>, затем проверяет цепочку событий. Код не подделывает timestamps из настоящего журнала и не повторяет сетевое поведение. Он показывает только значение, достаточное для E1 и E2. Это честная граница: можно проверить форму причинного рассуждения, но нельзя заявить, что изучены все внешние условия.'),
|
||||||
|
codeBlock(fieldWalkthroughCode),
|
||||||
|
paragraph('После запуска такого кода результатом является не приговор компоненту, а два наблюдения. Первое — <code>blocked</code>. Второе — отсутствие <code>currency</code>. H1 появляется отдельной строкой, потому что она связывает их с mapper. Если завтра fixture изменится и поле окажется потеряно раньше, H1 удалят или заменят. E1 и E2 останутся полезными. Это простой тест качества разбора: можно ли выбросить гипотезу, не переписывая заодно фактическую часть истории?'),
|
||||||
|
figure(
|
||||||
|
'/assets/editorial/2020/incident-learning-loop-2020.svg',
|
||||||
|
'Вертикальный цикл учебного разбора: собрать факт, ограничить временное действие, проверить его контролируемым сценарием, создать плановую профилактику и применить новую проверку при следующем изменении; профилактика не замыкается на слове готово',
|
||||||
|
'Цикл замыкается не отчётом, а будущей проверкой контракта при следующем изменении.',
|
||||||
|
),
|
||||||
|
heading('Временное действие должно оставлять след'),
|
||||||
|
paragraph('A1 в учебной линии выбирает предыдущий mapper. Это можно назвать обходом, но не исправлением причины. След остаётся в D1: trigger — H1, action — оставить известный вариант, expected check — V1. Такая запись помогает через день увидеть, что именно было принято и зачем. Если V1 оказался бы отрицательным, возвращаться к старому mapper было бы бессмысленно, и разбор пошёл бы к другой гипотезе. Без expected check команда рискует принять совпадение за результат действия.'),
|
||||||
|
paragraph('Этот пример также показывает, почему не стоит одновременно менять retry, timeout и преобразование. В синтетическом случае retry мог бы снова послать объект без <code>currency</code>; timeout лишь сделал бы ожидание длиннее. Оба хода изменяют поведение, но не проверяют H1. Они могли бы быть оправданы при другой наблюдаемой причине, однако тогда должны появиться новые строки в шкале и отдельные проверки. Разбор не запрещает инструменты. Он требует, чтобы инструмент соответствовал записанной границе.'),
|
||||||
|
codeBlock(decisionRecordCode),
|
||||||
|
heading('Профилактика начинается с нового проверяемого контракта'),
|
||||||
|
paragraph('После V1 легко написать «добавим тест» и закрыть вопрос. Вместо этого в fixture есть P1: contract fixture отклоняет mapper без <code>currency</code>. У P1 есть точка запуска — <code>runIncidentFixture()</code> — и status <code>planned</code>. Второй пункт P2 фиксирует обязательное поле в месте, где mapper передаёт данные в <code>pricing-adapter</code>, и предлагает ревью на маленьком входном примере. Это две разные профилактики: автоматический барьер и явность контракта.'),
|
||||||
|
paragraph('Planned здесь не формальность. В нём скрыт полезный вопрос: что изменится до следующей попытки? Пока тест не добавлен и не прошёл, разбор не может обещать, что поле не исчезнет снова. Пока контракт не стал видимым, ревьюер может не заметить регрессию. Разделение временного обхода и профилактики возвращает работе реальный масштаб: A1 уменьшил влияние учебного случая сейчас, P1/P2 должны уменьшить вероятность повторения после отдельной реализации и проверки.'),
|
||||||
|
codeBlock(fixtureRunCode),
|
||||||
|
paragraph('Запуск fixture проверяет семь простых утверждений: порядок timeline, связь H1 с E2, связь A1 с H1, связь V1 с A1, expected check у D1, конкретность P1/P2 и отсутствие имён людей. Это не интеграционный тест. Он не проверяет сеть, доступы, базу, browser, deployment или поведение внешнего адаптера. Его ценность в другом: будущая редактура текста или кода не сможет заменить проверяемую последовательность на убедительный рассказ без ссылок.'),
|
||||||
|
heading('Как читать отрицательный результат без поиска виноватого'),
|
||||||
|
paragraph('Допустим, V1 не прошёл. Это не повод написать, что предыдущий mapper тоже «плохой». Это новый факт: A1 не подтвердил ожидаемый эффект. Дальше надо расширить или изменить H1 и снять новое наблюдение. Возможно, <code>currency</code> отсутствует ещё во входном объекте. Возможно, <code>blocked</code> вызван другим полем. Важно, что запись сохраняет путь: неудачный обход не прячется, а показывает, какое предположение не выдержало проверки.'),
|
||||||
|
paragraph('Такой язык убирает обвинение не ради вежливости. Он сохраняет технические данные для следующей попытки. Вместо «кто-то сломал mapper» остаётся вопрос «какое преобразование нарушает обязательность <code>currency</code> и где должна сработать проверка». Вместо «мы всё исправили» остаётся «A1 подтвердился для fixture; P1/P2 пока planned». Эти формулировки короче, точнее и полезнее для автора следующего изменения.'),
|
||||||
|
heading('Маршрут разбора синтетического случая'),
|
||||||
|
orderedList([
|
||||||
|
'Назвать одну границу и один нежелательный эффект. В упражнении это <code>preview-order</code> и состояние <code>blocked</code>.',
|
||||||
|
'Собрать наблюдения до действия: результат вызова и значение обязательного поля после преобразования.',
|
||||||
|
'Сформулировать H1 как проверяемую связь с конкретным observation, а не как окончательный диагноз.',
|
||||||
|
'Выбрать обратимое действие, которое относится к H1. Записать его отдельно от причины и указать expected check.',
|
||||||
|
'Запустить контролируемую fixture и прочитать V1 только в пределах её входа. Не расширять вывод на неизвестные пути.',
|
||||||
|
'Создать P1/P2 с ясным изменением и будущей проверкой; оставить их planned до реальной реализации.',
|
||||||
|
'При следующем изменении прогнать fixture снова и дополнить временную шкалу новым observation, если контракт нарушен иначе.',
|
||||||
|
]),
|
||||||
|
heading('Что эта заметка не утверждает'),
|
||||||
|
paragraph('Учебный случай не оценивает нагрузку, доступность, время восстановления или поведение реальной зависимости. В нём нет заявлений о production-случае, реальном трафике, людях, логах или развертывании. Не было запущено внешнее API, browser, очередь, база, proxy или monitoring. Поэтому его нельзя использовать как отчёт о существующей системе. Его можно использовать как маленький шаблон мысли, прежде чем идти собирать факты в конкретной среде с её правами, версией и рисками.'),
|
||||||
|
paragraph('Материалы Google и NIST ниже не дают одну универсальную форму отчёта. Они подтверждают более общий принцип: зафиксировать состояние, анализировать evidence, ограничивать реакцию и оставлять follow-up. Для автора конца 2020 года достаточно научиться переносить этот принцип в локальный код и проверяемую fixture. Следующая взрослая ступень появится только после накопления настоящих измерений и командной практики, а не после того, как в текст добавят модные термины.'),
|
||||||
|
],
|
||||||
|
[googleIncidentManagement, googlePostmortemCulture, nistIncidentHandling, googleTroubleshooting],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const revisions = [practiceArticle, mechanismArticle, fieldArticle]
|
||||||
|
.map(({ proseLength, ...revision }) => revision);
|
||||||
|
|
||||||
|
if (process.argv.includes('--print-revisions')) {
|
||||||
|
process.stdout.write(JSON.stringify(revisions));
|
||||||
|
} else if (process.argv.includes('--verify-fixture')) {
|
||||||
|
const fixture = runIncidentFixture();
|
||||||
|
if (!Object.values(fixture.assertions).every(Boolean)) {
|
||||||
|
throw new Error('Incident fixture assertions failed');
|
||||||
|
}
|
||||||
|
process.stdout.write(JSON.stringify(fixture, null, 2) + '\n');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user