rewrite 2026-09 and 2027 articles for reader-facing quality
Build and deploy / deploy (push) Successful in 18s

This commit is contained in:
2026-07-31 22:26:56 +03:00
parent 3bfc3f21c4
commit 440c8721dc
69 changed files with 4424 additions and 3533 deletions
+274 -138
View File
@@ -6,155 +6,291 @@ const ol = (items) => `<ol>${items.map((item) => `<li>${item}</li>`).join('')}</
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]*$/, '')); }
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]*$/, ''));
}
const REFERENCES = deepFreeze({
provDm: { title: 'W3C PROV-DM: The PROV Data Model', url: 'https://www.w3.org/TR/2013/REC-prov-dm-20130430/', version: 'W3C Recommendation, 30 April 2013, dated immutable publication' },
c2pa13: { title: 'C2PA Technical Specification 1.3', url: 'https://spec.c2pa.org/specifications/specifications/1.3/specs/C2PA_Specification.html', version: 'C2PA Technical Specification 1.3, April 2023, version-pinned 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_INTERVIEW_LITERALS = deepFreeze({
'long-form-interview-plan-v1': { id: 'long-form-interview-plan-v1', editorialDate: '2026-07-31', planningIssue: '2027-10', sourceCutoff: '2026-07-31', assertion: { id: 'synthetic-assertion-contract-v1', state: 'named-synthetic-only' }, evidence: { claim: 'not-collected', source: 'not-collected', provenance: 'not-collected', independentConfirmation: 'not-collected', falsifiability: 'not-tested' }, record: { interview: 'not-created', transcript: 'not-created', speaker: 'not-named', quote: 'not-created', code: 'not-created', metric: 'not-collected', approval: 'not-requested', review: 'not-created', publication: 'not-attempted', ownership: 'not-assigned' }, requestedOutput: 'synthetic-plan-hand-off' },
'long-form-interview-undated-v1': { id: 'long-form-interview-undated-v1', editorialDate: '', planningIssue: '2027-10', sourceCutoff: '2026-07-31', assertion: { id: 'synthetic-assertion-contract-v1', state: 'named-synthetic-only' }, evidence: { claim: 'not-collected', source: 'not-collected', provenance: 'not-collected', independentConfirmation: 'not-collected', falsifiability: 'not-tested' }, record: { interview: 'not-created', transcript: 'not-created', speaker: 'not-named', quote: 'not-created', code: 'not-created', metric: 'not-collected', approval: 'not-requested', review: 'not-created', publication: 'not-attempted', ownership: 'not-assigned' }, requestedOutput: 'synthetic-plan-hand-off' },
'long-form-interview-unnamed-claim-v1': { id: 'long-form-interview-unnamed-claim-v1', editorialDate: '2026-07-31', planningIssue: '2027-10', sourceCutoff: '2026-07-31', assertion: { id: '', state: 'implicit' }, evidence: { claim: 'not-collected', source: 'not-collected', provenance: 'not-collected', independentConfirmation: 'not-collected', falsifiability: 'not-tested' }, record: { interview: 'not-created', transcript: 'not-created', speaker: 'not-named', quote: 'not-created', code: 'not-created', metric: 'not-collected', approval: 'not-requested', review: 'not-created', publication: 'not-attempted', ownership: 'not-assigned' }, requestedOutput: 'synthetic-plan-hand-off' },
'long-form-interview-hidden-source-v1': { id: 'long-form-interview-hidden-source-v1', editorialDate: '2026-07-31', planningIssue: '2027-10', sourceCutoff: '2026-07-31', assertion: { id: 'synthetic-assertion-contract-v1', state: 'named-synthetic-only' }, evidence: { claim: 'not-collected', source: 'hidden', provenance: 'not-collected', independentConfirmation: 'not-collected', falsifiability: 'not-tested' }, record: { interview: 'not-created', transcript: 'not-created', speaker: 'not-named', quote: 'not-created', code: 'not-created', metric: 'not-collected', approval: 'not-requested', review: 'not-created', publication: 'not-attempted', ownership: 'not-assigned' }, requestedOutput: 'synthetic-plan-hand-off' },
'long-form-interview-claimed-evidence-v1': { id: 'long-form-interview-claimed-evidence-v1', editorialDate: '2026-07-31', planningIssue: '2027-10', sourceCutoff: '2026-07-31', assertion: { id: 'synthetic-assertion-contract-v1', state: 'named-synthetic-only' }, evidence: { claim: 'claimed', source: 'not-collected', provenance: 'not-collected', independentConfirmation: 'not-collected', falsifiability: 'not-tested' }, record: { interview: 'not-created', transcript: 'not-created', speaker: 'not-named', quote: 'not-created', code: 'not-created', metric: 'not-collected', approval: 'not-requested', review: 'not-created', publication: 'not-attempted', ownership: 'not-assigned' }, requestedOutput: 'synthetic-plan-hand-off' },
'long-form-interview-fabricated-record-v1': { id: 'long-form-interview-fabricated-record-v1', editorialDate: '2026-07-31', planningIssue: '2027-10', sourceCutoff: '2026-07-31', assertion: { id: 'synthetic-assertion-contract-v1', state: 'named-synthetic-only' }, evidence: { claim: 'not-collected', source: 'not-collected', provenance: 'not-collected', independentConfirmation: 'not-collected', falsifiability: 'not-tested' }, record: { interview: 'claimed', transcript: 'claimed', speaker: 'named', quote: 'claimed', code: 'claimed', metric: 'claimed', approval: 'claimed', review: 'claimed', publication: 'claimed', ownership: 'assigned' }, requestedOutput: 'synthetic-plan-hand-off' },
'long-form-interview-disallowed-result-v1': { id: 'long-form-interview-disallowed-result-v1', editorialDate: '2026-07-31', planningIssue: '2027-10', sourceCutoff: '2026-07-31', assertion: { id: 'synthetic-assertion-contract-v1', state: 'named-synthetic-only' }, evidence: { claim: 'not-collected', source: 'not-collected', provenance: 'not-collected', independentConfirmation: 'not-collected', falsifiability: 'not-tested' }, record: { interview: 'not-created', transcript: 'not-created', speaker: 'not-named', quote: 'not-created', code: 'not-created', metric: 'not-collected', approval: 'not-requested', review: 'not-created', publication: 'not-attempted', ownership: 'not-assigned' }, requestedOutput: 'publication-approved' },
http: { title: 'RFC 9110 — HTTP Semantics', url: 'https://www.rfc-editor.org/rfc/rfc9110.html', version: 'IETF, июнь 2022 года, RFC 9110, Standards Track' },
tls: { title: 'RFC 8446 — The Transport Layer Security (TLS) Protocol Version 1.3', url: 'https://www.rfc-editor.org/rfc/rfc8446.html', version: 'IETF, август 2018 года, RFC 8446, Standards Track' },
pkix: { title: 'RFC 5280 — Internet X.509 Public Key Infrastructure Certificate and CRL Profile', url: 'https://www.rfc-editor.org/rfc/rfc5280.html', version: 'IETF, май 2008 года, RFC 5280, Standards Track' },
trace: { title: 'W3C Trace Context Level 1', url: 'https://www.w3.org/TR/2021/REC-trace-context-1-20211123/', version: 'W3C Recommendation, 23 ноября 2021 года, Trace Context Level 1' },
});
export function createLongFormInterviewLiteral(id = 'long-form-interview-plan-v1') { const value = FIXED_INTERVIEW_LITERALS[id]; return value ? deepFreeze(cloneFixed(value)) : undefined; }
function stop(status, reason, nextAction) { return deepFreeze({ status, reason, nextAction, productionEffect: 'not-attempted' }); }
export function assessLongFormInterviewPlan(input) {
const known = Object.values(FIXED_INTERVIEW_LITERALS).some((item) => JSON.stringify(item) === JSON.stringify(input));
if (!known) return stop('stop-unknown-fixed-literal', 'input-is-not-a-known-named-fixed-literal', 'select-a-named-fixed-literal');
if (input.editorialDate !== '2026-07-31' || input.planningIssue !== '2027-10' || input.sourceCutoff !== '2026-07-31') return stop('stop-temporal-boundary-required', 'editorial-date-planning-issue-and-source-cutoff-must-be-exact', 'restore-the-fixed-temporal-boundary');
if (input.assertion.id !== 'synthetic-assertion-contract-v1' || input.assertion.state !== 'named-synthetic-only') return stop('stop-unnamed-claim', 'assertion-must-be-a-named-fixed-synthetic-literal', 'restore-the-named-assertion-contract');
if (input.evidence.source !== 'not-collected') return stop('stop-hidden-source', 'source-cannot-be-hidden-or-inferred', 'keep-source-not-collected');
if (input.evidence.claim !== 'not-collected' || input.evidence.provenance !== 'not-collected' || input.evidence.independentConfirmation !== 'not-collected' || input.evidence.falsifiability !== 'not-tested') return stop('stop-claimed-evidence-or-confirmation', 'claim-provenance-confirmation-and-falsifiability-are-not-collected', 'remove-the-claimed-evidence');
if (Object.values(input.record).some((value) => !['not-created', 'not-named', 'not-collected', 'not-requested', 'not-attempted', 'not-assigned'].includes(value))) return stop('stop-fabricated-interview-record', 'interview-transcript-speaker-quote-code-metric-approval-review-publication-and-owner-cannot-be-created', 'restore-uncreated-record-states');
if (input.requestedOutput !== 'synthetic-plan-hand-off') return stop('stop-disallowed-positive-result', 'future-plan-cannot-claim-approval-publication-or-outcome', 'use-synthetic-plan-hand-off');
return deepFreeze({ status: 'synthetic-plan-hand-off', literalId: input.id, planningIssue: input.planningIssue, assertion: deepFreeze(cloneFixed(input.assertion)), evidence: deepFreeze(cloneFixed(input.evidence)), record: deepFreeze(cloneFixed(input.record)), productionEffect: 'not-attempted', nextAction: 'open-a-separate-authorized-scope-only-if-a-real-claim-needs-evidence' });
}
export function inspectAssertionContractLiteral() { const input = createLongFormInterviewLiteral(); const output = assessLongFormInterviewPlan(input); return deepFreeze({ assertion: input.assertion.id, claim: input.evidence.claim, status: output.status, productionEffect: output.productionEffect }); }
export function inspectEvidenceBoundaryLiteral() { const input = createLongFormInterviewLiteral(); return deepFreeze({ provenance: input.evidence.provenance, confirmation: input.evidence.independentConfirmation, falsifiability: input.evidence.falsifiability, status: assessLongFormInterviewPlan(input).status }); }
export function inspectEditorialHandoffLiteral() { const output = assessLongFormInterviewPlan(createLongFormInterviewLiteral()); return deepFreeze({ transcript: output.record.transcript, speaker: output.record.speaker, publication: output.record.publication, status: output.status }); }
export function runLongFormInterviewFixture() {
const expected = [['long-form-interview-plan-v1', 'synthetic-plan-hand-off'], ['long-form-interview-undated-v1', 'stop-temporal-boundary-required'], ['long-form-interview-unnamed-claim-v1', 'stop-unnamed-claim'], ['long-form-interview-hidden-source-v1', 'stop-hidden-source'], ['long-form-interview-claimed-evidence-v1', 'stop-claimed-evidence-or-confirmation'], ['long-form-interview-fabricated-record-v1', 'stop-fabricated-interview-record'], ['long-form-interview-disallowed-result-v1', 'stop-disallowed-positive-result']];
const checks = expected.map(([id, status]) => ({ id, expected: status, actual: assessLongFormInterviewPlan(createLongFormInterviewLiteral(id)).status }));
const sample = createLongFormInterviewLiteral();
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.assertion) && Object.isFrozen(sample.evidence) && Object.isFrozen(sample.record), checks: deepFreeze(checks) });
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>`;
}
function revision(meta, parts, referenceEntries) { const contentHtml = parts.join('') + h2('Проверяемые источники') + sources(referenceEntries); const proseLength = bodyText(contentHtml).length; if (proseLength < 10000 || proseLength > 13000) throw new Error(`${meta.slug}: body length ${proseLength}`); return deepFreeze({ ...meta, contentHtml, proseLength }); }
const refs = [
{ key: 'provDm', use: 'Даёт термины entity, activity, derivation и provenance для отделения утверждения от его происхождения.', boundary: 'Не подтверждает интервью, участника, высказывание, исходный код, метрику, review или будущую публикацию.' },
{ key: 'c2pa13', use: 'Даёт vocabulary assertions, provenance data, validation и distinction between verified association and value judgement.', boundary: 'Не делает synthetic literal свидетельством и не подтверждает правдивость, approval, ownership или outcome будущего материала.' },
export function allocateTimeoutBudget({ totalMs, dnsMs = 0, tlsMs = 0, requestMs = 0 }) {
if (![totalMs, dnsMs, tlsMs, requestMs].every((value) => Number.isFinite(value) && value >= 0)) return { ok: false, reason: 'durations-must-be-non-negative' };
const spentMs = dnsMs + tlsMs + requestMs;
return { ok: spentMs <= totalMs, spentMs, remainingMs: Math.max(0, totalMs - spentMs), reason: spentMs <= totalMs ? 'within-deadline' : 'deadline-exceeded' };
}
export function validateCertificateWindow({ host, sans = [], notBefore, notAfter, now }) {
if (typeof host !== 'string' || !Array.isArray(sans) || !notBefore || !notAfter || !now) return { ok: false, reason: 'certificate-fields-missing' };
const current = Date.parse(now);
const start = Date.parse(notBefore);
const end = Date.parse(notAfter);
if (![current, start, end].every(Number.isFinite)) return { ok: false, reason: 'certificate-date-invalid' };
if (current < start || current > end) return { ok: false, reason: 'certificate-outside-validity-window' };
if (!sans.includes(host)) return { ok: false, reason: 'host-not-listed-in-san' };
return { ok: true, reason: 'certificate-window-and-san-match' };
}
function isLowerHex(value, length) { return typeof value === 'string' && value.length === length && /^[0-9a-f]+$/.test(value); }
export function parseTraceparent(header) {
if (typeof header !== 'string') return { ok: false, reason: 'traceparent-must-be-string' };
const parts = header.split('-');
if (parts.length !== 4) return { ok: false, reason: 'traceparent-must-have-four-fields' };
const [version, traceId, parentId, flags] = parts;
if (!isLowerHex(version, 2) || version === 'ff') return { ok: false, reason: 'version-invalid' };
if (!isLowerHex(traceId, 32) || /^0+$/.test(traceId)) return { ok: false, reason: 'trace-id-invalid' };
if (!isLowerHex(parentId, 16) || /^0+$/.test(parentId)) return { ok: false, reason: 'parent-id-invalid' };
if (!isLowerHex(flags, 2)) return { ok: false, reason: 'flags-invalid' };
return { ok: true, version, traceId, parentId, flags };
}
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 httpRefs = [
{ key: 'http', use: 'Определяет границы HTTP-запроса, ответа и ошибки обмена, на которые опирается разбор задержки.', boundary: 'Не измеряет DNS, TLS или работу конкретного клиента; эти интервалы нужно наблюдать отдельно.' },
{ key: 'tls', use: 'Описывает handshake TLS 1.3 и позволяет считать установление защищённого соединения отдельной фазой.', boundary: 'Не задаёт таймауты приложения, повторные попытки и настройки конкретной TLS-библиотеки.' },
];
const practice = revision({ slug: 'editorial-2027-10-practice-long-form-interview', title: 'Длинное техническое интервью: контракт утверждения до вопроса', categories: ['Интервью', 'Разработка'], cover: '/assets/editorial/2027/long-form-interview-2027-claim-evidence-map.svg', excerpt: 'План на октябрь 2027: как до вопроса зафиксировать, что именно можно утверждать, чем это проверяется и что остаётся неизвестным.', readingMinutes: 27 }, [
p('P116 — редакционный сценарий на октябрь 2027, составленный 2026-07-31. Длинная техническая беседа опасна не длиной, а первым неразмеченным утверждением. Вопрос уже может нести вывод о причине, решении или результате, хотя ни источник, ни граница ответа не определены. Цена ошибки проста: будущая редактура получает гладкую историю и начинает считать её evidence, хотя в пакете есть только план вопроса.'),
p('Вторая цена появляется, когда question list называют подготовленным интервью. Здесь нет actual interview, transcript, speaker, quote, code, metric, approval, review, ownership, publication или production effect. Нет и скрытого substitute: незаполненная строка не превращается в наблюдение. Единственный положительный evaluator output — <code>synthetic-plan-hand-off</code> с <code>productionEffect: not-attempted</code>. Всякая другая ветка должна остановиться, а не стать осторожной формулировкой.'),
h2('Начинаем не с вопроса, а с типа утверждения'),
p('Практика для длинного формата начинается с короткой карточки assertion. Она не просит кого-либо отвечать и не описывает предполагаемый ответ. Карточка называет только форму будущего вопроса: identifier, предмет проверки, допустимый класс evidence, provenance state и condition, при котором фраза не должна попасть в текст. В P116 эта карточка называется <code>synthetic-assertion-contract-v1</code>. Имя нарочно не похоже на технологию, продукт или человека.'),
p('Такой порядок кажется медленным, пока не приходится чистить материал после красивого разговора. Если сначала написать вопрос о выборе технологии, в нём легко спрятать, что выбор был, что он имел альтернативы и что у него был известный эффект. Контракт разворачивает эти три допущения в независимые поля. До evidence они получают не удобный default, а <code>not-collected</code>. До отдельного scope даже falsifiability остаётся <code>not-tested</code>: мы не знаем, какой внешний факт сможет опровергнуть будущий ответ.'),
table('Контракт утверждения для P116', ['Поле', 'Допустимый literal', 'Что оно не разрешает'], [['Assertion', '<code>synthetic-assertion-contract-v1</code>', 'Реальный вопрос или известную тему'], ['Claim', '<code>not-collected</code>', 'Вывод о причине, выборе или результате'], ['Source и provenance', '<code>not-collected</code>', 'Ссылку на скрытый документ или память'], ['Independent confirmation', '<code>not-collected</code>', 'Независимую вторую проверку'], ['Falsifiability', '<code>not-tested</code>', 'Критерий, что будущая фраза уже выдержала проверку']]),
p('Здесь важен не словарь, а направление вывода. Claim — это то, что текст хотел бы сказать. Evidence — то, что могло бы поддержать claim. Provenance — откуда evidence произошло и какая activity его породила. Independent confirmation — отдельная проверка, которая не копирует тот же неизвестный источник. Falsifiability — заранее названное условие, при котором claim придётся снять или сузить. Эти слова не взаимозаменяемы: два одинаковых пересказа не становятся независимыми только потому, что лежат в разных абзацах.'),
p('У длинного интервью есть соблазн заменить контракт умной формулировкой. Например, написать вопрос так, чтобы он уже намекал на ответ. Для plan/scenario это неверная оптимизация. Чем конкретнее такой намёк, тем выше шанс, что редактор унаследует его как факт. Поэтому карточка не хранит домен, команду, дату события, канал, автора или цифру. Она хранит только named synthetic literal и отрицательные состояния. Нужная конкретика может появиться позже, но только вместе с новым разрешением и собственным происхождением.'),
figure('/assets/editorial/2027/long-form-interview-2027-claim-evidence-map.svg', 'Схема: контракт утверждения проходит через source, provenance, independent confirmation и falsifiability; при отсутствии любого статуса сохраняется остановка, а не текстовый вывод.', 'Карта показывает форму редакционной проверки P116. Это не карта реального интервью и не список полученных доказательств.'),
h2('Runnable example: literal не становится разговором'),
p('Пример ниже можно выполнить локально. Он импортирует только этот module, создаёт копию named fixed literal и запускает evaluator. Он не касается диска, соединений, переменных процесса, текущего времени, секретов, телеметрии, системных интерфейсов или внешних данных. Он не создаёт interview, не отправляет вопрос, не запрашивает approval и не публикует текст. PASS означает ровно одно: in-memory shape соответствует запрету на выдуманный narrative.'),
code("import { inspectAssertionContractLiteral } from './scripts/upgrade-2027-10.mjs';\n\nconst result = inspectAssertionContractLiteral();\nconsole.log(result);\n// { assertion: 'synthetic-assertion-contract-v1',\n// claim: 'not-collected',\n// status: 'synthetic-plan-hand-off',\n// productionEffect: 'not-attempted' }"),
p('Полезна и отрицательная проверка. Literal с пустым identifier не трактуется как вопрос, который можно уточнить позднее. Literal с <code>source: hidden</code> не получает мягкий warning. Literal с <code>claim: claimed</code> не принимается как черновик. В закрытом evaluator нет режима «почти верно»: неизвестный object тоже возвращает stop-status. Такая жёсткость нужна потому, что длинный формат сильнее обычной заметки создаёт доверие к связному рассказу.'),
h2('Как собрать question contract без подмены факта'),
p('Сначала отделите предмет от предположения. Предметом может быть область будущего разбора: граница решения, порядок проверки, ограничение подхода. Предположение звучит иначе: будто выбор уже был сделан, проблема наблюдалась или последствие измерено. В P116 предположение не переписывают более осторожным словом; его убирают. Вопрос остаётся вопросом только пока не содержит готового факта внутри синтаксиса.'),
p('Затем назовите минимальный evidence class, но не придумывайте экземпляр. Для statement о поведении кода классом мог бы быть versioned artifact и воспроизводимая проверка. Для statement о редакционном решении — явно созданная запись с датой, scope и полем происхождения. Это не означает, что такие артефакты существуют для P116. Наоборот, отсутствие экземпляра фиксируется отдельно. План не имеет права создать source, потому что красивому тексту нужен источник.'),
p('Третий шаг — описать stop condition прежде, чем придумывать follow-up. Если origin не виден, нельзя делать attribution. Если один и тот же origin пересказан дважды, нельзя называть это independent confirmation. Если нет условия опровержения, нельзя повышать уверенность от повторения. Эти правила не делают будущий материал беднее. Они сокращают ложные ветви до того, как редактор начнёт защищать их плотностью текста или именем предполагаемого собеседника.'),
h2('Карточка должна пережить смену формата'),
p('Question contract полезен и тогда, когда будущий формат перестанет быть длинным. Один и тот же assertion может попасть в короткую заметку, список тем, внутренний комментарий или вообще не попасть никуда. Если карточка содержит готовую драматургию, она тянет этот narrative за собой. Если содержит только claim boundary, она остаётся переносимой. Поэтому в ней нельзя хранить предполагаемый порядок реплик, настроение, мотивацию или обещанный финал. Эти детали принадлежат материалу, который пока не существует.'),
p('Проверяйте гранулярность отдельно. Формула «почему выбрали подход» слишком широка: она смешивает факт выбора, альтернативы, критерий и последствия. Формула «какой artefact мог бы различить два описания границы» уже указывает на техническое действие, но не предполагает его ответ. Сужение вопроса не должно незаметно стать сужением ответа. Когда это различие не фиксируют, редактор легко подменяет будущую проверку собственным знанием о типичном проекте.'),
p('Для каждого поля полезно задать запрещённую операцию. Claim нельзя повысить до confidence score. Source нельзя получить из свободного текста. Provenance нельзя заменить URL на общую документацию. Confirmation нельзя засчитать по совпадению слов. Falsifier нельзя назвать после того, как narrative уже выбран. Такой список звучит как защита от очевидного, но именно эти подстановки обычно не видны в больших черновиках: каждое решение выглядит локальным, а итог становится уверенной историей без опоры.'),
p('Контракт также защищает автора от ложной обязанности продолжать. После создания fixed literal не нужно изображать, что следующий вопрос уже сформирован лучше чужого. P116 не сравнивает варианты и не оценивает редакторскую зрелость. Он всего лишь сохраняет право остановиться на вопросе. В наставническом тоне это важная практическая граница: хорошая подготовка иногда состоит в том, чтобы не превращать неполный вход в работу для других людей.'),
h2('Порядок действий'),
ol(['Записать editor date <code>2026-07-31</code>, planDate <code>2027-10</code> и source cutoff <code>2026-07-31</code>; не выводить их из текущего времени.', 'Создать только named assertion literal; не добавлять тему, участника, quote, цифру, код или предполагаемое решение.', 'Для claim, source, provenance и independent confirmation оставить <code>not-collected</code>; для falsifiability — <code>not-tested</code>.', 'Запустить fixture и проверить, что unnamed claim, hidden source и claimed evidence получают stop-status.', 'Проверить, что будущий текст называет план вопроса, а не actual interview или готовый материал.', 'Открывать другой scope лишь когда конкретному утверждению действительно требуется разрешённое evidence.']),
h2('Где контракт намеренно слаб'),
p('Контракт не оценивает качество будущего вопроса, не назначает интервьюера и не выбирает площадку. Он не умеет решить, будет ли у будущего материала ценность для читателя. Это ограничения, а не дефекты: попытка решить их сейчас создала бы ownership, approval или production narrative без полномочий. In-memory fixture также не проверяет документы, записи, людей или внешние системы. Он проверяет только то, что программа отказалась выдавать план за свидетельство.'),
p('Контракт не делает вопрос безопасным сам по себе. Реальная работа может потребовать согласия, проверки доступа, редакционного решения, хранения материалов и отдельного review. Ничего из этого не существует в P116 и ничего из этого нельзя достроить из статуса <code>not-collected</code>. Сильная сторона планового документа — не полнота, а возможность честно показать, какой вход пока отсутствует. Поэтому visual и table здесь тоже описывают форму контроля, а не обстоятельства разговора.'),
h2('Следующий шаг'),
p('Если будущая задача захочет написать один claim, она должна открыть отдельный authorized scope и принести собственные temporal boundary, source policy, provenance record, method независимого подтверждения и условие опровержения. До такого решения P116 заканчивается без интервью, без transcript и без опубликованного результата. Его единственный допустимый итог — <code>synthetic-plan-hand-off</code> с <code>productionEffect: not-attempted</code>.')
], refs);
const practice = revision({
slug: 'editorial-2027-10-practice-long-form-interview',
title: 'HTTP-таймауты: как разложить общий deadline на измеримые фазы',
categories: ['HTTP', 'Надёжность'],
cover: '/assets/editorial/2027/long-form-interview-2027-claim-evidence-map.svg',
excerpt: 'Разбираем timeout по фазам DNS, TCP, TLS, отправки и чтения, чтобы не лечить один общий симптом неправильным retry.',
readingMinutes: 15,
}, [
p('Проблема выглядит просто: запрос к API иногда отвечает за 900 мс, а клиент прекращает ждать через 800 мс. Цена ошибки — пользователь получает «сервис недоступен», хотя сервер мог завершить операцию; либо клиент повторяет запись, не зная, успел ли первый запрос попасть в обработчик. Один общий timeout не объясняет, где потрачено время.'),
p('Причина — смешать connect timeout, TLS handshake, отправку тела, ожидание первого байта и чтение ответа в одну настройку. При этом разные библиотеки называют поля по-разному, а повторная попытка получает новый полный бюджет. Решение начинается с единого deadline запроса и наблюдаемого разложения этого бюджета по фазам.'),
h2('Deadline — это граница операции, а не число для каждой фазы'),
p('Общий deadline отвечает на вопрос: до какого момента результат операции имеет смысл для вызывающего кода. Фазовые таймауты отвечают на другой вопрос: сколько можно ждать конкретный переход. Если каждому этапу дать по 800 мс, цепочка из DNS, TCP, TLS и чтения незаметно превращается в несколько секунд. Если выставить connect в 50 мс для сети с редким холодным DNS, ошибка будет ложной.'),
p('Хорошая модель хранит абсолютный момент окончания или остаток бюджета, а не только независимые таймеры. Перед началом каждой фазы вычисляется <code>remaining = deadline - now</code>. Если остатка нет, библиотека должна завершить операцию до сетевого вызова. Это предотвращает ситуацию, в которой чтение получает ещё 300 мс после того, как общий deadline уже истёк.'),
table('Фазы HTTP-запроса и сигналы', ['Фаза', 'Что измеряем', 'Типичный симптом', 'Действие'], [
['DNS', 'время разрешения имени', 'первый запрос медленный', 'кэш, resolver и лимит DNS'],
['TCP connect', 'установление соединения', 'ошибка до TLS', 'маршрут, pool, connect timeout'],
['TLS handshake', 'обмен и проверка сертификата', 'connect есть, ответа нет', 'цепочка, crypto, TLS budget'],
['Request write', 'отправка заголовков и тела', 'upload зависает', 'размер тела, backpressure'],
['TTFB/read', 'ожидание ответа и чтение', 'сервер медленный', 'server time, read timeout, payload'],
]),
h2('Почему повторная попытка опасна'),
p('Timeout не сообщает, что сервер ничего не сделал. Для GET повтор часто безопаснее при корректной семантике ресурса, но для POST это зависит от идемпотентности операции и ключа дедупликации. Если клиент запускает повтор до окончания исходного запроса, два обработчика могут изменить состояние. Поэтому deadline должен попадать в журнал вместе с методом, идентификатором операции и фазой остановки.'),
p('Вторая ловушка — retry с тем же самым полным бюджетом. При трёх попытках по 800 мс пользователь ждёт 2,4 секунды плюс задержки, а upstream получает утроенную нагрузку. Бюджет можно делить между попытками, но сначала нужно доказать, что повтор допустим. Для записи полезнее короткий результат «неизвестно, завершилось ли» и проверка статуса операции, чем агрессивная отправка.'),
figure('/assets/editorial/2027/long-form-interview-2027-claim-evidence-map.svg', 'Карта HTTP-запроса: общий deadline проходит через DNS, TCP, TLS и чтение; каждая фаза получает только оставшееся время и собственный сигнал.', 'Схема отделяет бюджет операции от отдельных измерений. Ошибка фазы не должна превращаться в общий диагноз без соответствующего сигнала.'),
h2('Runnable-пример: проверяем бюджет без сети'),
p('Функция ниже получает общий бюджет и длительности уже измеренных фаз. Она возвращает сумму, остаток и причину. Это учебный пример: он не запускает socket и не заменяет таймеры HTTP-клиента, зато показывает инвариант, который удобно тестировать независимо от сети. Ожидаемый результат для превышения — <code>deadline-exceeded</code> и нулевой остаток.'),
code(`import { allocateTimeoutBudget } from './upgrade-2027-10.mjs';
const mechanism = revision({ slug: 'editorial-2027-10-mechanism-long-form-interview', title: 'Длинное техническое интервью: evidence, provenance и опровержимость', categories: ['Интервью', 'Разработка'], cover: '/assets/editorial/2027/long-form-interview-2027-source-boundary-matrix.svg', excerpt: 'План на октябрь 2027: почему совпадающие фразы не являются независимым подтверждением и как не выдать provenance за доказанный вывод.', readingMinutes: 28 }, [
p('P116 — плановый выпуск на 2027-10 с редакторской датой 2026-07-31 и source cutoff 2026-07-31. Ошибка механизма начинается, когда связная техническая история получает статус доказательства без visible origin. Одна и та же незафиксированная деталь может пройти через заметки, черновик и вопросник, а потом выглядеть как три подтверждения. Цена — редактор усиливает assertion количеством повторов и не замечает, что у всех повторов один неизвестный корень.'),
p('Другая цена — считать отсутствие возражения проверкой. В этом scenario нет actual interview, transcript, speaker, quote, code, metric, approval, review, publication, ownership или production effect. Поэтому нельзя сказать, что кто-то подтвердил или опроверг будущую версию. Positive output evaluator только один: <code>synthetic-plan-hand-off</code> с <code>productionEffect: not-attempted</code>. Любой hidden source, claimed evidence или fabricated record обязан завершиться fail-closed status.'),
h2('Evidence не равно тексту, который удобно цитировать'),
p('Механизм начинается с простого разделения. Evidence — конкретный material, который можно предъявить в разрешённом scope. Provenance — цепочка, объясняющая, откуда material появился, какая activity его породила и где лежит граница использования. Claim — вывод, который пытается опереться на этот material. Falsifiability — способ заранее сказать, что именно сделает вывод непригодным. Пока хотя бы одна часть отсутствует, текст может описывать вопрос, но не должен изображать установленный факт.'),
p('Independent confirmation требует не второго формулировочного варианта, а независимого пути к проверке. Два файла, созданные из одного неизвестного payload, не являются двумя evidence records. Два редактора, пересказавшие одну непомеченную фразу, не создают второго origin. Даже cryptographic binding не превращает содержание в истину: он может показывать связь assertion с asset и целостность формы, но не производит наблюдение, которого не было. Для P116 это важная граница между технической аккуратностью и фабрикацией.'),
table('Матрица механизма evidence', ['Слой', 'Что можно проверить в будущем scope', 'Что нельзя заключить в P116'], [['Claim', 'Точная формулировка и scope', 'Что он верен'], ['Evidence', 'Наличие разрешённого artefact', 'Что artefact уже существует'], ['Provenance', 'Origin, activity и temporal boundary', 'Кто сказал или сделал что-либо'], ['Independent confirmation', 'Отдельность метода и origin', 'Что два похожих текста независимы'], ['Falsifiability', 'Условие снятия или сужения claim', 'Что тест уже прошёл']]),
p('Плановый evaluator не моделирует реальные источники и не пытается угадать, будут ли они доступны. Его literals дают специальную форму для отсутствия: <code>not-collected</code> у claim, source, provenance и confirmation; <code>not-tested</code> у falsifiability. Эти значения не означают низкое качество материала. Они означают, что P116 не запускал activity, которая могла бы создать или проверить material. Механизм полезен именно потому, что не превращает неизвестность в рейтинг уверенности.'),
p('Здесь же видно, почему source boundary важнее количества ссылок. Внешние спецификации ниже поддерживают терминологию происхождения и validation. Они не являются источниками о будущих участниках или фактах беседы. Нельзя взять точное определение provenance и присоединить к нему вымышленную биографию процесса. Нормативный источник задаёт язык для разграничения, но не заменяет origin record конкретного claim. В P116 sources помогают читать поля, а не заполняют поля за будущую работу.'),
figure('/assets/editorial/2027/long-form-interview-2027-source-boundary-matrix.svg', 'Матрица: claim отделён от evidence, provenance, независимого подтверждения и условия опровержения; неизвестный или скрытый слой ведёт к stop-status.', 'Матрица показывает проверку формы P116. Она не фиксирует источник, участника или результат технического интервью.'),
h2('Почему falsifiability надо писать до narrative'),
p('Опровержимость в редакционной работе не означает обещание найти ошибку. Это договорённость не расширять claim, пока не определено, какое наблюдение заставит его снять или ограничить. Без такого условия любая новая деталь читается как поддержка, а несоответствие объясняется исключением. В длинном формате это особенно дорого: читатель получает десять связанных абзацев и естественно предполагает, что они прошли одинаковую проверку. Между тем один абзац мог быть только планом вопроса.'),
p('Для технической темы условие опровержения обычно привязано к виду evidence, а не к красивому контраргументу. Пример: будущий assertion о поведении кода мог бы требовать version-pinned artifact и результат минимальной воспроизводимой проверки. Если artifact относится к другой версии или проверка не различает альтернативы, claim остаётся неподтверждённым. Это не fictional code example P116; это форма границы. Мы не называем проект, функцию, output или число, чтобы не превратить пояснение в fake record.'),
p('Independent confirmation проверяют сразу после provenance. Сначала спрашивают: какие entities и activities связаны с первым evidence? Затем: использует ли второй путь те же entity, activity или скрытый интерпретатор? Только после этого можно говорить о независимости. Если связи неизвестны, корректный статус — не «вероятно независимо», а <code>not-collected</code>. Такая последовательность защищает от common-mode error: несколько аккуратных документов могут повторять один и тот же неподтверждённый слой.'),
h2('Runnable example: evaluator не дорисовывает подтверждение'),
p('Этот пример запускает только pure function над fixed in-memory literal. Он не открывает repository или файловые пути, не делает сетевых вызовов, не смотрит process variables, время, secrets, telemetry, system interfaces либо external data. Он не ищет origin, не вызывает валидатор, не создаёт signature и не публикует result. Вывод показывает состояние boundary, не состояние мира. Его можно исполнить безопасно, потому что data flow замкнут внутри модуля.'),
code("import { inspectEvidenceBoundaryLiteral } from './scripts/upgrade-2027-10.mjs';\n\nconst result = inspectEvidenceBoundaryLiteral();\nconsole.log(result);\n// { provenance: 'not-collected', confirmation: 'not-collected',\n// falsifiability: 'not-tested', status: 'synthetic-plan-hand-off' }"),
p('Отрицательные fixtures делают правило видимым. <code>long-form-interview-hidden-source-v1</code> останавливается до narrative. <code>long-form-interview-claimed-evidence-v1</code> не получает более высокий confidence, потому что в P116 нельзя собрать claim. <code>long-form-interview-fabricated-record-v1</code> останавливается одним статусом для claimed interview, transcript, speaker, quote, code, metric, approval, review, publication и ownership. Это не список вероятных ошибок реального процесса; это закрытая защита от того, чтобы synthetic literal имитировал наблюдение.'),
h2('Проверка механизма по шагам'),
ol(['Сформулировать один claim type без ответа, истории и оценки будущего материала.', 'Записать required evidence class, но оставить конкретный evidence instance отсутствующим.', 'Связать provenance с origin и activity только в новом authorised scope; не подставлять память, ссылку или чужой пересказ.', 'Проверить independence: второй путь не должен наследовать тот же неизвестный origin или метод.', 'Назвать falsifier до вывода; если он не определён, оставить <code>not-tested</code>.', 'Прогнать fixture и убедиться, что hidden source, claimed evidence и fabricated record не деградируют до warning.']),
h2('Ложные сокращения и их стоимость'),
p('Первое ложное сокращение — считать, что provenance равна quality. Provenance помогает понять происхождение и границу trust decision, но сама не делает statement полезным или истинным. Второе — считать, что signature закрывает смысловой спор. Она может защитить association и целостность, не подтверждая интерпретацию. Третье — считать, что два согласных абзаца уже образуют independent confirmation. Без разных origins это только более громкий повтор. Все три сокращения создают привлекательный текст, но лишают будущую проверку точки входа.'),
p('Есть и обратная ошибка: требовать от plan/scenario полный evidence ledger. Это заставляет автора изобретать documents, people и measurements, чтобы таблица не выглядела пустой. В P116 пустые states — правильный результат. Мы не знаем ни формата будущего материала, ни того, будет ли доказательство вообще нужно. Поэтому evaluator не хранит fallback source, confidence score или заранее одобренный route. Such defaults особенно опасны: они делают неполный context похожим на подготовленное решение.'),
h2('Разделяйте проверку формы и проверку содержания'),
p('Сначала проверяется форма записи: literal известен, даты точны, поля не получают скрытых значений. Затем, в другом scope, может проверяться содержание будущего claim. Смешивать эти этапы нельзя. Формальная проверка может доказать, что object не содержит fabricated field, но не может сказать, что техническое объяснение верно. И наоборот, даже сильный материал не отменяет необходимость показать его origin. Два вида проверки дополняют друг друга, но не заменяют один другой.'),
p('Это различие особенно полезно при независимом подтверждении. У второго пути должны быть отдельно названы material, method и relationship с первым путём. Нельзя считать независимостью тот случай, когда второй reviewer читает первый summary, а затем соглашается с ним. Нельзя считать независимостью тот случай, когда одна система экспортирует один и тот же state в два файла. Until the paths are described, P116 сохраняет <code>not-collected</code>, даже если потенциально можно было бы придумать правдоподобную схему.'),
p('Falsifier тоже надо держать маленьким. Он не обязан опровергать весь будущий материал; достаточно определить условие, которое не позволит произнести конкретный claim в заявленной широте. Например, несовпадение temporal boundary может снять право переносить statement между версиями. Отсутствие различающего теста может снять причинный вывод. Неудача не делает источник плохим и не делает автора некомпетентным. Она только возвращает assertion в состояние, где его нельзя использовать как установленную связь.'),
p('Такой механизм не запрещает редакционную интерпретацию. Он требует подписать её уровнем уверенности и не спутать с evidence. В будущем scope можно будет отдельно записать, где заканчивается observed material и начинается пояснение для читателя. P116 намеренно не создаёт даже эту границу для конкретного сюжета: нет сюжета, нет observation, нет authorial conclusion. Есть лишь правило, что empty provenance нельзя компенсировать гладкой логикой.'),
h2('Ограничения и следующий scope'),
p('P116 не является provenance system, fact-checking process, interview protocol или validation service. Он не определяет, какие источники будут допустимы в будущем, и не обещает, что independent confirmation достижимо. Никакой status здесь не говорит о правдивости, качестве, ценности или согласовании будущего текста. Fixture проверяет только closed-world equality с named literals. Внешний claim, даже правдоподобный, должен быть отвергнут до тех пор, пока другой scope не определит его данные и полномочия.'),
p('Следующий шаг — создать отдельный scope, если конкретный editorial claim действительно требует evidence. В нём надо зафиксировать source policy, origin record, access boundary, method независимой проверки, falsifier и правило, что делать при отсутствии данных. P116 не переносит в этот scope готовый вывод. Он оставляет только <code>synthetic-plan-hand-off</code> и <code>productionEffect: not-attempted</code>, чтобы future work не получила invented certainty вместе с планом.')
], refs);
const within = allocateTimeoutBudget({
totalMs: 800,
dnsMs: 42,
tlsMs: 88,
requestMs: 510,
});
const late = allocateTimeoutBudget({
totalMs: 800,
dnsMs: 120,
tlsMs: 210,
requestMs: 560,
});
const field = revision({ slug: 'editorial-2027-10-field-long-form-interview', title: 'Длинное техническое интервью: редакционный hand-off без transcript', categories: ['Интервью', 'Разработка'], cover: '/assets/editorial/2027/long-form-interview-2027-editorial-handoff-loop.svg', excerpt: 'План на октябрь 2027: как передать будущий редакционный вопрос, не создав transcript, speaker, approval или историю публикации.', readingMinutes: 27 }, [
p('P116 — future editorial plan/scenario: editor date 2026-07-31, planDate 2027-10, source cutoff 2026-07-31. В field hand-off самая дорогая ошибка выглядит как аккуратная папка: question list, схема, ссылка и краткое резюме. Получатель видит полноту формы и может решить, что interview уже состоялся, материалы собраны, а дальнейшая работа — лишь выпуск. Цена такой подмены — fabricated transcript и approval появляются не в тексте, а в ожиданиях следующего человека.'),
p('В этом пакете нет actual interview, transcript, speaker, quote, code, metric, evidence, review, publication, ownership или production effect. Нельзя прикрыть их neutral labels: <code>not-created</code> не означает потерянный файл, <code>not-named</code> не означает скрытого собеседника, <code>not-requested</code> не означает pending approval. Единственный positive evaluator result — <code>synthetic-plan-hand-off</code> с <code>productionEffect: not-attempted</code>. Всё, что пытается выдать plan за редакционный факт, закрывается fail-closed.'),
h2('Field hand-off передаёт границу, а не narrative'),
p('Хорошая передача в изолированном scope должна быть достаточно полезной, чтобы новый scope мог начать с правильного вопроса, и достаточно пустой, чтобы не унаследовать ложный результат. Поэтому P116 отдаёт четыре вещи: temporal boundary, named assertion contract, evidence states и record states. Он не отдаёт тезис будущей статьи, список вопросов к человеку, summary ответа или путь публикации. Такой набор намеренно похож на технический interface: у него есть доступные поля и явные запреты, но нет скрытого поведения.'),
p('Record states важнее, чем кажутся. <code>interview: not-created</code> запрещает называть план состоявшимся разговором. <code>transcript: not-created</code> запрещает делать вид, что существует расшифровка. <code>speaker: not-named</code> запрещает attribution. <code>quote: not-created</code> и <code>code: not-created</code> не позволяют добавить illustrative fragment без происхождения. <code>metric: not-collected</code> не оставляет цифре роль decoration. Остальные states так же явно не создают approval, review, publication или owner.'),
table('Field hand-off и недопустимая интерпретация', ['Поле hand-off', 'Состояние P116', 'Неверное чтение'], [['Interview', '<code>not-created</code>', 'Разговор назначен или состоялся'], ['Transcript и quote', '<code>not-created</code>', 'Есть запись или дословная фраза'], ['Speaker', '<code>not-named</code>', 'Можно приписать позицию роли или человеку'], ['Approval и review', '<code>not-requested</code> / <code>not-created</code>', 'Материал согласован или проверен'], ['Publication и ownership', '<code>not-attempted</code> / <code>not-assigned</code>', 'Есть канал выпуска или ответственный']]),
p('Эта точность не бюрократическая. Handoff часто переживает автора и исходный контекст. Через неделю status без происхождения легко читается как история: «не нашли transcript», «неизвестен speaker», «approval задержался». P116 говорит другое: соответствующие activities не создавались в этом scope. Следующий scope может начать совсем по-другому, сузить задачу или отказаться от неё. Он не обязан восстанавливать отсутствующий процесс и не несёт мнимую обязанность завершить опубликованный narrative.'),
p('Визуальная схема не должна компенсировать отсутствие реальных материалов реалистичными иконками. Поэтому SVG P116 содержит только gates и состояния, без портретов, сервисов, таймлайна или имитации документа. Русские alt и caption прямо называют её редакционным loop. На узком экране caption имеет тот же вес, что основной текст: если она скажет «интервью», а body скажет «план», читатель запомнит первое. Accessibility здесь поддерживает temporal honesty, а не только формальную разметку.'),
figure('/assets/editorial/2027/long-form-interview-2027-editorial-handoff-loop.svg', 'Петля hand-off: temporal boundary, assertion contract и evidence states ведут либо к отдельному authorised scope, либо к stop; transcript, speaker, approval и publication не создаются.', 'Схема показывает маршрут передачи P116 без fabricated transcript, интервью, владельца или публикации.'),
h2('Runnable example: передача не делает публикацию'),
p('Пример ниже проверяет output evaluator и выводит только fixed record states. Он не открывает папку или repository, не использует I/O, переменные окружения, часы процесса, секреты, телеметрию, системные API либо внешние records. Он не создаёт editor workflow, не назначает owner и не отправляет уведомления. Эти отсутствующие side effects важнее удобной демонстрации: запуск должен быть безопасен в любом локальном shell и не давать ложного впечатления, что package что-то передал реальному получателю.'),
code("import { inspectEditorialHandoffLiteral } from './scripts/upgrade-2027-10.mjs';\n\nconst result = inspectEditorialHandoffLiteral();\nconsole.log(result);\n// { transcript: 'not-created', speaker: 'not-named',\n// publication: 'not-attempted', status: 'synthetic-plan-hand-off' }"),
p('Если record получает <code>claimed</code>, evaluator не пытается выбрать менее смелую версию. Named bad literal собирает сразу interview, transcript, speaker, quote, code, metric, approval, review, publication и ownership в forbidden shape; результат — <code>stop-fabricated-interview-record</code>. Это также защищает от мелкой подмены: роль без имени всё равно была бы fabricated attribution, а условная цифра всё равно была бы fabricated metric. В P116 нельзя превращать illustrative content в quasi-evidence.'),
h2('Что получатель действительно получает'),
p('Получатель получает границу редакционной ответственности. Он знает, что P116 не устанавливает actual event, не даёт право на contact и не создаёт publication task. Он видит exact dates, positive status и список запрещённых переходов. Этого достаточно, чтобы не использовать package как доказательство при решении. Если для следующего решения evidence не требуется, hand-off может остаться конечным артефактом. Если требуется, он сообщает, что нужен новый scope, а не продолжение по умолчанию.'),
p('Получатель не получает owner. Такое отсутствие иногда кажется неудобным: кому тогда адресовать вопрос? Но owner — не decoration документа, а решение с последствиями. Его невозможно спрятать за generic role, потому что даже role создаёт ожидание и маршрут работы. P116 оставляет <code>ownership: not-assigned</code>. Новый authorised scope сам обязан определить полномочия, контакт, consent, storage и exit condition; до этого старый plan не должен толкать работу в конкретную сторону.'),
p('То же относится к approval. Approval не можно симулировать фразой «готово к согласованию», если не определены материал, reviewer и правило решения. В P116 approval имеет <code>not-requested</code>, потому что запрашивать нечего. Review — <code>not-created</code>, потому что нечего проверять. Такая пара предотвращает классическую ошибку hand-off: наличие аккуратной схемы ошибочно принимают за готовность начать publication. Схема показывает только условия, при которых будущая работа должна остановиться.'),
h2('Порядок редакционной передачи'),
ol(['Передать exact temporal boundary: editor date <code>2026-07-31</code>, planDate <code>2027-10</code>, source cutoff <code>2026-07-31</code>.', 'Передать named assertion literal и states evidence; не добавлять summary, предполагаемую тему или список людей.', 'Сохранить interview, transcript, quote, code и review в <code>not-created</code>; не создавать illustrative substitutes.', 'Сохранить speaker как <code>not-named</code>, approval как <code>not-requested</code>, ownership как <code>not-assigned</code> и publication как <code>not-attempted</code>.', 'Прогнать fixture: hidden source, claimed evidence, fabricated record и disallowed positive result должны остановиться.', 'Открыть отдельный scope только при конкретном решении; он может завершиться без интервью и без публикации.']),
h2('Граница источников и редакционной целостности'),
p('W3C PROV-DM полезен здесь как vocabulary: entity, activity, agent и derivation помогают не склеить plan с record. Но он не создаёт entity, activity или agent в P116. C2PA 1.3 полезна другим ограничением: validation association и tamper evidence не дают value judgement о содержании. В editorial hand-off это значит, что даже хорошо оформленный object нельзя объявить правдивым рассказом. Мы используем обе спецификации как язык границы, а не как authority над future interview.'),
p('Список sources находится после body и содержит exact URL, version/date, use и boundary. Это важная форма: source section должна быть проверяемой, но не должна выглядеть как скрытый пакет evidence о неизвестном событии. Ни одна ссылка не подтверждает person, statement, metric, approval или effect. Если новый scope принесёт собственные materials, он обязан отделить их provenance от этих нормативных ссылок. Иначе vocabulary quietly becomes a borrowed source, а hand-off снова начинает рассказывать историю вместо передачи ограничения.'),
h2('Как не испортить передачу полезной конкретикой'),
p('Первый риск — добавить образец transcript, чтобы получателю было понятнее. Даже если в нём нет имени, образец быстро становится шаблоном ожидаемого ответа и выглядит как fragment реального разговора. Второй риск — добавить условный code block. Он создаёт ложное впечатление, что будущая тема уже связана с конкретной системой. Третий риск — вставить приблизительную метрику для масштаба. Такая цифра почти неизбежно будет процитирована без слова «условная». P116 не использует ни один из этих приёмов.'),
p('Полезность hand-off строится иначе: на точных запретах и следующем безопасном входе. Получатель видит, какие states нельзя повышать, какие facts нельзя дописывать и какие вопросы должен решить новый scope. Это меньше похоже на brief, зато не давит на будущую работу готовой постановкой. В изолированном пакете ограничение часто ценнее детали, потому что detail без authorisation создаёт именно тот долг, который hand-off обязан был не переносить.'),
p('Четвёртый риск — назвать generic owner, чтобы пакет не выглядел бесхозным. Но generic owner не нейтрален: он меняет, кто предполагаемо отвечает за privacy, контакт и публикацию. Поэтому <code>not-assigned</code> не следует заменять названием функции или команды. Аналогично нельзя писать, что approval «остаётся за редактором»: это уже ownership decision. Новый scope должен принять его явно или оставить работу незапущенной. P116 не имеет мандата делать это вместо него.'),
p('Пятый риск — посчитать PASS fixture знаком готовности к выпуску. Fixture знает только fixed literals и сознательно не читает внешний мир. Он не может увидеть корректность текста, желание участника, право хранения или качество будущей редакторской работы. PASS доказывает исключительно, что запрещённые shapes не прошли evaluator. Это полезный, но узкий результат. В hand-off его надо писать рядом с <code>productionEffect: not-attempted</code>, иначе техническая проверка снова станет суррогатом реального approval.'),
console.log(within.ok, within.remainingMs);
console.log(late.ok, late.reason, late.remainingMs);
// true 160
// false deadline-exceeded 0`),
h2('Порядок диагностики'),
ol([
'Запишите общий deadline и отдельно отметьте, в какой момент клиент завершил ожидание. Не начинайте с увеличения числа.',
'Добавьте интервалы DNS, TCP, TLS, отправки и чтения в одну запись запроса. Одинаковые имена фаз важнее конкретной библиотеки.',
'Проверьте, передаётся ли остаток бюджета на следующую фазу. Новый таймер не должен начинаться после истечения абсолютного deadline.',
'Сопоставьте фазу отказа с методом. Для записи проверьте идемпотентность и возможность узнать результат по идентификатору операции.',
'Разделите connect/read timeout и deadline в конфигурации. Каждый параметр должен иметь владельца и тест на граничное значение.',
'Проверьте cold и warm соединение отдельно: pool может скрыть TLS в одном случае и показать его в другом.',
]),
h2('Что измерение не доказывает'),
p('Большой TTFB не доказывает, что сервер «медленный»: время могло уйти на proxy, очередь или повторный TLS. Малый TTFB не гарантирует быструю загрузку всего тела. Сетевой timeout также не равен HTTP-статусу: клиент может закрыть соединение, а сервер продолжить обработчик. Поэтому метрики фаз нужно связывать с серверным временем и размером ответа, но не подменять ими друг друга.'),
p('Точное разделение требует, чтобы библиотека действительно сообщала фазы. Если она отдаёт только total duration, не называйте реконструированные интервалы фактами. Можно начать с instrumented adapter или событий connection pool, а пока фиксировать только известную границу: «клиент прекратил ждать через N мс». Это менее эффектно, но безопаснее для решения.'),
h2('Ограничения и следующий шаг'),
p('P116 не заменяет consent process, editorial policy, fact check, records retention, interview format или publication workflow. Он не определяет язык будущего материала и не гарантирует, что работа будет кому-либо нужна. Fixture не является review, потому что проверяет synthetic literal, а не текст разговора. Он не создаёт production effect и не измеряет качество. Эти ограничения удерживают scope от расширения в систему редакционного управления, которой пользователь не поручал появиться.'),
p('Следующий шаг возможен, если отдельное решение требует реального claim. Новый scope должен назвать материалы, доступ, provenance, независимое подтверждение, falsifier, privacy boundary, owner и разрешённый выход без выпуска. Пока он не создан, P116 оставляет только <code>synthetic-plan-hand-off</code> с <code>productionEffect: not-attempted</code>. Это не недоделанное интервью; это честно завершённый hand-off, который не фабрикует ни transcript, ни speaker, ни outcome.')
], refs);
p('Функция не учитывает jitter часов, системные очереди, HTTP/2 multiplexing и работу proxy. В боевом клиенте deadline должен отменять все вложенные операции, иначе сокет продолжит жить после ответа вызывающему коду. Учебные числа не являются нормативными значениями для конкретной сети.'),
p('Следующий шаг — выбрать один endpoint, измерить пять фаз на холодном и тёплом соединении и построить таблицу бюджета для успешного и просроченного запроса. Только после этого меняйте timeout или retry. Для записи добавьте отдельную проверку идемпотентности и способ узнать, был ли первый запрос принят.'),
], httpRefs);
const certificateRefs = [
{ key: 'tls', use: 'Описывает TLS 1.3 как протокол защищённого канала и задаёт место проверки сертификата внутри handshake.', boundary: 'Не перечисляет доверенные корни конкретной ОС и не гарантирует настройку вашего клиента.' },
{ key: 'pkix', use: 'Описывает X.509-поля, цепочку сертификации, расширения и правила проверки имени/срока.', boundary: 'Не заменяет системное хранилище доверия, отзыв сертификата и политику конкретного домена.' },
];
const mechanism = revision({
slug: 'editorial-2027-10-mechanism-long-form-interview',
title: 'TLS-сертификат: почему «curl работает» не закрывает проверку',
categories: ['Security', 'HTTP'],
cover: '/assets/editorial/2027/long-form-interview-2027-source-boundary-matrix.svg',
excerpt: 'Разбираем цепочку доверия, срок действия и SAN: какие проверки проходят до HTTP и почему один успешный клиент ничего не доказывает для другого.',
readingMinutes: 16,
}, [
p('Проблема обычно выглядит противоречиво: браузер открывает адрес, а сервисный клиент получает certificate error; либо один контейнер подключается, а второй — нет. Цена ошибки — отключить проверку TLS «временно», потерять имя хоста в диагностике и превратить сетевую проблему в уязвимость.'),
p('Причина в том, что «сертификат валиден» — это не одна проверка. Клиент строит цепочку до доверенного корня, проверяет период действия, имя назначения и ограничения сертификата. Разные хранилища корней, SNI, proxy и часы системы меняют результат. Нужно разделить слой протокола, X.509-структуру и локальную политику доверия.'),
h2('Что проверяет клиент до HTTP'),
p('TLS handshake создаёт защищённый канал, но доверие к peer не появляется из шифрования автоматически. Сертификат содержит открытый ключ, имя и подпись издателя; клиент проверяет цепочку и применимость к назначенному хосту. Если запрос идёт на <code>api.example.test</code>, сертификат только для <code>admin.example.test</code> не должен считаться подходящим из-за того, что ключ технически рабочий.'),
p('Период действия проверяется по часам клиента. Ошибка в системном времени даёт симптом «сертификат ещё не действителен» или «истёк», хотя сервер ничего не менял. Переход на другой контейнер может поменять корневое хранилище и набор промежуточных сертификатов. Поэтому при сравнении сред нужно собирать не только URL, но и hostname, SNI, trust store, время и цепочку.'),
table('Минимальная проверка сертификата', ['Проверка', 'Вопрос', 'Отказ', 'Что собрать'], [
['Срок', 'now между notBefore и notAfter?', 'not-yet-valid / expired', 'UTC-время клиента и поля сертификата'],
['Имя', 'host есть в SAN?', 'hostname mismatch', 'SNI, hostname и SAN'],
['Цепочка', 'есть путь до доверенного корня?', 'unknown issuer', 'leaf, intermediate, trust store'],
['Подпись', 'алгоритм и ключ разрешены?', 'signature/algorithm error', 'TLS policy и negotiated version'],
['Отзыв', 'политика проверяет статус?', 'revoked/unknown', 'OCSP/CRL policy и доступность'],
]),
h2('Почему отключение verify ухудшает диагностику'),
p('Флаг вроде <code>insecure</code> меняет вопрос с «можно ли доверять peer» на «зашифрован ли канал до кого-то». Запрос начинает проходить, но факт успеха перестаёт говорить о подлинности сервера. Если потом этот флаг попадёт в общий клиент или пример конфигурации, временная отладка станет постоянной дырой.'),
p('Надёжнее вывести диагностическую информацию без обхода проверки: имя хоста, SNI, цепочку, срок, код ошибки и идентификатор корня. В тестовой среде можно добавить собственный CA в доверенное хранилище или передать его явно. Такой путь сохраняет настоящую проверку и делает отличие среды видимым.'),
figure('/assets/editorial/2027/long-form-interview-2027-source-boundary-matrix.svg', 'Матрица TLS-проверки: сертификат проходит срок действия, имя SAN, цепочку доверия и алгоритмическую политику до начала HTTP-обмена.', 'Диаграмма разделяет данные сертификата и локальную политику. Успешный запрос одного клиента не является доказательством для другого trust store.'),
h2('Runnable-пример: срок и SAN как отдельные причины'),
p('Функция ниже не строит X.509-цепочку и не заменяет TLS-библиотеку. Она принимает ISO-даты, hostname и список SAN, затем показывает две базовые проверки, которые полезно видеть в тестах и диагностическом выводе. Входы специально простые; ожидаемый результат различает успех, истёкший сертификат и несовпадение имени.'),
code(`import { validateCertificateWindow } from './upgrade-2027-10.mjs';
const common = {
host: 'api.example.test',
sans: ['api.example.test', 'api.internal.test'],
notBefore: '2026-01-01T00:00:00Z',
notAfter: '2027-01-01T00:00:00Z',
};
console.log(validateCertificateWindow({ ...common, now: '2026-06-01T00:00:00Z' }));
console.log(validateCertificateWindow({ ...common, host: 'cdn.example.test', now: '2026-06-01T00:00:00Z' }).reason);
// { ok: true, reason: 'certificate-window-and-san-match' }
// host-not-listed-in-san`),
h2('Порядок проверки в среде'),
ol([
'Зафиксируйте точный hostname и порт, который видит TLS-клиент. IP-адрес в логе не заменяет имя для проверки SAN.',
'Проверьте часы контейнера и узла в UTC. Ошибку времени нельзя лечить повторной загрузкой сертификата.',
'Снимите leaf и intermediate без отключения verify. Сравните цепочку с trust store конкретного процесса.',
'Проверьте SAN, SNI и redirect. Сертификат для исходного адреса не обязан подходить для нового host после перенаправления.',
'Разделите ошибку доверия, имени, срока и алгоритма. Для каждой причины оставьте отдельный тестовый fixture.',
'Исправляйте trust store или цепочку на сервере; флаг обхода проверки не используйте как решение.',
]),
h2('Цепочка не равна доверию'),
p('Наличие intermediate в файле сервера не означает, что клиент доверяет корню. И наоборот, локальный trust store может содержать корень, но сервер не отправить промежуточный сертификат. Успешная проверка строит путь по подписи и ограничениям, а не по совпадению строк в PEM-файле. Это объясняет, почему «в браузере работает» может быть правдой одновременно с ошибкой минимального контейнера.'),
p('Сертификат также не сообщает всю эксплуатационную политику. Клиент может проверять отзыв, запрещать старый алгоритм или требовать минимальную версию TLS. Если диагностический отчёт пишет только «certificate valid», он скрывает полезную часть причины. Сохраняйте код ошибки библиотеки и параметры соединения, а секретный ключ и полное содержимое лишний раз не логируйте.'),
h2('Ограничения и следующий шаг'),
p('Учебная функция не проверяет подпись, CRL, OCSP, wildcard-правила, DNS и реальное TLS-согласование. Она не является security scanner. Её роль — сделать две часто потерянные проверки явными и тестируемыми без сетевой зависимости.'),
p('Следующий шаг — воспроизвести ошибку в том же контейнере, где работает сервис, собрать hostname/SNI, цепочку, время и код ошибки, а затем исправить конкретный слой. После исправления оставьте regression test на истёкший срок и неверный SAN, чтобы повторное «временное» отключение доверия стало заметным.'),
], certificateRefs);
const traceRefs = [
{ key: 'trace', use: 'Задаёт формат traceparent, правила создания и передачи идентификаторов между HTTP-компонентами.', boundary: 'Не определяет backend storage, sampling, формат логов и семантику бизнес-операции.' },
{ key: 'http', use: 'Описывает HTTP-поля и обмен, в котором передаётся контекст трассировки.', boundary: 'Не гарантирует, что proxy пропустит или сервис корректно обработает пользовательский заголовок.' },
];
const field = revision({
slug: 'editorial-2027-10-field-long-form-interview',
title: 'Trace Context в HTTP: как не потерять запрос на границе proxy',
categories: ['Observability', 'HTTP'],
cover: '/assets/editorial/2027/long-form-interview-2027-editorial-handoff-loop.svg',
excerpt: 'Практический разбор traceparent: формат, правила передачи через proxy и безопасная проверка заголовка на входе сервиса.',
readingMinutes: 15,
}, [
p('Проблема видна во время сбоя: gateway сообщает timeout, backend пишет 500, а связать записи по одному запросу невозможно. Цена потери контекста — инженер читает десятки похожих логов, ошибочно обвиняет медленный сервис и дольше держит пользователя в неопределённости.'),
p('Причина часто не в отсутствии системы трассировки, а в границе передачи. Proxy удаляет неизвестный заголовок, middleware создаёт новый trace вместо продолжения или сервис принимает строку с неверным форматом. Исправление требует разделить идентификатор трассы, идентификатор родительского span, правила доверия и способ записи событий.'),
h2('Что именно передаёт traceparent'),
p('Заголовок <code>traceparent</code> содержит четыре поля: версию, trace-id, parent-id и flags. Trace-id связывает дерево обработки одного запроса, parent-id показывает непосредственный предыдущий участок, а flags несут небольшие свойства контекста. Формат строгий: lowercase hexadecimal, правильная длина и ненулевые идентификаторы. Если строка не проходит проверку, её нельзя молча преобразовывать.'),
p('Сервис на входе должен решить, продолжать ли внешний trace. Валидный контекст может быть принят как родительский, а новый span создаётся уже локальной библиотекой. Невалидный заголовок лучше отбросить и начать новый локальный trace, одновременно записав безопасную причину. Нельзя доверять trace-id для авторизации или помещать в него пользовательские данные.'),
table('Граница передачи HTTP-контекста', ['Участок', 'Проверка', 'Ошибка', 'Действие'], [
['Клиент → gateway', 'заголовок добавлен один раз', 'новый trace на каждый redirect', 'сохранять контекст по правилам клиента'],
['Gateway → service', 'proxy пропускает traceparent', 'поле удалено', 'allow-list заголовка и integration test'],
['Middleware', 'формат и ненулевые id', 'uppercase/короткое поле', 'отбросить и создать локальный trace'],
['Logger', 'trace-id попал в структурное поле', 'текстовый grep не находит', 'единый JSON key и sampling policy'],
['Сервис → очередь', 'HTTP-контекст не теряется при смене транспорта', 'новое дерево без link', 'использовать механизм контекста брокера'],
]),
h2('Proxy — часть контракта наблюдаемости'),
p('Заголовок, дошедший до приложения локально, ничего не доказывает о внешнем маршруте. Реальный proxy может нормализовать имя, удалить неизвестное поле, ограничить размер или создать свой контекст. Поэтому тестировать нужно пару gateway + service с теми же правилами allow-list, а не только чистую функцию парсинга.'),
p('Проверка также должна учитывать границы доверия. Внешний клиент может прислать любой trace-id. Это полезный идентификатор для диагностики, но не секрет и не доказательство личности. Логи должны экранировать значение, а sampling не должен удалять единственный event с ошибкой. Наблюдаемость помогает найти проблему, но не должна становиться каналом для доступа.'),
figure('/assets/editorial/2027/long-form-interview-2027-editorial-handoff-loop.svg', 'Цепочка HTTP-контекста: клиент передаёт traceparent gateway, gateway сохраняет его при пересылке, а сервис проверяет формат и создаёт локальный span.', 'Схема показывает место проверки на границе сервиса. Заголовок связывает события, но не является авторизацией и не заменяет логику хранения.'),
h2('Runnable-пример: отклоняем повреждённый traceparent'),
p('Парсер получает ровно одну строку и возвращает разобранные поля только после проверки длины, lowercase hexadecimal и ненулевых идентификаторов. Для плохого входа он возвращает причину, которую можно посчитать в метрике без записи полного пользовательского заголовка. Пример не создаёт span и не отправляет telemetry; он демонстрирует проверяемую границу формата.'),
code(`import { parseTraceparent } from './upgrade-2027-10.mjs';
const valid = parseTraceparent(
'00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01',
);
const invalid = parseTraceparent(
'00-4BF92F3577B34DA6A3CE929D0E0E4736-00f067aa0ba902b7-01',
);
console.log(valid.ok, valid.traceId.slice(0, 8));
console.log(invalid.ok, invalid.reason);
// true 4bf92f35
// false trace-id-invalid`),
h2('Порядок поиска потерянного контекста'),
ol([
'Выберите один запрос и выпишите его границы: клиент, gateway, service и downstream. Для каждой границы нужен отдельный trace-id в логах.',
'Проверьте raw-заголовок до proxy и после proxy. Если значение исчезает, не начинайте с backend-библиотеки.',
'Добавьте parser fixture на нулевые id, uppercase, неверную длину и запрещённую версию. Ошибка должна быть безопасной и короткой.',
'Убедитесь, что middleware продолжает валидный trace и создаёт новый локальный parent-id, а не переписывает весь trace.',
'Приведите логи к одному структурному ключу и добавьте ошибку парсинга как счётчик без полного заголовка.',
'Прогоните интеграционный тест через реальный proxy, затем отдельно проверьте sampling и задержку доставки telemetry.',
]),
h2('Почему один идентификатор не решает диагностику'),
p('Trace-id связывает события, но не объясняет, сколько времени занял каждый участок. Для этого нужны span-границы, timestamps с понятными часами и статус ошибки. Если gateway и сервис пишут один trace-id, но не записывают начало и конец span, инженер всё ещё не знает, где возникла задержка.'),
p('Не следует использовать trace-id как ключ бизнес-операции без отдельного поля. Пользователь может повторить операцию, а один trace может завершиться до асинхронной обработки. Для записи и очереди добавляйте безопасный operation-id и связывайте их в журнале по правилам приватности. Так диагностика не подменяет доменную идемпотентность.'),
h2('Ограничения и следующий шаг'),
p('Парсер не проверяет подпись, доверие к отправителю, sampling backend и форматы контекста Kafka/очереди. Он работает только с header string. Разные библиотеки могут дополнительно проверять version и flags, поэтому интеграционный тест должен закрепить выбранное поведение.'),
p('Следующий шаг — добавить один end-to-end тест через proxy: отправить валидный traceparent, проверить один trace-id во всех сервисных логах и отдельно послать повреждённую строку. Вторая проверка должна показать controlled fallback, а не 500 и не принятие внешнего значения как права доступа.'),
], traceRefs);
export const revisions = deepFreeze([practice, mechanism, field]);
export function verifyRevisionsAgainstFixture() { const fixture = runLongFormInterviewFixture(); const articleChecks = revisions.map((item) => { const text = bodyText(item.contentHtml); return text.length >= 10000 && text.length <= 13000 && /(цен[аы]|стоимост|дорог)/i.test(text.slice(0, 1500)) && /<table>/.test(item.contentHtml) && /<figure>/.test(item.contentHtml) && /<pre><code>/.test(item.contentHtml) && /<ol>/.test(item.contentHtml) && /2027-10/.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; }
export function runHttpBoundaryFixture() {
const cases = [
['deadline-within', allocateTimeoutBudget({ totalMs: 800, dnsMs: 40, tlsMs: 90, requestMs: 500 }).reason, 'within-deadline'],
['deadline-exceeded', allocateTimeoutBudget({ totalMs: 800, dnsMs: 120, tlsMs: 210, requestMs: 560 }).reason, 'deadline-exceeded'],
['certificate-valid', validateCertificateWindow({ host: 'api.test', sans: ['api.test'], notBefore: '2026-01-01T00:00:00Z', notAfter: '2027-01-01T00:00:00Z', now: '2026-06-01T00:00:00Z' }).ok, true],
['certificate-san-rejects', validateCertificateWindow({ host: 'cdn.test', sans: ['api.test'], notBefore: '2026-01-01T00:00:00Z', notAfter: '2027-01-01T00:00:00Z', now: '2026-06-01T00:00:00Z' }).reason, 'host-not-listed-in-san'],
['traceparent-valid', parseTraceparent('00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01').ok, true],
['traceparent-rejects-uppercase', parseTraceparent('00-4BF92F3577B34DA6A3CE929D0E0E4736-00f067aa0ba902b7-01').reason, 'trace-id-invalid'],
];
const checks = cases.map(([id, actual, expected]) => ({ id, actual, expected, passed: actual === expected }));
return deepFreeze({ passed: checks.filter((item) => item.passed).length, total: checks.length, accepted: checks.every((item) => item.passed), checks });
}
export function verifyRevisionsAgainstFixture() {
const fixture = runHttpBoundaryFixture();
const articleChecks = revisions.map((item) => {
const text = bodyText(item.contentHtml);
return text.length >= 5000 && text.length <= 15000 && /<table>/.test(item.contentHtml) && /<figure>/.test(item.contentHtml) && /<pre><code>/.test(item.contentHtml) && /<ol>/.test(item.contentHtml) && /Проблема/.test(text.slice(0, 900));
});
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');