This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
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({
|
||||
traceContext: { title: 'W3C Trace Context', url: 'https://www.w3.org/TR/2021/REC-trace-context-1-20211123/', version: 'W3C Recommendation, 23 November 2021' },
|
||||
rfc5424: { title: 'RFC 5424: The Syslog Protocol', url: 'https://www.rfc-editor.org/rfc/rfc5424', version: 'March 2009, DOI 10.17487/RFC5424' },
|
||||
nistLogs: { title: 'NIST SP 800-92: Guide to Computer Security Log Management', url: 'https://doi.org/10.6028/NIST.SP.800-92', version: 'September 2006, DOI 10.6028/NIST.SP.800-92' },
|
||||
});
|
||||
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_DIAGNOSTIC_CASES = deepFreeze({
|
||||
'planned-handoff-v1': {
|
||||
id: 'planned-handoff-v1', planDate: '2027-01', sourceCutoff: '2026-07-31',
|
||||
symptom: { id: 'route-result-mismatch', statement: 'synthetic-question-only' },
|
||||
hypothesis: { id: 'context-boundary-unmapped', statement: 'synthetic-question-only' },
|
||||
signals: [{ id: 'request-context-carrier', role: 'planned-input' }, { id: 'named-event-record', role: 'planned-input' }],
|
||||
evidence: { kind: 'synthetic-contract', strength: 'input', records: 'not-collected' },
|
||||
requestedConclusion: 'synthetic-plan-hand-off',
|
||||
boundary: 'Fixed in-memory planning literal. No report, log, trace, repro, browser, network, file, environment, clock, secret, telemetry backend, external tool, production system, customer data, investigation, measurement or incident is read, created or changed.',
|
||||
},
|
||||
'undated-scenario-v1': { id: 'undated-scenario-v1', planDate: '', sourceCutoff: '2026-07-31', symptom: { id: 'route-result-mismatch', statement: 'synthetic-question-only' }, hypothesis: { id: 'context-boundary-unmapped', statement: 'synthetic-question-only' }, signals: [{ id: 'request-context-carrier', role: 'planned-input' }], evidence: { kind: 'synthetic-contract', strength: 'input', records: 'not-collected' }, requestedConclusion: 'synthetic-plan-hand-off', boundary: 'Negative fixed literal only.' },
|
||||
'unnamed-signal-v1': { id: 'unnamed-signal-v1', planDate: '2027-01', sourceCutoff: '2026-07-31', symptom: { id: 'route-result-mismatch', statement: 'synthetic-question-only' }, hypothesis: { id: 'context-boundary-unmapped', statement: 'synthetic-question-only' }, signals: [{ id: '', role: 'planned-input' }], evidence: { kind: 'synthetic-contract', strength: 'input', records: 'not-collected' }, requestedConclusion: 'synthetic-plan-hand-off', boundary: 'Negative fixed literal only.' },
|
||||
'unnamed-hypothesis-v1': { id: 'unnamed-hypothesis-v1', planDate: '2027-01', sourceCutoff: '2026-07-31', symptom: { id: 'route-result-mismatch', statement: 'synthetic-question-only' }, hypothesis: { id: '', statement: '' }, signals: [{ id: 'request-context-carrier', role: 'planned-input' }], evidence: { kind: 'synthetic-contract', strength: 'input', records: 'not-collected' }, requestedConclusion: 'synthetic-plan-hand-off', boundary: 'Negative fixed literal only.' },
|
||||
'evidence-stronger-than-input-v1': { id: 'evidence-stronger-than-input-v1', planDate: '2027-01', sourceCutoff: '2026-07-31', symptom: { id: 'route-result-mismatch', statement: 'synthetic-question-only' }, hypothesis: { id: 'context-boundary-unmapped', statement: 'synthetic-question-only' }, signals: [{ id: 'request-context-carrier', role: 'planned-input' }], evidence: { kind: 'observed-trace', strength: 'observed', records: 'claimed-collected' }, requestedConclusion: 'synthetic-plan-hand-off', boundary: 'Negative fixed literal only.' },
|
||||
'positive-conclusion-v1': { id: 'positive-conclusion-v1', planDate: '2027-01', sourceCutoff: '2026-07-31', symptom: { id: 'route-result-mismatch', statement: 'synthetic-question-only' }, hypothesis: { id: 'context-boundary-unmapped', statement: 'synthetic-question-only' }, signals: [{ id: 'request-context-carrier', role: 'planned-input' }], evidence: { kind: 'synthetic-contract', strength: 'input', records: 'not-collected' }, requestedConclusion: 'root-cause-confirmed', boundary: 'Negative fixed literal only.' },
|
||||
});
|
||||
|
||||
export function createFixedDiagnosticCase(id = 'planned-handoff-v1') { const value = FIXED_DIAGNOSTIC_CASES[id]; return value ? deepFreeze(cloneFixed(value)) : undefined; }
|
||||
function stop(status, reason, nextAction) { return deepFreeze({ status, reason, nextAction, productionEffect: 'not-attempted' }); }
|
||||
export function assessFixedDiagnosticPlan(input) {
|
||||
if (!Object.values(FIXED_DIAGNOSTIC_CASES).some((item) => JSON.stringify(item) === JSON.stringify(input))) return stop('stop-unknown-fixed-input', 'input-is-not-a-known-named-fixed-literal', 'select-a-named-fixed-case');
|
||||
if (input.planDate !== '2027-01' || input.sourceCutoff !== '2026-07-31') return stop('stop-undated-scenario-or-cutoff', 'january-2027-plan-and-july-2026-cutoff-are-required', 'name-2027-01-and-2026-07-31');
|
||||
if (!input.symptom?.id || !input.symptom?.statement || !input.hypothesis?.id || !input.hypothesis?.statement) return stop('stop-unnamed-symptom-or-hypothesis', 'symptom-and-hypothesis-must-be-named-synthetic-questions', 'name-the-question-and-its-testable-alternative');
|
||||
if (!Array.isArray(input.signals) || input.signals.length < 1 || input.signals.some((signal) => !signal.id || signal.role !== 'planned-input')) return stop('stop-unnamed-signal', 'every-signal-must-have-a-name-and-planned-input-role', 'name-the-signal-without-claiming-a-record');
|
||||
if (input.evidence?.kind !== 'synthetic-contract' || input.evidence?.strength !== 'input' || input.evidence?.records !== 'not-collected') return stop('stop-evidence-stronger-than-input', 'a-plan-cannot-contain-observed-or-collected-evidence', 'keep-evidence-at-synthetic-input-strength');
|
||||
if (input.requestedConclusion !== 'synthetic-plan-hand-off') return stop('stop-disallowed-positive-conclusion', 'root-cause-confirmation-or-other-positive-conclusion-is-forbidden', 'use-synthetic-plan-hand-off');
|
||||
return deepFreeze({ status: 'synthetic-plan-hand-off', caseId: input.id, symptom: deepFreeze(cloneFixed(input.symptom)), hypothesis: deepFreeze(cloneFixed(input.hypothesis)), signals: deepFreeze(cloneFixed(input.signals)), evidence: deepFreeze(cloneFixed(input.evidence)), boundary: input.boundary, productionEffect: 'not-attempted', nextAction: 'give-the-fixed-question-contract-to-a-future-evidence-owner' });
|
||||
}
|
||||
export function runFixedDiagnosticFixture() {
|
||||
const expected = [['planned-handoff-v1', 'synthetic-plan-hand-off'], ['undated-scenario-v1', 'stop-undated-scenario-or-cutoff'], ['unnamed-signal-v1', 'stop-unnamed-signal'], ['unnamed-hypothesis-v1', 'stop-unnamed-symptom-or-hypothesis'], ['evidence-stronger-than-input-v1', 'stop-evidence-stronger-than-input'], ['positive-conclusion-v1', 'stop-disallowed-positive-conclusion']];
|
||||
const checks = expected.map(([id, status]) => ({ id, expected: status, actual: assessFixedDiagnosticPlan(createFixedDiagnosticCase(id)).status }));
|
||||
return deepFreeze({ passed: checks.filter((check) => check.expected === check.actual).length, total: checks.length, accepted: checks.every((check) => check.expected === check.actual) && Object.isFrozen(createFixedDiagnosticCase()) && Object.isFrozen(createFixedDiagnosticCase().signals), 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 practice = revision({
|
||||
slug: 'editorial-2027-01-practice-debugging-decade', title: 'Десять лет web-диагностики: практический маршрут', categories: ['Инженерные практики', 'Надёжность'], cover: '/assets/editorial/2027/debugging-decade-2027-evolution-timeline.svg', excerpt: 'План на январь 2027: аккуратная карта от synthetic симптома к гипотезе без придуманного расследования.', readingMinutes: 20,
|
||||
}, [
|
||||
p('План на январь 2027 с source cutoff 31.07.2026 начинается с привычной, дорогой поломки разговора: человек видит один симптом и сразу называет причину. В результате команда меняет слой, который просто оказался ближе, а не слой с проверяемой границей. Цена ошибки — часы на обратные изменения, новый шум в диагностике и более слабая память о том, какой вопрос вообще пытались решить.'),
|
||||
p('Вторая проблема — ретроспективный тон там, где будущего расследования ещё нет. У нас нет bug report, логов, trace, repro, incident или инструмента, который мог бы подтвердить маршрут. Цена такого вымысла не литературная: следующий читатель может принять удобную историю за evidence и пропустить реальную проверку. Поэтому это только план/сценарий на 2027-01; единственный положительный output — <code>synthetic-plan-hand-off</code> с <code>productionEffect: not-attempted</code>.'),
|
||||
h2('Диагностика начинается с формы вопроса'),
|
||||
p('За десять лет web-диагностики изменились интерфейсы наблюдения, форматы контекста и привычки команд. Не изменилась одна граница: симптом не равен причине. Симптом — короткое, наблюдаемое описание расхождения, например «результат маршрута не совпадает с ожидаемым вопросом». Гипотеза — отдельное объяснение, которое можно опровергнуть. Между ними нужна карта: какие сигналы вообще могли бы различить альтернативы, какой владелец вправе их собирать и что останется неизвестным после каждого шага.'),
|
||||
p('В этой статье карта намеренно synthetic. Имена <code>route-result-mismatch</code> и <code>context-boundary-unmapped</code> — fixed literals, не названия реального endpoint, сбоя или следа. Они полезны ровно тем, что не дают дорисовать детали. Хорошая карта не обещает универсальный метод: иногда первым нужен контракт API, иногда граница клиента и сервера, иногда вопрос должен быть снят как плохо поставленный. Здесь мы тренируем дисциплину связей, а не рассказываем о случившемся случае.'),
|
||||
figure('/assets/editorial/2027/debugging-decade-2027-evolution-timeline.svg', 'Временная шкала плановой карты диагностики: от симптома через именованную гипотезу и плановые сигналы к synthetic hand-off; красные стопы отмечают неназванный сигнал и чрезмерный вывод.', 'Схема показывает изменения языка и инструментов как контекст для вопроса, но не утверждает историю конкретной команды, incident или набора артефактов.'),
|
||||
table('Карта вопроса без выдуманного evidence', ['Узел', 'Что фиксируем в плане', 'Что он различает', 'Чего не разрешено говорить'], [
|
||||
['Симптом', 'id и synthetic question', 'что именно не совпадает', 'что событие наблюдалось'],
|
||||
['Гипотеза', 'id и alternative question', 'какое объяснение проверяется', 'что причина подтверждена'],
|
||||
['Сигнал', 'имя и роль planned-input', 'какой будущий input нужен', 'что лог или trace уже существует'],
|
||||
['Evidence contract', 'synthetic-contract, input', 'предел будущей передачи', 'что evidence сильнее входа'],
|
||||
['Выход', 'synthetic-plan-hand-off', 'кому передать вопрос', 'что разрешён production change'],
|
||||
]),
|
||||
h2('Сигнал — не синоним данных'),
|
||||
p('Слово «сигнал» легко маскирует незнание. В плановом record signal — это имя будущего input и его роль, а не запись в журнале. <code>request-context-carrier</code> не доказывает, что заголовок дошёл; <code>named-event-record</code> не доказывает, что событие было записано, сохранено или доступно. Такая разница скучна только пока не приходится объяснять, почему одна и та же строка кажется одновременно отсутствующим контекстом, ошибкой парсинга и задержкой доставки.'),
|
||||
p('Нужно держать минимум три границы. Первая: значение может отсутствовать, и это ещё не означает дефект. Вторая: значение может быть названо, но не иметь interpretation contract. Третья: даже корректно интерпретированное значение не доказывает гипотезу, если альтернативы не исключены. Публичные спецификации помогают говорить о переносе контекста и полях сообщения, однако они не подтверждают наш synthetic route и не назначают нам порядок расследования.'),
|
||||
h2('Гипотеза должна проигрывать честно'),
|
||||
p('У полезной гипотезы есть возможный проигрыш. «Что-то потерялось на границе» удобно звучит, но не даёт следующего действия: граница не названа, альтернативы не названы, любой исход можно объявить совместимым с фразой. <code>context-boundary-unmapped</code> уже уже: он просит будущего владельца назвать boundary и carrier, но не утверждает, что контекст когда-либо передавался. Если гипотезу нельзя сделать слабее от будущего input, это не гипотеза, а название тревоги.'),
|
||||
p('Эта строгость особенно нужна после долгой истории инструментов. Новый trace UI может дать ощущение причинности за счёт красивой связки, старый лог — ощущение простоты за счёт одной строки. Ни интерфейс, ни формат не отменяют inference boundary. Сначала формулируют, какая разница между вариантами имеет значение; затем определяют minimum input; потом отдельно решают, кому разрешено получить реальный evidence. В сценарии до последнего шага не происходит ничего, кроме проверки литерала.'),
|
||||
h2('Литеральная карта безопасного маршрута'),
|
||||
code("import { createFixedDiagnosticCase, assessFixedDiagnosticPlan } from './upgrade-2027-01.mjs';\n\nconst routeQuestion = createFixedDiagnosticCase('planned-handoff-v1');\nconst disposition = assessFixedDiagnosticPlan(routeQuestion);\nconsole.log(JSON.stringify([\n { field: 'kind', value: disposition.status },\n { field: 'effect', value: disposition.productionEffect },\n { field: 'owner-question', value: disposition.nextAction },\n]));\n// [{\"field\":\"kind\",\"value\":\"synthetic-plan-hand-off\"},{\"field\":\"effect\",\"value\":\"not-attempted\"},{\"field\":\"owner-question\",\"value\":\"give-the-fixed-question-contract-to-a-future-evidence-owner\"}]"),
|
||||
p('Пример импортирует только эту модель и получает заранее именованную структуру из памяти. Клонирование изолирует её от исходного набора, а рекурсивная заморозка сохраняет форму во время вызова. Evaluator принимает лишь известный literal; произвольный object закрывается. Код не читает сеть, browser, файлы, environment, часы, secrets, telemetry или внешний инструмент. Он не генерирует report, не собирает trace и не создаёт repro. Его положительный результат — hand-off формы вопроса, не диагноз и не разрешение менять систему.'),
|
||||
h2('Порядок подготовки карты на 2027-01'),
|
||||
ol(['Явно пометить материал как план/сценарий на 2027-01 и рядом оставить source cutoff 2026-07-31.', 'Назвать один synthetic симптом без URL, пользователя, времени, incident или правдоподобной истории.', 'Записать одну гипотезу как опровержимое объяснение и отдельно назвать альтернативу, которую она не покрывает.', 'Дать каждому будущему сигналу id и роль planned-input; не подменять имя записью или метрикой.', 'Оставить evidence на уровне <code>synthetic-contract/input/not-collected</code> и проверить fail-closed stops.', 'Передать только <code>synthetic-plan-hand-off</code> будущему владельцу evidence, не объявляя root cause, fix или production effect.']),
|
||||
h2('Карта полезна, когда сужает следующий вопрос'),
|
||||
p('В диагностике легко измерять активность вместо продвижения: добавить полей, нарисовать больше стрелок, попросить «полный trace». Карта должна выдерживать обратный вопрос: какое решение станет другим, если будущий signal окажется пустым, неоднозначным или недоступным? Если ничего не меняется, signal пока не относится к гипотезе. Его стоит убрать из contract, а не собирать на всякий случай. Так уменьшается не только объём будущих данных, но и число историй, которые можно случайно прочитать в одном и том же наборе полей.'),
|
||||
p('Есть полезная экономия и в порядке. Не надо сначала выбирать самый мощный инструмент, затем искать, что он мог бы показать. Сначала достаточно назвать smallest discriminating question: например, требуется ли вообще carrier на конкретной, пока synthetic границе. Если ответ нет, разговор о схеме записи и поиске не начинается. Если ответ да, будущий owner получает узкое требование, а не просьбу добыть всё. Это не правило для каждой системы; это способ не оплачивать наблюдаемость до того, как появился предмет различения.'),
|
||||
h2('Почему историческая тема не даёт исторических прав'),
|
||||
p('Название «десять лет» может соблазнить общими выводами: будто практика неизбежно движется от логов к trace или будто новый инструмент обязательно делает команду точнее. В источниках нет такого результата для нашего выпуска, а у редакции нет собственного корпуса case studies. Можно сказать скромнее: за период развивались стандарты и способы описывать контекст; мы используем их как словарь. Нельзя сказать, что конкретная организация прошла этот путь, быстрее находила дефекты или завершила десятилетний цикл улучшений.'),
|
||||
p('Эта разница важна для читателя в будущем. Когда он откроет номер после января, дата публикации не должна превращать preparation в report. Source cutoff сохраняет, какие документы были допустимы при подготовке, но не заменяет факт. План сохраняет вопрос, но не событие. Если появится реальный материал, его provenance, дата, авторизация и ограничения должны быть записаны отдельно. Нельзя приклеить его к fixed literal и объявить, что он всегда имелся в виду.'),
|
||||
h2('Ограничения карты и следующий шаг'),
|
||||
p('Карта не измеряет задержку, полноту, sampling, retention, privacy impact или полезность инструмента. Она не говорит, какой signal лучше, и не делает любой named signal доступным. JSON clone и freeze — свойства учебного JS literal, а не защита реальной телеметрии. RFC 5424 описывает protocol message, а W3C Trace Context — перенос trace context; ни один источник не подтверждает наш симптом, существование конкретного carrier или причинность между ними.'),
|
||||
p('Следующий шаг возможен только в будущем отдельном scope: назначить владельца evidence и определить разрешённые реальные inputs. До этого разумная работа — добавить ещё один negative fixed case и проверить точный stop. Не надо превращать этот draft в юбилейный отчёт задним числом. Десять лет здесь — тема планового выпуска, а не доказательство завершённой десятилетней ретроспективы.'),
|
||||
], [
|
||||
{ key: 'traceContext', use: 'Использован узкий факт о стандартизованном переносе trace context между границами.', boundary: 'Не подтверждает передачу, trace или причинность в synthetic сценарии.' },
|
||||
{ key: 'rfc5424', use: 'Использована терминология protocol message для отличия named signal от фактически полученной записи.', boundary: 'Не создаёт log record и не подтверждает его содержание.' },
|
||||
{ key: 'nistLogs', use: 'Использована общая дисциплина управления журналами как фон для разделения сбора и интерпретации.', boundary: 'Не является evidence о системе, incident или будущем расследовании.' },
|
||||
]);
|
||||
|
||||
const mechanism = revision({
|
||||
slug: 'editorial-2027-01-mechanism-debugging-decade', title: 'Десять лет web-диагностики: что сохраняется между логом и trace', categories: ['Архитектура', 'Наблюдаемость'], cover: '/assets/editorial/2027/debugging-decade-2027-signal-tool-limit-table.svg', excerpt: 'План на январь 2027: границы inference, которые не отменяют ни логи, ни trace.', readingMinutes: 20,
|
||||
}, [
|
||||
p('План на январь 2027 рассматривает конкретную проблему механики: лог и trace кладут рядом, после чего связь по id начинают читать как доказанную причину. Цена ошибки — ложное сужение поиска. Инженер исключает альтернативы, которых вход не исключал, и тратит время на исправление уверенного, но невыведенного объяснения.'),
|
||||
p('Есть и более тихая проблема: инструмент меняется, а сила вывода будто растёт сама. В действительности у нас нет production log, trace, trace-id, span, backend, report или реального времени. Цена выдуманного артефакта — путаница между термином из спецификации и фактом конкретной среды. Этот текст — план/сценарий на 2027-01 с source cutoff 2026-07-31; положительная ветка остаётся <code>synthetic-plan-hand-off</code>, а <code>productionEffect: not-attempted</code>.'),
|
||||
h2('Что может пережить смену инструмента'),
|
||||
p('Инструменты меняют разрешение и стоимость поиска, но не отменяют три простые обязанности inference. Нужны названный input, правило связи и альтернативное объяснение. Если нет input, нет факта для интерпретации. Если нет правила связи, совпадение идентификатора остаётся совпадением. Если нет альтернативы, любая картинка легко становится подтверждением любимой версии. Эти обязанности не универсальный метод расследования; это минимальные вопросы, которые не дают выдать удобство UI за доказательство.'),
|
||||
p('Лог обычно удобен для локального сообщения, trace — для связки работ в пределах заданного контекста. Но оба формата несут границы: запись может быть неполной, контекст — отсутствовать, а связь — не покрывать работу вне carrier. W3C Trace Context определяет формат propagation, не модель причинности. RFC 5424 описывает структуру syslog message, не полноту истории. Именно поэтому synthetic model ниже не хранит образцы этих объектов: она хранит только вопросы, которые будущий owner обязан уточнить до любой попытки вывода.'),
|
||||
figure('/assets/editorial/2027/debugging-decade-2027-signal-tool-limit-table.svg', 'Таблица сравнивает плановые роли лога, trace и ручного вопроса: каждый инструмент может дать вход, но ни один не снимает границы inference; красная колонка перечисляет запрещённые выводы.', 'Схема не содержит данных, производственных инструментов или результатов. Она отделяет терминологию источников от synthetic механизма P107.'),
|
||||
table('Инструмент и предел вывода', ['Форма input', 'Что может быть названо', 'Что ещё требуется', 'Что не следует выводить'], [
|
||||
['Логовое сообщение', 'record schema и поле', 'provenance, полнота, interpretation', 'что событие было единственным'],
|
||||
['Trace context', 'carrier и связь контекста', 'граница propagation и альтернативы', 'что связь означает причинность'],
|
||||
['Метрика', 'агрегированный вопрос', 'окно, population и расчёт', 'что известен конкретный путь'],
|
||||
['Synthetic contract', 'будущий input', 'отдельный scope и owner', 'что evidence уже собран'],
|
||||
]),
|
||||
h2('Связь не равна объяснению'),
|
||||
p('Связь полезна, когда она уменьшает число допустимых моделей, а не когда она просто выглядит правдоподобно. Даже в будущем случае общий идентификатор мог бы означать propagation, повторное использование, ошибочную корреляцию или неполное соединение двух потоков. Чтобы выбрать между ними, нужны конкретные правила и наблюдения. Их нет, поэтому fixed case запрещает evidence сильнее <code>input</code>. Значение <code>observed</code> не «почти проходит»: оно закрывается, потому что план не имеет права тайно превратиться в collection.'),
|
||||
p('Этот запрет удерживает важную асимметрию. Вход может разрешить задать вопрос, но не обязан разрешать утверждение. Выход может быть слабее входа: stop часто честнее hand-off. Выход не может быть сильнее входа без новой, явно авторизованной работы. Поэтому <code>evidence-stronger-than-input-v1</code> является негативным fixture, а не примером зрелости. Он не проверяет реальный trace; он проверяет, что слова «наблюдалось» не проникли в учебную карточку через одно поле.'),
|
||||
h2('Поле и схема должны быть разными объектами'),
|
||||
p('Схема говорит, как назвать поле; поле в конкретной записи было бы evidence. Смешать их особенно легко с документацией: прочитав normative описание carrier, автор начинает писать так, будто carrier присутствовал в его маршруте. Это неверный переход. Первичный источник полезен для терминов, версии и границы формата. Он не даёт разрешения переносить свойства specification на неизвестный deployment, browser или прокси. Наша редакционная модель добавляет статусы и fail-closed rule самостоятельно и не приписывает их стандартам.'),
|
||||
p('То же относится к старым и новым инструментам. Более удобный поиск не снимает вопрос retention; более подробная визуализация не снимает вопрос sampling; единый trace-id не снимает вопрос ownership границы. Практичный наставник не запрещает инструмент. Он спрашивает: какое конкретное решение этот input мог бы изменить и какое наблюдение оставит решение прежним? Если ответа нет, модель ещё не готова к измерению, зато готова к stop с точным следующим действием.'),
|
||||
h2('Запускаемый validator границы inference'),
|
||||
code("import { createFixedDiagnosticCase, assessFixedDiagnosticPlan } from './upgrade-2027-01.mjs';\n\nconst plan = createFixedDiagnosticCase('evidence-stronger-than-input-v1');\nconst result = assessFixedDiagnosticPlan(plan);\nconsole.log({ status: result.status, reason: result.reason });\n// { status: 'stop-evidence-stronger-than-input', reason: 'a-plan-cannot-contain-observed-or-collected-evidence' }"),
|
||||
p('Это literal runnable example. Он не открывает exporter, не создаёт span и не читает журнал. В памяти существуют только фиксированные строки. JSON clone отделяет case от константы, deep freeze делает вложенные значения неизменяемыми, а проверка JSON-равенства не принимает похожий объект. Так код демонстрирует policy: разница между «можно назвать будущий signal» и «можно сказать, что что-то наблюдалось» проверяется до того, как текст превратится в отчёт.'),
|
||||
h2('Порядок разбора механики'),
|
||||
ol(['Зафиксировать 2027-01 и source cutoff 2026-07-31; не выдавать будущую дату за свершившуюся историю.', 'Назвать symptom как вопрос, а hypothesis — как отдельное опровержимое объяснение.', 'Для каждого signal записать, является ли он будущим input, а не существующим log или trace.', 'Отделить формат и propagation rule из источника от утверждения о конкретном runtime.', 'Проверить, что evidence имеет только <code>synthetic-contract/input/not-collected</code>; observed и collected должны закрываться.', 'Отдать статус hand-off или stop будущему evidence owner, не формулируя root cause, fix, incident или production conclusion.']),
|
||||
h2('Время не возникает из порядка строк'),
|
||||
p('Ещё одна граница, которую не снимает ни лог, ни trace, — время. Порядок прихода записей не равен порядку работы; timestamp не равен общей шкале; длительность одного участка не объясняет ожидание в другом. В P107 нет timestamp вовсе, поэтому нельзя даже начать говорить о latency, race или window. Это полезная пустота. Она не позволяет подменить отсутствие temporal model красивой временной линией и не заставляет будущего owner доказывать обратное тому, чего план не должен был утверждать.'),
|
||||
p('Если будущему scope понадобится время, ему придётся отдельно назвать clock boundary, единицу, способ связывания и допустимую неопределённость. Даже после этого останется вопрос, что именно означает интервал: работа, ожидание, очередь или наблюдение collector. Такая детализация не нужна текущему literal, потому что он не сравнивает интервалы. Добавлять число только ради реалистичности было бы ошибкой того же класса, что и выдумать trace-id: syntactic detail не превращается в evidence от того, что выглядит знакомо.'),
|
||||
h2('Collector не является нейтральным окном'),
|
||||
p('Между приложением и тем, что увидит человек, обычно есть граница преобразования, отбора или хранения. Даже не описывая конкретный pipeline, полезно помнить: формат на отправителе и форма на экране могут быть разными объектами. Поэтому слова «лог сказал» или «trace показал» всегда требуют вопроса о provenance. В synthetic plan этот вопрос не решается и не маскируется названием vendor. В нём остаётся только named signal, потому что любое утверждение о collector уже было бы эмпирическим фактом без источника.'),
|
||||
p('Здесь проявляется отличие механизма от инструкции. Инструкция могла бы велеть включить экспорт или открыть консоль. Механизм описывает, почему такое действие ещё не даёт сильного вывода. Он не запрещает будущую работу, а удерживает её границы: новый input должен прийти с новой ответственностью за сбор, доступ и interpretation. Пока такой ответственности нет, precise stop лучше общего оптимизма. Это не недоверие к инструменту; это отказ давать ему полномочие, которого не передавали. Такой предел полезнее уверенного, но непроверяемого объяснения.'),
|
||||
h2('Ограничения механики и следующий шаг'),
|
||||
p('Модель не оценивает качество log schema, стоимость trace backend, доступ, персональные данные, clock skew или порядок доставки. Она не говорит, какой из инструментов выбрать, и не проверяет соответствие implementation спецификации. Она также не может определить причинность по имени гипотезы. Всё это требует реальных входов и другого полномочия. Здесь fail-closed нужен не для драматичности, а чтобы отсутствие таких входов оставалось видимым.'),
|
||||
p('Следующий шаг — будущему владельцу сформулировать отдельный evidence contract: какие input разрешены, по какому правилу они будут связаны и какой контрпример остановит вывод. Пока этого нет, не стоит расширять словарь статусов. Особенно не следует называть synthetic output «проверенным решением»: механизм проверяет только связность планового языка. Он не завершает декаду диагностики и не создаёт новый факт о web-системе.'),
|
||||
], [
|
||||
{ key: 'traceContext', use: 'Использован только факт существования стандартного формата trace-context propagation.', boundary: 'Не подтверждает trace, carrier или причинную связь конкретного сценария.' },
|
||||
{ key: 'rfc5424', use: 'Использована граница формата syslog message для различения схемы и фактической записи.', boundary: 'Не доказывает полноту, доставку или содержание журналов.' },
|
||||
{ key: 'nistLogs', use: 'Использован общий контекст log management для отделения collection от interpretation.', boundary: 'Не подтверждает incident, retention или работу конкретного инструмента.' },
|
||||
]);
|
||||
|
||||
const field = revision({
|
||||
slug: 'editorial-2027-01-field-debugging-decade', title: 'Десять лет web-диагностики: synthetic hand-off вместо кейса', categories: ['Полевые заметки', 'Инженерные решения'], cover: '/assets/editorial/2027/debugging-decade-2027-hypothesis-evidence-loop.svg', excerpt: 'План на январь 2027: как передать будущий вопрос диагностики, не сочинив полевой артефакт.', readingMinutes: 20,
|
||||
}, [
|
||||
p('Самая опасная поломка field-заметки — передать получателю фразу «причина подтверждена», когда передавать нечего кроме наброска вопроса. Цена ошибки — ложная операционная память: новый инженер защищает несуществующий report, ищет отсутствующий trace и строит дальнейшее решение на авторитете старой формулировки.'),
|
||||
p('Вторая проблема — назвать synthetic карточку реальным hand-off и добавить к ней правдоподобные время, среду, пользователя или инструмент. У нас нет исторического incident, bug report, logs, traces, repro и production-артефактов; очередь их требует, но создавать их текстом нельзя. Цена такой детализации — невозможность отличить факт от иллюстрации. Поэтому это план/сценарий на 2027-01 с source cutoff 2026-07-31 и только <code>synthetic-plan-hand-off</code> / <code>productionEffect: not-attempted</code>.'),
|
||||
h2('Field здесь — форма передачи, не отчёт с поля'),
|
||||
p('Слово field часто обещает место, следы, людей и последствия. В этом выпуске оно означает более скромную вещь: карточку, которую можно отдать будущему владельцу evidence, не добавляя evidence от себя. В карточке есть дата плана, cutoff, synthetic symptom, synthetic hypothesis, named planned signals, сила evidence и следующий вопрос. В ней нет URL, customer payload, временной линии, dashboard, tool output, идентификатора задачи или фразы о том, что кто-то уже начал работу.'),
|
||||
p('Это не уход от практики. Наоборот, честная передача экономит время будущей практики. Получатель сразу видит, что может проверить форму вопроса, вернуть stop или открыть новый scope с полномочием на реальные данные. Он не должен угадывать, какие строки были художественным сокращением. В зрелой технической речи отсутствие artefact тоже имеет provenance: <code>not-collected</code> означает не «плохо собрали», а «в данном synthetic плане сбор не происходил и не заявляется».'),
|
||||
figure('/assets/editorial/2027/debugging-decade-2027-hypothesis-evidence-loop.svg', 'Петля synthetic hand-off: симптом и гипотеза задают именованные плановые сигналы; затем либо stop при отсутствующем имени или чрезмерном evidence, либо передача вопроса будущему владельцу без production effect.', 'Диаграмма показывает только процесс проверки fixed literals. Она не изображает реальный incident, trace, log, пользователя или завершённое расследование.'),
|
||||
table('Содержимое synthetic hand-off', ['Поле', 'Допустимое fixed значение', 'Получатель может сделать', 'Получатель не должен заключать'], [
|
||||
['Дата и cutoff', '2027-01 / 2026-07-31', 'проверить временную границу', 'что выпуск уже завершён'],
|
||||
['Симптом и гипотеза', 'named synthetic questions', 'уточнить альтернативы', 'что есть root cause'],
|
||||
['Signals', 'named planned-input', 'назвать нужный future input', 'что запись доступна'],
|
||||
['Evidence', 'synthetic-contract/input/not-collected', 'открыть другой scope', 'что был сбор или анализ'],
|
||||
['Output', 'synthetic-plan-hand-off', 'принять либо вернуть contract', 'что допустимо менять production'],
|
||||
]),
|
||||
h2('Передача начинается с того, чего в ней нет'),
|
||||
p('Обычно hand-off читают как список выполненного. Здесь полезнее начать с запрещённых подмен. Нет «вчера», потому что дата плана уже названа. Нет «пользователь увидел», потому что пользователей в literal нет. Нет «trace показал», потому что trace не создавался. Нет «инструмент не нашёл», потому что инструмент не запускался. Нет «исправление помогло», потому что исправления не было. Эти отрицания не делают карточку богаче, но сохраняют у будущего reviewer возможность поставить реальный вопрос без конфликта с вымышленным прошлым.'),
|
||||
p('Полезная карточка не требует доверия к автору. Она содержит fixed literals, поэтому любой читатель может взять named case и получить тот же status. Но одинаковый status не делает контекст одинаковым с чужой системой. Детерминизм здесь локален: evaluator согласованно классифицирует словарь, а не воспроизводит world. Именно поэтому positive result не называется approval. Он лишь говорит, что в этом словаре есть дата, names и достаточная честность, чтобы передать вопрос дальше.'),
|
||||
h2('Fail-closed защищает получателя'),
|
||||
p('Отсутствующее имя сигнала — не мелкая опечатка. Без имени невозможно сказать, какой input запрашивается, а значит следующий человек получает просьбу «посмотрите что-нибудь». Отсутствующая гипотеза столь же опасна: она превращает любой signal в украшение. Сценарий без даты и cutoff лишает получателя границы источников. Эти три случая не дополняются default-значениями. Evaluator возвращает stop с точной причиной, потому что заполнять пробел удобной выдумкой означает перекладывать долг на человека, который этого не выбирал.'),
|
||||
p('Отдельный риск — evidence, сильнее входа. Слово <code>observed</code> выглядит естественно для field-ярлыка, но в данной карточке оно ложно. Если реальное наблюдение когда-нибудь появится, оно должно прийти из нового процесса с известным provenance, access и правилами интерпретации. Нельзя улучшить старый synthetic input только тем, что поменяли строку. Наконец, <code>root-cause-confirmed</code> запрещён независимо от красоты names: положительный диагноз потребовал бы доказательств, которых модель специально не содержит.'),
|
||||
h2('Литеральный hand-off и его отрицательная ветка'),
|
||||
code("import { createFixedDiagnosticCase, assessFixedDiagnosticPlan } from './upgrade-2027-01.mjs';\n\nconst plan = createFixedDiagnosticCase('positive-conclusion-v1');\nconst result = assessFixedDiagnosticPlan(plan);\nconsole.log({ status: result.status, next: result.nextAction });\n// { status: 'stop-disallowed-positive-conclusion', next: 'use-synthetic-plan-hand-off' }"),
|
||||
p('Этот example literally runs without a live system. Он читает только in-memory named literals; JSON clone исключает разделённую ссылку, deep freeze фиксирует все вложенные поля. В нём нет файлового fixture, переменной окружения, часов, сетевого вызова или API наблюдаемости. Проверяемый результат — отказ от чрезмерного вывода. Это важно для hand-off: получатель видит, что автор не оставил скрытую позитивную ветку, которая могла бы превратить план в готовое заключение.'),
|
||||
h2('Порядок передачи январского сценария'),
|
||||
ol(['Оставить на карточке явные <code>planDate: 2027-01</code> и <code>sourceCutoff: 2026-07-31</code>.', 'Назвать symptom и hypothesis как synthetic вопросы, не добавляя историю, источник или реальный эффект.', 'Проверить, что каждый signal имеет имя и <code>planned-input</code>; unnamed signal вернуть с stop.', 'Оставить evidence только <code>synthetic-contract/input/not-collected</code>; observed, collected и stronger claims закрыть.', 'Проверить, что conclusion равен только <code>synthetic-plan-hand-off</code> и effect всегда <code>not-attempted</code>.', 'Передать boundary и nextAction будущему evidence owner как вопрос, не как queue task, approval или incident conclusion.']),
|
||||
h2('Получатель должен суметь не согласиться'),
|
||||
p('Хорошая передача не заставляет получателя принять исходную framing. Он должен суметь вернуть карточку с фразой: symptom слишком широкий, гипотеза не различает альтернативы, signal не имеет владельца, а source cutoff не подходит новому вопросу. Такое несогласие — не отказ от hand-off, а его штатный результат. Если карточка принимает только одобрение, она становится маленькой очередью с замаскированным приоритетом. P107 специально не создаёт queue и не назначает срок, чтобы не выдать организационное действие за техническую проверку.'),
|
||||
p('По этой же причине nextAction сформулирован как передача fixed question contract, а не как «соберите данные». Последняя фраза предполагает полномочие, доступ и цель сбора. Первая сохраняет выбор у будущего owner. Он может признать, что evidence не нужен, или открыть новый authorized scope, или запросить другой model. Synthetic hand-off не теряет ценность от такого результата: он честно ограничивает то, что было известно на дату подготовки, и даёт получателю опору, не создавая несуществующей обязанности.'),
|
||||
h2('Как карточка стареет без подмены фактов'),
|
||||
p('После source cutoff документы, термины и реализации могут измениться. Это не делает старую карточку неправильной, но меняет её статус: она остаётся записью о подготовленном вопросе, не справочником о текущей системе. Будущий reviewer должен обновить источники в новом scope, а не молча приписать свежие сведения январскому плану. Точно так же нельзя задним числом заменить <code>not-collected</code> на «наблюдалось», даже если кто-то позднее действительно собрал материал. Это были бы разные artefacts с разной датой и provenance.'),
|
||||
p('Долговечность hand-off достигается не количеством контекста, а явными границами. Полезно, когда через год можно увидеть, почему statement был слабым, какие слова нельзя было использовать и где требовалась новая авторизация. Вредно, когда старая карточка выглядит настолько конкретно, что её принимают за архив. Поэтому в поле нет правдоподобных названий проектов, систем и инцидентов. Отсутствие таких деталей — не пробел редактора, а часть safety contract для читателя, который появится позже.'),
|
||||
h2('Ограничения hand-off и следующий шаг'),
|
||||
p('Карточка не назначает владельца, не создаёт ticket, не отправляет уведомление и не меняет очередь. Она не оценивает безопасность, приватность, доступность данных, retention, бюджет или пригодность существующих инструментов. Она также не покрывает historical reports/artifacts, которых у P107 нет. Технические источники закрепляют значение терминов, но не заполняют этот дефицит. Называть absence «архивом» было бы той же подменой, что и сочинить incident.'),
|
||||
p('Следующий шаг — будущему владельцу решить, нужен ли отдельный authorized discovery и какие реальные evidence допустимы. Если такого решения нет, hand-off остаётся конечным безопасным результатом. Для самого draft следующий шаг проще: добавить новый negative literal, например с неназванным symptom, и сохранить fail-closed ответ. Не усиливайте этот текст retrospective тоном. Январь 2027 ещё впереди относительно редакторской даты, а десятилетие здесь — только тема заранее подготовленного номера.'),
|
||||
], [
|
||||
{ key: 'traceContext', use: 'Использована только терминологическая граница propagation context для будущего evidence contract.', boundary: 'Не создаёт trace, incident или доступ к какому-либо carrier.' },
|
||||
{ key: 'rfc5424', use: 'Использована граница protocol message как напоминание, что schema не равна фактической записи.', boundary: 'Не подтверждает logs, reports или historical artifacts.' },
|
||||
{ key: 'nistLogs', use: 'Использована общая дисциплина provenance журналов как причина не выдумывать их в hand-off.', boundary: 'Не доказывает существование процесса сбора или расследования.' },
|
||||
]);
|
||||
|
||||
export const revisions = deepFreeze([practice, mechanism, field]);
|
||||
export function verifyRevisionsAgainstFixture() {
|
||||
const fixture = runFixedDiagnosticFixture();
|
||||
const articleChecks = revisions.map((revisionItem) => { const text = bodyText(revisionItem.contentHtml); return text.length >= 5000 && text.length <= 15000 && /(цен[аы]|стоимост|издержк|потер)/i.test(text.slice(0, 1000)) && /<table>/.test(revisionItem.contentHtml) && /<figure>/.test(revisionItem.contentHtml) && /<pre><code>/.test(revisionItem.contentHtml) && /<ol>/.test(revisionItem.contentHtml) && /2027-01/.test(text) && /2026-07-31/.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((revisionItem) => [revisionItem.slug, bodyText(revisionItem.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');
|
||||
Reference in New Issue
Block a user