function escapeHtml(value) { return String(value).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); } const p = (text) => '

' + text + '

'; const h2 = (text) => '

' + text + '

'; const code = (text) => '
' + escapeHtml(text) + '
'; const ol = (items) => '
    ' + items.map((item) => '
  1. ' + item + '
  2. ').join('') + '
'; const figure = (src, alt, caption) => '
' + alt + '
' + caption + '
'; const table = (caption, headers, rows) => '
' + headers.map((cell) => '').join('') + '' + rows.map((row) => '' + row.map((cell) => '').join('') + '').join('') + '
' + caption + '
' + cell + '
' + cell + '
'; 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>[\s\S]*?(?=

|$)/, '')); } const REFERENCES = deepFreeze({ timeline: { title: 'W3C Performance Timeline Level 2', url: 'https://www.w3.org/TR/2019/WD-performance-timeline-2-20190329/', version: 'W3C Working Draft, dated snapshot 2019-03-29' }, navigation: { title: 'W3C Navigation Timing Level 2', url: 'https://www.w3.org/TR/2026/WD-navigation-timing-2-20260225/', version: 'W3C Working Draft, 2026-02-25' }, resource: { title: 'W3C Resource Timing', url: 'https://www.w3.org/TR/2026/CRD-resource-timing-20260420/', version: 'W3C Candidate Recommendation Draft, 2026-04-20' }, longtasks: { title: 'W3C Long Tasks API', url: 'https://www.w3.org/TR/2026/WD-longtasks-1-20260319/', version: 'W3C Working Draft, 2026-03-19' }, }); function sources(entries) { return ''; } const FIXED_CRITICAL_PATH_CASES = deepFreeze({ 'critical-path-contract-2027-06-v1': { id: 'critical-path-contract-2027-06-v1', planDate: '2027-06', sourceCutoff: '2026-07-31', contract: { id: 'render-blocking-order-contract', revision: 'v1', decision: 'future-question-only' }, comparableConditions: { browserBuild: 'not-collected', deviceClass: 'not-collected', cacheState: 'not-collected', routeState: 'not-collected' }, evidence: { state: 'not-collected', benchmark: 'not-attempted', telemetry: 'not-collected', profile: 'not-collected' }, requestedOutcome: 'synthetic-plan-hand-off', boundary: 'Fixed in-memory plan literal only. It does not read, create, change, infer or contact files, network, environment, clock, secrets, telemetry, systems, services, browsers, profiles, benchmarks or production data.' }, 'missing-june-date-or-cutoff-v1': { id: 'missing-june-date-or-cutoff-v1', planDate: '', sourceCutoff: '2026-07-31', contract: { id: 'render-blocking-order-contract', revision: 'v1', decision: 'future-question-only' }, comparableConditions: { browserBuild: 'not-collected', deviceClass: 'not-collected', cacheState: 'not-collected', routeState: 'not-collected' }, evidence: { state: 'not-collected', benchmark: 'not-attempted', telemetry: 'not-collected', profile: 'not-collected' }, requestedOutcome: 'synthetic-plan-hand-off', boundary: 'Negative fixed literal only.' }, 'unnamed-path-contract-v1': { id: 'unnamed-path-contract-v1', planDate: '2027-06', sourceCutoff: '2026-07-31', contract: { id: '', revision: '', decision: 'future-question-only' }, comparableConditions: { browserBuild: 'not-collected', deviceClass: 'not-collected', cacheState: 'not-collected', routeState: 'not-collected' }, evidence: { state: 'not-collected', benchmark: 'not-attempted', telemetry: 'not-collected', profile: 'not-collected' }, requestedOutcome: 'synthetic-plan-hand-off', boundary: 'Negative fixed literal only.' }, 'conditions-claimed-without-record-v1': { id: 'conditions-claimed-without-record-v1', planDate: '2027-06', sourceCutoff: '2026-07-31', contract: { id: 'render-blocking-order-contract', revision: 'v1', decision: 'future-question-only' }, comparableConditions: { browserBuild: 'assumed', deviceClass: 'not-collected', cacheState: 'not-collected', routeState: 'not-collected' }, evidence: { state: 'not-collected', benchmark: 'not-attempted', telemetry: 'not-collected', profile: 'not-collected' }, requestedOutcome: 'synthetic-plan-hand-off', boundary: 'Negative fixed literal only.' }, 'observation-injected-into-plan-v1': { id: 'observation-injected-into-plan-v1', planDate: '2027-06', sourceCutoff: '2026-07-31', contract: { id: 'render-blocking-order-contract', revision: 'v1', decision: 'future-question-only' }, comparableConditions: { browserBuild: 'not-collected', deviceClass: 'not-collected', cacheState: 'not-collected', routeState: 'not-collected' }, evidence: { state: 'claimed', benchmark: 'completed', telemetry: 'not-collected', profile: 'not-collected' }, requestedOutcome: 'synthetic-plan-hand-off', boundary: 'Negative fixed literal only.' }, 'result-substitution-v1': { id: 'result-substitution-v1', planDate: '2027-06', sourceCutoff: '2026-07-31', contract: { id: 'render-blocking-order-contract', revision: 'v1', decision: 'future-question-only' }, comparableConditions: { browserBuild: 'not-collected', deviceClass: 'not-collected', cacheState: 'not-collected', routeState: 'not-collected' }, evidence: { state: 'not-collected', benchmark: 'not-attempted', telemetry: 'not-collected', profile: 'not-collected' }, requestedOutcome: 'winner-selected', boundary: 'Negative fixed literal only.' }, }); export function createCriticalPathPlan(id = 'critical-path-contract-2027-06-v1') { const value = FIXED_CRITICAL_PATH_CASES[id]; return value ? deepFreeze(cloneFixed(value)) : undefined; } function stop(status, reason, nextAction) { return deepFreeze({ status, reason, nextAction, productionEffect: 'not-attempted' }); } export function assessCriticalPathPlan(input) { if (!Object.values(FIXED_CRITICAL_PATH_CASES).some((item) => JSON.stringify(item) === JSON.stringify(input))) return stop('stop-unknown-fixed-literal', 'input-is-not-an-exact-named-fixed-literal', 'select-a-named-synthetic-case'); if (input.planDate !== '2027-06' || input.sourceCutoff !== '2026-07-31') return stop('stop-missing-june-date-or-cutoff', 'june-2027-plan-and-july-2026-cutoff-are-required', 'name-the-plan-date-and-source-cutoff'); if (!input.contract?.id || !input.contract?.revision || input.contract?.decision !== 'future-question-only') return stop('stop-unnamed-path-contract', 'the-critical-path-question-must-have-a-named-revision', 'name-a-synthetic-contract'); if (Object.values(input.comparableConditions || {}).some((value) => value !== 'not-collected')) return stop('stop-conditions-claimed-without-record', 'conditions-cannot-be-assumed-or-claimed', 'keep-all-conditions-not-collected'); if (input.evidence?.state !== 'not-collected' || input.evidence?.benchmark !== 'not-attempted' || input.evidence?.telemetry !== 'not-collected' || input.evidence?.profile !== 'not-collected') return stop('stop-observation-injected-into-plan', 'benchmark-telemetry-and-profile-must-not-be-created-or-claimed', 'retain-the-unobserved-state'); if (input.requestedOutcome !== 'synthetic-plan-hand-off') return stop('stop-result-substitution', 'a-winner-bottleneck-rollout-or-production-effect-is-not-a-plan-output', 'use-synthetic-plan-hand-off'); return deepFreeze({ status: 'synthetic-plan-hand-off', caseId: input.id, contract: deepFreeze(cloneFixed(input.contract)), comparableConditions: deepFreeze(cloneFixed(input.comparableConditions)), evidence: deepFreeze(cloneFixed(input.evidence)), boundary: input.boundary, productionEffect: 'not-attempted', nextAction: 'open-a-new-authorised-scope-before-any-observation' }); } export function runCriticalPathFixture() { const expected = [['critical-path-contract-2027-06-v1', 'synthetic-plan-hand-off'], ['missing-june-date-or-cutoff-v1', 'stop-missing-june-date-or-cutoff'], ['unnamed-path-contract-v1', 'stop-unnamed-path-contract'], ['conditions-claimed-without-record-v1', 'stop-conditions-claimed-without-record'], ['observation-injected-into-plan-v1', 'stop-observation-injected-into-plan'], ['result-substitution-v1', 'stop-result-substitution']]; const checks = expected.map(([id, expectedStatus]) => ({ id, expectedStatus, actualStatus: assessCriticalPathPlan(createCriticalPathPlan(id)).status })); const sample = createCriticalPathPlan(); return deepFreeze({ passed: checks.filter((item) => item.expectedStatus === item.actualStatus).length, total: checks.length, accepted: checks.every((item) => item.expectedStatus === item.actualStatus) && Object.isFrozen(sample) && Object.isFrozen(sample.contract) && Object.isFrozen(sample.evidence), checks: deepFreeze(checks) }); } function revision(meta, parts, referenceEntries) { const contentHtml = parts.join('') + h2('Проверяемые источники') + sources(referenceEntries); const proseLength = bodyText(contentHtml).length; if (proseLength < 9000 || proseLength > 12000) throw new Error(meta.slug + ': P112 body target 9000-12000, got ' + proseLength); return deepFreeze({ ...meta, contentHtml, proseLength }); } const practiceBoundary = 'Выпуск помечен как план/сценарий 2027-06; редактор зафиксировал знания на 2026-07-31. Никаких будущих benchmark, telemetry, profile, bottleneck, rollout, winner либо production effect здесь не существует. Fixed literal может завершиться только synthetic-plan-hand-off, причём productionEffect: not-attempted — часть его контракта.'; const mechanismBoundary = 'Дата сценария — 2027-06, а верхняя граница источников — 2026-07-31. Поэтому эта механика не сообщает profile, telemetry, benchmark, bottleneck, winner, rollout или эффект в production. Позитивный verdict один: synthetic-plan-hand-off с неизменным полем productionEffect: not-attempted.'; const fieldBoundary = 'Это будущая карточка июня 2027, подготовленная при cutoff 2026-07-31. В ней намеренно отсутствуют telemetry, profile, benchmark, bottleneck, rollout, winner и production effect. Разрешён не отчёт, а один outcome: synthetic-plan-hand-off, где productionEffect: not-attempted фиксирует отсутствие вмешательства.'; const refs = [ { key: 'timeline', use: 'Задаёт первичный vocabulary PerformanceEntry и PerformanceObserver для различения типов будущих evidence entries.', boundary: 'Датированный snapshot описывает API, но не подтверждает существование entry, путь рендеринга или результат в неизвестном приложении.' }, { key: 'navigation', use: 'Описывает интерфейс навигационных timing entries и их границы.', boundary: 'Working Draft не создаёт запись, замер или benchmark для будущего выпуска.' }, { key: 'resource', use: 'Задаёт vocabulary Resource Timing и ограничения видимости ресурсов.', boundary: 'Спецификация не делает условия двух запусков сопоставимыми и не выявляет bottleneck.' }, { key: 'longtasks', use: 'Разделяет long task и блокировку очереди главного потока.', boundary: 'Working Draft не доказывает наличие long task, profile или причину задержки в неизвестном продукте.' }, ]; const practice = revision({ slug: 'editorial-2027-06-practice-performance-capstone', title: 'Большой разбор производительности: договор о критическом пути', categories: ['Frontend', 'Инженерные практики'], cover: '/assets/editorial/2027/performance-capstone-2027-critical-path-contract.svg', excerpt: 'План на июнь 2027: сначала оформить договор о критическом пути, затем открывать scope для наблюдений.', readingMinutes: 22 }, [ h2('Сначала договор, потом измерение'), p('Проблема большого разбора производительности появляется ещё до инструмента: одна группа говорит «страница медленная», другая — «долго отвечает API», третья показывает тяжёлый скрипт. Это три разных вопроса. Цена смешения — недели спорных правок и опасный «фикс», который меняет путь пользователя, но не имеет проверяемого критерия.'), p(practiceBoundary + ' Поэтому практика P112 не выбирает способ ускорения и не ищет виновника. Она оформляет только договор: какой переход будет предметом будущего вопроса, какие условия надо назвать и какие выводы запрещены до отдельного authorised scope.'), p('Критический путь здесь не лозунг «сделать быстрее». Это именованная цепочка от начала навигации до выбранного доступного состояния, где каждый участок имеет владельца и тип доказательства. Слово «выбранного» важно: первый визуальный пиксель, доступность действия и завершение фоновой работы не обязаны совпадать. Если команда не выбрала один переход, она неизбежно сравнит разные вещи.'), figure('/assets/editorial/2027/performance-capstone-2027-critical-path-contract.svg', 'Схема договора: вопрос о критическом пути проходит через границы навигации, ресурсов, главного потока и доступного действия, а затем передаётся без измеренного вывода.', 'Договор не рисует реальную трассу: он показывает, какие границы будущий владелец обязан назвать до первого наблюдения.'), h2('Что именно фиксирует договор'), p('У договора четыре поля. Первое — наблюдаемое состояние, но не название метрики: например, «пользователь может совершить действие X» пока остаётся будущим вопросом, а не фактом. Второе — стартовая граница: navigation, route transition или другое явно выбранное событие. Третье — список условий сравнимости. Четвёртое — запрет на вывод: нельзя назвать bottleneck, выигравший вариант или production effect из пустой карточки.'), p('Эта форма намеренно скупа. В ней нет URL, имени сервиса, записи профиля, снимка telemetry и «примерно такого» устройства. Такие детали выглядят полезными, но без provenance превращают документ в неявное свидетельство. Для июньского plan-сценария корректнее оставить их not-collected, чем создать правдоподобный, но несуществующий контекст.'), table('Поля договора о критическом пути', ['Поле', 'Что можно назвать сейчас', 'Что запрещено подставлять'], [ ['Вопрос', 'named contract id и revision', 'готовый диагноз или «узкое место»'], ['Граница пути', 'тип перехода: navigation / resource / main-thread / action', 'реальный timestamp или trace'], ['Условия', 'набор обязательных полей', 'browser build, cache или device «по умолчанию»'], ['Доказательство', 'not-collected и not-attempted', 'benchmark, telemetry, profile или эффект в production'], ]), h2('Почему путь нельзя заменить одной метрикой'), p('Navigation Timing описывает запись навигации, Resource Timing — записи ресурсов, а Long Tasks API — отдельный класс работы главного потока. Их интерфейсы полезны именно различием. Они не дают лицензии сложить несопоставимые числа в одно «время страницы». Один участок может отсутствовать, быть ограничен политикой происхождения или относиться к другой фазе пользовательского пути.'), p('Практическая ошибка — выбрать известное поле потому, что оно легко экспортируется, и выдать его за цель. Тогда состояние «пользователь может продолжить» заменяется технической прокладкой. В будущем это может быть разумная proxy-метрика, но связь надо обосновать в новом scope: назвать UI-событие, вариант маршрута, условие кэша и способ проверить расхождение. P112 не делает такую связь задним числом.'), p('Договор полезен и владельцу интерфейса, и владельцу платформы. Первый видит, какое действие нельзя потерять за техническим жаргоном. Второй видит, что от него ждут не абстрактной оптимизации, а ограничения на будущий сбор. Оба могут возразить до запуска: изменить границу, добавить required condition или признать, что вопрос слишком широк. Это дешевле, чем спорить после цифры, происхождение которой никто не может восстановить.'), h2('Граница задаёт единицу работы'), p('Navigation boundary — не синоним «пользователь открыл страницу». В договоре это только названная точка, от которой будущий исследователь вправе начать отсчёт. Для документа она может быть типом навигации, а для перехода внутри интерфейса — отдельной границей, которую нельзя молча назвать navigation. Если команда меняет стартовую точку, она меняет единицу работы; прежнее сравнение перестаёт отвечать на тот же вопрос.'), p('Resource boundary отвечает не за весь экран, а за конкретный класс будущих записей. У неё должна быть причина попадания в договор: ресурс нужен для выбранного состояния, а его видимость допускает будущий authorised method. Список «всего, что грузится» не является границей. Он смешивает необходимые и фоновые операции, а затем заставляет владельца объяснять результат, который договор изначально не выделил.'), p('Main-thread boundary относится к очереди работы, а не к внешнему ресурсу. Даже если будущий артефакт когда-либо опишет длительную задачу, из этого не следует, что задача отвечает за недоступность нужного действия. В контракте полезно отдельно назвать: ищем ли мы связь между очередью и действием или только проверяем, не выбран ли неверный слой. Эта оговорка защищает от автоматического перехода «есть задача» → «найдено объяснение». '), p('Action boundary — единственная граница, которую читает пользователь. Она не обязана совпасть ни с окончанием navigation, ни с последним resource entry, ни с освобождением главного потока. Её формулируют через возможность выполнить одно действие без догадки о реализации: например, контроль должен иметь определённое состояние и принять допустимый ввод. Пока такое правило не названо, техническая величина остаётся лишь кандидатурой на proxy, а не итогом пути.'), h2('Контракт на единицу работы и ownership'), p('Единица работы полезна, когда её можно передать без устного контекста. В P112 она состоит из id вопроса, revision, стартовой и конечной границ, перечня обязательных conditions, владельца следующего решения и стоп-условия. Это не запись будущего запуска. Все поля описывают, что потребуется определить позже, и потому не содержат route, URL, сервис, идентификатор пользователя или фактический объект браузера.'), table('Кому принадлежит элемент договора', ['Элемент', 'Что обязан сделать владелец', 'Что не может сделать этим документом'], [ ['Product/UI owner', 'Уточнить проверяемое доступное действие', 'объявить технический proxy пользовательским результатом'], ['Frontend owner', 'Назвать границу интерфейсной единицы работы', 'приписать ей будущий resource или main-thread факт'], ['Evidence owner', 'Запросить новый scope и политику сбора', 'собрать или интерпретировать данные из P112'], ['Review owner', 'Проверить полноту контракта и stop path', 'выбрать оптимизацию или разрешить rollout'], ]), p('Ownership не означает, что один человек «владеет производительностью». Он означает, что у каждой спорной формулировки есть адресат до того, как появится какой-либо артефакт. Если owner действия не согласен с action boundary, нельзя компенсировать это более подробным техническим описанием. Если evidence owner не может принять data policy, нельзя заменить авторизацию предположением. Контракт удерживает эти отказы на правильной стороне будущей работы.'), p('Полезный договор также указывает, кому принадлежит отмена вопроса. Когда действие уже нельзя определить без продукта, когда путь касается нескольких несогласованных интерфейсов или когда запрашиваемое доказательство недоступно по политике, owner может вернуть карточку в stop. Это не потеря темпа. Это прекращение задачи, у которой ещё нет безопасной единицы работы и, следовательно, нет критерия корректности.'), h2('Контрпример: правдоподобный proxy ломает вопрос'), p('Возьмём учебный, неисполняемый сценарий. Команда выбрала раннюю техническую границу и назвала её «готовностью». Однако целевое действие требует ещё одного логического состояния интерфейса, которое не входит в эту границу. Даже если будущая проверка когда-нибудь увидит раннее техническое событие, она не сможет из него вывести доступность действия. Сценарий не описывает существующий продукт; он показывает, почему имя поля не должно заменять договор.'), p('Обратная ошибка тоже возможна: взять позднюю завершённость всей активности и считать её обязательной для действия. Тогда фоновые процессы, не влияющие на выбранный контроль, искусственно удлиняют путь. Вывод «надо ускорять всё» выглядит осторожным, но у него нет предмета: он не говорит, какая единица работы должна стать другой. В таком состоянии нельзя честно выбрать ни owner, ни будущий evidence artefact.'), p('Контрпример полезен тем, что проверяет договор до инструмента. Если команда не может объяснить, почему выбранная граница необходима и достаточна для action boundary, она не готова к сбору. Вместо поиска более удобной метрики нужно переписать вопрос: отделить действие от proxy, назвать условия или отказаться от прежней постановки. Это и есть экономия: не исправлять чужую интерпретацию после того, как она уже стала решением.'), h2('Как остановить неверную постановку'), p('Stop нужен не только evaluator. На редакционном уровне постановку останавливают четыре признака: одна фраза одновременно называет navigation и action; resource список не имеет роли в выбранном состоянии; main-thread claim уже содержит причину; owner не может назвать, что отменит вопрос. Каждый признак означает не «недостаточно деталей», а то, что контракт склеил разные типы работы. Добавление ещё одного поля не лечит такую ошибку.'), p('Практический маршрут остановки короткий: вернуть карточку автору вопроса, сохранить только named contract и дату/cutoff, пометить неясный слой как unresolved, затем потребовать одно из двух — новую формулировку либо явное закрытие. Нельзя оставлять старый текст как «черновую гипотезу для замера»: следующий читатель всё равно примет его за ориентир. До нового authorised scope честнее иметь остановленную постановку, чем удобный, но составной proxy.'), h2('Безопасный runnable literal'), code("import { createCriticalPathPlan, assessCriticalPathPlan } from './scripts/upgrade-2027-06.mjs';\n\nconst plan = createCriticalPathPlan('critical-path-contract-2027-06-v1');\nconst handOff = assessCriticalPathPlan(plan);\nconsole.log(handOff.status, handOff.productionEffect);\n// synthetic-plan-hand-off not-attempted"), p('Этот пример исполняет только clone, freeze и проверку fixed in-memory literal. Он не открывает браузер, не читает каталог, не берёт environment, не обращается к часам и не создаёт запрос. Положительный ответ означает лишь, что карточка соблюдает собственные границы. Он не означает, что путь существует, что он критический или что его изменение даст результат.'), h2('Маршрут команды'), ol(['Назовите пользовательское состояние и стартовую границу одним предложением; если получается два предложения с «и», это два вопроса.', 'Дайте договору стабильный id и revision, не называя метрику будущим результатом.', 'Составьте список сравнимых условий: browser build, device class, cache state и route state; до нового scope все остаются not-collected.', 'Запишите запреты на вывод: никакого benchmark, profile, bottleneck, winner, rollout или production effect.', 'Передайте карточку владельцу будущего исследования; только он может запросить отдельную авторизацию на наблюдение.']), h2('Ограничение и следующий шаг'), p('Договор не решает, какие ресурсы блокируют рендер, не объясняет отзывчивость и не заменяет профиль. Его сила в другом: он делает отсутствие знания видимым и не позволяет «быстрой правке» выглядеть доказанным решением. Если после hand-off вопрос всё ещё нужен, следующий шаг — новый authorised scope с отдельными target, политикой данных, условиями сравнения, методом и правилом интерпретации.'), p('До этого шага результат P112 уже завершён: существует проверяемая постановка. Она не обещает ни ускорения, ни находки. Такой порядок полезнее общего совета «оптимизировать critical path», потому что команда сначала договаривается, что именно будет иметь право назвать улучшением.'), ], refs); const mechanism = revision({ slug: 'editorial-2027-06-mechanism-performance-capstone', title: 'Большой разбор производительности: сравнимость и falsifiability', categories: ['Frontend', 'Качество'], cover: '/assets/editorial/2027/performance-capstone-2027-evidence-boundary-matrix.svg', excerpt: 'План на июнь 2027: как построить проверяемый вопрос о производительности, не выдав сравнение условий за результат.', readingMinutes: 23 }, [ h2('Сравнить можно только одинаковый вопрос'), p('Самая дорогая ошибка performance-разбора — считать два разных условия экспериментом. Один прогон мог бы иметь иной cache state, build браузера, маршрут или устройство; второй — другую границу завершения. Если записать рядом два числа без контракта, команда получает не сравнение, а материал для подтверждения уже выбранного решения. Цена — ложный winner и повторная работа, когда вывод нельзя опровергнуть.'), p(mechanismBoundary + ' В механической части P112 отсутствуют числа и записи инструментов. Она показывает, какие утверждения должны быть falsifiable: заранее известно, какое условие может разрушить вывод. Пока conditions и evidence имеют статус not-collected, допустим только synthetic-plan-hand-off, а не оценка будущего варианта.'), p('Falsifiability здесь не философский декор. Это конкретный вопрос к карточке: «какая наблюдаемая разница заставит нас отказаться от вывода?» Если ответ не назван, «улучшение» невозможно отделить от предпочтения автора. Например, нельзя проверить claim о порядке критического пути, пока не зафиксированы граница начала, состояние кэша и правило, по которому выбранное действие считается доступным.'), figure('/assets/editorial/2027/performance-capstone-2027-evidence-boundary-matrix.svg', 'Матрица отделяет будущие условия сравнения от типов доказательств: каждый незаполненный слой ведёт к остановке, а не к выводу о производительности.', 'Матрица — не таблица результатов. Она показывает, почему отсутствующее условие не может быть заменено предположением.'), h2('Четыре слоя, которые нельзя склеивать'), p('Первый слой — intent: что пользователь пытается сделать. Второй — execution boundary: где начинается и заканчивается выбранный путь. Третий — conditions: версия браузера, класс устройства, состояние кэша и состояние маршрута. Четвёртый — evidence: будущая запись, полученная в разрешённом процессе. Между слоями нельзя перепрыгнуть. Описание API относится к vocabulary, но не превращает intent в событие и не создаёт evidence.'), p('Такое разделение защищает от популярной подмены: технический artefact оказывается реальным, поэтому его пытаются считать ответом на продуктовый вопрос. Но Resource Timing может описывать ресурсную запись, а пользовательский путь зависеть от другого действия; Long Tasks API определяет категорию задачи, но не доказывает, что именно она мешает конкретному переходу. Доказательство имеет силу лишь в пределах заранее названного слоя.'), table('Матрица проверяемости будущего вывода', ['Слой', 'Вопрос до запуска', 'Что опровергнет claim', 'Недопустимая подмена'], [ ['Intent', 'Какое действие доступно пользователю?', 'другое определение действия', 'техническое поле вместо действия'], ['Boundary', 'Где начинается и заканчивается путь?', 'изменённая граница', 'сумма несвязанных фаз'], ['Conditions', 'Какие параметры должны совпасть?', 'зафиксированное различие условий', 'assumed cache или device'], ['Evidence', 'Какой artefact разрешён?', 'отсутствие provenance', 'документация инструмента как результат'], ]), h2('Почему документация не является замером'), p('Официальная спецификация сообщает семантику интерфейса и его ограничения. Это первичный источник для слов «navigation entry», «resource entry» или «long task». Она не сообщает, что будущая страница имеет такую запись, что entry доступен в выбранных условиях или что его значение объясняет пользовательскую задержку. Именно поэтому список источников в этой статье привязан к применимости каждого документа, а не играет роль списка подтверждённых фактов о системе.'), p('У такой дисциплины есть практическая цена: документ получается менее эффектным. В нём нет графика, который можно показать как победу, и нет profile, который можно обсудить. Зато он сохраняет falsifier. Если будущий сбор покажет несоответствие условий, owner обязан отменить сравнение или открыть новый вопрос, а не нормализовать неудобный факт словами «разница небольшая».'), p('Остановка — нормальный результат механизма. Она предотвращает дорогую ложную точность. В evaluator остановка возвращает причину и безопасный next action; она не заполняет пропуск значением по умолчанию. Эта деталь важнее удобства API: default browser или guessed cache превращают неизвестное условие в скрытую предпосылку, а затем вывод получает видимость повторяемости.'), h2('Condition ledger: условия не живут в голове'), p('Condition ledger — это не журнал результатов, а список переменных, без которых будущий вопрос нельзя назвать тем же самым. У каждой строки есть имя, причина включения, допустимое состояние до авторизации и правило, при котором сравнение прекращается. В P112 значения строк не заполняются: browser build, device class, cache state и route state остаются not-collected. Важна не их будущая величина, а обязанность не потерять переменную между формулировкой и решением.'), p('Леджер не должен собирать всё, что можно узнать. Избыточный список превращает проверку в ритуал и делает любое несоответствие удобным поводом ничего не решать. Для каждого condition нужен вопрос «как его различие изменит смысл claim?». Если ответ отсутствует, строка не является confounder и не должна попасть в контракт. Если различие может изменить ответ, строка обязана иметь fail condition, а не пометку «желательно совпадение». '), table('Condition ledger для будущего исследования', ['Condition', 'Почему влияет на вопрос', 'До нового scope', 'Что делает различие'], [ ['Browser build', 'семантика и доступность будущих API могут отличаться', 'not-collected', 'отменяет прямое сопоставление'], ['Device class', 'исполнение и ввод относятся к разным возможностям', 'not-collected', 'требует раздельного вопроса'], ['Cache state', 'путь может включать разные классы получения данных', 'not-collected', 'делает baseline другим'], ['Route state', 'одинаковое действие может иметь разную предысторию', 'not-collected', 'возвращает к уточнению boundary'], ]), p('В будущей работе ledger должен быть закрыт до интерпретации, а не после неё. Если сначала увидеть число, а затем подбирать conditions, команда почти неизбежно выберет только те параметры, которые сохраняют любимую историю. P112 не предлагает такой последовательности. Он оставляет condition list пустым по факту, но полным по требованиям: отсутствие записи честнее, чем ретроспективно подобранная объясняющая рамка.'), h2('Baseline — правило сравнения, а не удобное прошлое число'), p('Baseline часто называют «тем, что было раньше», и этим уже портят вопрос. В строгом смысле baseline — версия договора, с которой будущий evidence artefact можно сравнить по той же границе, тем же conditions и тому же правилу интерпретации. Он не возникает из памяти команды, из случайного скрина или из документации API. Пока в P112 нет authorised observation, baseline существует только как требование к следующему scope, а не как скрытый reference result.'), p('У baseline есть две части. Первая — semantic: та же пользовательская единица работы и та же boundary. Вторая — operational: тот же набор существенных conditions из ledger. Сохранение одной части без другой не даёт сравнения. Например, одинаковое техническое поле при другой route state не становится baseline только потому, что его легко вывести. Такой случай надо назвать новым вопросом или остановить до появления независимого решения.'), h2('Confounders, privacy и граница видимости'), p('Confounder — не любая неизвестность, а условие, которое может сменить объяснение наблюдаемого различия. Cache, device и route полезны именно потому, что могут изменить смысл будущего entry. Но леджер не даёт права добывать их из удобного источника. Источник, метод и разрешение принадлежат новому scope. В P112 нет ни одного такого значения, поэтому нельзя говорить, что confounder устранён или что он несущественен.'), p('Visibility имеет собственную границу. Resource Timing описывает механизм, в котором детализация cross-origin resource может зависеть от Timing-Allow-Origin (TAO). Это свойство спецификации, а не утверждение о каком-либо ответе. Для будущего контракта следствие скромное: отсутствие доступной детали нельзя трактовать ни как нулевую стоимость, ни как доказательство отсутствия ресурса. Сначала owner должен решить, допустимо ли вообще запрашивать такой evidence и как будет описана его неполнота.'), p('Privacy boundary появляется раньше аналитики. Даже безопасное с виду поле может стать связующим идентификатором в сочетании с route, временем или устройством. Поэтому P112 не предлагает собирать «минимальный набор на всякий случай». До нового разрешения допустим только schema вопроса. Будущий data policy должен отдельно описать цель, минимальность, хранение, redaction и того, кто увидит raw artefact. Ссылка на публичную спецификацию не отменяет эти обязанности.'), h2('Почему число ещё не causal claim'), p('Число само по себе обозначает значение в некотором artefact, но не объясняет его происхождение. Оно не указывает, какой слой пути изменился, какая переменная осталась постоянной и что произошло бы без предполагаемой причины. Даже два будущих значения при одинаковой подписи не становятся causal claim, если boundary, conditions или provenance не были зафиксированы до интерпретации. У числа есть формат; у причинного вывода есть дополнительные обязательства.'), p('Для причинного вопроса заранее называют candidate change, механизм, который должен быть затронут, и наблюдение, способное опровергнуть связь. Это не просьба обещать effect. Это запрет выдавать совпадение за объяснение. Если будущий artefact не различает, изменился ли action boundary, cache state или внешний слой, корректный вывод должен быть слабее: «данные не поддерживают выбранное объяснение», а не «причина найдена». '), h2('Фальсификация до интерпретации'), p('Перед любым новым scope команда может записать четыре будущих исхода без цифр: conditions совпали и вопрос остаётся открытым; condition разошёлся и сравнение отменено; visibility не позволила получить допустимый artefact; action boundary оказалась другой, чем предполагал договор. Каждый исход меняет следующий шаг, но ни один не превращается в benchmark result. Это делает план операционным: владелец знает, когда прекращать, а не только когда продолжать.'), h2('Runnable проверка отказа'), code("import { createCriticalPathPlan, assessCriticalPathPlan } from './scripts/upgrade-2027-06.mjs';\n\nconst impossibleComparison = createCriticalPathPlan('conditions-claimed-without-record-v1');\nconst verdict = assessCriticalPathPlan(impossibleComparison);\nconsole.log(verdict.status, verdict.productionEffect);\n// stop-conditions-claimed-without-record not-attempted"), p('Пример получает заранее описанный отрицательный literal и возвращает stop-status. Он не подменяет запись измерением: строка assumed находится внутри in-memory fixture и нужна, чтобы проверить fail-closed ветку. В модуле отсутствуют filesystem, network, process environment, clock, telemetry SDK и profile API. Поэтому пример можно запускать как проверку контракта, а не как скрытый benchmark.'), h2('Как подготовить falsifiable вопрос'), ol(['Сформулируйте один claim в условном будущем времени: что именно предполагается проверить, без обещания результата.', 'Укажите границу начала и конца, не используя одно техническое имя как замену пользовательскому состоянию.', 'Перечислите условия, различие которых отменяет сравнение, и оставьте их not-collected до отдельной авторизации.', 'Определите допустимый evidence artefact и правило provenance; source documentation в этот artefact не превращайте.', 'Заранее запишите stop path: что owner сделает при несовпадении условий, отсутствии данных или спорной границе.']), h2('Где кончается механизм'), p('Механизм не утверждает, что строгая форма автоматически даёт хороший performance-план. Он не выбирает метрику, не устанавливает sample size и не заменяет исследование реальными данными. Он всего лишь делает будущий вывод уязвимым для корректного опровержения. Это ограничение полезно: оно не даёт превратить контракт в универсальный шаблон для любой команды и любого интерфейса.'), p('Следующий шаг возможен только в новом authorised scope. Его владелец должен отдельно решить, допустимы ли target и данные, какие версии и состояния будут записаны, что считается comparability failure и кто принимает интерпретацию. До тех пор единственный честный итог P112 — структурированная возможность сказать «сравнения пока нет».'), ], refs); const field = revision({ slug: 'editorial-2027-06-field-performance-capstone', title: 'Большой разбор производительности: evidence hand-off без подмены результата', categories: ['Инженерные практики', 'Практика команд'], cover: '/assets/editorial/2027/performance-capstone-2027-handoff-loop.svg', excerpt: 'План на июнь 2027: передать evidence-вопрос следующему владельцу, не превращая пустую карточку в performance-отчёт.', readingMinutes: 21 }, [ h2('Передача — это не отчёт без цифр'), p('Проблема полевой работы — самый опасный артефакт выглядит аккуратно: в нём есть заголовок «performance», пара названий метрик и уверенное «проверить позже». Получатель часто читает такую карточку как сокращённый отчёт и начинает действовать так, будто bottleneck уже известен. Цена — лишний rollout, неверно выбранный owner и утрата вопроса, который действительно нужно было задать.'), p(fieldBoundary + ' Полевой фокус P112 — evidence hand-off. Он передаёт не наблюдение, а обязанность не подменять отсутствие наблюдения результатом. Карточка сохраняет named contract, blank conditions и явный next action: сначала открыть новый authorised scope, затем решать, возможен ли какой-либо сбор.'), figure('/assets/editorial/2027/performance-capstone-2027-handoff-loop.svg', 'Цикл передачи evidence: плановый договор проверяет дату, условия и отсутствие наблюдений, после чего передаёт вопрос новому владельцу либо останавливается.', 'Цикл завершает передачу синтетического вопроса. Он не содержит реальных профилей, benchmark-значений или решения о rollout.'), h2('Минимум provenance для будущего владельца'), p('В этой карточке provenance означает происхождение утверждения, а не логотип инструмента. У named contract происхождение простое: это fixed literal редакционного плана. У conditions и evidence происхождения нет, поэтому они честно имеют статус not-collected. У официальных спецификаций происхождение есть: точный URL, дата и версия. Но они подтверждают только термины и ограничения интерфейсов, не будущую картину продукта.'), p('Такое различение спасает от подмены «мы знаем, чем измерять» на «мы уже измерили». Библиотека, DevTools или API могут быть уместны в авторизованном исследовании, однако факт их существования ничего не говорит о выбранном target, конфиденциальности данных, representativeness или причине задержки. Hand-off должен сохранить эту дистанцию, иначе future owner начнёт с ложной предпосылки.'), table('Содержимое evidence hand-off', ['Элемент', 'Что несёт P112', 'Чего он не несёт'], [ ['Контракт', 'id, revision и future question', 'причину проблемы'], ['Границы', 'planDate и source cutoff', 'временную линию будущего события'], ['Состояния', 'not-collected / not-attempted', 'telemetry, profile или benchmark output'], ['Владелец', 'следующее безопасное действие', 'авторизацию, rollout или решение победителя'], ]), h2('Как не испортить передачу полезными деталями'), p('Соблазн велик: добавить предполагаемый route, название service, скрин из чужого профиля или «типичную» последовательность запросов. Эти детали ускоряют чтение, но меняют класс документа. Если происхождение не записано и scope не разрешён, detail становится необъявленным наблюдением. Дальше его трудно убрать: он уже влияет на выбор owner и на то, какие гипотезы команда считает допустимыми.'), p('Вместо детали hand-off оставляет вопрос на уровне, который можно пересмотреть. Он говорит: существует будущая проверка границы критического пути, но условия ещё не собраны. Это не уклонение от ответственности. Это точная ответственность автора плана: не добавлять сервисные и telemetry-факты, которые принадлежат будущему исследованию. Если вопрос не стоит дополнительного scope, owner имеет право закрыть его без запуска.'), p('Fail-closed evaluator поддерживает ту же дисциплину. Неизвестный объект не получает позитивный статус; дата не выводится из редакторского календаря; claimed observation не «понижается» до предупреждения. Такой интерфейс специально неудобен для обхода. Он показывает получателю, что корректная карточка либо полностью именована, либо остановлена с причиной и следующим безопасным действием.'), h2('Evidence record отличается от отчёта'), p('Evidence record фиксирует происхождение утверждения так, чтобы другой человек мог определить его силу. Для будущей performance-работы запись должна различать как минимум: contract, автора вопроса, разрешение на сбор, метод, время действия scope, применимые conditions, границы видимости, raw artefact и интерпретацию. В P112 заполнена лишь первая группа: редакционный named contract, версия, дата сценария и source cutoff. Остальные поля не пустые по забывчивости — они не принадлежат этому scope.'), p('Отчёт устроен иначе: он связывает вопрос с полученными artefact и выводом. Попытка сделать отчёт из plan record создаёт ложную непрерывность. Например, одна формулировка «данные будут собраны» уже звучит как факт о разрешении и методе, хотя P112 никого не уполномочивает. Карточка не обещает, что сбор когда-либо состоится. Она передаёт ограничение: если он появится, provenance должен быть создан рядом с ним, а не ретроспективно добавлен сюда.'), table('Поля будущего evidence record и статус P112', ['Поле', 'Зачем нужно будущей работе', 'Статус в P112'], [ ['Question contract', 'связывает artefact с одним проверяемым вопросом', 'named fixed literal'], ['Collection authority', 'показывает, кто и в каком scope вправе действовать', 'not-authorised here'], ['Method and visibility', 'объясняет, какие границы и ограничения применимы', 'not-selected'], ['Raw artefact and interpretation', 'отделяет запись от решения', 'not-collected'], ]), p('Особенно опасно смешать provenance источника и provenance данных. У W3C snapshot можно проверить URL и дату; это позволяет ссылаться на определение интерфейса. У будущего artefact происхождение будет другим: он потребует scope, способа получения и политики обращения с данными. Первый тип provenance не компенсирует второй. Публичная документация не делает неизвестный entry настоящим, а naming API не наделяет редакционный текст правом собрать его.'), h2('Кто вправе собирать и кто вправе решать'), p('В P112 роли описаны как будущая граница, а не как назначение конкретных людей. Editor может сформировать план и проверить temporal honesty. Evidence owner может предложить отдельный scope. Data/privacy owner может оценить допустимость категорий данных и ограничений хранения. Review owner может проверить, соответствует ли будущий artefact договору. Ни одна из этих ролей не получает из текущего модуля право открыть инструменты, увидеть raw telemetry или запустить сбор.'), p('В будущем scope право на действие должно быть уже, чем вопрос. Например, контракт может разрешать только заранее описанную категорию artefact, только для выбранной boundary и только в оговорённый срок. Если нужная деталь оказывается вне этих рамок, owner не расширяет её «по ходу». Он останавливает работу и возвращается к авторизации. Иначе provenance записи будет говорить об одном методе, а фактическая работа — о другом.'), h2('No-data hand-off — полноценный результат'), p('Передача без данных часто кажется слабой, потому что в ней нечего визуализировать. Но она полезна, если сохраняет четыре отрицания: evidence не collected, benchmark не attempted, profile не available здесь, а production effect не заявлен. Эти состояния дают следующему владельцу возможность начать с чистой границы, а не тратить время на отделение выдуманного фона от настоящих входов. No-data hand-off — не пустой файл, а контроль над тем, чего файл не утверждает.'), p('Чтобы такая передача не стала тупиком, в ней должен быть reasoned next action. В P112 он один: открыть новый authorised scope до любого наблюдения. Он не говорит «собрать всё», не задаёт target и не навязывает tool. Новый owner может принять этот маршрут, сузить question или закрыть его. Все три решения честны, потому что старый документ не предлагает скрытый baseline, готовую выборку или обещанный эффект.'), p('No-data hand-off также защищает от ретроспективной подмены. Если позже появится исследование, его автор не должен вставлять результат в этот plan, даже если contract совпадает по названию. Совпадение вопроса не делает совпадением evidence context. У новой работы будут собственные дату, scope, collector, policy, source set и результат интерпретации. Старая карточка может быть ссылкой на постановку, но не контейнером для полученных фактов.'), h2('Review не принимает решение за owner'), p('Редакционный review проверяет форму высказывания: temporal boundary, источники, отсутствие поддельного evidence, читаемость схемы и стоп-ветви. Он не проверяет существующую скорость и не выбирает, что менять. Даже если reviewer уверен в популярной гипотезе, его обязанность — зафиксировать, что она остаётся гипотезой до другой авторизованной работы. Иначе review превращается в канал, через который мнение получает видимость технического результата.'), p('Decision review появляется только после того, как новый scope определил допустимый evidence и его interpretation boundary. Его вопрос будет иным: поддерживают ли полученные artefact ограниченный claim и достаточно ли этого для решения. Это не продолжение текущего review по инерции. Между ними лежат авторизация, сбор, provenance, проверка conditions и возможный stop. Удаление любого звена делает решение зависимым от доверия, а не от контракта.'), h2('Контракт следующего scope'), p('Если hand-off будет принят, следующий документ начинает не с команды и не с dashboard, а с нового контракта. В нём отдельно фиксируют target class без лишних идентификаторов, action boundary, hypothesised mechanism, allowable evidence, condition ledger, privacy limits, retention/redaction policy, reviewer и decision rule. Каждый пункт должен быть написан до того, как появится результат. P112 не заполняет эти поля и не предлагает считать их значениями по умолчанию.'), p('Наконец, новый контракт должен разделить факт, интерпретацию и решение. Факт принадлежит artefact с provenance; интерпретация ограничена объявленной boundary; решение имеет owner и последствия, которых P112 не определяет. В этой последовательности нет гарантии, что появится полезная цифра. Зато нет и короткого пути, по которому одна плановая карточка превращается в якобы готовое обоснование действия.'), h2('Runnable hand-off без скрытого доступа'), code("import { createCriticalPathPlan, assessCriticalPathPlan } from './scripts/upgrade-2027-06.mjs';\n\nconst card = createCriticalPathPlan('critical-path-contract-2027-06-v1');\nconst result = assessCriticalPathPlan(card);\nconsole.log({ status: result.status, next: result.nextAction });\n// { status: 'synthetic-plan-hand-off', next: 'open-a-new-authorised-scope-before-any-observation' }"), p('Здесь нет чтения файлов, сети, environment, часов, secrets, telemetry, систем или данных. Factory возвращает копию fixed literal и freeze фиксирует вложенные поля. Evaluator сравнивает вход только с известными literal; произвольный объект закрывается stop-status. Вывод описывает передачу карточки, а не факт о browser, service, production или пользователе.'), h2('Порядок evidence hand-off'), ol(['Проверьте, что документ помечен как 2027-06 plan/scenario и содержит source cutoff 2026-07-31.', 'Оставьте только named contract и required conditions; удалите route, service, telemetry и profile details, если они не принадлежат новому scope.', 'Отметьте benchmark как not-attempted, а evidence как not-collected; отсутствие данных не смягчайте прогнозом.', 'Прогоните fixture с положительным hand-off и всеми stop cases, чтобы проверка не стала permissive.', 'Передайте owner действие открыть новый authorised scope или закрыть вопрос; не ставьте rollout, winner либо production change в очередь.']), h2('Граница ответственности и следующий шаг'), p('Hand-off не даёт доказательства и не обязан его имитировать. Он не покажет, существует ли bottleneck, не назовёт effect и не выберет optimisation. Его результат — меньшая вероятность, что команда примет уже сформулированную гипотезу за наблюдение. Это особенно важно для performance-тем: правдоподобная история о задержке часто убедительнее, чем честное «данных нет».'), p('Следующий шаг — только новый authorised scope с самостоятельным решением о target, data policy, collection method, comparability и interpretation. Его итог нельзя вставить в P112 и нельзя использовать P112 как evidence. Пока такого scope нет, synthetic-plan-hand-off с productionEffect: not-attempted остаётся полным и единственным положительным результатом этого выпуска.'), ], refs); export const revisions = deepFreeze([practice, mechanism, field]); export function verifyRevisionsAgainstFixture() { const fixture = runCriticalPathFixture(); const articleChecks = revisions.map((item) => { const text = bodyText(item.contentHtml); return text.length >= 9000 && text.length <= 12000 && /(цен[аы]|стоимост|издержк|потер|дороже)/i.test(text.slice(0, 1100)) && //.test(item.contentHtml) && /
/.test(item.contentHtml) && /
/.test(item.contentHtml) && /
    /.test(item.contentHtml) && /2027-06/.test(text) && /2026-07-31/.test(text) && /productionEffect: not-attempted/.test(text); }); return deepFreeze({ passed: fixture.passed + articleChecks.filter(Boolean).length, total: fixture.total + articleChecks.length, accepted: fixture.accepted && articleChecks.every(Boolean), fixture, articleChecks, characters: Object.fromEntries(revisions.map((item) => [item.slug, bodyText(item.contentHtml).length])) }); } if (process.argv.includes('--verify-fixture')) { const result = verifyRevisionsAgainstFixture(); process.stdout.write(JSON.stringify(result, null, 2) + '\n'); if (!result.accepted) process.exitCode = 1; } if (process.argv.includes('--print-revisions')) process.stdout.write(JSON.stringify(revisions) + '\n');