rewrite 2026-09 and 2027 articles for reader-facing quality
Build and deploy / deploy (push) Successful in 18s
Build and deploy / deploy (push) Successful in 18s
This commit is contained in:
+267
-168
@@ -6,197 +6,296 @@ 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({
|
||||
rfc2119: { title: 'RFC 2119 — Key words for use in RFCs to Indicate Requirement Levels', url: 'https://www.rfc-editor.org/rfc/rfc2119.html', version: 'IETF RFC 2119, March 1997, immutable RFC publication' },
|
||||
rfc8174: { title: 'RFC 8174 — Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words', url: 'https://www.rfc-editor.org/rfc/rfc8174.html', version: 'IETF RFC 8174, May 2017, immutable RFC publication' },
|
||||
provdm: { title: '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 Recommendation' },
|
||||
});
|
||||
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_MANIFESTO_LITERALS = deepFreeze({
|
||||
'author-manifesto-plan-v1': { id: 'author-manifesto-plan-v1', editorialDate: '2026-07-31', planDate: '2027-12', sourceCutoff: '2026-07-31', sourceSet: 'author-manifesto-official-sources-v1', question: 'author-manifesto-decision-record-before-advice-v1', reviewInputs: { evidence: 'not-provided', temporal: 'not-provided', voice: 'not-provided' }, declarations: { quality: 'not-declared', pass: 'not-declared', publication: 'not-declared', correction: 'not-declared', outcome: 'not-declared' }, requestedOutput: 'synthetic-plan-hand-off' },
|
||||
'author-manifesto-undated-v1': { id: 'author-manifesto-undated-v1', editorialDate: '', planDate: '2027-12', sourceCutoff: '2026-07-31', sourceSet: 'author-manifesto-official-sources-v1', question: 'author-manifesto-decision-record-before-advice-v1', reviewInputs: { evidence: 'not-provided', temporal: 'not-provided', voice: 'not-provided' }, declarations: { quality: 'not-declared', pass: 'not-declared', publication: 'not-declared', correction: 'not-declared', outcome: 'not-declared' }, requestedOutput: 'synthetic-plan-hand-off' },
|
||||
'author-manifesto-unknown-source-v1': { id: 'author-manifesto-unknown-source-v1', editorialDate: '2026-07-31', planDate: '2027-12', sourceCutoff: '2026-07-31', sourceSet: 'unknown-source-set', question: 'author-manifesto-decision-record-before-advice-v1', reviewInputs: { evidence: 'not-provided', temporal: 'not-provided', voice: 'not-provided' }, declarations: { quality: 'not-declared', pass: 'not-declared', publication: 'not-declared', correction: 'not-declared', outcome: 'not-declared' }, requestedOutput: 'synthetic-plan-hand-off' },
|
||||
'author-manifesto-unknown-question-v1': { id: 'author-manifesto-unknown-question-v1', editorialDate: '2026-07-31', planDate: '2027-12', sourceCutoff: '2026-07-31', sourceSet: 'author-manifesto-official-sources-v1', question: 'unknown-question', reviewInputs: { evidence: 'not-provided', temporal: 'not-provided', voice: 'not-provided' }, declarations: { quality: 'not-declared', pass: 'not-declared', publication: 'not-declared', correction: 'not-declared', outcome: 'not-declared' }, requestedOutput: 'synthetic-plan-hand-off' },
|
||||
'author-manifesto-hidden-review-v1': { id: 'author-manifesto-hidden-review-v1', editorialDate: '2026-07-31', planDate: '2027-12', sourceCutoff: '2026-07-31', sourceSet: 'author-manifesto-official-sources-v1', question: 'author-manifesto-decision-record-before-advice-v1', reviewInputs: { evidence: 'claimed', temporal: 'not-provided', voice: 'not-provided' }, declarations: { quality: 'not-declared', pass: 'not-declared', publication: 'not-declared', correction: 'not-declared', outcome: 'not-declared' }, requestedOutput: 'synthetic-plan-hand-off' },
|
||||
'author-manifesto-declared-quality-v1': { id: 'author-manifesto-declared-quality-v1', editorialDate: '2026-07-31', planDate: '2027-12', sourceCutoff: '2026-07-31', sourceSet: 'author-manifesto-official-sources-v1', question: 'author-manifesto-decision-record-before-advice-v1', reviewInputs: { evidence: 'not-provided', temporal: 'not-provided', voice: 'not-provided' }, declarations: { quality: 'passed', pass: 'claimed', publication: 'published', correction: 'issued', outcome: 'successful' }, requestedOutput: 'synthetic-plan-hand-off' },
|
||||
'author-manifesto-disallowed-output-v1': { id: 'author-manifesto-disallowed-output-v1', editorialDate: '2026-07-31', planDate: '2027-12', sourceCutoff: '2026-07-31', sourceSet: 'author-manifesto-official-sources-v1', question: 'author-manifesto-decision-record-before-advice-v1', reviewInputs: { evidence: 'not-provided', temporal: 'not-provided', voice: 'not-provided' }, declarations: { quality: 'not-declared', pass: 'not-declared', publication: 'not-declared', correction: 'not-declared', outcome: 'not-declared' }, requestedOutput: 'published-manifesto' },
|
||||
csp: { title: 'W3C Content Security Policy Level 3', url: 'https://www.w3.org/TR/CSP3/', version: 'W3C, CSP Level 3 Working Draft, опубликованная редакция спецификации' },
|
||||
hsts: { title: 'RFC 6797 — HTTP Strict Transport Security', url: 'https://www.rfc-editor.org/rfc/rfc6797.html', version: 'IETF, ноябрь 2012 года, RFC 6797, Standards Track' },
|
||||
lcp: { title: 'W3C Largest Contentful Paint', url: 'https://www.w3.org/TR/largest-contentful-paint/', version: 'W3C Web Performance Working Group, Working Draft, страница проверена 31 июля 2026 года' },
|
||||
performance: { title: 'W3C Performance Timeline', url: 'https://www.w3.org/TR/2025/CRD-performance-timeline-20250521/', version: 'W3C Candidate Recommendation Draft, 21 мая 2025 года' },
|
||||
webVitals: { title: 'Web Vitals — web.dev', url: 'https://web.dev/articles/vitals', version: 'Google web.dev, опубликовано 4 мая 2020 года, обновлено 31 октября 2024 года' },
|
||||
nistIncident: { title: 'NIST SP 800-61 Revision 2 — Computer Security Incident Handling Guide', url: 'https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-61r2.pdf', version: 'NIST, revision 2, май 2012 года, Special Publication 800-61' },
|
||||
rfc2119: { title: 'RFC 2119 — Key words for use in RFCs to Indicate Requirement Levels', url: 'https://www.rfc-editor.org/rfc/rfc2119.html', version: 'IETF, март 1997 года, RFC 2119' },
|
||||
});
|
||||
|
||||
export function createAuthorManifestoLiteral(id = 'author-manifesto-plan-v1') { const literal = FIXED_MANIFESTO_LITERALS[id]; return literal ? deepFreeze(cloneFixed(literal)) : undefined; }
|
||||
function stop(status, reason) { return deepFreeze({ status, reason, productionEffect: 'not-attempted', nextAction: 'open-a-separate-authorized-scope-only-when-a-decision-needs-evidence' }); }
|
||||
export function assessAuthorManifestoPlan(input) {
|
||||
const known = Object.values(FIXED_MANIFESTO_LITERALS).some((literal) => JSON.stringify(literal) === JSON.stringify(input));
|
||||
if (!known) return stop('stop-unknown-fixed-literal', 'input-is-not-a-known-named-fixed-literal');
|
||||
if (input.editorialDate !== '2026-07-31' || input.planDate !== '2027-12' || input.sourceCutoff !== '2026-07-31') return stop('stop-temporal-boundary-required', 'editorial-date-plan-date-and-source-cutoff-must-be-exact');
|
||||
if (input.sourceSet !== 'author-manifesto-official-sources-v1') return stop('stop-unknown-source-set', 'source-set-must-be-a-known-fixed-literal');
|
||||
if (input.question !== 'author-manifesto-decision-record-before-advice-v1') return stop('stop-unknown-question', 'question-must-be-a-known-fixed-literal');
|
||||
if (Object.values(input.reviewInputs).some((value) => value !== 'not-provided')) return stop('stop-hidden-review-input', 'evidence-temporal-and-voice-inputs-cannot-be-claimed-by-a-plan');
|
||||
if (Object.values(input.declarations).some((value) => value !== 'not-declared')) return stop('stop-declared-quality-pass-publication-correction-or-outcome', 'a-plan-cannot-declare-quality-pass-publication-correction-or-outcome');
|
||||
if (input.requestedOutput !== 'synthetic-plan-hand-off') return stop('stop-disallowed-positive-result', 'the-only-accepted-positive-output-is-synthetic-plan-hand-off');
|
||||
return deepFreeze({ status: 'synthetic-plan-hand-off', literalId: input.id, planDate: input.planDate, question: input.question, reviewInputs: deepFreeze(cloneFixed(input.reviewInputs)), declarations: deepFreeze(cloneFixed(input.declarations)), productionEffect: 'not-attempted', nextAction: 'preserve-the-question-without-claiming-a-future-editorial-result' });
|
||||
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>`;
|
||||
}
|
||||
export function inspectDecisionRecordLiteral() { const output = assessAuthorManifestoPlan(createAuthorManifestoLiteral()); return deepFreeze({ question: output.question, status: output.status, productionEffect: output.productionEffect }); }
|
||||
export function inspectRubricLiteral() { const output = assessAuthorManifestoPlan(createAuthorManifestoLiteral()); return deepFreeze({ evidence: output.reviewInputs.evidence, temporal: output.reviewInputs.temporal, voice: output.reviewInputs.voice, status: output.status }); }
|
||||
export function inspectHandoffLiteral() { const output = assessAuthorManifestoPlan(createAuthorManifestoLiteral()); return deepFreeze({ quality: output.declarations.quality, correction: output.declarations.correction, outcome: output.declarations.outcome, status: output.status }); }
|
||||
export function runAuthorManifestoFixture() {
|
||||
const expected = [['author-manifesto-plan-v1', 'synthetic-plan-hand-off'], ['author-manifesto-undated-v1', 'stop-temporal-boundary-required'], ['author-manifesto-unknown-source-v1', 'stop-unknown-source-set'], ['author-manifesto-unknown-question-v1', 'stop-unknown-question'], ['author-manifesto-hidden-review-v1', 'stop-hidden-review-input'], ['author-manifesto-declared-quality-v1', 'stop-declared-quality-pass-publication-correction-or-outcome'], ['author-manifesto-disallowed-output-v1', 'stop-disallowed-positive-result']];
|
||||
const checks = expected.map(([id, status]) => ({ id, expected: status, actual: assessAuthorManifestoPlan(createAuthorManifestoLiteral(id)).status }));
|
||||
const unknown = assessAuthorManifestoPlan({ id: 'not-fixed' });
|
||||
return deepFreeze({ passed: checks.filter((item) => item.expected === item.actual).length + Number(unknown.status === 'stop-unknown-fixed-literal'), total: checks.length + 1, accepted: checks.every((item) => item.expected === item.actual) && unknown.status === 'stop-unknown-fixed-literal', checks: deepFreeze(checks) });
|
||||
|
||||
export function buildSecurityHeaders({ nonce, hstsMaxAge = 31536000 } = {}) {
|
||||
if (typeof nonce !== 'string' || nonce.length < 16 || !Number.isInteger(hstsMaxAge) || hstsMaxAge < 0) return { ok: false, reason: 'security-header-input-invalid' };
|
||||
return {
|
||||
ok: true,
|
||||
headers: {
|
||||
'Content-Security-Policy': `default-src 'self'; script-src 'self' 'nonce-${nonce}'; object-src 'none'; base-uri 'self'`,
|
||||
'Strict-Transport-Security': `max-age=${hstsMaxAge}; includeSubDomains`,
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function classifyWebVitals({ lcpMs, inpMs, cls }) {
|
||||
if (![lcpMs, inpMs, cls].every((value) => Number.isFinite(value) && value >= 0)) return { ok: false, reason: 'vital-input-invalid' };
|
||||
const lcp = lcpMs <= 2500 ? 'good' : lcpMs <= 4000 ? 'needs-improvement' : 'poor';
|
||||
const inp = inpMs <= 200 ? 'good' : inpMs <= 500 ? 'needs-improvement' : 'poor';
|
||||
const layout = cls <= 0.1 ? 'good' : cls <= 0.25 ? 'needs-improvement' : 'poor';
|
||||
const overall = [lcp, inp, layout].includes('poor') ? 'poor' : [lcp, inp, layout].includes('needs-improvement') ? 'needs-improvement' : 'good';
|
||||
return { ok: true, lcp, inp, layout, overall };
|
||||
}
|
||||
|
||||
export function validateRunbookCard(card) {
|
||||
const required = ['symptom', 'scope', 'precondition', 'action', 'rollback', 'verification'];
|
||||
if (!card || typeof card !== 'object') return { ok: false, reason: 'runbook-must-be-object' };
|
||||
const missing = required.filter((key) => typeof card[key] !== 'string' || card[key].trim().length < 10);
|
||||
if (missing.length > 0) return { ok: false, reason: 'runbook-fields-missing', missing };
|
||||
if (!/rollback|откат|вернуть/i.test(card.rollback)) return { ok: false, reason: 'rollback-must-be-explicit' };
|
||||
if (!/провер|verify|метрик|threshold/i.test(card.verification)) return { ok: false, reason: 'verification-must-be-observable' };
|
||||
return { ok: true, order: required };
|
||||
}
|
||||
|
||||
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}`);
|
||||
if (proseLength < 5000 || proseLength > 15000) throw new Error(`${meta.slug}: body length ${proseLength}`);
|
||||
return deepFreeze({ ...meta, contentHtml, proseLength });
|
||||
}
|
||||
|
||||
const practiceRefs = [
|
||||
{ key: 'rfc2119', use: 'Даёт датированный нормативный словарь для явного различения требования и рекомендации в карточке решения.', boundary: 'Не подтверждает существование статьи, решения, редактора, review или публикации P118.' },
|
||||
{ key: 'provdm', use: 'Даёт терминологию сущности, действия и происхождения для объяснения, почему совету нужен проверяемый след.', boundary: 'Не доказывает происхождение локального архива, исправления, отзыв читателя или эффект.' },
|
||||
];
|
||||
const mechanismRefs = [
|
||||
{ key: 'rfc8174', use: 'Уточняет чтение нормативных слов и помогает отделить проверяемую границу от риторического усиления.', boundary: 'Не задаёт редакционный процесс, уровень качества, результат прохода или факт выпуска.' },
|
||||
{ key: 'provdm', use: 'Поддерживает различение утверждения и его происхождения при описании уровней evidence.', boundary: 'Не является доказательством реальных входов review, оценок, коррекций или результатов P118.' },
|
||||
];
|
||||
const fieldRefs = [
|
||||
{ key: 'rfc2119', use: 'Фиксирует точность модальных форм, когда hand-off запрещает подменять неизвестное обязательством.', boundary: 'Не назначает владельца, получателя, публикацию, исправление или author development.' },
|
||||
{ key: 'rfc8174', use: 'Помогает показать, что сильная формулировка должна иметь определённую область действия.', boundary: 'Не подтверждает прохождение review, feedback, корректирующей работы или production effect.' },
|
||||
const securityRefs = [
|
||||
{ key: 'csp', use: 'Описывает Content-Security-Policy, директивы источников, nonce и режим Report-Only/Enforce.', boundary: 'Working Draft может изменяться; конкретную поддержку браузеров и собственные inline-скрипты нужно проверить отдельно.' },
|
||||
{ key: 'hsts', use: 'Определяет Strict-Transport-Security и поведение браузера после получения политики по HTTPS.', boundary: 'Не исправляет mixed content, сертификат, redirect до первого безопасного ответа и настройки API-клиента.' },
|
||||
];
|
||||
|
||||
const practice = revision({ slug: 'editorial-2027-12-practice-author-manifesto', title: 'Инженерное письмо: сначала карточка решения, потом технический совет', categories: ['Практика', 'Техническое письмо'], cover: '/assets/editorial/2027/author-manifesto-2027-editorial-decision-graph.svg', excerpt: 'План на декабрь 2027: как не превратить полезный совет в правдоподобную, но не проверяемую историю.', readingMinutes: 28 }, [
|
||||
p('P118 — сценарий на декабрь 2027: редакционная дата 2026-07-31, а граница источников зафиксирована тем же срезом. Точная проблема инженерного письма возникает до первого абзаца: автор хочет дать совет, но не записывает, какое решение этот совет должен поддержать. Тогда текст смешивает условие, предположение и действие. Цена ошибки практическая: следующий инженер принимает гладкую формулировку за готовый маршрут и применяет её там, где граница системы, версия или риск совсем другие.'),
|
||||
p('Вторая стоимость появляется, когда карточку решения заменяют «опытом автора». В будущем выпуске легко написать, что совет уже помог, что его проверяли или что он вырос из набора прошлых материалов. Для P118 это запрещённый narrative: нет будущей статьи, публикации, редактора, разбора, обратной связи, коррекции, команды, метрики и production effect. Единственный положительный результат evaluator — <code>synthetic-plan-hand-off</code>; вместе с ним всегда остаётся <code>productionEffect: not-attempted</code>.'),
|
||||
h2('Совет начинается с решения, которого ещё нет'),
|
||||
p('Под «карточкой решения» здесь понимается не ADR и не заявка на изменение. Это короткая форма, которая заставляет назвать вопрос, допустимый источник, границу вывода и следующий шаг. В P118 она существует только как named fixed in-memory literal <code>author-manifesto-decision-record-before-advice-v1</code>. Его нельзя принять за файл, запись, шаблон команды или будущий процесс. Literal нужен, чтобы проверить форму рассуждения, а не сообщить о том, что решение когда-либо было принято.'),
|
||||
p('Технический совет без вопроса опасен не потому, что он обязательно неверен. Он опасен тем, что не говорит, что именно опровергнет его применимость. Фраза «добавьте проверку» не указывает вход, требуемое свойство, владельца исключения и цену отказа. В карточке вместо этого сначала появляется один вопрос: какое решение нельзя принять, пока неизвестны границы claim. До ответа на него команда не выбирает библиотеку, не пишет правило и не обещает выпуск.'),
|
||||
p('Такой порядок особенно полезен в долгих текстах. Чем убедительнее объяснение, тем проще читателю забыть, откуда взялась исходная посылка. Короткое поле source boundary возвращает разговор к проверяемому: нормативный документ может объяснить значение слова, но не может доказать факт в чужом проекте. Прошлая заметка могла бы быть историческим материалом, но в этом плановом scope она не является evidence. Поэтому P118 не строит цепочку «архив → правило → успех».'),
|
||||
table('Карточка перед техническим советом', ['Поле', 'Значение в P118', 'Что разрешает', 'Чего не разрешает'], [
|
||||
['Вопрос', '<code>author-manifesto-decision-record-before-advice-v1</code>', 'Сохранить предмет будущего решения', 'Принять решение или назвать его владельца'],
|
||||
['Источник', '<code>author-manifesto-official-sources-v1</code>', 'Проверить терминологическую границу', 'Считать официальный текст свидетельством локального события'],
|
||||
['Вход review', '<code>not-provided</code>', 'Признать отсутствие входа', 'Сказать, что текст был прочитан или оценён'],
|
||||
['Декларация', '<code>not-declared</code>', 'Не смешивать форму и результат', 'Объявить качество, pass, публикацию или коррекцию'],
|
||||
['Выход', '<code>synthetic-plan-hand-off</code>', 'Передать вопрос дальше', 'Обещать production effect'],
|
||||
const practice = revision({
|
||||
slug: 'editorial-2027-12-practice-author-manifesto',
|
||||
title: 'Security headers: CSP и HSTS без иллюзии защиты',
|
||||
categories: ['Security', 'Web'],
|
||||
cover: '/assets/editorial/2027/author-manifesto-2027-editorial-decision-graph.svg',
|
||||
excerpt: 'Практический разбор Content-Security-Policy и HSTS: что именно ограничивают заголовки, как вводить nonce и где нужна отдельная проверка.',
|
||||
readingMinutes: 15,
|
||||
}, [
|
||||
p('Проблема проявляется после XSS или downgrade-атаки: приложение отдаёт страницу по HTTPS, но разрешает любой inline script или продолжает открываться по HTTP. Цена ошибки — выполнение чужого кода в контексте origin, утечка токена и ложное ощущение, что один security header закрыл весь риск.'),
|
||||
p('Причина — копировать длинную строку заголовка без модели ресурсов. CSP ограничивает, откуда браузер может загружать или выполнять ресурсы; HSTS заставляет браузер обращаться к домену по HTTPS после получения политики. Ни один из заголовков не исправляет серверный XSS, плохой сертификат или секрет, уже попавший в JavaScript.'),
|
||||
h2('CSP начинается с карты ресурсов'),
|
||||
p('Сначала перечислите, что странице действительно нужно: собственные scripts, стили, изображения, API и frame. Затем для каждого типа выберите минимальную директиву. <code>default-src</code> задаёт fallback, но не объясняет исключения; <code>script-src</code> управляет JavaScript, <code>object-src none</code> закрывает старый plugin-механизм, а <code>base-uri self</code> не даёт странице незаметно изменить базовый URL.'),
|
||||
p('Nonce применяют к конкретному inline script, когда убрать inline-код сразу нельзя. Значение должно быть непредсказуемым и новым для ответа; статическая строка превращается в разрешение для любого, кто её узнал. Шаблон должен вставить nonce и в CSP, и в атрибут script, а логирование полного значения создаёт лишний риск.'),
|
||||
table('Директива и её граница', ['Директива', 'Что ограничивает', 'Частая ошибка', 'Проверка'], [
|
||||
['default-src', 'fallback для типов ресурсов', 'считать её полной политикой', 'проверить исключения по типам'],
|
||||
['script-src', 'источники JavaScript и nonce', 'добавить unsafe-inline навсегда', 'найти все inline и third-party scripts'],
|
||||
['object-src', 'plugin/object загрузку', 'оставить широкое значение', 'поставить none, если object не нужен'],
|
||||
['base-uri', 'изменение базового URL', 'забыть директиву', 'ограничить self или отключить'],
|
||||
['report-only', 'наблюдение нарушений', 'принять отчёт за блокировку', 'после анализа перейти к enforce'],
|
||||
]),
|
||||
h2('Граница между рецептом и утверждением'),
|
||||
p('Рецепт отвечает на вопрос «что попробовать при известном симптоме». Утверждение отвечает на другой вопрос: «почему это действие относится к данной причине». В инженерной статье оба слоя нужны, но их нельзя маскировать один другим. Если указан только рецепт, reader не понимает, когда остановиться. Если указан только вывод, reader не может повторить проверку. Карточка решения заставляет держать слой действия рядом с его ограничением.'),
|
||||
p('Например, совет о логировании не должен начинаться с названия поля. Сначала автор определяет решение: достаточно ли данных, чтобы различить два состояния. Затем называет допустимые входы, чувствительные поля и условие, при котором лог ничего не доказывает. Только после этого возможен пример формата события. P118 не содержит такой реальный event и не предлагает его собрать. Он показывает последовательность вопросов, которую будущий scope обязан сделать явной.'),
|
||||
p('У этой дисциплины есть стоимость: начало статьи становится медленнее, а заголовок не обещает универсальную кнопку. Зато читатель видит, где заканчивается утверждение. Это снижает blast radius советов, которые кажутся мелкими: параметр timeout, признак готовности, правило миграции или формат review могут менять поведение целого контура. Писать «лучше всегда» дешевле, чем назвать границу, но именно эта экономия создаёт дорогую диагностику позже.'),
|
||||
figure('/assets/editorial/2027/author-manifesto-2027-editorial-decision-graph.svg', 'Граф решения: вопрос, источник и граница вывода сходятся в синтетический hand-off; неизвестный источник или заявленный результат ведут к остановке.', 'Схема показывает форму планового решения для P118. Она не сообщает о реальной статье, архиве, редакционном действии, публикации или исправлении.'),
|
||||
h2('Runnable пример оставляет совет неполным'),
|
||||
p('Пример ниже безопасно запускается в Node рядом с модулем. Он не читает файлы, сеть, environment, clock, secrets, telemetry, system или data. Он берёт только зашитый literal и возвращает статус его формы. Поэтому вывод не является оценкой текста и не указывает, какой совет нужно написать. Не добавляйте сюда repository path, issue, автора или реалистичный контекст: такие детали выглядели бы как скрытые входы, которых у P118 нет.'),
|
||||
code("import { inspectDecisionRecordLiteral } from './upgrade-2027-12.mjs';\n\nconst card = inspectDecisionRecordLiteral();\nconsole.log(card.question, card.status, card.productionEffect);\n// author-manifesto-decision-record-before-advice-v1 synthetic-plan-hand-off not-attempted"),
|
||||
h2('Порядок работы до появления текста'),
|
||||
ol(['Назвать одно решение, которое без дополнительного evidence нельзя принять, и оставить его вопросом, а не выводом.', 'Зафиксировать editorial date, planDate и source cutoff; будущий период не даёт права описывать будущие события.', 'Выбрать только проверяемые источники и рядом написать, что каждый из них не может подтвердить.', 'Отделить рецептурный шаг от factual claim: для каждого указать условия применимости и условие остановки.', 'Если требуется review, publication, correction или feedback, перенести их в другой authorised scope с собственными входами.', 'Передать исходный вопрос как <code>synthetic-plan-hand-off</code>, не заменяя его декларацией качества.']),
|
||||
h2('Почему декларация не заменяет решение'),
|
||||
p('Слово «манифест» часто провоцирует обратный порядок: сначала объявить принцип, затем подобрать примеры. Для технического письма это слабая конструкция. Принцип без decision record не показывает, кто должен действовать, какой вход допустим и чем можно опровергнуть правило. P118 использует название выпуска как тему проверки: не лозунг о письме, а разбор того, какую форму должен иметь совет до появления уверенного тона.'),
|
||||
p('Даже аккуратная формула «пишите доказуемо» недостаточна. Неясно, о доказательстве какого типа идёт речь: ссылка на норму, воспроизводимый запуск, наблюдение в среде или вывод из нескольких артефактов. Эти типы нельзя свести к одному слову evidence. Карточка решения сначала фиксирует вид вопроса, а следующий scope выбирает метод. Так future reader не получит ложного разрешения заменить отсутствующий trace ссылкой на стандарт.'),
|
||||
p('Уместный технический язык здесь прямой. Вместо «повысим качество текста» лучше спросить: какое утверждение нельзя проверить без источника, даты или допустимого ввода? Вместо «учтём контекст» — какой контекст меняет выбор действия? Вместо «сформируем культуру» — кому принадлежит решение открыть новый scope? Такие замены не делают прозу холоднее; они возвращают ей объект, действие и проверку.'),
|
||||
p('План не обязан заранее решить, будет ли следующий scope создан. Может оказаться, что вопрос снимается без исследования, потому что изменение не требуется. Может оказаться, что нужный вход нельзя законно или безопасно собрать. Может оказаться, что совет нужно сузить до условия версии. Любой из этих результатов совместим с хорошей карточкой. Несовместимо только одно: описать один из вариантов как уже случившийся в будущем декабре 2027.'),
|
||||
h2('Как карточка удерживает цену решения'),
|
||||
p('Цена решения не сводится к числу часов. У совета есть цена неверного применения, цена объяснения исключения, цена обратимости и цена поддержки следующего читателя. Если автор не называет хотя бы одну из них, рекомендация обычно выглядит дешевле, чем есть. Карточка не обязана рассчитывать стоимость. Ей достаточно обозначить, какое последствие меняет выбор: риск потерять данные, расширить доступ, скрыть деградацию или закрепить неподдерживаемую зависимость.'),
|
||||
p('Такой подход меняет работу с примером. Код в статье не служит знаком компетентности и не должен быть самым убедительным элементом страницы. Он отвечает на узкий вопрос: как выглядит действие после того, как условия названы. Рядом полезно сказать, чего пример не содержит: production configuration, реальный request, секрет, профиль нагрузки и подтверждённый результат. Тогда читатель не переносит литературную компактность кода на свою систему без проверки.'),
|
||||
p('Решение также нуждается в обратимости. Если совет можно отменить только ценой миграции, потери совместимости или ручного восстановления, это не повод отказаться от него, но повод записать границу заранее. В P118 нет конкретного изменения, поэтому нет и обещания rollback. Будущий scope должен отдельно определить, что будет означать отказ от выбранного пути. Нельзя подменять эту работу фразой «при необходимости откатим»: она скрывает owner, данные и порядок восстановления.'),
|
||||
p('Полезная карточка различает адресата текста и владельца решения. Статья может быть написана для инженера, но не назначает ему право менять систему. Совпадение этих ролей нельзя предполагать по должности или тону. В данном сценарии оба поля сознательно отсутствуют: P118 не знает будущей команды. Благодаря этому advice остаётся техническим артефактом, а не неявным приказом тому, кто первым его прочитает.'),
|
||||
p('Наконец, advice должен переживать неизвестность. Если новая версия, другой источник или более узкий риск меняют вывод, текст не считается провалом. Провалом будет скрыть условие и заставить reader обнаружить его в production. Поэтому практическая форма манифеста — не набор правил, а привычка оставлять у утверждения место для опровержения. Она делает следующий разговор короче, потому что участники спорят о названном условии, а не о стиле автора.'),
|
||||
h2('Ограничения и следующий шаг'),
|
||||
p('P118 practice не является editorial policy, ADR, историей публикаций, каталогом статей, review record, evidence package или рекомендацией для конкретной команды. Official sources здесь задают язык границ, но не являются доказательством местного решения. Fixed literals не принимают пользовательский input и не моделируют проект. Fixture проверяет закрытый набор значений; он не проверяет ясность будущей статьи и не измеряет её эффект.'),
|
||||
p('Следующий шаг возможен только в отдельном authorised scope, если конкретный технический вопрос потребует решения. В нём надо назвать допустимые inputs, владельца решения, метод проверки, правила redaction и возможность отрицательного вывода. До этого P118 сохраняет честно ограниченную форму: вопрос остаётся вопросом, а результат — только <code>synthetic-plan-hand-off</code> с <code>productionEffect: not-attempted</code>.')
|
||||
], practiceRefs);
|
||||
h2('HSTS имеет момент включения'),
|
||||
p('HSTS действует после того, как браузер получил заголовок через доверенный HTTPS-ответ. Он не защищает самый первый HTTP-переход, если домен ещё не известен браузеру; для этого существует отдельная политика preload с собственными требованиями и риском. <code>includeSubDomains</code> распространяет правило на поддомены, поэтому включать его можно только после проверки всех нужных имён.'),
|
||||
p('Большой max-age нельзя трактовать как кнопку «попробовать». Если поддомен ещё не умеет HTTPS, браузер перестанет подключаться к нему по HTTP на весь период. Перед расширением политики проверьте redirect, сертификаты, mixed content и административные endpoint. Безопасность заголовка включает и возможность восстановить ошибочную конфигурацию.'),
|
||||
figure('/assets/editorial/2027/author-manifesto-2027-editorial-decision-graph.svg', 'Граф security headers: карта ресурсов формирует CSP, HTTPS-ответ включает HSTS, а неизвестный script или неподготовленный поддомен останавливает расширение политики.', 'Схема показывает два независимых слоя. CSP управляет ресурсами страницы, HSTS — схемой соединения; один заголовок не заменяет другой.'),
|
||||
h2('Runnable-пример: собрать минимальные headers'),
|
||||
p('Функция принимает nonce длиной не менее 16 символов и max-age HSTS. Она возвращает набор заголовков или явную ошибку входа. Это учебный генератор: он не устанавливает response headers и не проверяет ваш шаблонизатор. Ожидаемый результат показывает, что CSP содержит nonce, а HSTS — числовой срок и includeSubDomains.'),
|
||||
code(`import { buildSecurityHeaders } from './upgrade-2027-12.mjs';
|
||||
|
||||
const mechanism = revision({ slug: 'editorial-2027-12-mechanism-author-manifesto', title: 'Инженерное письмо: рубрика качества вместо заявленного качества', categories: ['Процесс', 'Техническое письмо'], cover: '/assets/editorial/2027/author-manifesto-2027-quality-rubric-matrix.svg', excerpt: 'План на декабрь 2027: как проверять claims и границы тремя проходами, не объявляя pass и не выдумывая evidence.', readingMinutes: 29 }, [
|
||||
p('P118 — плановый сценарий на 2027-12, составленный 2026-07-31. Механическая проблема качества текста точна: автор ставит ярлык «проверено», хотя не разделил claim, вход для проверки и условие опровержения. Цена ошибки — спор о впечатлении вместо работы с границей. В результате ссылка, пример и уверенная интонация склеиваются в один сигнал, а reader не может понять, какая часть текста относится к факту, а какая к предложенному действию.'),
|
||||
p('Следующая цена — превратить три прохода review в рассказ о трёх прошедших review. У P118 нет будущих reviewer, их входов, оценок, замечаний, pass, публикации, corrections, feedback или outcome. Неизвестные данные не подставляются из local standards, trajectory или существующих неопубликованных artefacts: это vocabulary будущего контракта, а не доказательство будущего события. Единственный accepted positive output — <code>synthetic-plan-hand-off</code> с <code>productionEffect: not-attempted</code>.'),
|
||||
h2('Рубрика измеряет полноту вопроса, не качество автора'),
|
||||
p('Рубрика полезна, когда она не выдаёт балл за стиль. В этой статье она раскладывает фразу на четыре поля: claim, evidence level, falsifier и boundary. Claim — то, что текст предлагает считать верным. Evidence level — происхождение, которое могло бы поддержать claim. Falsifier — наблюдение или условие, после которого claim нельзя оставлять прежним. Boundary — то, чего claim не охватывает. Без этой четвёрки review быстро становится спором о том, «достаточно ли убедительно» написано.'),
|
||||
p('P118 не заполняет эти поля реальным материалом. В named literal review inputs имеют значение <code>not-provided</code>. Это не отрицательная оценка и не скрытый result: input не был передан модулю. Такое различие важно для evidence levels. «Нет данных» не равно «данные слабые», а «источник известен» не равно «источник поддерживает именно этот вывод». Любая будущая рубрика должна сохранять это различие, иначе уровень доказательности станет декоративной шкалой.'),
|
||||
p('Falsifiability в инженерном письме не требует научного эксперимента для каждого предложения. Она требует назвать, что изменило бы решение. Если статья рекомендует ограничить retry, опровергнуть применимость может семантика операции. Если предлагает кэш, опровергнуть её может отсутствие допустимой инвалидации. Если описывает правило review, опровергнуть его может неизвестный тип входа. В P118 эти примеры не говорят о существующей системе; они различают форму условия и утверждение о факте.'),
|
||||
table('Матрица уровней evidence и фальсифицируемости', ['Уровень', 'Допустимая форма', 'Чего недостаточно', 'Вопрос falsifier'], [
|
||||
['E0 — отсутствует', '<code>not-provided</code>', 'Уверенная формулировка или правдоподобный пример', 'Какой вход вообще разрешён?'],
|
||||
['E1 — нормативный', 'Датированный стандарт или спецификация', 'Вывод о локальном поведении', 'Какая реализация или версия меняет чтение нормы?'],
|
||||
['E2 — воспроизводимый', 'Описанный метод и сохранённый результат в отдельном scope', 'Один пересказ запуска', 'Какая одинаковая проверка даст другой результат?'],
|
||||
['E3 — контекстный', 'Связанные inputs, метод и ограниченный inference', 'Метрика без условий измерения', 'Какая граница данных ломает перенос вывода?'],
|
||||
['P118', 'Только форма будущей рубрики', 'Quality/pass/publication/correction/outcome', 'Почему вообще нельзя объявить результат?'],
|
||||
]),
|
||||
h2('Три прохода идут по разным рискам'),
|
||||
p('Первый проход — evidence. Он не спрашивает, нравится ли источник. Он сопоставляет каждое существенное утверждение с происхождением: норма, версия, метод, наблюдение или неизвестность. Если claim живёт за пределами источника, рядом должна быть boundary. Для планового выпуска этот проход закономерно получает <code>not-provided</code>. Он не может превратить ссылку на RFC или W3C Recommendation в свидетельство будущего редакционного действия.'),
|
||||
p('Второй проход — temporal. Он проверяет, не перескочил ли текст через дату. Здесь dates — не метаданные после публикации, а часть значения. Editorial date 2026-07-31, planDate 2027-12 и source cutoff 2026-07-31 закреплены exact literals. Любая другая дата останавливает evaluator. Тем самым формулировка «будет исправлено» остаётся недопустимой: она незаметно сообщает о том, чего в плановом scope не происходило.'),
|
||||
p('Третий проход — voice. Его задача не угадать личность автора по оборотам. Он проверяет техническую плотность: первые абзацы называют проблему и цену, пример ограничен входом, таблица различает варианты, а финал оставляет следующее действие и ограничение. Local editorial standards и voice trajectory могут быть названы только как vocabulary будущего контракта. Они не доказывают, что голос уже проверялся, что editor видел статью или что какая-либо траектория продолжилась в 2027.'),
|
||||
p('Три прохода нельзя склеивать в общий stamp. Хорошая ссылка не исправляет future tense. Спокойный голос не заменяет falsifier. Удобная таблица не делает недоступный input известным. Когда критерии смешаны, найденная слабость растворяется в общем «выглядит неплохо». Когда они разделены, каждый stop имеет понятную причину и не требует выдумывать среднее качество текста.'),
|
||||
figure('/assets/editorial/2027/author-manifesto-2027-quality-rubric-matrix.svg', 'Матрица рубрики: evidence, temporal и voice проходят отдельно; неизвестный вход и заявленный pass останавливают синтетический evaluator.', 'Рисунок объясняет три независимые проверки формы P118. Он не фиксирует реальный review, качество статьи, редакторский verdict или выпуск.'),
|
||||
h2('Runnable пример показывает отсутствие review'),
|
||||
p('Этот вызов безопасен: функции работают с immutable literals, не принимают ввод, не открывают filesystem и не обращаются к сети. Console output не означает, что evidence, temporal или voice действительно были проверены. Он лишь показывает, что valid plan хранит их как <code>not-provided</code>. Это точнее, чем подставить «pass pending»: pending уже создаёт историю процесса, которой здесь нет.'),
|
||||
code("import { inspectRubricLiteral } from './upgrade-2027-12.mjs';\n\nconst rubric = inspectRubricLiteral();\nconsole.log(rubric.evidence, rubric.temporal, rubric.voice, rubric.status);\n// not-provided not-provided not-provided synthetic-plan-hand-off"),
|
||||
h2('Порядок трёх проходов'),
|
||||
ol(['Выписать отдельные claims и отметить, что является нормативным словом, предположением или действием.', 'Для каждого claim выбрать только известный evidence level; если вход отсутствует, оставить <code>not-provided</code>.', 'Сформулировать falsifier как условие смены решения, а не как обещание будущего теста.', 'Проверить exact temporal boundary: редакторская дата, planDate и cutoff не могут быть заполнены по умолчанию.', 'Прочитать текст в голосовом проходе: убрать лозунг, вернуть предмет, цену, ограничение и следующий шаг.', 'Остановить пакет, если появляется hidden review input либо declared quality, pass, publication, correction или outcome.']),
|
||||
h2('Почему уровни не являются рейтингом'),
|
||||
p('E3 не «лучше» E1 во всех задачах. Нормативный источник может быть достаточен, когда вопрос касается значения обязательного слова. Воспроизводимый метод нужен, когда речь о поведении конкретной версии. Контекстный inference требует ещё больше условий, но не даёт права сделать его универсальным. Рубрика работает, когда показывает соответствие формы вопроса и источника, а не когда стимулирует собирать наиболее эффектные доказательства.'),
|
||||
p('Это также защищает от ложной точности. Число баллов, процент готовности или зелёный badge кажутся удобными, потому что упрощают передачу. Но при отсутствии входов они превращают unknown в красивый verdict. P118 намеренно не содержит шкалу pass/fail для будущей статьи. Его evaluator принимает один fixed literal и возвращает один hand-off; он не умеет повышать quality, выдавать publication status или считать correction завершённой.'),
|
||||
p('Механизм fail closed важнее позитивной ветки. Unknown object не нормализуется до plan. Unknown source set не превращается в «источник позже уточним». Unknown question не получает стандартный текст. Claimed review не становится warning. Любое из этих состояний получает свой stop. Такой дизайн неудобен для демонстрации гладкого процесса, зато не даёт случайно использовать модуль как генератор правдоподобного editorial history.'),
|
||||
p('Техническому лидеру полезно показывать эту неудобную часть. В реальной работе самый дорогой спор часто начинается не с ошибки в коде, а с невидимого перехода от наблюдения к выводу. Рубрика делает переходы явными: источник описывает норму, метод описывает воспроизводимость, reviewer input описывает review, а output описывает только форму hand-off. Если одного звена нет, цепочка не должна притворяться полной.'),
|
||||
h2('Как читать claim без лишней уверенности'),
|
||||
p('Claim полезно писать в проверяемом времени и области. «Этот механизм сокращает ошибки» слишком широк: неизвестны механизм, ошибка, период и способ сравнения. «Для этого входа обработчик отвергает неизвестный literal» уже связывает действие с наблюдаемым контрактом. Второе предложение не делает систему лучше само по себе, но даёт reviewer предмет для проверки. Если поле меняется, claim можно сузить или снять без спора о намерениях автора.'),
|
||||
p('Evidence level не следует прятать в сноске. Когда ключевой вывод поддержан только нормативным текстом, это надо назвать возле вывода. Норма сообщает, как читать требование; она не сообщает, что конкретная реализация следует ему. Когда вывод получен запуском, читателю нужны версия, вход, метод и ограничение результата. Когда этих полей нет, аккуратный текст говорит об отсутствии основания, а не повышает убедительность прилагательными.'),
|
||||
p('Falsifier должен быть доступен тому, кто принимает решение. Формула «проверим все случаи» не годится: она не определяет случаи и может оказаться невыполнимой. Гораздо полезнее назвать один класс входов, который меняет выбор, и признать остальные неизвестными. Это не упрощение ради отчёта. Такая граница определяет, сколько работы действительно нужно до решения и когда следует открыть отдельное исследование вместо бесконечной редактуры.'),
|
||||
p('Temporal проход ловит не только будущие даты. Он также ловит ретроспективную причинность: «после этой статьи команда стала делать X» требует событий и связи между ними. В плановом документе даже нейтральное «позже исправим» может выглядеть как согласованный backlog. P118 избегает такого намёка. Он описывает лишь то, что другой scope мог бы решить после получения разрешённых inputs, и оставляет возможность не предпринимать ничего.'),
|
||||
p('Voice проход имеет техническую цену. Абстрактная фраза заставляет reader достраивать объект; чрезмерный жаргон переносит стоимость расшифровки на менее опытного коллегу; декларация качества снимает вопрос слишком рано. Поэтому хороший поздний голос не становится авторитетнее за счёт громкости. Он становится полезнее, когда прямо называет вход, риск, решение, ограничение и следующую проверку. Рубрика удерживает эту последовательность даже там, где нет права утверждать результат.'),
|
||||
h2('Ограничения и следующий шаг'),
|
||||
p('P118 mechanism не является quality policy, rubric rollout, редакторским протоколом, published checklist, evidence audit или доказательством качества будущего текста. W3C и IETF sources применены к терминологии и модальным границам, не к локальной истории. The fixture не проверяет логику реального review и не делает conclusion об авторе, редакторе, архиве или продукте.'),
|
||||
p('Следующий шаг требует отдельного authorised scope с разрешёнными источниками, датой, owner, способом хранения evidence и правом на отрицательный verdict. Там можно будет определить, какие levels подходят конкретному technical claim. Нельзя перенести в него объявленный результат P118: текущий сценарий заканчивается только <code>synthetic-plan-hand-off</code>, а <code>productionEffect: not-attempted</code> сохраняется на каждой ветке.')
|
||||
], mechanismRefs);
|
||||
const result = buildSecurityHeaders({
|
||||
nonce: '7c2f1b8e9a4d6f0c',
|
||||
hstsMaxAge: 31536000,
|
||||
});
|
||||
|
||||
const field = revision({ slug: 'editorial-2027-12-field-author-manifesto', title: 'Инженерное письмо: корректирующий hand-off без вымышленной редакционной истории', categories: ['Практика', 'Техническое письмо'], cover: '/assets/editorial/2027/author-manifesto-2027-revision-handoff-loop.svg', excerpt: 'План на декабрь 2027: как передать вопрос о правке и развитии автора, не подделывая feedback, публикацию или эффект.', readingMinutes: 28 }, [
|
||||
p('P118 — план/scenario для 2027-12 с editorial date 2026-07-31 и source cutoff 2026-07-31. Полевая проблема корректирующей передачи начинается, когда незавершённый вопрос оформляют как историю: «получили замечание, поправили, текст стал сильнее». Цена ошибки высока: следующему человеку достаётся не вопрос, а обязательство согласиться с несуществующим feedback и продолжать якобы начатую работу. Так редакционный язык создаёт ложное происхождение решения.'),
|
||||
p('Вторая стоимость — считать развитие автора доказанным по самому факту планирования. У P118 нет автора как объекта оценки, reviewer, комментария, текста будущей статьи, публикации, correction, реакции читателя, команды, метрики или production effect. Также нельзя брать local standards, trajectory, unpublished artifacts или очереди как factual evidence. Они допустимы лишь как имена будущих contract vocabulary. Accepted positive output один: <code>synthetic-plan-hand-off</code>; production effect во всех исходах — <code>not-attempted</code>.'),
|
||||
h2('Коррекция начинается с неизвестного, а не с замечания'),
|
||||
p('Corrective hand-off нужен не для того, чтобы послать человеку список улучшений. В строгом scope он сохраняет вопрос: какой claim потребует пересмотра, если authorised evidence появится позже. Пока evidence нет, нельзя говорить, что claim ошибочен, что редактура обнаружила проблему или что автор изменил подход. В fixed literal это отражено пятью отдельными полями: quality, pass, publication, correction и outcome имеют значение <code>not-declared</code>.'),
|
||||
p('Отрицательное значение имеет происхождение. <code>not-declared</code> не означает «декларация ждёт подтверждения» и не означает «кто-то не заполнил форму». Оно означает, что P118 не создавал такого утверждения. Аналогично <code>not-provided</code> для review inputs не означает потерянный комментарий. Вход не передавался. Такая точность убирает привычное давление: будто следующий scope обязан найти подтверждение, чтобы завершить заранее написанную историю.'),
|
||||
p('Author development требует ещё большей осторожности. Полезно обсуждать практику письма, но небезопасно выводить из текста личную динамику. Формулировки «научился», «перестал ошибаться», «стал самостоятельнее» требуют наблюдаемого, разрешённого и контекстного evidence. В P118 ничего такого нет. Field lens поэтому не моделирует человека. Он моделирует границу передачи: неизвестность можно передать честно, а оценку — только после отдельного решения о допустимых inputs.'),
|
||||
table('Корректирующая передача без ложного происхождения', ['Поле', 'Literal P118', 'Безопасное чтение', 'Запрещённая история'], [
|
||||
['Review input', '<code>not-provided</code>', 'P118 не получил вход', 'Редактор увидел и одобрил текст'],
|
||||
['Quality', '<code>not-declared</code>', 'Нет verdict', 'Статья качественная или слабая'],
|
||||
['Publication', '<code>not-declared</code>', 'Выпуск не заявлен', 'Материал опубликован в декабре 2027'],
|
||||
['Correction', '<code>not-declared</code>', 'Правка не заявлена', 'Ошибка найдена и исправлена'],
|
||||
['Outcome', '<code>not-declared</code>', 'Эффект не заявлен', 'Автор или production изменились'],
|
||||
console.log(result.ok);
|
||||
console.log(result.headers['Content-Security-Policy']);
|
||||
console.log(result.headers['Strict-Transport-Security']);
|
||||
// true
|
||||
// default-src 'self'; script-src 'self' 'nonce-7c2f1b8e9a4d6f0c'; object-src 'none'; base-uri 'self'
|
||||
// max-age=31536000; includeSubDomains`),
|
||||
h2('Порядок внедрения'),
|
||||
ol([
|
||||
'Соберите список ресурсов страницы и найдите inline scripts, eval, object, iframe, внешние CDN и API. Не начинайте с копирования чужой политики.',
|
||||
'Включите CSP в Report-Only и соберите нарушения по URL, директиве и типу ресурса. Отчёт не блокирует выполнение, поэтому не называйте его исправлением.',
|
||||
'Уберите лишние источники, замените inline-код на файл или nonce и добавьте тест на отсутствие unsafe-inline и unsafe-eval без обоснования.',
|
||||
'Переведите CSP в enforce на одной проверяемой странице и сравните ошибки загрузки с разрешённым списком.',
|
||||
'Включите HSTS только после проверки HTTPS для основного домена и поддоменов. Начните с контролируемого max-age, затем расширяйте.',
|
||||
'Проверьте rollback конфигурации: изменение header должно быть версионируемым, а не ручной правкой в одном proxy.',
|
||||
]),
|
||||
h2('Петля hand-off не должна замыкаться сама'),
|
||||
p('В корректирующем процессе обычно рисуют цикл: claim, feedback, revision, release. Для P118 такая схема опасна, если стрелки читаются как произошедшие события. Здесь петля обрывается до первого факта. Она принимает только named question и fixed temporal boundary, затем проверяет отсутствие hidden review inputs и declarations. При любом claimed quality, pass, publication, correction или outcome evaluator выдаёт stop. Он не предлагает исправление и не выбирает адресата.'),
|
||||
p('Это не отказ от ответственности. Наоборот, hand-off сохраняет ответственность на правильной границе. Будущий owner, если он вообще появится в новом scope, должен решить, нужен ли review, какие данные допустимы, кто увидит материалы и что будет считаться корректирующим действием. P118 не называет owner, потому что назначение роли тоже является действием с последствиями. Предсказать его «для полноты» означало бы создать несуществующую обязанность.'),
|
||||
p('В поле особенно легко подменить unknown правдоподобной деталью: вставить короткую цитату reviewer, номер revision, дату публикации или пример комментария. Даже synthetic-looking detail будет работать как evidence, если reader не видит её происхождения. Поэтому visual, таблица и code P118 описывают только статусы формы. Они не содержат заголовка будущей статьи, имени человека, оценки формулировки или примера исправленной фразы.'),
|
||||
figure('/assets/editorial/2027/author-manifesto-2027-revision-handoff-loop.svg', 'Петля корректирующей передачи: временная граница и отсутствие review ведут к hand-off; declared quality, publication, correction или outcome останавливают маршрут.', 'Диаграмма показывает fail-closed форму P118. Она не подтверждает review, feedback, правку, выпуск, развитие автора или производственный эффект.'),
|
||||
h2('Runnable пример не создаёт correction'),
|
||||
p('Запуск ниже не открывает процесс и не меняет запись. Код делает только одно: создаёт копию named in-memory literal, проверяет exact values и возвращает поля declarations. Нет обращения к filesystem, network, environment, clock, secrets, telemetry, system или data. Вывод <code>not-declared</code> не является рекомендацией ничего не исправлять; он показывает, что у этого scope нет права утверждать факт исправления.'),
|
||||
code("import { inspectHandoffLiteral } from './upgrade-2027-12.mjs';\n\nconst handoff = inspectHandoffLiteral();\nconsole.log(handoff.quality, handoff.correction, handoff.outcome, handoff.status);\n// not-declared not-declared not-declared synthetic-plan-hand-off"),
|
||||
h2('Порядок корректирующей передачи'),
|
||||
ol(['Оставить claim вопросом до появления разрешённого evidence; не называть его ошибкой заранее.', 'Зафиксировать даты и cutoff вместе с вопросом, чтобы future tense не стал fabricated history.', 'Проверить, что reviewer input, feedback и publication не спрятаны в метаданных, примере или подписи к визуалу.', 'Сохранить quality, pass, publication, correction и outcome как <code>not-declared</code>; не заменять неизвестное оптимистичным pending.', 'Передать только boundary и условия отдельного scope, не назначая автора, reviewer, owner или получателя.', 'Остановить evaluator на unknown source, unknown date, unknown question, hidden input или declared result.']),
|
||||
h2('Почему развитие автора не является итогом статьи'),
|
||||
p('Техническое письмо влияет на автора только через цепочку конкретных действий и наблюдений, а не через название раздела. Нужно различать: статья может описывать правило; reviewer может дать комментарий; автор может принять или отклонить его; затем возможна новая версия и отдельная проверка. Каждый переход требует своего evidence и права работать с ним. Склеить цепочку в «развитие» удобно для резюме, но бесполезно для инженерного решения.'),
|
||||
p('Будущий scope может вообще отказаться от персонального измерения. Возможно, достаточно поправить технический claim, а вопрос об авторе не относится к задаче. Возможно, feedback содержит чувствительные данные и не должен храниться в редакционном артефакте. Возможно, корректировка окажется не нужна, потому что новый источник изменит interpretation. P118 не выбирает между этими вариантами. Его полезность в том, что варианты остаются открытыми и не получают фальшивый prior.'),
|
||||
p('Такой hand-off также не разрешает задним числом использовать local trajectory как доказательство. Траектория голоса — будущая contract vocabulary: она может подсказать, какие проверки поставить, но не сообщает, что человек уже прошёл эти проверки. Editorial standard — vocabulary критериев, но не timestamp review. Unpublished artefact — не источник подтверждённого эффекта. Каждый из этих объектов может стать входом только после того, как другой scope задаст происхождение, доступ и допустимый вывод.'),
|
||||
p('Видимый stop — часть заботы о читателе. Если evaluator возвращает <code>stop-hidden-review-input</code>, это не ошибка будущего reviewer. Если возвращает <code>stop-declared-quality-pass-publication-correction-or-outcome</code>, это не оценка качества текста. Stop говорит только о несоответствии границе P118. Такое название не даёт оператору «починить» статус добавлением деталей; правильное действие — убрать неподтверждённый claim либо открыть другой authorised scope.'),
|
||||
h2('Что передаётся вместе с неизвестностью'),
|
||||
p('Хорошая передача не оставляет получателю пустой экран. Она даёт точное имя вопроса, даты, source boundary и перечень утверждений, которые пока нельзя делать. Этого достаточно, чтобы будущий scope решил, нужен ли вообще сбор данных. Нельзя добавлять к hand-off предполагаемую причину, потому что она начнёт направлять поиск. Нельзя добавлять готовый verdict, потому что тогда сбор превратится в подтверждение заранее выбранного ответа.'),
|
||||
p('Для корректировки важен разрыв между ошибкой и следствием. Даже если однажды обнаружится неверный claim, из него не следует, что нужно менять текст, публиковать заметку или оценивать автора. Возможно, claim был ограничен версией; возможно, аудитория не может применить исправление; возможно, источник сам неоднозначен. Новый scope обязан выбирать действие по доступному evidence и риску. P118 не создаёт этот выбор и не подменяет его заранее написанной петлёй revision.'),
|
||||
p('Полевой документ должен быть устойчив к смене получателя. Если hand-off зависит от конкретного имени, команды или внутренней очереди, он быстро становится историей о людях и требует дополнительных прав. Здесь получатель не назван. Это не недостаток коммуникации: так вопрос можно передать в надлежащий контекст, не создавая ложного обязательства человеку, который не видел исходных материалов и не соглашался их разбирать.'),
|
||||
p('Отдельно стоит различать correction и deletion. Удаление неподтверждённой фразы может быть правильным действием, но в P118 оно не произошло и не рекомендуется как результат. Пока нет реального текста и входов, нельзя определить, что именно требует изменения. Fixed evaluator охраняет именно эту границу: он не правит строку и не выбирает форму публикации. Его stop — сигнал, что вопрос нельзя завершить данными, которых никто не предоставил.'),
|
||||
p('Такой режим не делает field lens бесполезным. Он снижает стоимость будущего разбирательства: новый owner не тратит время на развенчание вымышленной chronology, не ищет несуществующий feedback и не объясняет, почему promised correction не выпущена. Вместо этого он получает маленький, точный контракт неизвестности. Если работа будет авторизована, этот контракт можно заменить доказательствами; если нет, он остаётся честным и не создаёт долгов перед людьми или production.'),
|
||||
h2('Почему nonce не лечит XSS'),
|
||||
p('Nonce разрешает конкретные скрипты, но не санитизирует пользовательский HTML и не исправляет небезопасный sink. Если приложение вставляет строку в <code>innerHTML</code>, разрешённый bootstrap может помочь атакующему выполнить уже загруженный код. CSP снижает последствия и ловит часть нарушений, но контекстное экранирование и безопасные API остаются обязательными.'),
|
||||
p('Диагностические отчёты CSP тоже требуют осторожности: URL может содержать чувствительные параметры, а third-party ресурс может присылать много шума. В отчёте храните только нужные поля, ограничивайте доступ и отделяйте нарушение политики от подтверждённой уязвимости. Заголовок — контроль браузера, не verdict о безопасности приложения.'),
|
||||
h2('Ограничения и следующий шаг'),
|
||||
p('P118 field не является персональной оценкой, development plan, review log, publication record, correction record, редакционным календарём или production report. Он не утверждает, что текст требует или не требует изменения. Его sources не подтверждают действие конкретного человека. Его SVG не является журналом процесса. The fixture проверяет только известные literals и fail-closed statuses; он не создаёт feedback и не измеряет качественный эффект.'),
|
||||
p('Следующий шаг допустим лишь там, где новый authorised scope назовёт вопрос, owner, разрешённые inputs, правила защиты данных, метод revision и возможный отказ от correction. Этот scope может принять или закрыть вопрос без действия. Он не должен задним числом приписывать P118 review или publishing history. Завершение P118 остаётся ограниченным: <code>synthetic-plan-hand-off</code> и <code>productionEffect: not-attempted</code>.')
|
||||
], fieldRefs);
|
||||
p('Генератор не проверяет браузерную поддержку, CDN, service worker, iframe-политику, сертификаты и preload. CSP Level 3 — рабочая редакция W3C, поэтому конкретную совместимость и статус директивы нужно сверять с целевыми браузерами. RFC 6797 не защищает первый небезопасный переход и не заменяет TLS.'),
|
||||
p('Следующий шаг — взять одну страницу, собрать Report-Only нарушения, закрыть источники по одному и добавить автоматический тест на заголовки. После enforce отдельно проверьте HSTS на каждом поддомене и храните процедуру возврата конфигурации рядом с кодом, чтобы ошибка не требовала ручной импровизации.'),
|
||||
], securityRefs);
|
||||
|
||||
const performanceRefs = [
|
||||
{ key: 'lcp', use: 'Определяет Largest Contentful Paint и API наблюдения за крупнейшей отрисовкой, чтобы измерение имело точный объект.', boundary: 'Working Draft может изменяться; LCP не измеряет всю скорость страницы, отзывчивость или стабильность layout.' },
|
||||
{ key: 'performance', use: 'Задаёт Performance Timeline и доступ к измеряемым entry, на которых строятся браузерные наблюдения.', boundary: 'Не задаёт ваши пороги, backend-агрегацию, sampling и причинность медленной страницы.' },
|
||||
{ key: 'webVitals', use: 'Фиксирует рекомендованные пороги LCP, INP и CLS и правило p75 по сегментам для практического triage.', boundary: 'Это guidance, а не гарантия UX и не доказательство причины конкретной регрессии.' },
|
||||
];
|
||||
|
||||
const mechanism = revision({
|
||||
slug: 'editorial-2027-12-mechanism-author-manifesto',
|
||||
title: 'Web performance budget: LCP, INP и CLS без одной магической метрики',
|
||||
categories: ['Frontend', 'Производительность'],
|
||||
cover: '/assets/editorial/2027/author-manifesto-2027-quality-rubric-matrix.svg',
|
||||
excerpt: 'Как читать пользовательские web-метрики: разделить LCP, INP и CLS, выбрать пороги и не менять код по одному красивому числу.',
|
||||
readingMinutes: 16,
|
||||
}, [
|
||||
p('Проблема начинается с отчёта «страница медленная». Цена такого диагноза — оптимизировать не тот участок: уменьшить JavaScript, пока главный баннер ждёт шрифт, или ускорить первый paint, оставив клик заблокированным длинной задачей. Одно среднее число скрывает разные виды задержки.'),
|
||||
p('Причина — смешать LCP, INP и CLS в общий score без определения окна и percentile. LCP отвечает за крупнейший видимый элемент, INP — за отзывчивость взаимодействий, CLS — за неожиданные сдвиги. У каждой метрики свой источник, порог и способ исправления. Сначала нужно понять измерение, затем выбирать действие.'),
|
||||
h2('Три метрики — три пользовательских вопроса'),
|
||||
p('LCP показывает, когда крупнейший контентный элемент стал видимым в пределах загрузки. Большой LCP часто связан с TTFB, критическим CSS, размером изображения или шрифтом. INP оценивает задержку взаимодействий и указывает на работу main thread после ввода. CLS суммирует неожиданные сдвиги layout, например из-за изображения без размеров или поздней рекламы.'),
|
||||
p('Метрика не говорит, где находится причина. Плохой LCP может быть следствием сервера, сети или браузера; плохой INP — длинной задачи, стороннего скрипта или тяжёлого обработчика; плохой CLS — отсутствующего места под контент. Поэтому budget должен включать и измерение, и диагностический разрез: URL, устройство, connection, release и элемент.'),
|
||||
table('Как читать Core Web Vitals', ['Метрика', 'Вопрос пользователя', 'Хорошая граница', 'Первый разрез'], [
|
||||
['LCP', 'крупнейший контент появился?', '≤ 2500 ms', 'TTFB, resource, element'],
|
||||
['INP', 'интерфейс ответил после ввода?', '≤ 200 ms', 'long task, handler, device'],
|
||||
['CLS', 'страница не сдвинулась?', '≤ 0.1', 'element, font, reserved space'],
|
||||
['Percentile', 'у какой доли пользователей проблема?', 'p75 по сегменту', 'country, device, release'],
|
||||
['Budget', 'какой порог блокирует выпуск?', 'явно в CI/monitoring', 'threshold + owner action'],
|
||||
]),
|
||||
h2('Budget не равен среднему'),
|
||||
p('Среднее значение сглаживает хвост и может выглядеть здоровым при плохом опыте части пользователей. Для пользовательских web-метрик часто нужен p75 в определённом сегменте, но даже percentile не спасает от смешения мобильных и десктопных данных. Порог должен быть привязан к одинаковому URL, устройству, версии и периоду наблюдения.'),
|
||||
p('Лабораторный Lighthouse и field data отвечают на разные вопросы. Лаборатория воспроизводима и удобна для CI, но не содержит реального разнообразия сети. Field data показывает пользователей, но зависит от sampling, трафика и состава сегмента. Решение об оптимизации подтверждайте обоими видами данных или честно называйте, какой слой измерен.'),
|
||||
figure('/assets/editorial/2027/author-manifesto-2027-quality-rubric-matrix.svg', 'Матрица web-performance: LCP, INP и CLS имеют разные объекты и пороги; lab и field measurement нельзя складывать в один безымянный score.', 'Схема связывает метрику с вопросом и первым диагностическим разрезом. Улучшение одного показателя не доказывает исправление остальных.'),
|
||||
h2('Runnable-пример: классифицировать три числа'),
|
||||
p('Функция принимает миллисекунды LCP и INP, а также значение CLS. Она возвращает статус каждой метрики и общий худший статус. Это учебный классификатор, не реализация браузерного PerformanceObserver: реальные значения нужно собирать из API и агрегировать по сегментам. Входы ниже показывают пороги без округления.'),
|
||||
code(`import { classifyWebVitals } from './upgrade-2027-12.mjs';
|
||||
|
||||
const release = classifyWebVitals({
|
||||
lcpMs: 2180,
|
||||
inpMs: 240,
|
||||
cls: 0.08,
|
||||
});
|
||||
const invalid = classifyWebVitals({
|
||||
lcpMs: 1200,
|
||||
inpMs: -1,
|
||||
cls: 0.02,
|
||||
});
|
||||
|
||||
console.log(release.overall, release.lcp, release.inp, release.layout);
|
||||
console.log(invalid.ok, invalid.reason);
|
||||
// needs-improvement good needs-improvement good
|
||||
// false vital-input-invalid`),
|
||||
h2('Порядок поиска причины'),
|
||||
ol([
|
||||
'Определите URL, сегмент, percentile и окно измерения. Не сравнивайте p75 мобильного трафика со средним для всех устройств.',
|
||||
'Для плохого LCP найдите element и разделите TTFB, загрузку ресурса и отрисовку. Для INP найдите interaction и long task; для CLS — shifted element.',
|
||||
'Сформулируйте один budget на релиз и один diagnostic signal. Не блокируйте сборку по метрике, которую CI не может воспроизвести.',
|
||||
'Проверьте lab fixture и field distribution отдельно. Разница между ними — информация о среде, а не повод выбрать удобный источник.',
|
||||
'Измените один тяжёлый участок: critical resource, handler, image dimensions или layout reservation.',
|
||||
'Повторите измерение тем же сегментом и окном. Снижение LCP не закрывает INP/CLS автоматически.',
|
||||
]),
|
||||
h2('Почему порог не является причинностью'),
|
||||
p('Пересечение границы 2500 мс сообщает о классификации, но не объясняет, что исправить. Порог полезен для triage и разговора о риске, а не для выбора виновника. Если после preload LCP улучшился, это ещё не доказывает, что preload был единственной причиной: изменились сервер, кэш или состав трафика.'),
|
||||
p('У performance есть побочный эффект оптимизации. Сжатие изображения уменьшает LCP, но может увеличить CPU-декодирование или ухудшить качество. Разделение JavaScript может помочь INP, но добавить запросы и повлиять на LCP. В budget следует держать соседние ограничения — error rate, size, long tasks — и проверять, что выигрыш одной метрики не создаёт новый долг.'),
|
||||
h2('Ограничения и следующий шаг'),
|
||||
p('Классификатор использует учебные пороги и не считает реальные percentile. W3C LCP и Performance Timeline описывают API и объект измерения, но не обещают, что конкретный dashboard правильно собрал данные. INP и CLS требуют своих источников и сегментации; один локальный запуск не является field evidence.'),
|
||||
p('Следующий шаг — выбрать один URL и сделать таблицу p75 для LCP, INP и CLS по двум сегментам. Для каждой плохой строки добавьте element/interaction и один проверяемый сигнал причины. После оптимизации повторите ту же выборку и проверьте соседние метрики, а не только тот показатель, который был в заголовке задачи.'),
|
||||
], performanceRefs);
|
||||
|
||||
const writingRefs = [
|
||||
{ key: 'nistIncident', use: 'Даёт дисциплину подготовки, обнаружения, анализа, containment, восстановления и работы после инцидента для структуры эксплуатационной инструкции.', boundary: 'Не знает ваших команд, прав доступа, сервисных зависимостей и порогов остановки.' },
|
||||
{ key: 'rfc2119', use: 'Фиксирует различие между обязательным, рекомендуемым и необязательным действием, чтобы инструкция не прятала приоритет в тоне.', boundary: 'Не является руководством по эксплуатации, не проверяет команду и не даёт разрешение менять production.' },
|
||||
];
|
||||
|
||||
const field = revision({
|
||||
slug: 'editorial-2027-12-field-author-manifesto',
|
||||
title: 'Эксплуатационная инструкция: симптом, действие, откат и проверка',
|
||||
categories: ['Техническая документация', 'Надёжность'],
|
||||
cover: '/assets/editorial/2027/author-manifesto-2027-revision-handoff-loop.svg',
|
||||
excerpt: 'Как написать короткую инструкцию для опасной операции, чтобы читатель видел вход, ограничения, обратимое действие и критерий завершения.',
|
||||
readingMinutes: 15,
|
||||
}, [
|
||||
p('Проблема инструкции обнаруживается в первый же сбой: читатель знает, что сервис нездоров, но не понимает, какой командой начать и как не усугубить ситуацию. Цена расплывчатого текста — параллельные ручные действия, потеря исходных метрик и откат без проверки данных.'),
|
||||
p('Причина — писать статью как последовательность уверенных советов. В эксплуатации важнее не количество команд, а граница каждой команды: какое условие должно быть истинным, какой результат ожидается и когда нужно остановиться. Reader-facing текст должен позволить сверить вход, выполнить один шаг и увидеть измеримый выход.'),
|
||||
h2('Карточка операции — минимальная единица'),
|
||||
p('Полезная инструкция начинается с симптома и scope: например, «5xx выше 5% на POST /payments в одном регионе». Затем идут precondition, действие, rollback и verification. Эти поля не формальность. Без scope оператор может отключить здоровый трафик; без precondition — выполнить команду на неправильной версии; без verification — принять завершение команды за восстановление.'),
|
||||
p('Порядок должен отражать риск, а не удобство автора документа. Сначала сохранить наблюдаемый факт, потом ограничить влияние, затем изменить один рычаг. После действия нужен интервал наблюдения и критерий возврата. Если операция необратима, инструкция должна прямо сказать, что её нельзя запускать без отдельного разрешения и резервного пути.'),
|
||||
table('Структура проверяемой инструкции', ['Поле', 'Что написать', 'Проверяемый вопрос', 'Нельзя заменять'], [
|
||||
['Symptom', 'метрика, endpoint, время', 'что именно нарушено?', '«сервис плохой»'],
|
||||
['Scope', 'регион, версия, процент', 'кого затрагивает?', '«все пользователи»'],
|
||||
['Precondition', 'доступ, версия, backup', 'можно ли выполнять шаг?', '«должно работать»'],
|
||||
['Action', 'одна команда/изменение', 'что изменится?', 'список несвязанных команд'],
|
||||
['Rollback', 'обратное действие и условие', 'как вернуть состояние?', '«откатить при проблеме»'],
|
||||
['Verification', 'метрика и окно', 'что считать восстановлением?', '«проверить вручную»'],
|
||||
]),
|
||||
h2('Глаголы задают риск'),
|
||||
p('Рекомендации вроде «проверьте», «убедитесь» и «при необходимости» слишком широки, если рядом нет объекта. RFC 2119 полезен как дисциплина модальности: <code>MUST</code> можно применять к обязательной precondition, <code>SHOULD</code> — к шагу с допустимым исключением, а <code>MAY</code> — к необязательной диагностике. В русском тексте это можно перевести обычными словами, сохранив однозначность.'),
|
||||
p('Каждый command block должен иметь входы и ожидаемый результат. Если команда меняет состояние, рядом укажите право, namespace и способ увидеть diff. Не вставляйте секрет в пример и не предполагайте, что читатель знает локальные alias. Хорошая краткость убирает лишние слова, но не убирает условия безопасности.'),
|
||||
figure('/assets/editorial/2027/author-manifesto-2027-revision-handoff-loop.svg', 'Петля эксплуатационной инструкции: симптом и precondition ведут к одному действию, затем к проверке метрики и условному откату.', 'Диаграмма показывает reader-facing последовательность. Текст считается завершённым только после наблюдаемой проверки, а не после окончания команды.'),
|
||||
h2('Runnable-пример: проверить карточку runbook'),
|
||||
p('Функция принимает объект с шестью полями и проверяет, что каждое достаточно содержательно, rollback назван явно, а verification ссылается на наблюдаемый сигнал. Это учебная проверка структуры документа, не оценка литературного стиля и не разрешение выполнить команду. Вход ниже показывает минимальный принятый набор и отказ без измеримой проверки.'),
|
||||
code(`import { validateRunbookCard } from './upgrade-2027-12.mjs';
|
||||
|
||||
const card = validateRunbookCard({
|
||||
symptom: '5xx выше 5 процентов на POST /payments',
|
||||
scope: 'region eu-west, release 42, 10 percent traffic',
|
||||
precondition: 'есть доступ к flag и сохранён dashboard за 15 минут',
|
||||
action: 'отключить flag payments-v2 для 10 процентов трафика',
|
||||
rollback: 'вернуть flag payments-v2 после проверки результата',
|
||||
verification: 'проверить error rate и p95 в течение 10 минут',
|
||||
});
|
||||
|
||||
console.log(card.ok, card.order.join(' -> '));
|
||||
// true symptom -> scope -> precondition -> action -> rollback -> verification`),
|
||||
h2('Порядок редакторской проверки инструкции'),
|
||||
ol([
|
||||
'В первых двух абзацах назовите симптом и цену ошибки. Reader должен понять, для какой ситуации текст предназначен.',
|
||||
'Сделайте scope измеримым: endpoint, регион, версия, доля трафика и временное окно.',
|
||||
'Перед каждой опасной командой поставьте precondition и ожидаемый output. Если output не наблюдаем, шаг нельзя считать проверенным.',
|
||||
'Разделите один шаг изменения и rollback. Для rollback укажите условие, а не только команду возврата.',
|
||||
'Добавьте таблицу решений для соседних симптомов, чтобы reader не применил одинаковое действие к 500, timeout и 409.',
|
||||
'Запустите учебный пример с валидной и неполной карточкой, затем перечитайте текст на мобильной ширине и уберите длинные строки.',
|
||||
]),
|
||||
h2('Технический текст не заменяет разрешение'),
|
||||
p('Даже подробная инструкция не даёт права менять production. Доступ, approval и окно операции должны жить в локальном процессе, а статья должна честно указать, какие precondition ей неизвестны. Если шаг может удалить данные или нарушить доступность, reader должен увидеть остановку до команды, а не бодрый призыв продолжать.'),
|
||||
p('Не стоит добавлять в runbook вымышленные метрики и имена сервисов только для гладкого чтения. Лучше оставить placeholder с точным описанием входа, чем заставить оператора повторить чужой пример. Учебный пример должен быть маркирован как учебный и не содержать секретов, настоящих hostnames или команд с необратимым эффектом.'),
|
||||
h2('Ограничения и следующий шаг'),
|
||||
p('Проверка карточки не знает прав, shell, облака, backup, lock и реальных порогов. NIST SP 800-61 задаёт общий цикл обращения с инцидентом, но ваша инструкция всё равно должна назвать локальные сигналы и способ остановки. RFC 2119 помогает выбрать модальность, но не тестирует исполнимость команды.'),
|
||||
p('Следующий шаг — взять один существующий alert и переписать его в шесть полей, затем прогнать на staging с безопасным флагом и настоящей проверкой метрики. Если reader не может назвать ожидаемый output любого шага, вернитесь к precondition и добавьте наблюдаемый критерий.'),
|
||||
], writingRefs);
|
||||
|
||||
export const revisions = deepFreeze([practice, mechanism, field]);
|
||||
|
||||
export function runTechnicalWritingFixture() {
|
||||
const validCard = { symptom: '5xx выше 5 процентов на endpoint', scope: 'region eu-west release 42', precondition: 'есть доступ и сохранён dashboard', action: 'отключить flag на десяти процентах', rollback: 'вернуть flag после проверки', verification: 'проверить метрику error rate 10 минут' };
|
||||
const cases = [
|
||||
['security-headers-accept', buildSecurityHeaders({ nonce: '7c2f1b8e9a4d6f0c' }).ok, true],
|
||||
['security-headers-reject', buildSecurityHeaders({ nonce: 'short' }).reason, 'security-header-input-invalid'],
|
||||
['vitals-classify', classifyWebVitals({ lcpMs: 2180, inpMs: 240, cls: 0.08 }).overall, 'needs-improvement'],
|
||||
['vitals-reject', classifyWebVitals({ lcpMs: -1, inpMs: 200, cls: 0.1 }).reason, 'vital-input-invalid'],
|
||||
['runbook-accept', validateRunbookCard(validCard).ok, true],
|
||||
['runbook-reject', validateRunbookCard({ ...validCard, verification: 'посмотреть' }).reason, 'verification-must-be-observable'],
|
||||
];
|
||||
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 = runAuthorManifestoFixture();
|
||||
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-12/.test(text) && /2026-07-31/.test(text) && /productionEffect: not-attempted/.test(text); });
|
||||
const fixture = runTechnicalWritingFixture();
|
||||
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('--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