351 lines
66 KiB
JavaScript
351 lines
66 KiB
JavaScript
function escapeHtml(value) {
|
||
return String(value)
|
||
.replaceAll('&', '&')
|
||
.replaceAll('<', '<')
|
||
.replaceAll('>', '>')
|
||
.replaceAll('"', '"')
|
||
.replaceAll("'", ''');
|
||
}
|
||
|
||
const p = (text) => '<p>' + text + '</p>';
|
||
const h2 = (text) => '<h2>' + text + '</h2>';
|
||
const code = (text) => '<pre><code>' + escapeHtml(text) + '</code></pre>';
|
||
const ol = (items) => '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
|
||
const figure = (src, alt, caption) => '<figure><img src="' + src + '" alt="' + alt + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
|
||
const table = (caption, headers, rows) => '<div class="table-scroll"><table><caption>' + caption + '</caption><thead><tr>' + headers.map((cell) => '<th scope="col">' + cell + '</th>').join('') + '</tr></thead><tbody>' + rows.map((row) => '<tr>' + row.map((cell) => '<td>' + cell + '</td>').join('') + '</tr>').join('') + '</tbody></table></div>';
|
||
|
||
function cloneFixed(value) { return JSON.parse(JSON.stringify(value)); }
|
||
function deepFreeze(value) {
|
||
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
||
Object.values(value).forEach(deepFreeze);
|
||
Object.freeze(value);
|
||
}
|
||
return value;
|
||
}
|
||
function plainText(html) {
|
||
return html.replace(/<[^>]+>/g, ' ').replace(/&(?:quot|amp|lt|gt|#039);/g, ' ').replace(/\s+/g, ' ').trim();
|
||
}
|
||
function bodyText(html) {
|
||
return plainText(html.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, ''));
|
||
}
|
||
|
||
const REFERENCES = deepFreeze({
|
||
rfc9110: {
|
||
title: 'RFC 9110 — HTTP Semantics',
|
||
url: 'https://www.rfc-editor.org/rfc/rfc9110.html',
|
||
version: 'IETF Standards Track, June 2022, RFC 9110 / STD 97, immutable publication',
|
||
},
|
||
nist800160: {
|
||
title: 'NIST SP 800-160 Volume 2 Revision 1 — Developing Cyber-Resilient Systems',
|
||
url: 'https://csrc.nist.gov/pubs/sp/800/160/v2/r1/final',
|
||
version: 'NIST Special Publication 800-160 Vol. 2 Rev. 1, December 2021, dated revision',
|
||
},
|
||
rfc9000: {
|
||
title: 'RFC 9000 — QUIC: A UDP-Based Multiplexed and Secure Transport',
|
||
url: 'https://www.rfc-editor.org/rfc/rfc9000.html',
|
||
version: 'IETF Standards Track, May 2021, immutable publication',
|
||
},
|
||
});
|
||
function sources(entries) {
|
||
return '<ul>' + entries.map(({ key, use, boundary }) => {
|
||
const ref = REFERENCES[key];
|
||
return '<li><a href="' + ref.url + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(ref.title) + '</a> — ' + escapeHtml(ref.version) + '. ' + escapeHtml(use) + ' Не доказывает: ' + escapeHtml(boundary) + '</li>';
|
||
}).join('') + '</ul>';
|
||
}
|
||
|
||
const FIXED_RELIABILITY_CASES = deepFreeze({
|
||
'reliability-contract-hand-off-v1': {
|
||
id: 'reliability-contract-hand-off-v1',
|
||
editorialDate: '2026-07-31',
|
||
planningIssue: '2027-07',
|
||
sourceCutoff: '2026-07-31',
|
||
scenario: {
|
||
id: 'named-synthetic-dependency-unavailable-scenario',
|
||
retry: 'explicit-not-selected',
|
||
replica: 'explicit-not-selected',
|
||
recovery: 'explicit-not-selected',
|
||
},
|
||
evidence: {
|
||
incident: 'not-collected',
|
||
serviceLevel: 'not-defined',
|
||
observation: 'not-collected',
|
||
configuration: 'not-collected',
|
||
load: 'not-collected',
|
||
},
|
||
requestedOutput: 'synthetic-plan-hand-off',
|
||
},
|
||
'missing-temporal-boundary-v1': {
|
||
id: 'missing-temporal-boundary-v1', editorialDate: '', planningIssue: '2027-07', sourceCutoff: '2026-07-31',
|
||
scenario: { id: 'named-synthetic-dependency-unavailable-scenario', retry: 'explicit-not-selected', replica: 'explicit-not-selected', recovery: 'explicit-not-selected' },
|
||
evidence: { incident: 'not-collected', serviceLevel: 'not-defined', observation: 'not-collected', configuration: 'not-collected', load: 'not-collected' }, requestedOutput: 'synthetic-plan-hand-off',
|
||
},
|
||
'hidden-evidence-or-configuration-v1': {
|
||
id: 'hidden-evidence-or-configuration-v1', editorialDate: '2026-07-31', planningIssue: '2027-07', sourceCutoff: '2026-07-31',
|
||
scenario: { id: 'named-synthetic-dependency-unavailable-scenario', retry: 'explicit-not-selected', replica: 'explicit-not-selected', recovery: 'explicit-not-selected' },
|
||
evidence: { incident: 'not-collected', serviceLevel: 'not-defined', observation: 'not-collected', configuration: 'hidden', load: 'not-collected' }, requestedOutput: 'synthetic-plan-hand-off',
|
||
},
|
||
'implicit-retry-v1': {
|
||
id: 'implicit-retry-v1', editorialDate: '2026-07-31', planningIssue: '2027-07', sourceCutoff: '2026-07-31',
|
||
scenario: { id: 'named-synthetic-dependency-unavailable-scenario', retry: 'implicit', replica: 'explicit-not-selected', recovery: 'explicit-not-selected' },
|
||
evidence: { incident: 'not-collected', serviceLevel: 'not-defined', observation: 'not-collected', configuration: 'not-collected', load: 'not-collected' }, requestedOutput: 'synthetic-plan-hand-off',
|
||
},
|
||
'implicit-replica-or-recovery-v1': {
|
||
id: 'implicit-replica-or-recovery-v1', editorialDate: '2026-07-31', planningIssue: '2027-07', sourceCutoff: '2026-07-31',
|
||
scenario: { id: 'named-synthetic-dependency-unavailable-scenario', retry: 'explicit-not-selected', replica: 'implicit', recovery: 'assumed' },
|
||
evidence: { incident: 'not-collected', serviceLevel: 'not-defined', observation: 'not-collected', configuration: 'not-collected', load: 'not-collected' }, requestedOutput: 'synthetic-plan-hand-off',
|
||
},
|
||
'claimed-evidence-v1': {
|
||
id: 'claimed-evidence-v1', editorialDate: '2026-07-31', planningIssue: '2027-07', sourceCutoff: '2026-07-31',
|
||
scenario: { id: 'named-synthetic-dependency-unavailable-scenario', retry: 'explicit-not-selected', replica: 'explicit-not-selected', recovery: 'explicit-not-selected' },
|
||
evidence: { incident: 'claimed', serviceLevel: 'not-defined', observation: 'not-collected', configuration: 'not-collected', load: 'not-collected' }, requestedOutput: 'synthetic-plan-hand-off',
|
||
},
|
||
'claimed-operational-result-v1': {
|
||
id: 'claimed-operational-result-v1', editorialDate: '2026-07-31', planningIssue: '2027-07', sourceCutoff: '2026-07-31',
|
||
scenario: { id: 'named-synthetic-dependency-unavailable-scenario', retry: 'explicit-not-selected', replica: 'explicit-not-selected', recovery: 'explicit-not-selected' },
|
||
evidence: { incident: 'not-collected', serviceLevel: 'not-defined', observation: 'not-collected', configuration: 'not-collected', load: 'not-collected' }, requestedOutput: 'reliability-improved',
|
||
},
|
||
});
|
||
|
||
export function createFixedReliabilityCase(id = 'reliability-contract-hand-off-v1') {
|
||
const value = FIXED_RELIABILITY_CASES[id];
|
||
return value ? deepFreeze(cloneFixed(value)) : undefined;
|
||
}
|
||
function stop(status, reason, nextAction) {
|
||
return deepFreeze({ status, reason, nextAction, productionEffect: 'not-attempted' });
|
||
}
|
||
export function assessFixedReliabilityPlan(input) {
|
||
const known = Object.values(FIXED_RELIABILITY_CASES).some((item) => JSON.stringify(item) === JSON.stringify(input));
|
||
if (!known) return stop('stop-unknown-fixed-input', 'input-is-not-a-known-named-fixed-literal', 'select-a-named-fixed-literal');
|
||
if (input.editorialDate !== '2026-07-31' || input.planningIssue !== '2027-07' || input.sourceCutoff !== '2026-07-31') {
|
||
return stop('stop-temporal-boundary-required', 'editorial-date-planning-issue-and-source-cutoff-must-be-exact', 'name-the-fixed-temporal-boundary');
|
||
}
|
||
if (!input.scenario?.id || input.evidence?.configuration !== 'not-collected' || input.evidence.incident !== 'not-collected' || input.evidence.serviceLevel !== 'not-defined' || input.evidence.observation !== 'not-collected' || input.evidence.load !== 'not-collected') {
|
||
return stop('stop-hidden-evidence-or-configuration', 'evidence-and-configuration-must-remain-explicitly-uncollected', 'remove-hidden-or-claimed-material');
|
||
}
|
||
if (input.scenario.retry !== 'explicit-not-selected') {
|
||
return stop('stop-implicit-retry', 'retry-cannot-be-inferred-or-defaulted', 'state-that-retry-is-not-selected');
|
||
}
|
||
if (input.scenario.replica !== 'explicit-not-selected' || input.scenario.recovery !== 'explicit-not-selected') {
|
||
return stop('stop-implicit-replica-or-recovery', 'replica-and-recovery-cannot-be-assumed', 'state-that-replica-and-recovery-are-not-selected');
|
||
}
|
||
if (input.requestedOutput !== 'synthetic-plan-hand-off') {
|
||
return stop('stop-disallowed-operational-result', 'a-future-scenario-cannot-claim-an-operational-result', 'use-synthetic-plan-hand-off');
|
||
}
|
||
return deepFreeze({
|
||
status: 'synthetic-plan-hand-off', caseId: input.id, planningIssue: input.planningIssue,
|
||
scenario: deepFreeze(cloneFixed(input.scenario)), evidence: deepFreeze(cloneFixed(input.evidence)),
|
||
productionEffect: 'not-attempted', nextAction: 'open-a-separate-authorized-evidence-scope-if-a-decision-requires-it',
|
||
});
|
||
}
|
||
export function inspectFailureContractLiteral() {
|
||
const input = createFixedReliabilityCase();
|
||
return deepFreeze({ namedScenario: input.scenario.id, retry: input.scenario.retry, outcome: assessFixedReliabilityPlan(input).status });
|
||
}
|
||
export function inspectFaultTreeLiteral() {
|
||
const result = assessFixedReliabilityPlan(createFixedReliabilityCase());
|
||
return deepFreeze({ topQuestion: 'named-synthetic-dependency-unavailable-scenario', branches: ['request-shape-not-collected', 'capacity-not-collected', 'recovery-not-selected'], outcome: result.productionEffect });
|
||
}
|
||
export function inspectHandoffLiteral() {
|
||
const result = assessFixedReliabilityPlan(createFixedReliabilityCase());
|
||
return deepFreeze({ accepted: result.status, incident: result.evidence.incident, serviceLevel: result.evidence.serviceLevel, nextAction: result.nextAction });
|
||
}
|
||
export function runFixedReliabilityFixture() {
|
||
const expected = [
|
||
['reliability-contract-hand-off-v1', 'synthetic-plan-hand-off'],
|
||
['missing-temporal-boundary-v1', 'stop-temporal-boundary-required'],
|
||
['hidden-evidence-or-configuration-v1', 'stop-hidden-evidence-or-configuration'],
|
||
['implicit-retry-v1', 'stop-implicit-retry'],
|
||
['implicit-replica-or-recovery-v1', 'stop-implicit-replica-or-recovery'],
|
||
['claimed-evidence-v1', 'stop-hidden-evidence-or-configuration'],
|
||
['claimed-operational-result-v1', 'stop-disallowed-operational-result'],
|
||
];
|
||
const checks = expected.map(([id, status]) => ({ id, expected: status, actual: assessFixedReliabilityPlan(createFixedReliabilityCase(id)).status }));
|
||
const sample = createFixedReliabilityCase();
|
||
return deepFreeze({ passed: checks.filter((item) => item.expected === item.actual).length, total: checks.length, accepted: checks.every((item) => item.expected === item.actual) && Object.isFrozen(sample) && Object.isFrozen(sample.scenario) && Object.isFrozen(sample.evidence), checks: deepFreeze(checks) });
|
||
}
|
||
|
||
function revision(meta, parts, referenceEntries) {
|
||
const contentHtml = parts.join('') + h2('Проверяемые источники') + sources(referenceEntries);
|
||
const proseLength = bodyText(contentHtml).length;
|
||
if (proseLength < 5000 || proseLength > 15000) throw new Error(meta.slug + ': body length ' + proseLength);
|
||
return deepFreeze({ ...meta, contentHtml, proseLength });
|
||
}
|
||
const refs = [
|
||
{ key: 'rfc9110', use: 'Закрепляет различие безопасного метода, идемпотентности и автоматического повторения после сбоя связи.', boundary: 'Не подтверждает метод, запрос, зависимость, повтор или результат в данном сценарии.' },
|
||
{ key: 'nist800160', use: 'Даёт официальный словарь устойчивости как способности противостоять, восстанавливаться и адаптироваться к неблагоприятным условиям.', boundary: 'Не является incident record, SLO, планом восстановления или доказательством готовности системы.' },
|
||
{ key: 'rfc9000', use: 'Показывает, что транспортный слой имеет собственные состояния и ограничения, которые нельзя выдать за семантику прикладного действия.', boundary: 'Не доказывает наличие QUIC, конкретной нагрузки, соединения или восстановления.' },
|
||
];
|
||
|
||
const practice = revision({
|
||
slug: 'editorial-2027-07-practice-reliability-capstone',
|
||
title: 'Большой разбор надёжности: практический маршрут',
|
||
categories: ['Надёжность', 'Кейс'],
|
||
cover: '/assets/editorial/2027/reliability-capstone-2027-fault-tree-contract.svg',
|
||
excerpt: 'План на июль 2027: как зафиксировать контракт отказного сценария, не выдавая предположение о retry за решение.',
|
||
readingMinutes: 23,
|
||
}, [
|
||
p('Это редакционный план на 2027-07 с source cutoff 2026-07-31, а не отчёт о работе системы. Практическая проблема начинается до первого кода: зависимость названа «недоступной», но не сказано, какое действие остаётся незавершённым, что именно разрешено повторить и что должно остановиться. Такая пустота быстро превращается в неверный retry. Его цена не абстрактна: один и тот же запрос может быть отправлен повторно без доказанного права на это, а команда получает спор о последствиях вместо ясной границы сценария.'),
|
||
p('Вторая цена — ложная экономия на формулировке. Если в будущем описании незаметно появляются резервная копия, вторая реплика или «обычное восстановление», читатель воспринимает их как уже выбранный путь. На редакторскую дату у этой статьи нет реального incident, значения SLO или SLI, deployment, наблюдений, конфигурации, нагрузки либо результата. Разрешён только <code>synthetic-plan-hand-off</code> с <code>productionEffect: not-attempted</code>. Поэтому ниже не рецепт обработки сбоя, а контракт, который не подставляет отсутствующие решения.'),
|
||
h2('Контракт начинается с действия, а не с названия сбоя'),
|
||
p('Фраза «сервис упал» слишком велика для решения о повторе. В ней не видно, был ли вызов начат, принято ли прикладное действие другой стороной, есть ли у вызывающего кода ключ идемпотентности и допустимо ли вообще сохранять попытку. В будущем scope эти вопросы могут получить разные ответы. Пока ответ не собран, безопаснее назвать только синтетический класс: named dependency unavailable. Это не диагноз и не намёк на конкретный компонент; это ограничитель для разговора.'),
|
||
p('Контракт полезен, когда в нём есть четыре независимых поля: действие, наблюдаемая граница, разрешение на повтор и состояние результата. В P113 действие намеренно не получает endpoint, payload или consumer. Наблюдаемая граница остаётся <code>not-collected</code>. Разрешение на повтор не вычисляется из HTTP-слова или названия операции: оно явно остаётся <code>explicit-not-selected</code>. Состояние результата не меняется на успех или восстановление, потому что данных для такого перехода нет.'),
|
||
table('Части синтетического контракта отказного сценария', ['Часть', 'Что можно назвать сейчас', 'Что нельзя достраивать'], [
|
||
['Граница действия', 'Именованный synthetic scenario', 'Реальный запрос, ключ, consumer или side effect'],
|
||
['Повтор', '<code>explicit-not-selected</code>', 'Автоматический retry по умолчанию'],
|
||
['Резервирование', '<code>explicit-not-selected</code>', 'Реплику, запасной маршрут или переключение'],
|
||
['Восстановление', '<code>explicit-not-selected</code>', 'Готовый recovery path или время возврата'],
|
||
['Свидетельство', '<code>not-collected</code>', 'Incident, конфигурацию, нагрузку или production outcome'],
|
||
]),
|
||
h2('Почему идемпотентность не заменяет контракт'),
|
||
p('RFC 9110 различает безопасные методы и идемпотентные методы, а также описывает условия, при которых клиент может повторить идемпотентный запрос после сбоя связи. Это полезная норма для языка разговора, но не лицензия на скрытый retry. Документ не знает бизнес-действие, дедупликацию, состояние принимающей стороны и политику конкретного клиента. Поэтому нельзя взять слово <code>idempotent</code> и выдать его за доказательство, что повтор не изменит будущий предметный результат.'),
|
||
p('Проверка должна идти в обратную сторону. Сначала будущий scope формулирует, что считается одним логическим действием. Затем определяет, есть ли явный способ различить первую попытку и повтор. Потом отделяет транспортную неясность от прикладного подтверждения. Лишь после этого можно обсуждать policy. Этот порядок медленнее одной строки с retry, зато не переносит риск из слоя связи в слой данных под видом надёжности.'),
|
||
figure('/assets/editorial/2027/reliability-capstone-2027-fault-tree-contract.svg', 'Схема контракта отказного сценария: один синтетический вопрос разложен на действие, границу, явный retry и запрещённые предположения о реплике и восстановлении.', 'Схема не показывает реальный путь запроса. Она удерживает четыре поля контракта и красные стоп-границы для невыбранных решений.'),
|
||
h2('Исполнимый пример без доступа к системе'),
|
||
p('Пример ниже запускается в Node и работает только с named fixed in-memory literal. Он не импортирует HTTP-клиент, не читает файл, не использует переменные окружения, часы, секреты или внешние данные. Его результат не говорит, что retry безопасен: он показывает, что literal не способен подставить retry сам.'),
|
||
code("import { inspectFailureContractLiteral } from './upgrade-2027-07.mjs';\n\nconst contract = inspectFailureContractLiteral();\nconsole.log(contract);\n// { namedScenario: 'named-synthetic-dependency-unavailable-scenario',\n// retry: 'explicit-not-selected', outcome: 'synthetic-plan-hand-off' }"),
|
||
p('Здесь <code>namedScenario</code> — имя формы, а не запись об инфраструктуре. <code>retry</code> — намеренный отказ от implicit policy. Если изменить fixed literal или передать произвольный объект, evaluator не ищет недостающие сведения и не выбирает fallback. Он возвращает stop-status. Это важнее красивого примера с сетевым клиентом: пример не создаёт следов, не обещает повторного выполнения и не маскирует конфигурацию.'),
|
||
h2('Порядок постановки будущего вопроса'),
|
||
ol([
|
||
'Назвать одно логическое действие без endpoint, payload и рассказа о прошлой операции.',
|
||
'Указать редакционную дату 2026-07-31, planning issue 2027-07 и source cutoff 2026-07-31.',
|
||
'Оставить evidence, configuration и load в состоянии <code>not-collected</code>.',
|
||
'Зафиксировать retry, replica и recovery как <code>explicit-not-selected</code>, пока отдельное решение не даст им смысл.',
|
||
'Остановить карточку при hidden field, implicit retry или claimed result; не компенсировать пробел предположением.',
|
||
'Передать только synthetic plan question в отдельный будущий scope, если появится решение, которому нужны доказательства.',
|
||
]),
|
||
h2('Цена слишком раннего retry'),
|
||
p('Неверный retry опасен не количеством попыток, а подменой вопроса. Команда может начать спорить о задержке между попытками, хотя ещё не определила право повторять действие. Она может обсуждать backoff, хотя неизвестно, какой сигнал отличает временную недоступность от подтверждённого принятия. Она может назвать этот разговор восстановлением, хотя не названа точка, из которой что-либо восстанавливается. Каждая такая подмена создаёт видимость управления без проверяемого предмета.'),
|
||
p('Практичный контракт режет эту цепочку раньше. Он не пытается быть универсальной схемой отказоустойчивости и не выносит оценку в продакшен. Его цель уже: сохранить для будущего обсуждения место, где решение о повторе должно быть явным. Сценарий остаётся полезным, даже если позже будет выбран запрет на повтор, другой интерфейс или прекращение операции. Никакой из этих исходов не записан заранее.'),
|
||
h2('Контракт не должен притворяться политикой'),
|
||
p('Есть соблазн считать, что достаточно назвать policy хорошими словами: «осторожный retry», «защищённое переключение», «мягкое восстановление». Но такие ярлыки скрывают, кто принимает решение и по какому наблюдаемому условию. Один читатель понимает «осторожный» как запрет на повтор, другой — как повтор после паузы, третий — как передачу в очередь. Если этот разрыв обнаруживается после начала реализации, цена выше, чем у прямого вопроса: уже появились несовместимые ожидания от данных и интерфейса.'),
|
||
p('В P113 policy не выбирается намеренно. Такой отказ не является пропуском обязательного пункта. Он сохраняет обратимость: будущая работа сможет выбрать любой путь, который будет обоснован её собственным контекстом, либо не выбирать путь вовсе. Fixed literal не хранит флаг, который можно принять за включённый механизм. Вместо этого он хранит понятное отрицательное состояние. У читателя нет оснований превратить его в скрытый default при переносе текста в задачу или обсуждение.'),
|
||
p('Эту же дисциплину полезно применить к слову «ошибка». Ошибка соединения, ошибка ответа и ошибка предметного действия не обязаны иметь одну реакцию. В текущем сценарии мы не узнаём, был ли любой из них. Поэтому контракт не предлагает классификатор и не рисует таблицу кодов. Он фиксирует более раннюю развилку: без явно определённого смысла действия нельзя вывести допустимость повтора из внешнего симптома. Это узкая, но проверяемая граница практики.'),
|
||
p('Краткий текст наставника здесь не означает, что можно опустить цену выбора. Стоимость явной невыбранности — необходимость вернуться к вопросу позже. Стоимость неявного выбора — риск, что его будут исполнять уже сейчас, не понимая ограничений. Для будущей системы первый вариант обычно честнее: он не обещает экономию времени там, где отсутствует право на решение. Контракт помогает увидеть именно эту цену до того, как спор станет спором о якобы уже работающем механизме.'),
|
||
p('Полезный критерий качества такого контракта прост: другой читатель может повторить его проверку, не получив доступ к системе. Он должен увидеть временную границу, имя synthetic scenario, отрицательные states и причины stop. Если для понимания потребуются подразумеваемый сервис, предполагаемый журнал или внутреннее правило, контракт уже протекает. Тогда нужно не дописывать догадку, а вернуть поле к явной невыбранности и сохранить вопрос для отдельного решения.'),
|
||
h2('Пределы и следующий шаг'),
|
||
p('Этот материал не создаёт runbook, не проводит failure-injection и не определяет recovery. Он не утверждает наличие реплик, очереди, retries, метрик, логов или людей, которые будут исполнять дальнейшую работу. NIST SP 800-160 помогает назвать устойчивость как инженерную область с неблагоприятными условиями и восстановлением, но не превращает synthetic literal в доказательство готовности. RFC 9110 также не говорит за неизвестное прикладное действие.'),
|
||
p('Следующий шаг ограничен: только если появится отдельное авторизованное решение, открыть новый scope и заново определить допустимые evidence, метод и границы вывода. До этого P113 завершён честно: он показывает цену неверного retry и оставляет retry невыбранным. В этом плане нет production effect, победителя или обещания, что система когда-либо будет изменена.'),
|
||
], refs);
|
||
|
||
const mechanism = revision({
|
||
slug: 'editorial-2027-07-mechanism-reliability-capstone',
|
||
title: 'Большой разбор надёжности: как принять инженерное решение',
|
||
categories: ['Надёжность', 'Кейс'],
|
||
cover: '/assets/editorial/2027/reliability-capstone-2027-recovery-evidence-matrix.svg',
|
||
excerpt: 'План на июль 2027: как сделать fault tree проверяемым, не выдавая структуру вопросов за восстановление или результат нагрузки.',
|
||
readingMinutes: 24,
|
||
}, [
|
||
p('Это план/сценарий P113 на 2027-07 с source cutoff 2026-07-31. Механическая ошибка в разборе надёжности появляется, когда fault tree рисуют как готовое объяснение: верхняя вершина названа, ветви выглядят правдоподобно, и дальше их начинают читать как факты. Цена такой схемы — неверное инженерное решение. Она может направить внимание к восстановлению или нагрузке, хотя ни причина, ни состояние системы, ни сама нагрузка не были собраны.'),
|
||
p('Есть и вторая цена: слово «recovery» создаёт ощущение завершённости. В будущей статье легко написать, что путь восстановления существует, что запасная ветвь примет работу или что система выдержит давление. Но на редакционную дату нет actual incident, проверки под нагрузкой, SLO/SLI values, deployment, наблюдений, recovery result или production outcome. Здесь допускается единственный положительный output — <code>synthetic-plan-hand-off</code> с <code>productionEffect: not-attempted</code>; fault tree служит для фальсифицируемых вопросов, не для уверенного диагноза.'),
|
||
h2('Fault tree — карта условий опровержения'),
|
||
p('Полезное дерево отказов не отвечает «почему произошёл сбой». Оно делает видимым, что пришлось бы опровергнуть или подтвердить, прежде чем считать верхнее утверждение объяснённым. В P113 верхняя вершина намеренно синтетическая: named dependency unavailable scenario. Под ней нет hostname, client, трассы или сообщения. Вместо этого есть группы неизвестного: форма действия, условие нагрузки и выбранность восстановления. Каждая ветвь остаётся вопросом, пока evidence не разрешён и не получен в другом scope.'),
|
||
p('Такое устройство защищает от ложной полноты. Можно нарисовать пять причин и всё равно не знать, относятся ли они к одному времени, одной версии или одному действию. Можно назвать их независимыми, хотя для этого нет материала. В сценарии лучше меньше ветвей, но у каждой есть статус. <code>not-collected</code> значит, что состояние не наблюдалось; <code>explicit-not-selected</code> значит, что решение не принималось; <code>not-defined</code> значит, что критерий ещё не сформулирован. Эти слова не являются плохими данными — они запрещают делать из пустоты вывод.'),
|
||
table('Матрица ветвей для будущей фальсификации', ['Ветка дерева', 'Допустимое состояние P113', 'Какой вывод остановлен'], [
|
||
['Форма действия', 'named synthetic question', 'Что была конкретная операция или side effect'],
|
||
['Нагрузка', '<code>not-collected</code>', 'Что система выдержала либо не выдержала давление'],
|
||
['Восстановление', '<code>explicit-not-selected</code>', 'Что есть готовый путь возврата'],
|
||
['Критерий уровня сервиса', '<code>not-defined</code>', 'Что соблюдён SLO или измерен SLI'],
|
||
['Наблюдение', '<code>not-collected</code>', 'Что причина или порядок событий известны'],
|
||
]),
|
||
h2('Нагрузка не равна одной оси на графике'),
|
||
p('Нагрузка часто выглядит как удобная переменная, которую можно добавить в диаграмму позже. Но без описания действия она ничего не объясняет. Неизвестно, что именно считается единицей работы, где возникает конкуренция, как различаются очереди и синхронные вызовы, какие данные допустимо использовать и какой результат вообще сравнивается. Поэтому P113 не содержит нагрузочного профиля, числа, генератора или запуска. Слово <code>load: not-collected</code> специально не оставляет места для фиктивной шкалы.'),
|
||
p('Это ограничение не запрещает будущую проверку. Оно отделяет её от текущего текста. Когда появится отдельный scope, ему понадобятся датированные входы, разрешение на метод, критерий останова и правила интерпретации. Даже тогда результат нельзя извлечь из одного количества операций: нужно проверить, что оно связано с конкретным верхним вопросом дерева. Пока эта связь отсутствует, график выглядел бы убедительнее, чем заслуживает его provenance.'),
|
||
figure('/assets/editorial/2027/reliability-capstone-2027-recovery-evidence-matrix.svg', 'Матрица evidence для fault tree: ветви формы действия, нагрузки, восстановления, критерия и наблюдения отмечены как не собранные или не выбранные, а зелёный результат остаётся только synthetic hand-off.', 'Матрица различает отсутствие evidence и отсутствие решения. Красные ячейки показывают выводы, которые нельзя получить из такого состояния.'),
|
||
h2('Восстановление нужно разложить на решения'),
|
||
p('Восстановление — не переключатель в диаграмме. У него есть исходное состояние, сохранность предметного результата, порядок возврата и пределы допустимого действия. Если хотя бы один из этих элементов назван по памяти, читатель получает легенду вместо механизма. В P113 recovery не скрыт за словом «позже разберёмся»: он имеет точный статус <code>explicit-not-selected</code>. Это говорит не о недостатке инженерной зрелости, а о том, что выбор нельзя сделать из статьи без предметных ограничений.'),
|
||
p('NIST SP 800-160 Vol. 2 Rev. 1 использует устойчивость как способность предвидеть, выдерживать, восстанавливаться и адаптироваться к неблагоприятным условиям. Для этой статьи важен именно масштаб утверждения: документ задаёт направление системной инженерии, но не подтверждает, что конкретная система уже прошла любой из этих этапов. Поэтому ссылки на resilience нельзя использовать как подпись под synthetic tree с обещанием восстановления.'),
|
||
h2('Исполнимый пример проверяет границу, а не систему'),
|
||
p('Этот пример строит компактный вид fixed literal. Он вычисляет только строки и массивы, заданные в модуле. В нём нет чтения файлов, сети, clock, environment, секрета, телеметрии, процесса, данных или обращения к внешнему миру. Запуск не создаёт нагрузку и не делает recovery-попытку.'),
|
||
code("import { inspectFaultTreeLiteral } from './upgrade-2027-07.mjs';\n\nconst tree = inspectFaultTreeLiteral();\nconsole.log(tree.branches.join(' | '));\nconsole.log(tree.outcome);\n// request-shape-not-collected | capacity-not-collected | recovery-not-selected\n// not-attempted"),
|
||
p('Ветка <code>capacity-not-collected</code> не измеряет capacity и не предполагает её. <code>recovery-not-selected</code> не обозначает скрытый резервный механизм. А <code>not-attempted</code> относится к production effect всего evaluator, не к вымышленной попытке восстановления. У этого примера есть полезный критерий: при произвольном объекте или скрытом значении функция возвращает stop-status и не расширяет дерево догадкой.'),
|
||
h2('Порядок, который делает дерево проверяемым'),
|
||
ol([
|
||
'Сформулировать одну synthetic верхнюю вершину без имени реальной системы и без причины задним числом.',
|
||
'Разделить ветви действия, нагрузки, критерия и восстановления так, чтобы одна ветвь не служила доказательством другой.',
|
||
'Проставить <code>not-collected</code>, <code>not-defined</code> или <code>explicit-not-selected</code> вместо заполняющих слов.',
|
||
'Проверить, что никакая ветвь не содержит hidden configuration, implicit retry, replica или recovery.',
|
||
'Потребовать stop для claimed incident, claimed service level и любого operational result.',
|
||
'Передать дерево только как вопрос о будущем evidence, не как основание менять систему.',
|
||
]),
|
||
h2('Что значит фальсифицируемость здесь'),
|
||
p('Фальсифицируемость в этом сценарии не означает тестировать систему прямо сейчас. Она означает, что у будущего утверждения заранее есть условия, при которых оно не может быть принято. Например, если не определён критерий уровня сервиса, нельзя назвать его соблюдённым. Если нет наблюдения, нельзя утверждать порядок причин. Если recovery не выбран, нельзя рассказывать о времени возврата. Такое дерево не делает надежность измеренной; оно делает преждевременный вывод недопустимым.'),
|
||
p('Это особенно важно в материале, который читатель может принять за «большой разбор». Большим должен быть не масштаб уверенности, а полнота границ: какие слои не смешиваются, что не собрано, откуда мог бы появиться будущий факт и что его всё равно не докажет. Конкретный вывод появится только в новом record с собственной датой, входами и правилами. P113 не резервирует для него финал.'),
|
||
h2('Дерево не заменяет причинную связь'),
|
||
p('Связи на схеме легко читаются сильнее, чем задумано. Стрелка от нагрузки к верхней вершине может выглядеть как утверждение, что нагрузка вызвала недоступность. Стрелка от восстановления к результату может выглядеть как доказательство, что выбран путь возврата. В P113 связи показывают только состав будущей проверки: эти темы нельзя склеить в один вывод, пока каждая не получит собственное основание. Схема должна помогать читателю задавать вопрос, а не отвечать вместо него.'),
|
||
p('Поэтому здесь нет вероятностей, весов ветвей, ранжирования причин и числового порога. Такие значения могут быть нужны в другом artefact, однако им необходимы происхождение и условия. Число без method превращает дерево в декорацию точности. Даже ранжирование «самая вероятная причина» недопустимо, когда отсутствуют observations. Текущий literal делает более скромное, но ценное действие: одинаково отказывает и красивой догадке, и скрытой конфигурации.'),
|
||
p('Нагрузка особенно хорошо показывает эту разницу. В одном контексте вопрос о нагрузке касается входного потока, в другом — конкуренции за ресурс, в третьем — внешнего ограничения. Перенос готовой ветви между ними без определения единицы работы создаёт ложную сопоставимость. P113 не заставляет будущий scope измерять всё подряд. Он требует только, чтобы намерение проверить нагрузку не называлось уже проведённой проверкой и не становилось доказательством recovery.'),
|
||
p('Такое дерево можно считать отрицательной спецификацией. Оно описывает утверждения, которых нельзя сделать, а не действия, которые уже выполнены. В инженерной работе это бывает полезнее предварительного решения: команда видит, какие данные действительно изменят выбор, а какие лишь сделают отчёт объёмнее. Если ни один будущий факт не нужен для решения, отдельный scope можно не открывать. Сценарий не подталкивает к сбору ради самого дерева.'),
|
||
h2('Пределы и следующий шаг'),
|
||
p('Статья не описывает реальное дерево существующего инцидента, не назначает SLO/SLI и не предлагает load test, failure injection, deploy или rollout. RFC 9000 и RFC 9110 помогают различать уровни протокола и семантики, но они не заменяют evidence о неизвестном действии. У P113 нет скрытого конфигурационного файла, инструмента проверки, monitoring output или recovery plan; их отсутствие отражено в literal, а не спрятано в сноске.'),
|
||
p('Следующий шаг возможен только как новый авторизованный scope: там можно решить, нужен ли факт для конкретного выбора, какие inputs допустимы и какой результат будет честно отрицательным. Если такого scope не будет, дерево всё равно выполнило работу: оно не позволило назвать нагрузку, recovery или service level там, где их нет. Для текущего пакета итог остаётся <code>synthetic-plan-hand-off</code> и <code>productionEffect: not-attempted</code>.'),
|
||
], refs);
|
||
|
||
const field = revision({
|
||
slug: 'editorial-2027-07-field-reliability-capstone',
|
||
title: 'Большой разбор надёжности: кейс с ограничениями и выводами',
|
||
categories: ['Надёжность', 'Кейс'],
|
||
cover: '/assets/editorial/2027/reliability-capstone-2027-response-handoff-loop.svg',
|
||
excerpt: 'План на июль 2027: как передать вопрос о надёжности без легенды об incident, SLO или уже принятом operational решении.',
|
||
readingMinutes: 22,
|
||
}, [
|
||
p('P113 — будущий редакционный сценарий на 2027-07 с source cutoff 2026-07-31. В полевом hand-off самая дорогая ошибка — написать гладкую историю вместо границы знания: «был incident», «уровень сервиса нарушен», «система восстановилась». Такие фразы удобно передавать дальше, но цена — ложная оперативная память. Следующий читатель тратит время на объяснение уже якобы известных событий, хотя исходные evidence, время, наблюдения и результат никогда не были частью этого пакета.'),
|
||
p('Вторая цена возникает, когда hand-off путают с поручением на работу. В карточку добавляют неявный retry, предполагаемую реплику, способ восстановления или ожидаемую победу. В P113 этого нет: не названы actual incident, SLO/SLI values, telemetry, deployment, owner, runbook, rollout и production outcome. Положительный evaluator output ровно один — <code>synthetic-plan-hand-off</code> с <code>productionEffect: not-attempted</code>. Полевая статья передаёт не результат и не легенду, а честно ограниченный вопрос.'),
|
||
h2('Что делает hand-off пригодным к чтению'),
|
||
p('Хорошая передача не пытается заполнить все поля. Она отделяет то, что названо, от того, что ещё не существует как evidence. В нашем fixed literal названы editorial date, planning issue, source cutoff и synthetic scenario. Это достаточно, чтобы понять временную границу и тему. Incident, observation, configuration и load остаются <code>not-collected</code>; service level остаётся <code>not-defined</code>. Такой набор не выглядит подробным отчётом — и именно поэтому его нельзя ошибочно процитировать как отчёт.'),
|
||
p('У hand-off есть и отрицательная часть: он должен явно сказать, какие shortcuts запрещены. Нельзя подставить endpoint, реальный сервис, команду, файл конфигурации, след внешней системы или фразу о завершённом восстановлении. Нельзя считать, что отсутствие значения означает «по умолчанию можно повторить». Если читателю нужен один из этих фактов, это причина открыть отдельный scope, а не редактировать значение задним числом в будущем плане.'),
|
||
table('Карточка передачи без легенды об операции', ['Поле', 'Значение в P113', 'Почему этого достаточно'], [
|
||
['Временная граница', '<code>2026-07-31 → 2027-07</code>', 'Не даёт перепутать будущий материал с историей'],
|
||
['Сценарий', 'named synthetic dependency unavailable', 'Фокусирует вопрос без указания реального target'],
|
||
['Incident и наблюдение', '<code>not-collected</code>', 'Не превращает отсутствие записи в вымышленный факт'],
|
||
['Уровень сервиса', '<code>not-defined</code>', 'Не имитирует SLO/SLI без условия и значения'],
|
||
['Выход evaluator', '<code>synthetic-plan-hand-off</code>', 'Передаёт план без попытки production effect'],
|
||
]),
|
||
h2('Почему отсутствие owner — тоже граница'),
|
||
p('В реальной работе адресат передачи имеет значение, но здесь его нельзя выдумать. Имя, команда или роль выглядели бы как назначение, а значит как уже совершённое организационное действие. P113 не назначает и не предполагает owner. Вместо этого output называет только следующий тип действия: открыть отдельный авторизованный evidence scope, если конкретное решение потребует фактов. Такая формулировка сохраняет свободу будущего контекста и не заставляет несуществующего участника отвечать за вымышленную историю.'),
|
||
p('Эта осторожность не делает карточку безличной. Она даёт будущему читателю ясное правило: используйте её как постановку вопроса, а не как основание для изменения системы. Если новая работа будет разрешена, ей понадобятся собственные дата, допустимые inputs, метод, хранение evidence и правило отрицательного вывода. Ничто из этого нельзя вывести из P113 автоматически. Любая такая попытка должна быть остановлена evaluator до того, как станет частью текста.'),
|
||
figure('/assets/editorial/2027/reliability-capstone-2027-response-handoff-loop.svg', 'Схема передачи: временная граница и синтетический вопрос проходят через три проверки, а скрытый evidence, implicit retry и заявленный результат направляются в стоп; единственный выход — synthetic plan hand-off.', 'Петля показывает не реальный incident response, а редакционный маршрут для будущего вопроса без owner, deployment или operational результата.'),
|
||
h2('Исполнимый пример сохраняет неполноту'),
|
||
p('Ниже пример для локального Node запуска. Он берёт только immutable literal, вызывает чистую функцию и печатает уже ограниченный hand-off. Модуль не читает file, network, environment, clock, secret, telemetry, system или data; он не создаёт внешнюю работу. Поэтому вывод демонстрирует форму проверки, а не состояние какой-либо системы.'),
|
||
code("import { inspectHandoffLiteral } from './upgrade-2027-07.mjs';\n\nconst handoff = inspectHandoffLiteral();\nconsole.log(handoff.accepted, handoff.incident, handoff.serviceLevel);\n// synthetic-plan-hand-off not-collected not-defined"),
|
||
p('Важная часть примера — не строка success, а отсутствие скрытого содержимого. <code>accepted</code> означает, что форма literal удовлетворяет редакционным ограничениям. Оно не означает success операции, восстановление или соответствие уровню сервиса. <code>incident</code> и <code>serviceLevel</code> остаются видимо пустыми по смыслу. Если передать input с claimed evidence, evaluator вернёт stop-status, а не преобразует claim в более осторожный текст.'),
|
||
h2('Порядок передачи, который не создаёт легенду'),
|
||
ol([
|
||
'Положить в карточку только fixed editorial date, planning issue, source cutoff и named synthetic scenario.',
|
||
'Отметить incident, observation, configuration и load как <code>not-collected</code>; не заменять их пересказом.',
|
||
'Оставить service level как <code>not-defined</code>, пока отдельный scope не определит условие и смысл измерения.',
|
||
'Проверить, что retry, replica и recovery имеют явный статус невыбранности, а не default.',
|
||
'Остановить карточку при hidden evidence, любом claimed operational result или произвольном input.',
|
||
'Передать только synthetic plan hand-off; новый scope открывается лишь при отдельно разрешённом решении.',
|
||
]),
|
||
h2('Что не следует добавлять для «пользы»'),
|
||
p('Самые опасные добавления обычно выглядят практично: короткая команда, пример параметра, предполагаемый порядок восстановления, имя будущей роли, ссылка на несуществующий график или фраза «это уже проверяли». Все они сужают интерпретацию так, будто у автора были доступ и наблюдения. В P113 даже пример кода не становится мостом к реальной системе: он работает только с литералом и демонстрирует, что отсутствующие факты не имеют fallback.'),
|
||
p('Так же опасна ретроспективная уверенность. Выражение «в этом случае» может незаметно звучать как рассказ о случившемся. Здесь точнее говорить «если будущий scope определит» и сразу называть границу. Это не бюрократическая оговорка. Она сохраняет возможность для будущего исследования прийти к отсутствию проблемы, к иной постановке или к отказу от действия. Хороший hand-off не заставляет следующие evidence подтвердить его заголовок.'),
|
||
h2('Передача должна оставаться обратимой'),
|
||
p('Hand-off часто становится необратимым из-за одной лишней детали. Допустим, в нём появляется имя условного компонента. Следующий читатель начинает искать его историю, связывает с ним будущую задачу и уже не замечает, что имя было лишь примером. То же происходит с фразой о «типичном» механизме: она незаметно превращается в обязательный путь. В P113 обратимость охраняется буквально: в fixed literal нет target, идентификатора системы, предполагаемого payload или конфигурационного ключа.'),
|
||
p('Обратимость нужна не для того, чтобы бесконечно откладывать решение. Она даёт будущему scope право сузить, изменить или закрыть вопрос без переписывания прошлого. Если окажется, что решение не требует новых evidence, hand-off останется корректным. Если окажется, что нужен другой вопрос, старая карточка не будет мешать ему притворной точностью. Если разрешённый материал укажет на отсутствие основания для действия, это также не противоречит P113. План ничего не обещал доказать.'),
|
||
p('У такой формы есть цена: читатель не получает готовый operational маршрут. Но это честная цена за то, что пакет не содержит скрытого доступа или назначений. В поле особенно легко перепутать скорость передачи и скорость решения. Первая достигается короткой карточкой с фактами; вторая требует, чтобы факты существовали и были пригодны к применению. Когда второй части нет, ускорять её вымыслом означает передать долг следующему человеку под видом помощи.'),
|
||
p('Проверяемый literal делает эту норму технической, а не только редакторской. Нельзя случайно добавить object с дополнительными данными: evaluator сравнивает вход с известными fixed cases и fail-closed отвергает произвольную форму. Нельзя спрятать улучшенный вывод в строке requested output. Нельзя превратить пустое поле в положительное заключение. Такой контроль не проверяет мир, зато проверяет, что текст о мире не стал сильнее своего источника.'),
|
||
h2('Связь с официальными источниками без подмены доказательства'),
|
||
p('RFC 9110 нужен этой статье не для описания запуска. Он помогает не смешивать семантику действия с уровнем связи и напоминает, что условия автоматического повтора имеют границы. RFC 9000 показывает, что у транспорта есть самостоятельные механизмы, но ни один из них не является доказательством прикладного результата. NIST SP 800-160 задаёт официальную рамку для разговора об устойчивости, включая восстановление, однако не выдаёт future hand-off за осуществлённую практику.'),
|
||
p('Таким образом, источники поддерживают словарь и пределы интерпретации. Они не поддерживают fictional incident materials и не могут подтвердить, что в P113 существует реальный сервис, SLO, след наблюдения или восстановление. Это различие стоит написать прямо: ссылка на норму говорит о том, как точно рассуждать; evidence говорит о том, что произошло. В этой статье evidence отсутствует намеренно.'),
|
||
h2('Пределы и следующий шаг'),
|
||
p('Field-материал не создаёт ticket, очередь, owner, runbook, мониторинг, deployment, rollout или запись об incident. Он не проверяет доступность и не делает recovery, не собирает SLO/SLI и не выбирает winner. Его собственный результат не нужно «доводить до продакшена»: <code>productionEffect: not-attempted</code> — это обязательное свойство безопасного evaluator, а не временная задержка скрытой работы.'),
|
||
p('Следующий шаг узкий: при появлении отдельного авторизованного решения создать новый artefact с самостоятельной временной границей и методикой; он может закончиться также отсутствием результата. Пока такого разрешения нет, корректный hand-off не разрастается. P113 оставляет читателю один честный предмет: synthetic question о надёжности без легенды о прошлом и без обещания будущей операции.'),
|
||
], refs);
|
||
|
||
export const revisions = deepFreeze([practice, mechanism, field]);
|
||
export function verifyRevisionsAgainstFixture() {
|
||
const fixture = runFixedReliabilityFixture();
|
||
const articleChecks = revisions.map((item) => {
|
||
const text = bodyText(item.contentHtml);
|
||
return text.length >= 5000 && text.length <= 15000 && /(цен[аы]|стоимост|потер|дорог)/i.test(text.slice(0, 1200)) && /<table>/.test(item.contentHtml) && /<figure>/.test(item.contentHtml) && /<pre><code>/.test(item.contentHtml) && /<ol>/.test(item.contentHtml) && /2027-07/.test(text) && /2026-07-31/.test(text) && /productionEffect: not-attempted/.test(text);
|
||
});
|
||
return deepFreeze({ passed: fixture.passed + articleChecks.filter(Boolean).length, total: fixture.total + articleChecks.length, accepted: fixture.accepted && articleChecks.every(Boolean), fixture, articleChecks, characters: Object.fromEntries(revisions.map((item) => [item.slug, bodyText(item.contentHtml).length])) });
|
||
}
|
||
if (process.argv.includes('--verify-fixture')) {
|
||
const result = verifyRevisionsAgainstFixture();
|
||
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
||
if (!result.accepted) process.exitCode = 1;
|
||
}
|
||
if (process.argv.includes('--print-revisions')) process.stdout.write(JSON.stringify(revisions) + '\n');
|