This commit is contained in:
@@ -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