revise December 2025 year synthesis articles
Build and deploy / deploy (push) Successful in 18s

This commit is contained in:
2026-07-31 18:43:29 +03:00
parent def4f0a1f5
commit 50d783d0d5
7 changed files with 662 additions and 1 deletions
+2
View File
@@ -90,6 +90,7 @@ import { revisions as august2025Revisions } from '../scripts/upgrade-2025-08.mjs
import { revisions as september2025Revisions } from '../scripts/upgrade-2025-09.mjs';
import { revisions as october2025Revisions } from '../scripts/upgrade-2025-10.mjs';
import { revisions as november2025Revisions } from '../scripts/upgrade-2025-11.mjs';
import { revisions as december2025Revisions } from '../scripts/upgrade-2025-12.mjs';
// This layer replaces archived source entries without losing their stable slug and date.
export const editorialRevisions = [
@@ -185,4 +186,5 @@ export const editorialRevisions = [
...september2025Revisions,
...october2025Revisions,
...november2025Revisions,
...december2025Revisions,
];
@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 900" role="img" aria-labelledby="title desc">
<title id="title">Цикл counterfactual review годового решения</title>
<desc id="desc">Замкнутая схема показывает последовательность: прочитать запись, найти альтернативу, отметить стоимость и наблюдение, сформулировать неизвестное, проверить границу сравнения и вернуть только synthetic hand-off или запрос на исправление.</desc>
<rect width="1200" height="900" fill="#0b1220"/>
<rect x="44" y="40" width="1112" height="820" rx="28" fill="#111c31" stroke="#365277" stroke-width="3"/>
<text x="92" y="112" fill="#f5f8ff" font-family="Arial, sans-serif" font-size="42" font-weight="700">Counterfactual review</text>
<text x="92" y="154" fill="#a9bddb" font-family="Arial, sans-serif" font-size="25">Не пересказ года, а короткая проверка границы вывода</text>
<g font-family="Arial, sans-serif" text-anchor="middle">
<rect x="438" y="198" width="324" height="92" rx="20" fill="#183b5b" stroke="#4db6ff" stroke-width="3"/><text x="600" y="238" fill="#e8f4ff" font-size="27" font-weight="700">Прочитать запись</text><text x="600" y="270" fill="#c3d9ef" font-size="21">decision + observation</text>
<rect x="774" y="392" width="312" height="96" rx="20" fill="#183b5b" stroke="#4db6ff" stroke-width="3"/><text x="930" y="430" fill="#e8f4ff" font-size="27" font-weight="700">Найти альтернативу</text><text x="930" y="462" fill="#c3d9ef" font-size="21">не выбранный путь</text>
<rect x="438" y="596" width="324" height="96" rx="20" fill="#3a2948" stroke="#d3a5ff" stroke-width="3"/><text x="600" y="634" fill="#f5eaff" font-size="27" font-weight="700">Назвать unknown</text><text x="600" y="666" fill="#e7cfff" font-size="21">чего запись не знает</text>
<rect x="114" y="392" width="312" height="96" rx="20" fill="#183b5b" stroke="#4db6ff" stroke-width="3"/><text x="270" y="430" fill="#e8f4ff" font-size="27" font-weight="700">Проверить cost</text><text x="270" y="462" fill="#c3d9ef" font-size="21">принятый trade-off</text>
<rect x="438" y="744" width="324" height="70" rx="18" fill="#293b2f" stroke="#7ae0a1" stroke-width="3"/><text x="600" y="788" fill="#eafff0" font-size="25" font-weight="700">synthetic hand-off only</text>
</g>
<g fill="none" stroke="#6ea8fe" stroke-width="6" stroke-linecap="round" stroke-linejoin="round">
<path d="M762 244 C880 248 940 302 930 390"/><path d="M930 488 C900 590 790 642 764 644"/><path d="M438 644 C320 634 270 580 270 490"/><path d="M270 390 C302 290 398 248 436 246"/>
</g>
<g fill="#6ea8fe"><path d="M929 380 l-14 -23 h28z"/><path d="M773 646 l-22 -14 v28z"/><path d="M270 500 l-14 -23 h28z"/><path d="M427 246 l22 -14 v28z"/></g>
<path d="M600 692 V742" stroke="#7ae0a1" stroke-width="6" stroke-linecap="round"/>
<path d="M600 735 l-15 -23 h30z" fill="#7ae0a1"/>
<text x="600" y="340" fill="#ffd9a6" font-family="Arial, sans-serif" font-size="24" text-anchor="middle">Если comparison boundary отсутствует — stop and repair</text>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 900" role="img" aria-labelledby="title desc">
<title id="title">Матрица решения, стоимости и наблюдения</title>
<desc id="desc">Таблица показывает, что факт решения, принятая стоимость и наблюдение после изменения не образуют доказательства эффекта без неизвестного и заранее указанной границы сравнения.</desc>
<rect width="1200" height="900" fill="#0b1220"/>
<rect x="44" y="40" width="1112" height="820" rx="28" fill="#111c31" stroke="#365277" stroke-width="3"/>
<text x="92" y="112" fill="#f5f8ff" font-family="Arial, sans-serif" font-size="42" font-weight="700">Наблюдение не равно эффекту</text>
<text x="92" y="154" fill="#a9bddb" font-family="Arial, sans-serif" font-size="25">Матрица удерживает границу между записью и причинным выводом</text>
<g font-family="Arial, sans-serif" font-size="25">
<rect x="92" y="214" width="250" height="76" fill="#244469"/><rect x="342" y="214" width="250" height="76" fill="#244469"/><rect x="592" y="214" width="250" height="76" fill="#244469"/><rect x="842" y="214" width="266" height="76" fill="#244469"/>
<text x="128" y="260" fill="#eff7ff" font-weight="700">Слой</text><text x="375" y="260" fill="#eff7ff" font-weight="700">Есть в записи</text><text x="625" y="260" fill="#eff7ff" font-weight="700">Чего нет</text><text x="876" y="248" fill="#eff7ff" font-size="22" font-weight="700">Можно</text><text x="876" y="274" fill="#eff7ff" font-size="22" font-weight="700">написать</text>
<rect x="92" y="290" width="1016" height="112" fill="#162a43" stroke="#365277"/><text x="120" y="334" fill="#dcecff">Решение</text><text x="370" y="334" fill="#c3d9ef">вариант и дата</text><text x="620" y="334" fill="#c3d9ef">контрфакт</text><text x="870" y="326" fill="#e8f4ff">выбор был</text><text x="870" y="360" fill="#e8f4ff">зафиксирован</text>
<rect x="92" y="402" width="1016" height="112" fill="#162a43" stroke="#365277"/><text x="120" y="446" fill="#dcecff">Стоимость</text><text x="370" y="446" fill="#c3d9ef">принятый расход</text><text x="620" y="446" fill="#c3d9ef">полная цена</text><text x="870" y="438" fill="#e8f4ff">trade-off</text><text x="870" y="472" fill="#e8f4ff">назван</text>
<rect x="92" y="514" width="1016" height="112" fill="#162a43" stroke="#365277"/><text x="120" y="558" fill="#dcecff">Наблюдение</text><text x="370" y="550" fill="#c3d9ef">fixed before/after</text><text x="620" y="550" fill="#c3d9ef">другая причина</text><text x="870" y="542" fill="#e8f4ff">факт виден</text><text x="870" y="576" fill="#e8f4ff">после решения</text>
<rect x="92" y="626" width="1016" height="112" fill="#43283a" stroke="#d37aa9" stroke-width="2"/><text x="120" y="670" fill="#ffe4f2">Effect claim</text><text x="370" y="670" fill="#ffd2e8">корреляция</text><text x="620" y="670" fill="#ffd2e8">comparison boundary</text><text x="870" y="662" fill="#ffe4f2">stop:</text><text x="870" y="696" fill="#ffe4f2">не доказано</text>
</g>
<path d="M216 775 H988" stroke="#d3a5ff" stroke-width="4" stroke-dasharray="10 10"/>
<text x="600" y="827" fill="#e7cfff" font-family="Arial, sans-serif" font-size="24" text-anchor="middle">Добавить unknown и границу сравнения до причинного вывода</text>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

@@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 900" role="img" aria-labelledby="title desc">
<title id="title">Таймлайн годового инженерного review</title>
<desc id="desc">Вертикальная последовательность от зафиксированного решения через альтернативы, стоимость, наблюдение, неизвестное и границу сравнения к synthetic review hand-off.</desc>
<rect width="1200" height="900" fill="#0b1220"/>
<rect x="48" y="42" width="1104" height="816" rx="28" fill="#111c31" stroke="#365277" stroke-width="3"/>
<text x="100" y="116" fill="#f5f8ff" font-family="Arial, sans-serif" font-size="42" font-weight="700">Годовой review: запись до вывода</text>
<text x="100" y="158" fill="#a9bddb" font-family="Arial, sans-serif" font-size="25">Каждый следующий блок сужает допустимое утверждение</text>
<line x1="246" y1="234" x2="246" y2="732" stroke="#6ea8fe" stroke-width="8" stroke-linecap="round"/>
<g fill="#6ea8fe"><circle cx="246" cy="234" r="17"/><circle cx="246" cy="334" r="17"/><circle cx="246" cy="434" r="17"/><circle cx="246" cy="534" r="17"/><circle cx="246" cy="634" r="17"/><circle cx="246" cy="732" r="17"/></g>
<g font-family="Arial, sans-serif">
<rect x="306" y="190" width="748" height="88" rx="16" fill="#183b5b" stroke="#4db6ff" stroke-width="2"/>
<text x="340" y="226" fill="#e8f4ff" font-size="29" font-weight="700">1. Решение</text><text x="560" y="226" fill="#c3d9ef" font-size="25">что изменили в fixed модели</text>
<rect x="306" y="290" width="748" height="88" rx="16" fill="#183b5b" stroke="#4db6ff" stroke-width="2"/>
<text x="340" y="326" fill="#e8f4ff" font-size="29" font-weight="700">2. Альтернативы</text><text x="620" y="326" fill="#c3d9ef" font-size="25">что отвергли и почему</text>
<rect x="306" y="390" width="748" height="88" rx="16" fill="#183b5b" stroke="#4db6ff" stroke-width="2"/>
<text x="340" y="426" fill="#e8f4ff" font-size="29" font-weight="700">3. Стоимость</text><text x="570" y="426" fill="#c3d9ef" font-size="25">какой расход или риск принят</text>
<rect x="306" y="490" width="748" height="88" rx="16" fill="#183b5b" stroke="#4db6ff" stroke-width="2"/>
<text x="340" y="526" fill="#e8f4ff" font-size="29" font-weight="700">4. Наблюдение</text><text x="610" y="526" fill="#c3d9ef" font-size="25">что зафиксировано после</text>
<rect x="306" y="590" width="748" height="88" rx="16" fill="#3a2948" stroke="#d3a5ff" stroke-width="2"/>
<text x="340" y="626" fill="#f5eaff" font-size="29" font-weight="700">5. Unknown</text><text x="560" y="626" fill="#e7cfff" font-size="25">какой ответ ещё отсутствует</text>
<rect x="306" y="690" width="748" height="84" rx="16" fill="#293b2f" stroke="#7ae0a1" stroke-width="2"/>
<text x="340" y="726" fill="#eafff0" font-size="29" font-weight="700">6. Граница сравнения</text><text x="740" y="726" fill="#c7f1d6" font-size="23">не эффект</text>
</g>
<path d="M246 786 L246 816 L1020 816" fill="none" stroke="#7ae0a1" stroke-width="5" stroke-linecap="round"/>
<circle cx="1042" cy="816" r="22" fill="#7ae0a1"/>
<text x="620" y="827" fill="#d8ffe5" font-family="Arial, sans-serif" font-size="25" font-weight="700" text-anchor="middle">только synthetic review hand-off</text>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

+458
View File
@@ -0,0 +1,458 @@
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
const p = (text) => '<p>' + text + '</p>';
const h2 = (text) => '<h2>' + text + '</h2>';
const code = (text) => '<pre><code>' + escapeHtml(text) + '</code></pre>';
const ol = (items) => '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
const figure = (src, alt, caption) => '<figure><img src="' + src + '" alt="' + alt + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
const table = (caption, headers, rows) => '<div class="table-scroll"><table><caption>' + caption + '</caption><thead><tr>' + headers.map((item) => '<th scope="col">' + item + '</th>').join('') + '</tr></thead><tbody>' + rows.map((row) => '<tr>' + row.map((item) => '<td>' + item + '</td>').join('') + '</tr>').join('') + '</tbody></table></div>';
function plainText(content) {
return content
.replace(/<[^>]+>/g, ' ')
.replace(/&(?:quot|amp|lt|gt|#039);/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
function bodyText(content) {
return plainText(content.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, ''));
}
function deepFreeze(value) {
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
Object.values(value).forEach(deepFreeze);
Object.freeze(value);
}
return value;
}
const sources = deepFreeze([
{
title: 'RFC 3339, Date and Time on the Internet: Timestamps',
url: 'https://www.rfc-editor.org/rfc/rfc3339.txt',
version: 'RFC 3339, July 2002, immutable RFC text',
claim: 'Раздел 5.6 задаёт форму date-time как full-date, букву T и full-time с часовым offset. В fixed модели это даёт проверяемый синтаксис временной отметки записи.',
boundary: 'RFC описывает формат времени, а не причинность решения, смысл метрики или правила годового review.',
},
{
title: 'RFC 7282, On Consensus',
url: 'https://www.rfc-editor.org/rfc/rfc7282.txt',
version: 'RFC 7282, June 2014, immutable RFC text',
claim: 'Документ называет trade-off неустранимой частью инженерного выбора и отделяет техническое рассмотрение возражения от простого подсчёта голосов.',
boundary: 'Это контекст инженерного компромисса в IETF, не готовая процедура для другой организации и не доказательство результата synthetic модели.',
},
{
title: 'NIST SP 800-61r3, Incident Response Recommendations and Considerations for Cybersecurity Risk Management',
url: 'https://nvlpubs.nist.gov/nistpubs/specialpublications/nist.sp.800-61r3.pdf',
version: 'NIST SP 800-61r3, April 2025, immutable published PDF',
claim: 'В модели документа lessons learned анализируются, приоритизируются и возвращаются в Improvement. Это узкий пример цикла наблюдение → разбор → изменение процесса.',
boundary: 'Публикация посвящена cybersecurity incident response; она не предписывает наш формат ADR, не валидирует causal claim и не делает годовой текст доказательством эффекта.',
},
]);
function sourceList() {
return '<ul>' + sources.map((source) => '<li><a href="' + source.url + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(source.title) + '</a> — версия: ' + escapeHtml(source.version) + '. ' + escapeHtml(source.claim) + ' Граница: ' + escapeHtml(source.boundary) + '</li>').join('') + '</ul>';
}
const FIXED_YEAR_SYNTHESIS = deepFreeze({
'complete-timeline-v1': {
timeline: [
{
at: '2025-02-03T09:00:00Z',
decision: 'в fixed учебной очереди разделить проверку записи и формирование сводки',
alternatives: ['оставить один общий шаг', 'сначала формировать сводку, затем проверять запись'],
cost: 'один дополнительный проход по fixed literal и отдельный статус unknown',
observation: 'в трёх фиксированных прогонах порядок статусов читается без догадки о причине',
unknown: 'как тот же порядок поведёт себя вне frozen учебного входа',
comparisonBoundary: 'сопоставлены только заданные before/after literals; внешних условий, контрольной группы и причинного вывода нет',
claim: 'no-effect-claim',
},
{
at: '2025-06-18T11:30:00Z',
decision: 'в fixed отчёте оставить отвергнутый вариант рядом с выбранным',
alternatives: ['хранить только выбранный путь', 'заменить альтернативу общим словом «проще»'],
cost: 'карточка становится длиннее и требует явного описания trade-off',
observation: 'читатель видит, на какой вопрос выбранный путь отвечал в synthetic примере',
unknown: 'был ли в реальной среде тот же набор ограничений',
comparisonBoundary: 'наблюдение относится к читаемости одного fixed literal, а не к качеству работы, скорости или результату производства',
claim: 'no-effect-claim',
},
{
at: '2025-10-07T14:45:00Z',
decision: 'в конце fixed записи отделить observation от следующего запроса',
alternatives: ['написать итог одним абзацем', 'объявить наблюдение итоговым эффектом'],
cost: 'нужно оставить неполный ответ и не закрыть историю красивым выводом',
observation: 'проверка возвращает отдельные поля observation, unknown и comparison boundary',
unknown: 'какой внешний фактор мог бы объяснить различие в другом контексте',
comparisonBoundary: 'model не сравнивает реальные периоды и не измеряет causal effect; она только проверяет полноту учебной записи',
claim: 'no-effect-claim',
},
],
},
'correlation-only-v1': {
timeline: [
{
at: '2025-04-21T08:20:00Z',
decision: 'в fixed карточке сменить порядок двух учебных шагов',
alternatives: ['оставить прежний порядок', 'добавить отдельную проверку перед сменой'],
cost: 'не указан',
observation: 'после смены в literal встречается другое значение',
unknown: '',
comparisonBoundary: '',
claim: 'effect-established',
},
],
},
'missing-traceability-v1': {
timeline: [
{
at: 'not-rfc3339',
decision: '',
alternatives: ['неизвестно'],
cost: '',
observation: '',
unknown: '',
comparisonBoundary: '',
claim: 'no-effect-claim',
},
],
},
});
function isKnownFixedInput(input) {
return Object.values(FIXED_YEAR_SYNTHESIS).includes(input);
}
function hasText(value) {
return typeof value === 'string' && value.trim().length > 0;
}
function addIssue(issues, issue) {
if (!issues.includes(issue)) issues.push(issue);
}
function hasRfc3339Utc(value) {
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(value);
}
function inspectFixedYearSynthesis(input) {
if (!isKnownFixedInput(input)) {
return deepFreeze({
accepted: false,
reasons: ['unknown-fixed-input'],
handoff: 'stop-and-repair-boundary',
productionEffect: 'not-attempted',
});
}
const reasons = [];
for (const item of input.timeline) {
if (!hasRfc3339Utc(item.at)) addIssue(reasons, 'invalid-timeline-time');
if (!hasText(item.decision)) addIssue(reasons, 'missing-decision-traceability');
if (!Array.isArray(item.alternatives) || item.alternatives.length < 2 || item.alternatives.some((alternative) => !hasText(alternative))) {
addIssue(reasons, 'missing-alternative');
}
if (!hasText(item.cost) || item.cost === 'не указан') addIssue(reasons, 'missing-cost');
if (!hasText(item.observation)) addIssue(reasons, 'missing-observation');
if (!hasText(item.unknown)) addIssue(reasons, 'missing-unknown');
if (!hasText(item.comparisonBoundary)) addIssue(reasons, 'missing-comparison-boundary');
if (item.claim !== 'no-effect-claim') addIssue(reasons, 'effect-claim-not-allowed');
}
return deepFreeze({
accepted: reasons.length === 0,
reasons,
handoff: reasons.length === 0 ? 'synthetic-review-hand-off-only' : 'stop-and-repair-boundary',
productionEffect: 'not-attempted',
});
}
export function createFixedYearSynthesisInput(name) {
return FIXED_YEAR_SYNTHESIS[name] || null;
}
export function inspectYearSynthesis(input) {
return inspectFixedYearSynthesis(input);
}
export function prepareSyntheticYearReviewHandOff(report) {
const accepted = report?.accepted === true && report?.handoff === 'synthetic-review-hand-off-only';
return deepFreeze({
kind: 'fixed-synthetic-year-review-hand-off-v1',
accepted,
status: accepted ? 'synthetic-review-hand-off-only' : 'stop-and-repair-boundary',
productionEffect: 'not-attempted',
});
}
export function runYearSynthesisFixture() {
const complete = inspectFixedYearSynthesis(FIXED_YEAR_SYNTHESIS['complete-timeline-v1']);
const correlation = inspectFixedYearSynthesis(FIXED_YEAR_SYNTHESIS['correlation-only-v1']);
const missing = inspectFixedYearSynthesis(FIXED_YEAR_SYNTHESIS['missing-traceability-v1']);
const handoff = prepareSyntheticYearReviewHandOff(complete);
const blockedHandoff = prepareSyntheticYearReviewHandOff(correlation);
return deepFreeze({
assertions: deepFreeze({
completeAccepted: complete.accepted,
completeHasNoReasons: complete.reasons.length === 0,
completeOnlyHandsOff: complete.handoff === 'synthetic-review-hand-off-only',
traceabilityFailsClosed: missing.reasons.includes('missing-decision-traceability'),
timestampFailsClosed: missing.reasons.includes('invalid-timeline-time'),
alternativeFailsClosed: missing.reasons.includes('missing-alternative'),
costFailsClosed: correlation.reasons.includes('missing-cost'),
observationFailsClosed: missing.reasons.includes('missing-observation'),
unknownFailsClosed: correlation.reasons.includes('missing-unknown'),
boundaryFailsClosed: correlation.reasons.includes('missing-comparison-boundary'),
effectClaimFailsClosed: correlation.reasons.includes('effect-claim-not-allowed'),
unknownInputFailsClosed: !inspectFixedYearSynthesis({ timeline: [] }).accepted,
positiveIsSyntheticOnly: handoff.status === 'synthetic-review-hand-off-only',
blockedCannotHandOff: blockedHandoff.status === 'stop-and-repair-boundary',
noProductionEffect: handoff.productionEffect === 'not-attempted' && blockedHandoff.productionEffect === 'not-attempted',
inputsFrozen: Object.isFrozen(FIXED_YEAR_SYNTHESIS),
}),
});
}
function revision(meta, parts) {
const contentHtml = parts.join('\n') + '\n' + h2('Проверяемые источники') + sourceList();
const proseLength = bodyText(contentHtml).length;
if (proseLength < 5000 || proseLength > 15000) {
throw new Error(meta.slug + ': body length ' + proseLength);
}
return deepFreeze({ ...meta, contentHtml, proseLength });
}
const practice = revision({
slug: 'editorial-2025-12-practice-year-synthesis',
title: 'Синтез инженерного года: как собрать timeline решений без витрины успехов',
categories: ['Архитектура', 'Практика'],
cover: '/assets/editorial/2025/year-synthesis-2025-decision-timeline.svg',
excerpt: 'Практический маршрут для годовой инженерной записи: решение, альтернативы, стоимость, наблюдение, unknown и граница сравнения вместо ретроспективы из одних побед.',
readingMinutes: 15,
}, [
p('Годовой инженерный текст часто начинается с запусков: сделали изменение, увидели удобный график, добавили его в список достижений. Проблема появляется позже: читатель не видит, какое решение действительно было принято, что ему противопоставляли и что именно наблюдалось. Цена такой витрины — следующий выбор строится на памяти о «успехе», а не на проверяемой записи; спор возвращается уже после того, как время на повторную работу потрачено.'),
p('Нужна не более красивая хронология, а короткий контракт для каждой точки года. В нём есть решение, хотя бы две альтернативы, принятая стоимость, наблюдение, неизвестное и граница сравнения. Проверка проста: если из карточки можно прочитать только «после стало лучше», запись не готова. Действие — сначала собрать timeline, а итоговую фразу писать только после проверки всех полей.'),
h2('Почему год лучше начинать не с итогов'),
p('Итог удобен автору: он убирает детали, в которых пришлось отказаться от варианта или оставить вопрос открытым. Для техлида это опасная экономия. Отсутствующая альтернатива делает выбранный путь похожим на единственно возможный. Отсутствующая стоимость превращает компромисс в бесплатное улучшение. Отсутствующее unknown создаёт впечатление, будто наблюдение уже объяснило причину.'),
p('Timeline меняет порядок чтения. Мы идём от момента выбора к доступным вариантам, затем к тому, чем заплатили, и только после этого смотрим на зафиксированный факт. Такой порядок не превращает год в протокол каждой переписки. Он удерживает ровно те связи, без которых следующий инженер не отличит обоснованный выбор от аккуратно оформленного воспоминания.'),
figure('/assets/editorial/2025/year-synthesis-2025-decision-timeline.svg', 'Вертикальный timeline годового review: решение, альтернативы, стоимость, наблюдение, unknown и граница сравнения ведут к единственному допустимому результату — synthetic review hand-off.', 'Хронология не доказывает эффект. Она сохраняет путь к проверяемому учебному hand-off и не позволяет пропустить неизвестное.'),
table('Минимальная карточка точки timeline', ['Поле', 'Что записать', 'Зачем нужно', 'Что ломается без поля'], [
['Решение', 'какой путь выбран в конкретный момент', 'связывает запись с действием', 'нельзя проверить traceability'],
['Альтернативы', 'минимум два отвергнутых пути', 'виден предмет выбора', 'выбор выглядит неизбежным'],
['Стоимость', 'время, сложность или принятый риск', 'trade-off становится явным', 'появляется миф о бесплатном улучшении'],
['Наблюдение', 'факт после изменения и окно его чтения', 'отделяет запись от интерпретации', 'мнение выдают за данные'],
['Unknown', 'чего запись пока не отвечает', 'не даёт закрыть причинность раньше времени', 'корреляция становится эффектом'],
['Граница сравнения', 'какие условия реально сопоставлены', 'ограничивает вывод', 'before/after получают лишний смысл'],
]),
h2('Контракт записи: шесть полей, а не один вывод'),
p('Решение отвечает на вопрос «что сделали», но не на вопрос «почему это сработало». Альтернатива объясняет, какие ограничения были существенными в момент выбора. Стоимость показывает, что выбор что-то отнял: дополнительный проход, новая проверка, более длинная инструкция, риск неполного покрытия. Наблюдение должно быть названо без оценки: «в трёх fixed прогонах виден такой порядок статусов», а не «процесс стал надёжнее».'),
p('Unknown не является признанием слабости. Это метка границы знания. Полезный unknown конкретен: не «нужны дополнительные исследования», а «неизвестно, сохранится ли этот порядок вне frozen входа». Граница сравнения рядом с ним фиксирует, что именно было сопоставлено. Если сравнивались только два заранее заданных литерала, нельзя подменять их реальными периодами, командами или производственным результатом.'),
h2('Исполняемая fixed модель'),
code([
"import {",
" createFixedYearSynthesisInput,",
" inspectYearSynthesis,",
" prepareSyntheticYearReviewHandOff,",
"} from './upgrade-2025-12.mjs';",
"",
"const report = inspectYearSynthesis(",
" createFixedYearSynthesisInput('complete-timeline-v1'),",
");",
"console.log(prepareSyntheticYearReviewHandOff(report));",
"// { accepted: true, status: 'synthetic-review-hand-off-only',",
"// productionEffect: 'not-attempted', ... }",
].join('\n')),
p('Пример хранит только frozen JavaScript literals в памяти. В нём нет файла, сети, часов исполнения, telemetry, Git, CI, production-данных или людей. Положительный ответ не принимает архитектурное решение и не отправляет задачу: он разрешает только synthetic review hand-off. Эта узость важна. Код проверяет полноту учебной карточки, а не подтверждает, что выбранный путь дал эффект вне модели.'),
h2('Как читать timestamp без лишнего смысла'),
p('У временной отметки одна обязанность: расположить запись на оси времени. В RFC 3339 дата и время имеют явную форму с offset; в fixed примере мы ограничили её UTC-суффиксом Z, чтобы проверка не угадывала часовой пояс. Timestamp не создаёт причинную связь. Тот факт, что observation находится после decision, означает только порядок записи. Любое «поэтому» требует отдельного сравнения и контроля факторов, которых в нашем literal намеренно нет.'),
h2('Практический порядок сборки года'),
ol([
'<strong>Соберите черновые точки.</strong> Для каждой оставьте только момент решения и короткое описание; не пишите общий вывод.',
'<strong>Верните отвергнутые пути.</strong> Добавьте минимум две альтернативы, которые были доступны именно тогда, а не придуманы задним числом.',
'<strong>Назовите цену выбора.</strong> Укажите конкретный расход, риск или ограничение, которое команда согласилась не устранять.',
'<strong>Отделите observation.</strong> Запишите видимый факт без слова «улучшилось», «ускорилось» или другого причинного ярлыка.',
'<strong>Сформулируйте unknown.</strong> Если отсутствует ответ о внешнем факторе, аудитории или другом периоде, оставьте его в карточке.',
'<strong>Поставьте comparison boundary.</strong> Зафиксируйте, что именно сравнивалось, и пропустите карточку через fail-closed проверку.',
]),
h2('Альтернативы нельзя дорисовывать для красоты'),
p('В конце года есть соблазн написать слабый альтернативный вариант, чтобы выбранный путь выглядел очевидным. Такая карточка бесполезна: она не показывает компромисс и не помогает повторить выбор при других ограничениях. Лучше честно оставить простой вариант, который действительно был возможен: не менять порядок, сделать дополнительную проверку, разделить два шага. Тогда видно, чем выбранный путь отличался и какую цену автор считал приемлемой.'),
p('RFC 7282 говорит о trade-off как об обычной части инженерного выбора. Это не даёт нам универсальную формулу компромисса. Оно напоминает о более скромной дисциплине: возражение и альтернатива должны быть названы технически, а не стерты голосованием или красивым финалом. В годовом тексте такая дисциплина полезнее списка абстрактных принципов.'),
h2('Окно решения и окно наблюдения — разные объекты'),
p('У решения есть момент выбора: в карточке это timestamp и описание варианта. У observation есть окно чтения: один прогон, серия fixed состояний или заранее названный срез. Ошибка возникает, когда дата решения подменяет окно наблюдения. Например, автор пишет, что в октябре увидел новый порядок статусов, хотя в записи нет ни того, сколько учебных состояний прочитано, ни того, какие состояния не попадали в набор. Дата отвечает только на вопрос «когда», а не «на чём».'),
p('Для годового текста достаточно назвать окно словами, а не выдумывать точность: «три frozen прогона», «две заданные карточки до и после», «одна проверка структуры». Если окно не записано, observation остаётся слабее, чем кажется. Его всё ещё можно оставить как заметку, но нельзя использовать для аргумента о повторении решения. Это особенно важно в декабре, когда множество разных условий уже слились в одну удобную память о годе.'),
h2('Неполная линия не должна получать счастливый финал'),
p('Иногда исходных следов действительно мало: решение помнят, но альтернативы не зафиксированы; есть observation, но неизвестна цена; timestamp есть, а граница сравнения потеряна. Не надо реконструировать пропуск уверенным языком. В timeline можно оставить строку с unknown и вопросом на следующий цикл. Такая строка полезнее вымышленного ADR: она показывает будущему читателю, где заканчивается воспроизводимость.'),
p('Неполная карточка имеет простой маршрут. Сначала она не попадает в итоговые выводы. Затем автор решает, можно ли восстановить только первичный факт без домыслов. Если нельзя, строка остаётся контекстом, а не основанием рекомендации. Это сохраняет честную форму отчёта: в нём могут быть не только достижения и не только решения, но и границы того, что годовая память уже не может доказать.'),
h2('Где практика заканчивается'),
p('Этот шаблон не заменяет ADR, incident review или исследование метрик. ADR может потребовать контекст, владельца и статус; разбор инцидента — технические причины и меры; метрика — метод сбора, окно и единицу. Здесь намеренно меньше: fixed модель проверяет, что в годовой карточке не исчезли decision, alternative, cost, observation, unknown и boundary. Отсутствие любого поля останавливает hand-off.'),
p('Также нельзя переносить synthetic wording в закрытые данные без отдельной политики. Реальная запись может содержать чувствительные детали, владельцев, договорённости и доступы. Этот draft не собирает такие сведения и не предлагает обходить правила хранения. Его следующий шаг локален: возьмите одну обезличенную учебную карточку, заполните шесть полей и проверьте, не превратилось ли наблюдение в неподтверждённый эффект.'),
h2('Следующий проверяемый шаг'),
p('Начните с одной точки, а не с полного календаря. Если для неё не удаётся назвать две альтернативы или стоимость, не заполняйте итог «влияние за год». Вернитесь к исходной записи или оставьте пробел явно. Если все поля есть, результатом будет только synthetic review hand-off. Для реального решения потребуются отдельный владелец, метод сравнения, правила данных и контекст, которых этот пример сознательно не имитирует.'),
]);
const mechanism = revision({
slug: 'editorial-2025-12-mechanism-year-synthesis',
title: 'Синтез инженерного года: почему наблюдение после решения не доказывает эффект',
categories: ['Данные', 'Архитектура'],
cover: '/assets/editorial/2025/year-synthesis-2025-decision-cost-observation-matrix.svg',
excerpt: 'Механика годового вывода: отделить решение, наблюдение и причинность, указать стоимость, unknown и границу сравнения до того, как correlation станет эффектом.',
readingMinutes: 16,
}, [
p('Самая дорогая ошибка годового разбора выглядит безобидно: рядом стоят решение и удобное наблюдение, поэтому текст объявляет одно причиной другого. Проблема не в наличии метрики или timeline, а в пропущенных звеньях между ними. Цена ложного эффекта — команда продолжает финансировать выбранный путь, хотя изменение могло совпасть с другим фактором, окном измерения или особенностью выборки.'),
p('Механизм защиты начинается с разъединения трёх объектов: decision фиксирует намерение, cost фиксирует принятый trade-off, observation фиксирует увиденный факт. Ни один из них по отдельности не создаёт causal claim. Проверка должна искать unknown и comparison boundary: какой контрфакт отсутствует, что именно сопоставлено, какие условия остались за рамкой. Если ответы пусты, действие одно — остановить вывод, а не подобрать более убедительный график.'),
h2('Три слоя, которые часто склеивают'),
p('Decision — это запись выбранного пути в конкретный момент. Она говорит, что автор хотел изменить и какой вариант отверг. Cost — это цена выбора: дополнительные шаги, сложность сопровождения, неполное покрытие или иной заранее принятый расход. Observation — это факт, который появился после решения: значение в fixed literal, порядок событий, наличие статуса, результат заранее заданной проверки.'),
p('Склейка начинается со слова «после». Оно верно описывает время, но не объясняет причину. В нашем synthetic наборе решение находится раньше observation, однако между ними нет реальной среды, независимого сравнения, случайного распределения, control group или доступа к production telemetry. Поэтому единственная честная интерпретация: запись полнее или неполнее, а не «изменение сработало».'),
figure('/assets/editorial/2025/year-synthesis-2025-decision-cost-observation-matrix.svg', 'Матрица из четырёх строк: решение, стоимость и наблюдение допускают только узкие фактические выводы; строка effect claim окрашена как stop до появления unknown и границы сравнения.', 'Матрица отделяет то, что действительно лежит в карточке, от того, что хочется вывести из её порядка.'),
table('Что допускает каждый тип записи', ['Есть в карточке', 'Допустимый текст', 'Запрещённый скачок', 'Ремонт'], [
['decision и alternatives', 'выбран путь A вместо B и C', 'A был лучшим вариантом вообще', 'назвать требования и последующую проверку'],
['cost', 'выбор добавил отдельный fixed шаг', 'цена окупилась', 'указать, что измерялось и по какой границе'],
['observation', 'после decision есть такой факт', 'decision вызвал этот факт', 'добавить контрфакт и внешний контекст'],
['unknown', 'ответ о внешнем факторе отсутствует', 'причина известна', 'оставить статус open'],
['comparison boundary', 'сопоставлены два literal', 'сравнены реальные периоды', 'сузить формулировку до факта'],
]),
h2('Fail-closed модель не пропускает корреляцию'),
code([
"import {",
" createFixedYearSynthesisInput,",
" inspectYearSynthesis,",
"} from './upgrade-2025-12.mjs';",
"",
"const report = inspectYearSynthesis(",
" createFixedYearSynthesisInput('correlation-only-v1'),",
");",
"console.log(report.reasons);",
"// ['missing-cost', 'missing-unknown',",
"// 'missing-comparison-boundary', 'effect-claim-not-allowed']",
].join('\n')),
p('Этот literal специально соблазнителен: в нём есть decision, варианты и observation после него. Но стоимость написана как «не указан», unknown и comparison boundary пусты, а claim прямо объявляет эффект установленным. Проверка не пытается угадать недостающую историю. Она возвращает точные причины остановки. Так автор видит, что проблема не в недостаточно сильной формулировке, а в том, что модель не содержит нужных оснований для вывода.'),
h2('Контрфакт — не декоративный вопрос'),
p('Контрфакт формулирует, что могло бы быть при другом пути или без изменения. Он не обязан быть сложной статистической моделью, чтобы быть полезным. В годовой записи достаточно сначала признать его отсутствие: «мы не знаем, каким был бы наблюдаемый факт при другом порядке». Эта фраза не обесценивает работу. Она запрещает выдать единственный увиденный след за доказательство причины.'),
p('Comparison boundary делает отсутствие контрфакта проверяемым. Для fixed примера это фраза о том, что сопоставлены только заданные before/after literals. Для реального исследования boundary должна назвать популяцию, окно, способ сбора, исключения и правила сравнения. Пока этих деталей нет, читать можно только observation. Причинный глагол нужно отложить.'),
h2('Как строится корректная цепочка рассуждения'),
ol([
'<strong>Назовите question.</strong> Что именно пытаемся понять: порядок событий, наличие дефекта, стоимость процесса или влияние изменения?',
'<strong>Разложите запись.</strong> Отдельно выпишите decision, alternatives, cost и observation; не объединяйте их одним выводом.',
'<strong>Спросите про контрфакт.</strong> Что могло бы измениться без выбранного пути или при другой альтернативе?',
'<strong>Сформулируйте unknown.</strong> Укажите первый фактор, который текущая запись не наблюдает.',
'<strong>Определите comparison boundary.</strong> Проверьте, есть ли общие условия и метод сопоставления, а не только порядок дат.',
'<strong>Выберите исход.</strong> Либо узкий факт, либо stop-and-repair-boundary; synthetic hand-off не становится production decision.',
]),
h2('Почему стоимость относится к механизму, а не к украшению'),
p('Без cost решение легко выглядит как техническая победа. На деле дополнительная валидация может увеличить путь, отдельный статус unknown — нагрузить review, сохранение альтернатив — сделать карточку длиннее. Это не аргументы против дисциплины записи. Это условия выбора. Если в декабрьском тексте нет стоимости, читатель не может повторить компромисс в другой границе: он видит только пользу, которую автор уже отобрал задним числом.'),
p('RFC 7282 не предлагает считать инженерный выбор голосованием. В нём важно, что техническое возражение может быть реальным, даже если его не берут в выбранную конструкцию. Для нашего механизма это означает простое правило: alternative и cost должны остаться видимыми рядом с decision. Они не превращают model в доказательство, но убирают ложную неизбежность.'),
h2('Lessons learned не равны набору красивых фактов'),
p('NIST SP 800-61r3 описывает цикл, где lessons learned анализируют, приоритизируют и возвращают в improvement. Мы используем это только как узкий пример направления записи: наблюдение должно вести к проверяемому следующему вопросу, а не к немедленному заявлению об успехе. Документ относится к cybersecurity incident response, поэтому из него нельзя выводить формат нашей таблицы или переносить security-рекомендации в synthetic годовой текст.'),
p('В практической карточке следующий вопрос может быть совсем небольшим: «какой внешний фактор не виден», «какой вариант не сопоставлен», «какой расход не записан». Такой вопрос полезнее громкой формулы «извлечены уроки», потому что по нему можно вернуть запись на ремонт. Нельзя исправить причинность дополнительным эпитетом; её можно только сузить или собрать новый метод сравнения.'),
h2('Три ложных механизма, которые стоит остановить'),
p('Первый ложный механизм: «было решение, потом уменьшилось значение, значит решение уменьшило значение». В нём пропущен контрфакт. Второй: «альтернатива была дороже, значит выбранный путь окупился». Здесь cost заменяют наблюдением пользы, которого запись не содержит. Третий: «несколько похожих историй подтверждают эффект». Похожесть без общей границы сравнения только увеличивает число совпадений, но не делает их причиной.'),
p('Во всех трёх случаях ремонт начинается не с вычисления. Сначала надо назвать фактическое утверждение, которое уже разрешено: выбор сделан, расход записан, определённый факт замечен. Затем определить, какого условия недостаёт для следующего уровня вывода. Иногда ответом будет новый сбор данных; иногда — отказ от эффекта как цели годовой заметки. Механизм становится честным именно в тот момент, когда уровень утверждения совпадает с содержимым карточки.'),
h2('Когда метрическое чтение превращается в метод'),
p('Число в отчёте ещё не является measurement method. Для метода нужны единица, источник, правила расчёта, временное окно, исключения и заранее определённый способ сравнения. Без этого число остаётся observation: оно может быть полезным симптомом или поводом проверить гипотезу, но не доказательством того, что решение изменило систему. В synthetic literal вместо реального числа мы намеренно оставили текст о заданном факте, чтобы не симулировать такую методику.'),
p('Это различие помогает планировать работу. Если целью является causal question, надо явно открыть отдельный трек: определить сравниваемые условия, доступы, владельца данных и риск неправильной интерпретации. Если такой трек не нужен, годовой текст должен остаться на уровне traceability и cost. Оба исхода нормальны; ошибка появляется, когда второй маскируют под первый.'),
h2('Граница технического примера'),
p('Фабрика принимает только известные frozen inputs. Произвольный объект, даже похожий по полям, получает unknown-fixed-input и не проходит дальше. Это сделано не ради криптографической защиты, а чтобы пример не имитировал проверку реальных данных. В нём нет идентификаторов, организаций, людей, интеграций, сетевых вызовов, файлов, timestamps исполнения, incident records или метрик из внешнего мира.'),
p('Временные строки внутри literal соответствуют ограниченному формату UTC RFC 3339. Они нужны для проверки порядка карточек, но не для измерения длительности или доказательства влияния. Положительный результат функции содержит только synthetic-review-hand-off-only и productionEffect: not-attempted. Если требуется реальное решение, нужен другой процесс с владельцем, законным доступом и заранее определённой методикой.'),
h2('Следующий проверяемый шаг'),
p('Возьмите одну фразу из годового отчёта, где рядом стоят изменение и результат. Разрежьте её на четыре поля: decision, cost, observation, unknown. Затем напишите comparison boundary одной строкой. Если любой из двух последних элементов получается вымышленным, не делайте causal claim. Верните формулировку к наблюдаемому факту и поставьте конкретный вопрос для следующего review.'),
]);
const field = revision({
slug: 'editorial-2025-12-field-year-synthesis',
title: 'Синтез инженерного года: полевой counterfactual review без истории из одних побед',
categories: ['Процессы', 'Практика'],
cover: '/assets/editorial/2025/year-synthesis-2025-counterfactual-review-loop.svg',
excerpt: 'Полевой сценарий для разбора годовой записи: проверить альтернативу, стоимость, observation, unknown и границу сравнения, а затем вернуть только safe synthetic hand-off.',
readingMinutes: 15,
}, [
p('На встрече по итогам года легко собрать гладкую историю: сначала было решение, потом появился хороший сигнал, значит надо повторить путь. Проблема в том, что такой рассказ прячет неудобные альтернативы, цену и неизвестные. Цена ошибки — следующий цикл получает не материал для выбора, а подтверждение уже любимого объяснения; спор вспыхивает, когда старый рецепт сталкивается с новой границей.'),
p('Полевой review должен быть короче презентации и строже пересказа. Он не ищет виноватого и не ранжирует людей. Его задача — пройти одну запись по кругу: прочитать decision, найти альтернативу, назвать cost, отделить observation, сформулировать unknown и проверить comparison boundary. Если хотя бы один узел пуст, итогом является repair request. Если все заполнены, допускается только synthetic review hand-off, а не production decision.'),
h2('Полевой контекст без реального поля'),
p('Ниже «полевой» означает упражнение над fixed literals, а не доступ к настоящим системам. Мы не читаем логи, ticket, ADR, метрики, документы, чаты или incident record. Тем самым модель не может случайно сделать вывод о реальной компании, команде или человеке. Она проверяет лишь форму отчёта: оставил ли автор путь от выбора к наблюдению и признал ли границу знания.'),
p('Такая искусственная среда полезна для репетиции review. В ней можно безопасно увидеть типичную ошибку: удобный факт после изменения превращается в эффект. Но учебный результат нельзя переносить в отчёт о производстве. Фиксированный hand-off означает только, что карточку допустимо обсудить человеком по её ограниченному содержанию.'),
figure('/assets/editorial/2025/year-synthesis-2025-counterfactual-review-loop.svg', 'Цикл counterfactual review: запись ведёт к альтернативе, стоимости, unknown и проверке границы сравнения; вниз выходит только synthetic hand-off, а отсутствие boundary требует stop and repair.', 'Круг заставляет вернуться к пропущенному полю, вместо того чтобы компенсировать его более уверенным финальным абзацем.'),
table('Результат прохода review', ['Проверяемый узел', 'Что спрашивает reviewer', 'Допустимый ответ', 'Маршрут'], [
['Decision', 'какой путь выбран?', 'конкретное действие в записи', 'дальше к alternatives'],
['Alternatives', 'что не выбрали?', 'два реальных для literal варианта', 'дальше к cost'],
['Cost', 'чем заплатили?', 'явный расход или риск', 'дальше к observation'],
['Observation', 'какой факт увидели?', 'описание без causal verb', 'дальше к unknown'],
['Unknown', 'чего ещё не знаем?', 'первый непокрытый вопрос', 'дальше к boundary'],
['Boundary', 'что сравнили?', 'точное ограничение набора', 'hand-off или repair'],
]),
h2('Исполняемая карточка review'),
code([
"import {",
" createFixedYearSynthesisInput,",
" inspectYearSynthesis,",
" prepareSyntheticYearReviewHandOff,",
"} from './upgrade-2025-12.mjs';",
"",
"const draft = createFixedYearSynthesisInput('missing-traceability-v1');",
"const report = inspectYearSynthesis(draft);",
"console.log(report.handoff);",
"// 'stop-and-repair-boundary'",
"console.log(prepareSyntheticYearReviewHandOff(report).status);",
"// 'stop-and-repair-boundary'",
].join('\n')),
p('В этом случае отсутствуют decision, cost, observation, unknown и boundary; timestamp не проходит ограниченный RFC 3339 UTC-pattern. Функция не исправляет карточку за автора и не выводит вероятную причину. Она возвращает набор конкретных gaps. Второй вызов сохраняет остановку: статус нельзя превратить в положительный hand-off только потому, что код был запущен без ошибки.'),
h2('Как провести review за двадцать минут'),
ol([
'<strong>Выберите одну fixed карточку.</strong> Не открывайте рядом календарь успехов и не пытайтесь покрыть весь год за один проход.',
'<strong>Прочитайте decision вслух.</strong> Если фраза описывает только результат, а не выбор, верните её на переписывание.',
'<strong>Попросите альтернативы.</strong> Сравнивайте только варианты, доступные в момент записи; не добавляйте невозможный идеал задним числом.',
'<strong>Проверьте cost.</strong> Найдите, что стало длиннее, сложнее, рискованнее или осталось непокрытым.',
'<strong>Заморозьте observation.</strong> Замените оценочные глаголы на наблюдаемый факт и зафиксируйте окно чтения.',
'<strong>Спросите «что могло быть иначе?»</strong> Ответ формирует unknown и comparison boundary; без них оформите repair request.',
'<strong>Передайте узкий исход.</strong> В synthetic модели это только hand-off для human review; никаких запусков, оценок или решений вне памяти.',
]),
h2('Counterfactual вопрос не требует придумать другой год'),
p('Частая защита гладкой истории: «нельзя же узнать, что было бы иначе». Именно поэтому годовой текст не должен обещать, что знает. Counterfactual review не заставляет строить альтернативную реальность. Он проверяет, есть ли граница у фразы. Если границы нет, неизвестное записывают как unknown; если нужен ответ, для него проектируют отдельное сравнение, а не добавляют уверенности в ретроспективный абзац.'),
p('В fixed карточке вопрос звучит скромно: «какой внешний фактор мог бы дать то же observation?» В реальной работе он может потребовать владельца методики, этической оценки данных, окна измерения и времени. Этот пакет ничего из этого не предоставляет. Он тренирует дисциплину не объявлять отсутствующий контрфакт скрытым доказательством.'),
h2('Почему review не является судом'),
p('Если reviewer начинает искать автора ошибки, поля быстро становятся декоративными. Alternative превращается в оправдание, cost скрывается, unknown воспринимают как слабость. У полевого прохода другая единица анализа — сама запись. Мы спрашиваем, какое поле не позволяет повторить reasoning, и возвращаем только его. Это снижает риск спорить о мотивах вместо того, чтобы проверить технический след решения.'),
p('Публичный RFC 7282 здесь полезен ограниченно: он показывает, что инженерный выбор живёт среди trade-off, а возражение может содержать реальную техническую проблему. Мы не переносим правила IETF в другую среду. Мы берём более малое правило: если alternative или cost нельзя сформулировать, текст не готов к итоговому утверждению.'),
h2('Наблюдение должно привести к следующему вопросу'),
p('NIST SP 800-61r3 связывает lessons learned с анализом, приоритизацией и последующим улучшением. В нашем review это не шаблон incident response и не заявка на security-процесс. Аналогия только в направлении движения: observation возвращается в вопрос, который можно проверить позже. Например, «какой фактор за пределами literal не учтён?» — это следующий запрос; «решение дало эффект» — уже результат, которого запись не умеет обосновать.'),
p('Удобная карточка имеет ровно один следующий запрос. Если их десять, значит timeline пытается заменить исследование. Если запросов нет, скорее всего unknown был вырезан ради финала. В обоих случаях редактору полезнее сократить историю до одной точки, чем добавлять ещё один список достижений.'),
h2('Как вернуть карточку без пустого замечания'),
p('Комментарий «не хватает контекста» не помогает исправить запись: непонятно, нужно ли вернуть альтернативу, цену, окно observation или сравнение. Repair request должен называть один разрыв и ожидаемое дополнение. Например: «оставь в строке отвергнутый вариант, который был доступен в момент выбора»; «замени вывод об эффекте на факт и добавь unknown»; «опиши, какие именно два fixed состояния сопоставлены».'),
p('Такой маршрут не требует соглашаться с выбранным решением. Reviewer может считать альтернативу сильнее, но сначала обязан проверить, что сама карточка различает choice и result. Только затем начинается предметный спор о требованиях. Это делает несогласие техническим: участники обсуждают пропущенное поле, уровень утверждения или границу сравнения, а не характер автора годового текста.'),
h2('Два вида несогласия не стоит смешивать'),
p('Первый вид — несогласие с данными записи. Например, observation неточно описан или comparison boundary шире, чем позволяет набор. Такой случай возвращает карточку на исправление. Второй — несогласие с trade-off: все поля есть, но другой вариант кажется приемлемее. Его не надо маскировать как ошибку в фактах. Он остаётся отдельной заметкой рядом с alternatives и может стать темой следующего решения.'),
p('Разделение экономит время в review. Когда сначала чинят traceability, спор о стратегии не разрастается вокруг неверно названного эффекта. Когда запись полна, стратегическое возражение можно сохранить без требования переписать прошлое. В обоих случаях annual synthesis остаётся инструментом чтения решений, а не сценой для окончательного вердикта.'),
h2('Стоимость самого review'),
p('У review тоже есть cost: нужно время на чтение, неудобно оставлять часть выводов открытыми, а карточка с alternatives длиннее красивого тезиса. Этот расход оправдан не обещанием эффективности, а качеством следующего разговора. Участники обсуждают конкретное поле: какой вариант не назван, где потеряна граница, какой факт ещё не наблюдался. Это намного дешевле, чем спорить о том, кто помнит год правильнее.'),
p('Не нужно проводить такой проход для каждого мелкого изменения. Подходите к нему там, где годовой текст пытается связать решение с результатом или предлагает повторить путь. Для коротких заметок достаточно decision и границы. Полный круг с alternatives, cost и unknown нужен ровно там, где риск ложного причинного вывода дороже времени reviewer-а.'),
h2('Граница и следующий шаг'),
p('Фабрика и fixture существуют только в памяти процесса. В данных нет реальных идентификаторов, организаций, имён, интеграций, производственных значений или claims об эффекте. Все timestamps, ADR-подобные решения, observations и costs — fixed synthetic literals. После проверки не создаётся файл, HTTP request, задача, метрика, Git-изменение или external message.'),
p('Проверьте на своей учебной карточке последний абзац. Есть ли там observation, который вы уже назвали причиной? Верните рядом alternatives, cost, unknown и comparison boundary. Если одно поле честно пустое, оставьте stop-and-repair-boundary. Если заполнены все шесть, отправьте только synthetic hand-off на человеческое чтение; реальный вывод требует отдельного контекста и метода, которого здесь нет.'),
]);
export const revisions = deepFreeze([practice, mechanism, field]);
if (process.argv.includes('--verify-fixture')) {
const assertions = runYearSynthesisFixture().assertions;
const failed = Object.entries(assertions)
.filter(([, value]) => value !== true)
.map(([key]) => key);
if (failed.length > 0) {
process.stderr.write('FAIL fixture: ' + failed.join(', ') + '\n');
process.exitCode = 1;
} else {
process.stdout.write('PASS fixture: ' + Object.keys(assertions).length + '/' + Object.keys(assertions).length + ' assertions\n');
}
}
if (process.argv.includes('--print-revisions')) {
process.stdout.write(JSON.stringify(revisions) + '\n');
}