This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Производство редакционных партий
|
# Производство редакционных партий
|
||||||
|
|
||||||
На 31 июля 2026 года строгий аудит проходит 145 из 358 созданных материалов. Остальные 213 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
|
На 31 июля 2026 года строгий аудит проходит 148 из 358 созданных материалов. Остальные 210 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
|
||||||
|
|
||||||
## Одна партия
|
## Одна партия
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
# Автономное тройное ревью П48 · февраль 2022 · «Бюджет производительности пути»
|
||||||
|
|
||||||
|
Статус: **готово к независимой интеграционной приёмке**. Это только sidecar
|
||||||
|
author package: registry, README, <code>articles.json</code>, общие audit
|
||||||
|
scripts, редакционные правила, очередь и чужие файлы не менялись. Git add,
|
||||||
|
commit и push не выполнялись.
|
||||||
|
|
||||||
|
Созданы ровно пять разрешённых файлов:
|
||||||
|
|
||||||
|
1. <code>web/scripts/upgrade-2022-02.mjs</code>;
|
||||||
|
2. <code>editorial/reviews/2022-02-draft.md</code>;
|
||||||
|
3. <code>web/public/assets/editorial/2022/performance-budget-allocation-2022.svg</code>;
|
||||||
|
4. <code>web/public/assets/editorial/2022/performance-budget-trend-2022.svg</code>;
|
||||||
|
5. <code>web/public/assets/editorial/2022/performance-budget-diagnosis-2022.svg</code>.
|
||||||
|
|
||||||
|
Revision-модуль экспортирует ровно три overlay revision для stable slug:
|
||||||
|
|
||||||
|
- <code>editorial-2022-02-practice-performance-budget</code>;
|
||||||
|
- <code>editorial-2022-02-mechanism-performance-budget</code>;
|
||||||
|
- <code>editorial-2022-02-field-performance-budget</code>.
|
||||||
|
|
||||||
|
В export есть только поля <code>slug</code>, <code>title</code>,
|
||||||
|
<code>categories</code>, <code>cover</code>, <code>excerpt</code>,
|
||||||
|
<code>readingMinutes</code> и <code>contentHtml</code>. Полей
|
||||||
|
<code>date</code> и <code>author</code> нет: их сохраняет базовый архив.
|
||||||
|
<code>--print-revisions</code> печатает тот же JSON, что и import-safe export;
|
||||||
|
draft gate проверил это сравнением.
|
||||||
|
|
||||||
|
## Проход 1. Голос, структура и объём — пройдено
|
||||||
|
|
||||||
|
| Revision | Проблема и цена в первых двух абзацах | Главный вопрос | Body |
|
||||||
|
| --- | --- | --- | ---: |
|
||||||
|
| Практика | На пути оформления маленькие изменения проходят общий PASS, а часть пути постепенно съедает запас; цена — зелёный сигнал без владельца изменения | Как зафиксировать route, conditions, allocation и named component rule до сравнения candidate | **9 995** |
|
||||||
|
| Механизм | Total PASS принимают за доказательство здоровья пути; цена — budget превращается в таблицу, которая не запрещает частную регрессию | Почему contract, snapshot, component checks, aggregate и diagnosis имеют разные роли | **9 989** |
|
||||||
|
| Полевой разбор | Total остаётся зелёным, а один component красный; цена — накопление диагностического долга и широкие правки по одной строке | Как остановить несопоставимое сравнение и сузить следующий факт до одного component и одной boundary | **9 949** |
|
||||||
|
|
||||||
|
- Все тексты находятся в требуемом П48 коридоре **7–10 тыс.** и обязательном
|
||||||
|
диапазоне 5–15 тыс. знаков body.
|
||||||
|
- В каждом revision: девять или больше смысловых <code>h2</code>, accessible
|
||||||
|
table с <code>caption</code>/<code>thead</code>, свой figure с точным
|
||||||
|
<code>alt</code>/<code>figcaption</code>, несколько согласованных
|
||||||
|
JavaScript examples, нумерованный маршрут и раздел
|
||||||
|
«Проверяемые источники» с тремя official URLs.
|
||||||
|
- Голос соответствует М5 / февралю 2022: текст начинает с конкретного
|
||||||
|
пользовательского пути, говорит коротко «симптом → причина → проверка →
|
||||||
|
действие», а архитектурный вывод появляется только после описанного
|
||||||
|
наблюдения. Нигде не приписывается существующая production-команда, CI,
|
||||||
|
Chrome trace, LCP, real-user metric или SLA.
|
||||||
|
- В первой ручной вычитке draft gate обнаружил, что у практической статьи
|
||||||
|
формальное слово «проблема» находилось позже первых 800 знаков. Первый
|
||||||
|
абзац переписан как «Проблема начинается…»; повторный audit прошёл.
|
||||||
|
|
||||||
|
Вердикт прохода: **пройден**. Тексты не растягивают один вывод: каждый
|
||||||
|
переходит от contract к snapshot, compare, ограниченной диагностике и
|
||||||
|
проверке следующего изменения.
|
||||||
|
|
||||||
|
## Проход 2. Fixture и техническая согласованность — пройдено
|
||||||
|
|
||||||
|
<code>createTeachingBudget()</code> и
|
||||||
|
<code>runPerformanceBudgetFixture()</code> работают только с frozen plain
|
||||||
|
objects и arrays в памяти. Условный navigation workload имеет route
|
||||||
|
<code>/training/checkout/review</code>, scenario
|
||||||
|
<code>anonymous-cart-with-one-item</code> и четыре собственных поля:
|
||||||
|
<code>documentTicks</code>, <code>styleTicks</code>,
|
||||||
|
<code>scriptTicks</code>, <code>renderTicks</code>.
|
||||||
|
|
||||||
|
Это **не** названия Web Vitals и не browser fields. Unit
|
||||||
|
<code>teaching-ticks</code>, а browser observation, CI observation и trace
|
||||||
|
явно имеют <code>not-collected</code>; сеть, CPU и cache —
|
||||||
|
<code>not-modelled</code>. Fixture не вызывает browser, Lighthouse, Chrome,
|
||||||
|
Performance API, PerformanceObserver, HTTP, сеть, таймер, file system или CI.
|
||||||
|
|
||||||
|
| Объект | Значения и правило | Результат модели | Честная граница |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Baseline | 24 / 15 / 26 / 20; каждый component не больше limit + tolerance | overall PASS, total 85 | не измеренный маршрут |
|
||||||
|
| Candidate | меняется только <code>scriptTicks</code>: 26 → 34 | component FAIL: 34 больше allowed 30 | не установленная причина изменения |
|
||||||
|
| Aggregate | candidate total 93; allowed 95 + 3 = 98 | aggregate PASS | не имеет права отменить named FAIL |
|
||||||
|
| Conditions mismatch | меняется только учебное cache condition | <code>measurement-contract-invalid</code>, aggregate <code>not-comparable</code> | не regression verdict |
|
||||||
|
| Diagnosis | failed name — <code>scriptTicks</code> | <code>inspect-script-input-and-one-route-boundary</code> | не global performance claim |
|
||||||
|
|
||||||
|
Проверены **15/15 assertions**:
|
||||||
|
|
||||||
|
1. baseline проходит каждый named budget;
|
||||||
|
2. candidate падает по именованному <code>scriptTicks</code>;
|
||||||
|
3. overall становится FAIL при одном failed component;
|
||||||
|
4. aggregate PASS не скрывает named failure;
|
||||||
|
5. component tolerance печатается явно;
|
||||||
|
6. aggregate tolerance печатается явно;
|
||||||
|
7. baseline и candidate имеют один declared contract;
|
||||||
|
8. несовпадение conditions блокирует compare;
|
||||||
|
9. все четыре components остаются отдельными в output;
|
||||||
|
10. diagnosis ограничена <code>scriptTicks</code>;
|
||||||
|
11. diagnosis ведёт к focused action, а не global conclusion;
|
||||||
|
12. учебная арифметика totals согласована;
|
||||||
|
13. неизвестное или отрицательное поле не проходит validation;
|
||||||
|
14. fixture не выполняет external observation;
|
||||||
|
15. повторное сравнение детерминировано.
|
||||||
|
|
||||||
|
Проверены по два code fragments в каждом тексте: practice читает
|
||||||
|
<code>baselineComparison</code>/<code>candidateComparison</code> и guard для
|
||||||
|
first failure; mechanism создаёт baseline/candidate и отдельный condition
|
||||||
|
mismatch; field читает <code>diagnosis</code>/<code>measurementConditions</code>
|
||||||
|
и повторно проверяет границу scope. Ни один snippet не обращается к
|
||||||
|
несуществующему export или полю.
|
||||||
|
|
||||||
|
Вердикт прохода: **пройден**. Модель доказывает только собственный контракт:
|
||||||
|
component rule сильнее aggregate, tolerance и conditions видимы, а failure
|
||||||
|
создаёт ограниченную следующую проверку. Она не вычисляет реальную скорость.
|
||||||
|
|
||||||
|
## Проход 3. Исторические источники и визуал — пройдено
|
||||||
|
|
||||||
|
| Источник | Историческая рамка | Что разрешено использовать | Что текст не утверждает |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| [W3C Navigation Timing Level 2, 17 Jan 2022](https://www.w3.org/TR/2022/WD-navigation-timing-2-20220117/) | опубликован до всех дат февраля 2022 | существование <code>PerformanceNavigationTiming</code> и navigation timing information | что fixture fields являются platform timing fields |
|
||||||
|
| [W3C User Timing Level 3, 13 Sep 2021](https://www.w3.org/TR/2021/WD-user-timing-3-20210913/) | существовал до февраля 2022 | исторические mark/measure и metadata terminology | что package записывает mark, measure или browser timestamp |
|
||||||
|
| [W3C Performance Timeline Level 2, 19 Aug 2021](https://www.w3.org/TR/2021/WD-performance-timeline-2-20210819/) | существовал до февраля 2022 | historical meaning of performance entries and retrieval | что CI PASS, budget allocation или ticks определены W3C |
|
||||||
|
|
||||||
|
Все три источника — датированные W3C working drafts; в текстах это прямо
|
||||||
|
названо. Использованы только как историческая рамка официальных platform
|
||||||
|
interfaces. В пакете нет поздних interaction metrics, future INP, current-only
|
||||||
|
Lighthouse recommendations или придуманной метрики Chrome/пользователя.
|
||||||
|
|
||||||
|
SVG прошли mobile-first проверку:
|
||||||
|
|
||||||
|
- <code>performance-budget-allocation-2022.svg</code> показывает четыре
|
||||||
|
named checks, explicit limit+tolerance, aggregate PASS и overall FAIL в
|
||||||
|
одной вертикальной композиции;
|
||||||
|
- <code>performance-budget-trend-2022.svg</code> сравнивает baseline и
|
||||||
|
candidate для total и <code>scriptTicks</code>, не называя ticks
|
||||||
|
browser result;
|
||||||
|
- <code>performance-budget-diagnosis-2022.svg</code> показывает stop при
|
||||||
|
condition mismatch, порядок component → aggregate и focused action.
|
||||||
|
|
||||||
|
У каждого SVG есть <code>title</code>, <code>desc</code>,
|
||||||
|
<code>role="img"</code>, контрастные labels и viewBox для узкого экрана.
|
||||||
|
В XML нет <code>script</code>, <code>foreignObject</code>, external
|
||||||
|
<code>href</code>/<code>src</code> и raster data URI.
|
||||||
|
|
||||||
|
### Фактически выполненные проверки
|
||||||
|
|
||||||
|
Команды выполнены из <code>web/</code>:
|
||||||
|
|
||||||
|
<pre><code>node --check scripts/upgrade-2022-02.mjs
|
||||||
|
npm run audit:draft -- scripts/upgrade-2022-02.mjs
|
||||||
|
node scripts/upgrade-2022-02.mjs --verify-fixture
|
||||||
|
xmllint --noout \
|
||||||
|
public/assets/editorial/2022/performance-budget-allocation-2022.svg \
|
||||||
|
public/assets/editorial/2022/performance-budget-trend-2022.svg \
|
||||||
|
public/assets/editorial/2022/performance-budget-diagnosis-2022.svg
|
||||||
|
rg -n -i '<script|<foreignObject|(?:href|src)="(?:https?:|data:)|xlink:href=' \
|
||||||
|
public/assets/editorial/2022/performance-budget-allocation-2022.svg \
|
||||||
|
public/assets/editorial/2022/performance-budget-trend-2022.svg \
|
||||||
|
public/assets/editorial/2022/performance-budget-diagnosis-2022.svg
|
||||||
|
node -e "import('sharp').then(async ({default: sharp}) => { ... resize({width:375}) ... })"</code></pre>
|
||||||
|
|
||||||
|
| Проверка | Фактический результат |
|
||||||
|
| --- | --- |
|
||||||
|
| <code>node --check</code> | PASS, code 0 |
|
||||||
|
| Draft gate, финальный запуск | PASS: **9 995 / 9 989 / 9 949** body chars; проверены archive slugs, overlay shape без date/author, sections, tables, figures, code, ordered routes, sources, local assets и import/CLI equality |
|
||||||
|
| In-memory fixture | PASS: **15/15 assertions** |
|
||||||
|
| <code>xmllint --noout</code> | PASS, все три SVG — корректный XML |
|
||||||
|
| SVG safety scan | PASS: совпадений нет; <code>rg</code> завершился с code 1, что для поиска означает ожидаемый пустой результат |
|
||||||
|
| Sharp 375 px | PASS: все три SVG отрендерены в PNG и вручную просмотрены; headings, cards, arrows, labels и нижние ограничения читаемы, clipping/overlap/horizontal overflow не обнаружены |
|
||||||
|
| Scope/self-review | PASS: созданы только пять разрешённых P48 файлов; shared registry, README, base archive, queue, Git и чужие mascot PNG не затронуты |
|
||||||
|
|
||||||
|
<code>npm run audit:draft</code> завершилась с code 0. npm вывел уже
|
||||||
|
существующие предупреждения о <code>store-dir</code>, <code>cache-dir</code>
|
||||||
|
и <code>public-hoist-pattern</code>; они не относятся к П48 и не менялись.
|
||||||
|
|
||||||
|
Не запускались strict audit после registry integration, production build,
|
||||||
|
browser or screen-reader test, Lighthouse, Chrome, реальный CI job, network
|
||||||
|
workload, trace, RUM, commit или публикация. Визуальная проверка относится к
|
||||||
|
локальному Sharp render 375 px, а не к фактическому browser layout. Это
|
||||||
|
намеренные ограничения автономной партии.
|
||||||
|
|
||||||
|
## Итог
|
||||||
|
|
||||||
|
Три прохода завершены. Один дефект формальной постановки проблемы найден и
|
||||||
|
исправлен до финального audit. П48 готова к независимой приёмке: следующие
|
||||||
|
шаги интегратора — отдельно проверить фактический mapping реального проекта,
|
||||||
|
подключить overlay и только затем запускать strict audit/build/publish.
|
||||||
|
|
||||||
|
## Независимое редакторское ревью и интеграция
|
||||||
|
|
||||||
|
Статус: **принято и подключено в editorial overlay 31 июля 2026 года**.
|
||||||
|
|
||||||
|
### 1. Факты и техника
|
||||||
|
|
||||||
|
- Источники закреплены на датированных W3C Working Draft: Navigation Timing
|
||||||
|
Level 2 от 17 января 2022, User Timing Level 3 от 13 сентября 2021 и
|
||||||
|
Performance Timeline Level 2 от 19 августа 2021. Они поддерживают только
|
||||||
|
исторический словарь platform timing interfaces; поля
|
||||||
|
<code>documentTicks</code>, <code>styleTicks</code>,
|
||||||
|
<code>scriptTicks</code> и <code>renderTicks</code> явно остаются
|
||||||
|
проектными teaching units.
|
||||||
|
- Независимый import fixture подтвердил 15 из 15 assertions. Baseline имеет
|
||||||
|
total 85 и проходит; candidate поднимает только <code>scriptTicks</code>
|
||||||
|
с 26 до 34. Его aggregate 93 остаётся внутри allowed 98, но overall верно
|
||||||
|
остаётся FAIL из-за component limit 30. Изменение declared conditions
|
||||||
|
возвращает <code>measurement-contract-invalid</code>, а не verdict о
|
||||||
|
регрессии. Это проверяет именно контрпример «зелёный total скрывает красную
|
||||||
|
часть».
|
||||||
|
- <code>node --check</code> и draft audit прошли: body — 9 995, 9 989 и
|
||||||
|
9 949 знаков. После подключения strict audit подтвердил 1 figure, 1 table
|
||||||
|
и 2 code examples в каждой статье. В registry 139 revisions; есть ровно
|
||||||
|
три февральских slug без duplicate и без полей <code>date</code>/<code>author</code>
|
||||||
|
в overlay.
|
||||||
|
|
||||||
|
### 2. Редактура и голос
|
||||||
|
|
||||||
|
- Тексты перечитаны как материалы 2022 года: они уже говорят о контракте
|
||||||
|
сравнения и owner компонента, но не присваивают автору CI, Chrome trace,
|
||||||
|
RUM, Web Vitals или реальную performance regression. Каждая статья
|
||||||
|
заканчивается локальным следующим действием, а не обещанием ускорения.
|
||||||
|
- Термины `aggregate`, `component`, `tolerance` и `conditions` раскрыты через
|
||||||
|
object, таблицу и fixture. Вывод не расширяется от named failure до общей
|
||||||
|
оценки маршрута, что сохраняет прагматичный тон и границу применимости.
|
||||||
|
|
||||||
|
### 3. Визуал и выпуск
|
||||||
|
|
||||||
|
- Три SVG повторно прошли XML и safety scan: нет <code>script</code>,
|
||||||
|
<code>foreignObject</code>, external URL или raster data. Sharp renders на
|
||||||
|
ширине 375 px просмотрены вручную: named checks, aggregate/overall split и
|
||||||
|
diagnostic stop condition читаемы без clipping/overlap.
|
||||||
|
- <code>npm run build</code> завершилась с code 0 и сгенерировала 374
|
||||||
|
статические страницы; <code>git diff --check</code> прошёл. Старые npm
|
||||||
|
warnings о пользовательских <code>store-dir</code>, <code>cache-dir</code>
|
||||||
|
и <code>public-hoist-pattern</code> не относятся к пакету.
|
||||||
|
|
||||||
|
Итог: февральские статьи заменяют архивные записи через overlay. После этой
|
||||||
|
интеграции строгий аудит проходит **148 из 358**, остаётся **210**.
|
||||||
@@ -44,6 +44,7 @@ import { revisions as october2021Revisions } from '../scripts/upgrade-2021-10.mj
|
|||||||
import { revisions as november2021Revisions } from '../scripts/upgrade-2021-11.mjs';
|
import { revisions as november2021Revisions } from '../scripts/upgrade-2021-11.mjs';
|
||||||
import { revisions as december2021Revisions } from '../scripts/upgrade-2021-12.mjs';
|
import { revisions as december2021Revisions } from '../scripts/upgrade-2021-12.mjs';
|
||||||
import { revisions as january2022Revisions } from '../scripts/upgrade-2022-01.mjs';
|
import { revisions as january2022Revisions } from '../scripts/upgrade-2022-01.mjs';
|
||||||
|
import { revisions as february2022Revisions } from '../scripts/upgrade-2022-02.mjs';
|
||||||
|
|
||||||
// This layer replaces archived source entries without losing their stable slug and date.
|
// This layer replaces archived source entries without losing their stable slug and date.
|
||||||
export const editorialRevisions = [
|
export const editorialRevisions = [
|
||||||
@@ -93,4 +94,5 @@ export const editorialRevisions = [
|
|||||||
...november2021Revisions,
|
...november2021Revisions,
|
||||||
...december2021Revisions,
|
...december2021Revisions,
|
||||||
...january2022Revisions,
|
...january2022Revisions,
|
||||||
|
...february2022Revisions,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 720" role="img" aria-labelledby="title desc">
|
||||||
|
<title id="title">Учебное распределение бюджета пользовательского пути</title>
|
||||||
|
<desc id="desc">Вертикальная схема показывает четыре именованные части учебного маршрута checkout review. Baseline укладывается в каждый limit с tolerance. Candidate превышает allowed у scriptTicks, хотя его общий total остаётся в aggregate allowed. Значения подписаны как teaching ticks, а не как браузерная метрика.</desc>
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.bg { fill: #f8fafc; }
|
||||||
|
.title { fill: #102a43; font: 700 25px Arial, sans-serif; }
|
||||||
|
.subtitle { fill: #486581; font: 15px Arial, sans-serif; }
|
||||||
|
.label { fill: #102a43; font: 700 16px Arial, sans-serif; }
|
||||||
|
.small { fill: #334e68; font: 14px Arial, sans-serif; }
|
||||||
|
.tiny { fill: #486581; font: 13px Arial, sans-serif; }
|
||||||
|
.card { fill: #ffffff; stroke: #bcccdc; stroke-width: 1.5; }
|
||||||
|
.pass { fill: #2f855a; }
|
||||||
|
.fail { fill: #c53030; }
|
||||||
|
.blue { fill: #3182ce; }
|
||||||
|
.amber { fill: #dd6b20; }
|
||||||
|
.pass-bg { fill: #f0fff4; stroke: #68d391; stroke-width: 1.5; }
|
||||||
|
.fail-bg { fill: #fff5f5; stroke: #fc8181; stroke-width: 1.5; }
|
||||||
|
.note { fill: #fffaf0; stroke: #dd6b20; stroke-width: 1.5; }
|
||||||
|
.line { stroke: #829ab1; stroke-width: 2; }
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<rect class="bg" width="480" height="720" rx="22"/>
|
||||||
|
<text class="title" x="30" y="44">Бюджет: части раньше total</text>
|
||||||
|
<text class="subtitle" x="30" y="70">/training/checkout/review · teaching ticks</text>
|
||||||
|
|
||||||
|
<g transform="translate(30 94)">
|
||||||
|
<rect class="card" width="420" height="58" rx="12"/>
|
||||||
|
<text class="label" x="18" y="25">Один contract</text>
|
||||||
|
<text class="small" x="18" y="46">route + scenario + declared conditions</text>
|
||||||
|
<text class="tiny" x="270" y="46">browser / CI: not-collected</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<text class="label" x="30" y="184">Named component checks</text>
|
||||||
|
<text class="tiny" x="30" y="205">actual / allowed = limit + tolerance</text>
|
||||||
|
|
||||||
|
<g transform="translate(30 224)">
|
||||||
|
<rect class="pass-bg" width="420" height="58" rx="10"/>
|
||||||
|
<circle class="pass" cx="22" cy="29" r="8"/>
|
||||||
|
<text class="label" x="42" y="26">documentTicks</text>
|
||||||
|
<text class="small" x="42" y="47">24 / 32 = 30 + 2</text>
|
||||||
|
<text class="label" x="350" y="36">PASS</text>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(30 294)">
|
||||||
|
<rect class="pass-bg" width="420" height="58" rx="10"/>
|
||||||
|
<circle class="pass" cx="22" cy="29" r="8"/>
|
||||||
|
<text class="label" x="42" y="26">styleTicks</text>
|
||||||
|
<text class="small" x="42" y="47">15 / 20 = 18 + 2</text>
|
||||||
|
<text class="label" x="350" y="36">PASS</text>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(30 364)">
|
||||||
|
<rect class="fail-bg" width="420" height="66" rx="10"/>
|
||||||
|
<circle class="fail" cx="22" cy="29" r="8"/>
|
||||||
|
<text class="label" x="42" y="26">scriptTicks</text>
|
||||||
|
<text class="small" x="42" y="47">34 / 30 = 28 + 2</text>
|
||||||
|
<text class="tiny" x="42" y="62">named failure stays visible</text>
|
||||||
|
<text class="label" x="350" y="36">FAIL</text>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(30 442)">
|
||||||
|
<rect class="pass-bg" width="420" height="58" rx="10"/>
|
||||||
|
<circle class="pass" cx="22" cy="29" r="8"/>
|
||||||
|
<text class="label" x="42" y="26">renderTicks</text>
|
||||||
|
<text class="small" x="42" y="47">20 / 26 = 24 + 2</text>
|
||||||
|
<text class="label" x="350" y="36">PASS</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<line class="line" x1="240" y1="514" x2="240" y2="536"/>
|
||||||
|
<g transform="translate(30 540)">
|
||||||
|
<rect class="card" width="202" height="72" rx="12"/>
|
||||||
|
<rect class="blue" x="14" y="15" width="11" height="42" rx="3"/>
|
||||||
|
<text class="label" x="38" y="29">Aggregate</text>
|
||||||
|
<text class="small" x="38" y="51">93 / 98 = 95 + 3</text>
|
||||||
|
<text class="label" x="142" y="29">PASS</text>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(248 540)">
|
||||||
|
<rect class="fail-bg" width="202" height="72" rx="12"/>
|
||||||
|
<rect class="fail" x="14" y="15" width="11" height="42" rx="3"/>
|
||||||
|
<text class="label" x="38" y="29">Overall</text>
|
||||||
|
<text class="small" x="38" y="51">scriptTicks is over</text>
|
||||||
|
<text class="label" x="137" y="29">FAIL</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g transform="translate(30 632)">
|
||||||
|
<rect class="note" width="420" height="62" rx="12"/>
|
||||||
|
<text class="label" x="18" y="25">Граница диаграммы</text>
|
||||||
|
<text class="small" x="18" y="47">Ticks — учебные поля, не trace, не Web Vitals,</text>
|
||||||
|
<text class="small" x="18" y="62">не real-user result и не SLA.</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.6 KiB |
@@ -0,0 +1,75 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 720" role="img" aria-labelledby="title desc">
|
||||||
|
<title id="title">Диагностический маршрут для budget comparison</title>
|
||||||
|
<desc id="desc">Схема начинается с проверки route, scenario и declared conditions. При несовпадении comparison останавливается. При совпадении сначала проверяются именованные компоненты, затем aggregate. scriptTicks 34 при allowed 30 ведёт к focused action, хотя total 93 при allowed 98 остаётся aggregate pass.</desc>
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.bg { fill: #f8fafc; }
|
||||||
|
.title { fill: #102a43; font: 700 25px Arial, sans-serif; }
|
||||||
|
.subtitle { fill: #486581; font: 15px Arial, sans-serif; }
|
||||||
|
.label { fill: #102a43; font: 700 16px Arial, sans-serif; }
|
||||||
|
.small { fill: #334e68; font: 14px Arial, sans-serif; }
|
||||||
|
.tiny { fill: #486581; font: 13px Arial, sans-serif; }
|
||||||
|
.card { fill: #ffffff; stroke: #bcccdc; stroke-width: 1.5; }
|
||||||
|
.pass { fill: #f0fff4; stroke: #68d391; stroke-width: 1.5; }
|
||||||
|
.fail { fill: #fff5f5; stroke: #fc8181; stroke-width: 1.5; }
|
||||||
|
.stop { fill: #fffaf0; stroke: #dd6b20; stroke-width: 1.5; }
|
||||||
|
.arrow { stroke: #486581; stroke-width: 2.2; fill: none; marker-end: url(#arrow); }
|
||||||
|
.red-arrow { stroke: #c53030; stroke-width: 2.2; fill: none; marker-end: url(#red-arrow); }
|
||||||
|
</style>
|
||||||
|
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#486581"/></marker>
|
||||||
|
<marker id="red-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#c53030"/></marker>
|
||||||
|
</defs>
|
||||||
|
<rect class="bg" width="480" height="720" rx="22"/>
|
||||||
|
<text class="title" x="30" y="44">Диагностика: ограничить вывод</text>
|
||||||
|
<text class="subtitle" x="30" y="70">Budget comparison — не глобальная оценка скорости.</text>
|
||||||
|
|
||||||
|
<g transform="translate(30 96)">
|
||||||
|
<rect class="card" width="420" height="70" rx="12"/>
|
||||||
|
<text class="label" x="18" y="27">1. Measurement contract</text>
|
||||||
|
<text class="small" x="18" y="50">route + scenario + conditions match?</text>
|
||||||
|
</g>
|
||||||
|
<path class="arrow" d="M 240 166 L 240 194"/>
|
||||||
|
<text class="tiny" x="252" y="184">yes</text>
|
||||||
|
<path class="arrow" d="M 110 166 L 110 204 L 52 204 L 52 232"/>
|
||||||
|
<text class="tiny" x="60" y="197">no</text>
|
||||||
|
<g transform="translate(30 236)">
|
||||||
|
<rect class="stop" width="158" height="86" rx="12"/>
|
||||||
|
<text class="label" x="14" y="27">Stop compare</text>
|
||||||
|
<text class="small" x="14" y="49">not-comparable</text>
|
||||||
|
<text class="tiny" x="14" y="69">restore one contract</text>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(218 198)">
|
||||||
|
<rect class="card" width="232" height="86" rx="12"/>
|
||||||
|
<text class="label" x="16" y="27">2. Named checks</text>
|
||||||
|
<text class="small" x="16" y="49">actual / (limit + tolerance)</text>
|
||||||
|
<text class="tiny" x="16" y="69">every component remains visible</text>
|
||||||
|
</g>
|
||||||
|
<path class="arrow" d="M 334 284 L 334 314"/>
|
||||||
|
<g transform="translate(218 318)">
|
||||||
|
<rect class="fail" width="232" height="82" rx="12"/>
|
||||||
|
<text class="label" x="16" y="26">scriptTicks FAIL</text>
|
||||||
|
<text class="small" x="16" y="48">34 is greater than allowed 30</text>
|
||||||
|
<text class="tiny" x="16" y="68">named component only</text>
|
||||||
|
</g>
|
||||||
|
<path class="red-arrow" d="M 334 400 L 334 432"/>
|
||||||
|
<g transform="translate(218 436)">
|
||||||
|
<rect class="pass" width="232" height="68" rx="12"/>
|
||||||
|
<text class="label" x="16" y="26">3. Aggregate PASS</text>
|
||||||
|
<text class="small" x="16" y="48">93 is within allowed 98</text>
|
||||||
|
</g>
|
||||||
|
<text class="tiny" x="42" y="459">context only:</text>
|
||||||
|
<text class="tiny" x="42" y="477">cannot cancel</text>
|
||||||
|
<text class="tiny" x="42" y="495">the red branch</text>
|
||||||
|
<path class="red-arrow" d="M 334 504 L 334 536"/>
|
||||||
|
<g transform="translate(30 540)">
|
||||||
|
<rect class="fail" width="420" height="72" rx="12"/>
|
||||||
|
<text class="label" x="18" y="27">4. Focused action</text>
|
||||||
|
<text class="small" x="18" y="50">inspect script input and one route boundary</text>
|
||||||
|
</g>
|
||||||
|
<path class="arrow" d="M 240 612 L 240 632"/>
|
||||||
|
<g transform="translate(30 636)">
|
||||||
|
<rect class="stop" width="420" height="58" rx="12"/>
|
||||||
|
<text class="small" x="18" y="24">Allowed claim: one named rule failed under declared conditions.</text>
|
||||||
|
<text class="tiny" x="18" y="43">Not allowed: global speed, browser trace, real-user or SLA verdict.</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.6 KiB |
@@ -0,0 +1,75 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 700" role="img" aria-labelledby="title desc">
|
||||||
|
<title id="title">Baseline и candidate в учебном сравнении бюджета</title>
|
||||||
|
<desc id="desc">Диаграмма сравнивает baseline и candidate по total и scriptTicks. Total candidate 93 остаётся ниже aggregate allowed 98, а scriptTicks candidate 34 превышает component allowed 30. Поэтому итоговый verdict — named component fail, не глобальная оценка производительности.</desc>
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.bg { fill: #f8fafc; }
|
||||||
|
.title { fill: #102a43; font: 700 25px Arial, sans-serif; }
|
||||||
|
.subtitle { fill: #486581; font: 15px Arial, sans-serif; }
|
||||||
|
.label { fill: #102a43; font: 700 16px Arial, sans-serif; }
|
||||||
|
.small { fill: #334e68; font: 14px Arial, sans-serif; }
|
||||||
|
.tiny { fill: #486581; font: 13px Arial, sans-serif; }
|
||||||
|
.card { fill: #ffffff; stroke: #bcccdc; stroke-width: 1.5; }
|
||||||
|
.grid { stroke: #d9e2ec; stroke-width: 1; }
|
||||||
|
.axis { stroke: #829ab1; stroke-width: 2; }
|
||||||
|
.baseline { fill: #3182ce; }
|
||||||
|
.candidate { fill: #805ad5; }
|
||||||
|
.allowed { stroke: #2f855a; stroke-width: 3; stroke-dasharray: 7 5; }
|
||||||
|
.danger { stroke: #c53030; stroke-width: 3; stroke-dasharray: 7 5; }
|
||||||
|
.pass-bg { fill: #f0fff4; stroke: #68d391; stroke-width: 1.5; }
|
||||||
|
.fail-bg { fill: #fff5f5; stroke: #fc8181; stroke-width: 1.5; }
|
||||||
|
.note { fill: #fffaf0; stroke: #dd6b20; stroke-width: 1.5; }
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<rect class="bg" width="480" height="700" rx="22"/>
|
||||||
|
<text class="title" x="30" y="44">Сравнение: total и component</text>
|
||||||
|
<text class="subtitle" x="30" y="70">Один contract · условные teaching ticks</text>
|
||||||
|
|
||||||
|
<g transform="translate(30 98)">
|
||||||
|
<rect class="card" width="420" height="46" rx="10"/>
|
||||||
|
<rect class="baseline" x="18" y="15" width="16" height="16" rx="3"/>
|
||||||
|
<text class="small" x="43" y="28">baseline</text>
|
||||||
|
<rect class="candidate" x="142" y="15" width="16" height="16" rx="3"/>
|
||||||
|
<text class="small" x="167" y="28">candidate</text>
|
||||||
|
<line class="allowed" x1="282" y1="23" x2="312" y2="23"/>
|
||||||
|
<text class="small" x="321" y="28">allowed</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<text class="label" x="30" y="180">Aggregate total</text>
|
||||||
|
<text class="tiny" x="30" y="201">Secondary guard: it does not decide component health.</text>
|
||||||
|
<line class="axis" x1="52" y1="386" x2="430" y2="386"/>
|
||||||
|
<line class="grid" x1="52" y1="329" x2="430" y2="329"/>
|
||||||
|
<line class="grid" x1="52" y1="272" x2="430" y2="272"/>
|
||||||
|
<line class="grid" x1="52" y1="215" x2="430" y2="215"/>
|
||||||
|
<rect class="baseline" x="118" y="242" width="74" height="144" rx="6"/>
|
||||||
|
<rect class="candidate" x="250" y="229" width="74" height="157" rx="6"/>
|
||||||
|
<line class="allowed" x1="52" y1="220" x2="430" y2="220"/>
|
||||||
|
<text class="small" x="118" y="410">baseline 85</text>
|
||||||
|
<text class="small" x="245" y="410">candidate 93</text>
|
||||||
|
<text class="tiny" x="330" y="215">allowed 98</text>
|
||||||
|
<g transform="translate(30 432)">
|
||||||
|
<rect class="pass-bg" width="420" height="52" rx="10"/>
|
||||||
|
<text class="label" x="18" y="23">Aggregate status: PASS</text>
|
||||||
|
<text class="small" x="18" y="42">93 remains within 95 + 3.</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<text class="label" x="30" y="526">Named component: scriptTicks</text>
|
||||||
|
<g transform="translate(30 542)">
|
||||||
|
<rect class="card" width="420" height="66" rx="12"/>
|
||||||
|
<text class="small" x="18" y="24">baseline</text>
|
||||||
|
<rect class="baseline" x="94" y="12" width="104" height="16" rx="4"/>
|
||||||
|
<text class="small" x="208" y="25">26</text>
|
||||||
|
<text class="small" x="18" y="52">candidate</text>
|
||||||
|
<rect class="candidate" x="94" y="40" width="136" height="16" rx="4"/>
|
||||||
|
<text class="small" x="240" y="53">34</text>
|
||||||
|
<line class="danger" x1="214" y1="8" x2="214" y2="60"/>
|
||||||
|
<text class="tiny" x="278" y="25">allowed 30</text>
|
||||||
|
<text class="label" x="333" y="53" fill="#c53030">FAIL</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g transform="translate(30 626)">
|
||||||
|
<rect class="note" width="420" height="48" rx="11"/>
|
||||||
|
<text class="small" x="18" y="22">Verdict: named component fail → inspect one route boundary.</text>
|
||||||
|
<text class="tiny" x="18" y="40">Not a browser trace, real-user metric or global speed claim.</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,675 @@
|
|||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value)
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll("'", ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function paragraph(text) {
|
||||||
|
return '<p>' + text + '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function heading(text) {
|
||||||
|
return '<h2>' + text + '</h2>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function codeBlock(lines) {
|
||||||
|
return '<pre><code>' + escapeHtml(Array.isArray(lines) ? lines.join('\n') : lines) + '</code></pre>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function figure(src, alt, caption) {
|
||||||
|
return '<figure><img src="' + src + '" alt="' + alt + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function orderedList(items) {
|
||||||
|
return '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function dataTable(caption, headers, rows) {
|
||||||
|
const head = '<thead><tr>' + headers.map((header) => '<th scope="col">' + header + '</th>').join('') + '</tr></thead>';
|
||||||
|
const body = '<tbody>' + rows.map((row) => '<tr>' + row.map((cell) => '<td>' + cell + '</td>').join('') + '</tr>').join('') + '</tbody>';
|
||||||
|
return '<div class="table-scroll"><table><caption>' + caption + '</caption>' + head + body + '</table></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceList(items) {
|
||||||
|
return '<ul>' + items.map((item) => '<li><a href="' + item.url + '" target="_blank" rel="noopener noreferrer">' + item.title + '</a> — ' + item.note + '</li>').join('') + '</ul>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function plainText(content) {
|
||||||
|
return content
|
||||||
|
.replace(/<[^>]+>/g, ' ')
|
||||||
|
.replaceAll(' ', ' ')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll(''', "'")
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function bodyText(content) {
|
||||||
|
return plainText(content.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRevision(meta, bodyParts, sources) {
|
||||||
|
if (sources.length < 2) {
|
||||||
|
throw new Error(meta.slug + ': нужно минимум два первичных или официальных источника');
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentHtml = bodyParts.join('\n') + '\n' + heading('Проверяемые источники') + '\n' + sourceList(sources);
|
||||||
|
const proseLength = bodyText(contentHtml).length;
|
||||||
|
|
||||||
|
if (proseLength < 5000 || proseLength > 15000) {
|
||||||
|
throw new Error(meta.slug + ': основной текст вне 5 000–15 000 знаков: ' + proseLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...meta, contentHtml, proseLength };
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigationTimingDraft = {
|
||||||
|
title: 'W3C Navigation Timing Level 2, Working Draft от 17 января 2022 года',
|
||||||
|
url: 'https://www.w3.org/TR/2022/WD-navigation-timing-2-20220117/',
|
||||||
|
note: 'исторический снимок W3C, существовавший до февральских дат публикации. Он описывает интерфейс PerformanceNavigationTiming и временную шкалу навигации; учебные поля fixture не являются его полями.',
|
||||||
|
};
|
||||||
|
|
||||||
|
const userTimingDraft = {
|
||||||
|
title: 'W3C User Timing Level 3, Working Draft от 13 сентября 2021 года',
|
||||||
|
url: 'https://www.w3.org/TR/2021/WD-user-timing-3-20210913/',
|
||||||
|
note: 'исторический W3C draft о mark и measure. Он позволяет говорить о названных точках и измерениях, но не задаёт наш допуск и не превращает ticks fixture в данные браузера.',
|
||||||
|
};
|
||||||
|
|
||||||
|
const performanceTimelineDraft = {
|
||||||
|
title: 'W3C Performance Timeline Level 2, Working Draft от 19 августа 2021 года',
|
||||||
|
url: 'https://www.w3.org/TR/2021/WD-performance-timeline-2-20210819/',
|
||||||
|
note: 'исторический W3C draft о хранении и получении performance entries. Он не определяет правило CI PASS, бюджет маршрута или интерпретацию учебных чисел.',
|
||||||
|
};
|
||||||
|
|
||||||
|
const commonSources = [navigationTimingDraft, userTimingDraft, performanceTimelineDraft];
|
||||||
|
|
||||||
|
const practiceExample = [
|
||||||
|
'import { runPerformanceBudgetFixture } from "./upgrade-2022-02.mjs";',
|
||||||
|
'',
|
||||||
|
'const fixture = runPerformanceBudgetFixture();',
|
||||||
|
'const baseline = fixture.baselineComparison;',
|
||||||
|
'const candidate = fixture.candidateComparison;',
|
||||||
|
'',
|
||||||
|
'console.log(baseline.overall.status);',
|
||||||
|
'// pass',
|
||||||
|
'console.log(candidate.aggregate.status);',
|
||||||
|
'// pass — aggregate не заметил сдвиг одной части',
|
||||||
|
'console.log(candidate.overall.status);',
|
||||||
|
'// fail — named component budget имеет приоритет',
|
||||||
|
'',
|
||||||
|
'if (candidate.componentChecks.scriptTicks.status !== "fail") {',
|
||||||
|
' throw new Error("fixture must expose the named regression");',
|
||||||
|
'}',
|
||||||
|
'',
|
||||||
|
'// In-memory training model: no browser, CI, trace, network or Web Vitals.',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const mechanismExample = [
|
||||||
|
'import { compareRouteWorkload, createTeachingBudget } from "./upgrade-2022-02.mjs";',
|
||||||
|
'',
|
||||||
|
'const budget = createTeachingBudget();',
|
||||||
|
'const baseline = {',
|
||||||
|
' route: "/training/checkout/review",',
|
||||||
|
' scenario: "anonymous-cart-with-one-item",',
|
||||||
|
' conditions: budget.conditions,',
|
||||||
|
' timingFields: { documentTicks: 24, styleTicks: 15, scriptTicks: 26, renderTicks: 20 },',
|
||||||
|
'};',
|
||||||
|
'const candidate = {',
|
||||||
|
' ...baseline,',
|
||||||
|
' timingFields: { ...baseline.timingFields, scriptTicks: 34 },',
|
||||||
|
'};',
|
||||||
|
'',
|
||||||
|
'const comparison = compareRouteWorkload(baseline, candidate, budget);',
|
||||||
|
'console.log(comparison.componentChecks.scriptTicks.allowed);',
|
||||||
|
'// 30: limit 28 plus explicit tolerance 2',
|
||||||
|
'console.log(comparison.diagnosis.scope);',
|
||||||
|
'// scriptTicks only',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const fieldExample = [
|
||||||
|
'import { runPerformanceBudgetFixture } from "./upgrade-2022-02.mjs";',
|
||||||
|
'',
|
||||||
|
'const { candidateComparison } = runPerformanceBudgetFixture();',
|
||||||
|
'const { diagnosis, measurementConditions } = candidateComparison;',
|
||||||
|
'',
|
||||||
|
'console.log(diagnosis.kind);',
|
||||||
|
'// named-component-over-budget',
|
||||||
|
'console.log(diagnosis.component);',
|
||||||
|
'// scriptTicks',
|
||||||
|
'console.log(diagnosis.nextAction);',
|
||||||
|
'// inspect-script-input-and-one-route-boundary',
|
||||||
|
'',
|
||||||
|
'if (measurementConditions.browserObservation !== "not-collected") {',
|
||||||
|
' throw new Error("fixture must not pretend to hold a browser observation");',
|
||||||
|
'}',
|
||||||
|
'',
|
||||||
|
'// The result narrows a next check; it is not a global speed claim.',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const practiceGuardExample = [
|
||||||
|
'const firstFailure = Object.values(candidate.componentChecks)',
|
||||||
|
' .find((check) => check.status === "fail");',
|
||||||
|
'',
|
||||||
|
'if (candidate.aggregate.status === "pass" && firstFailure) {',
|
||||||
|
' console.log(firstFailure.component);',
|
||||||
|
' // scriptTicks: aggregate must not erase this result',
|
||||||
|
'}',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const mechanismContractExample = [
|
||||||
|
'const changedConditions = {',
|
||||||
|
' ...candidate,',
|
||||||
|
' conditions: { ...candidate.conditions, cache: "changed-training-cache-state" },',
|
||||||
|
'};',
|
||||||
|
'const unavailable = compareRouteWorkload(baseline, changedConditions, budget);',
|
||||||
|
'',
|
||||||
|
'console.log(unavailable.overall.reason);',
|
||||||
|
'// measurement-contract-invalid',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const fieldRecheckExample = [
|
||||||
|
'const next = runPerformanceBudgetFixture().candidateComparison;',
|
||||||
|
'',
|
||||||
|
'if (next.diagnosis.component !== "scriptTicks") {',
|
||||||
|
' throw new Error("do not widen investigation without a new observation");',
|
||||||
|
'}',
|
||||||
|
'console.log(next.overall.failedComponents);',
|
||||||
|
'// ["scriptTicks"]',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const requiredTimingFields = Object.freeze(['documentTicks', 'styleTicks', 'scriptTicks', 'renderTicks']);
|
||||||
|
|
||||||
|
function sumTimingFields(timingFields) {
|
||||||
|
return requiredTimingFields.reduce((total, field) => total + timingFields[field], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sameObject(left, right) {
|
||||||
|
return JSON.stringify(left) === JSON.stringify(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Учебный контракт измерения. Он намеренно не создаёт browser/CI workload и
|
||||||
|
* не использует Performance API. Значения timingFields — условные ticks из
|
||||||
|
* локального object, а не LCP, TTFB, trace, user metric или SLA.
|
||||||
|
*/
|
||||||
|
export function createTeachingBudget() {
|
||||||
|
const conditions = Object.freeze({
|
||||||
|
scenario: 'anonymous-cart-with-one-item',
|
||||||
|
route: '/training/checkout/review',
|
||||||
|
workloadVersion: 'training-navigation-workload-v1',
|
||||||
|
timingUnit: 'teaching-ticks',
|
||||||
|
browserObservation: 'not-collected',
|
||||||
|
ciObservation: 'not-collected',
|
||||||
|
network: 'not-modelled',
|
||||||
|
cpu: 'not-modelled',
|
||||||
|
cache: 'not-modelled',
|
||||||
|
trace: 'not-collected',
|
||||||
|
});
|
||||||
|
|
||||||
|
const components = Object.freeze({
|
||||||
|
documentTicks: Object.freeze({ limit: 30, tolerance: 2, owner: 'document-input' }),
|
||||||
|
styleTicks: Object.freeze({ limit: 18, tolerance: 2, owner: 'style-boundary' }),
|
||||||
|
scriptTicks: Object.freeze({ limit: 28, tolerance: 2, owner: 'script-input' }),
|
||||||
|
renderTicks: Object.freeze({ limit: 24, tolerance: 2, owner: 'render-boundary' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.freeze({
|
||||||
|
kind: 'teaching-route-performance-budget-v1',
|
||||||
|
conditions,
|
||||||
|
components,
|
||||||
|
aggregate: Object.freeze({ limit: 95, tolerance: 3, label: 'training-total-ticks' }),
|
||||||
|
boundary: Object.freeze({
|
||||||
|
fact: 'a comparison requires one route, one named scenario and matching declared conditions',
|
||||||
|
notClaim: 'not-browser-trace-not-ci-result-not-real-user-metric-not-web-vitals-not-sla',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateRouteWorkload(workload, budget) {
|
||||||
|
const missingFields = requiredTimingFields.filter((field) => !Number.isFinite(workload.timingFields?.[field]));
|
||||||
|
const hasOnlyKnownFields = Object.keys(workload.timingFields || {}).every((field) => requiredTimingFields.includes(field));
|
||||||
|
const routeMatches = workload.route === budget.conditions.route;
|
||||||
|
const scenarioMatches = workload.scenario === budget.conditions.scenario;
|
||||||
|
const conditionsMatch = sameObject(workload.conditions, budget.conditions);
|
||||||
|
const nonNegative = requiredTimingFields.every((field) => workload.timingFields?.[field] >= 0);
|
||||||
|
|
||||||
|
return Object.freeze({
|
||||||
|
ok: missingFields.length === 0 && hasOnlyKnownFields && routeMatches && scenarioMatches && conditionsMatch && nonNegative,
|
||||||
|
missingFields,
|
||||||
|
hasOnlyKnownFields,
|
||||||
|
routeMatches,
|
||||||
|
scenarioMatches,
|
||||||
|
conditionsMatch,
|
||||||
|
nonNegative,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createComponentChecks(timingFields, budget) {
|
||||||
|
return Object.freeze(Object.fromEntries(requiredTimingFields.map((field) => {
|
||||||
|
const rule = budget.components[field];
|
||||||
|
const actual = timingFields[field];
|
||||||
|
const allowed = rule.limit + rule.tolerance;
|
||||||
|
const status = actual <= allowed ? 'pass' : 'fail';
|
||||||
|
return [field, Object.freeze({
|
||||||
|
component: field,
|
||||||
|
owner: rule.owner,
|
||||||
|
actual,
|
||||||
|
limit: rule.limit,
|
||||||
|
tolerance: rule.tolerance,
|
||||||
|
allowed,
|
||||||
|
status,
|
||||||
|
deltaFromLimit: actual - rule.limit,
|
||||||
|
})];
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDiagnosis(componentChecks, validation) {
|
||||||
|
if (!validation.ok) {
|
||||||
|
return Object.freeze({
|
||||||
|
kind: 'measurement-contract-invalid',
|
||||||
|
component: null,
|
||||||
|
scope: 'route-scenario-or-conditions-contract',
|
||||||
|
nextAction: 'restore-one-named-route-scenario-and-controlled-conditions-before-comparing',
|
||||||
|
notClaim: 'no-performance-conclusion-is-permitted',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const failed = Object.values(componentChecks).find((check) => check.status === 'fail');
|
||||||
|
if (!failed) {
|
||||||
|
return Object.freeze({
|
||||||
|
kind: 'no-named-component-over-budget',
|
||||||
|
component: null,
|
||||||
|
scope: 'no-diagnosis-required-by-training-model',
|
||||||
|
nextAction: 'keep-the-snapshot-and-repeat-only-under-the-same-declared-conditions',
|
||||||
|
notClaim: 'not-proof-of-global-performance',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.freeze({
|
||||||
|
kind: 'named-component-over-budget',
|
||||||
|
component: failed.component,
|
||||||
|
scope: failed.component + ' only',
|
||||||
|
observed: failed.actual,
|
||||||
|
allowed: failed.allowed,
|
||||||
|
nextAction: failed.component === 'scriptTicks'
|
||||||
|
? 'inspect-script-input-and-one-route-boundary'
|
||||||
|
: 'inspect-the-named-component-and-one-route-boundary',
|
||||||
|
notClaim: 'not-a-global-speed-claim-not-a-browser-trace-not-a-real-user-result-not-an-sla-verdict',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сравнивает два in-memory объекта. Первый задаёт snapshot, второй —
|
||||||
|
* candidate. Функция не запускает browser, Lighthouse, CI, HTTP, таймер,
|
||||||
|
* PerformanceObserver, файловую систему или сетевой workload.
|
||||||
|
*/
|
||||||
|
export function compareRouteWorkload(baseline, candidate, budget = createTeachingBudget()) {
|
||||||
|
const baselineValidation = validateRouteWorkload(baseline, budget);
|
||||||
|
const candidateValidation = validateRouteWorkload(candidate, budget);
|
||||||
|
const comparable = baselineValidation.ok && candidateValidation.ok
|
||||||
|
&& sameObject(baseline.conditions, candidate.conditions);
|
||||||
|
const componentChecks = comparable
|
||||||
|
? createComponentChecks(candidate.timingFields, budget)
|
||||||
|
: Object.freeze({});
|
||||||
|
const candidateTotal = comparable ? sumTimingFields(candidate.timingFields) : null;
|
||||||
|
const baselineTotal = baselineValidation.ok ? sumTimingFields(baseline.timingFields) : null;
|
||||||
|
const aggregateAllowed = budget.aggregate.limit + budget.aggregate.tolerance;
|
||||||
|
const aggregate = Object.freeze({
|
||||||
|
label: budget.aggregate.label,
|
||||||
|
baselineTotal,
|
||||||
|
candidateTotal,
|
||||||
|
limit: budget.aggregate.limit,
|
||||||
|
tolerance: budget.aggregate.tolerance,
|
||||||
|
allowed: aggregateAllowed,
|
||||||
|
status: comparable && candidateTotal <= aggregateAllowed ? 'pass' : 'not-comparable',
|
||||||
|
interpretation: 'aggregate is a secondary guard and cannot turn a failed named component into pass',
|
||||||
|
});
|
||||||
|
const failedComponents = Object.values(componentChecks).filter((check) => check.status === 'fail');
|
||||||
|
const overall = Object.freeze({
|
||||||
|
status: comparable && failedComponents.length === 0 ? 'pass' : 'fail',
|
||||||
|
reason: comparable
|
||||||
|
? (failedComponents.length === 0 ? 'all-named-components-within-budget' : 'named-component-over-budget')
|
||||||
|
: 'measurement-contract-invalid',
|
||||||
|
failedComponents: Object.freeze(failedComponents.map((check) => check.component)),
|
||||||
|
});
|
||||||
|
const diagnosis = createDiagnosis(componentChecks, Object.freeze({ ok: comparable }));
|
||||||
|
|
||||||
|
return Object.freeze({
|
||||||
|
kind: 'teaching-route-budget-comparison',
|
||||||
|
measurementConditions: budget.conditions,
|
||||||
|
validation: Object.freeze({ baseline: baselineValidation, candidate: candidateValidation, comparable }),
|
||||||
|
baseline: Object.freeze({ timingFields: baseline.timingFields, total: baselineTotal }),
|
||||||
|
candidate: Object.freeze({ timingFields: candidate.timingFields, total: candidateTotal }),
|
||||||
|
componentChecks,
|
||||||
|
aggregate,
|
||||||
|
overall,
|
||||||
|
diagnosis,
|
||||||
|
boundary: budget.boundary,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runPerformanceBudgetFixture() {
|
||||||
|
const budget = createTeachingBudget();
|
||||||
|
const baseline = Object.freeze({
|
||||||
|
route: budget.conditions.route,
|
||||||
|
scenario: budget.conditions.scenario,
|
||||||
|
conditions: budget.conditions,
|
||||||
|
timingFields: Object.freeze({
|
||||||
|
documentTicks: 24,
|
||||||
|
styleTicks: 15,
|
||||||
|
scriptTicks: 26,
|
||||||
|
renderTicks: 20,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const candidate = Object.freeze({
|
||||||
|
route: budget.conditions.route,
|
||||||
|
scenario: budget.conditions.scenario,
|
||||||
|
conditions: budget.conditions,
|
||||||
|
timingFields: Object.freeze({
|
||||||
|
documentTicks: 24,
|
||||||
|
styleTicks: 15,
|
||||||
|
scriptTicks: 34,
|
||||||
|
renderTicks: 20,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const mismatchedConditionsCandidate = Object.freeze({
|
||||||
|
...candidate,
|
||||||
|
conditions: Object.freeze({ ...budget.conditions, cache: 'changed-training-cache-state' }),
|
||||||
|
});
|
||||||
|
const baselineComparison = compareRouteWorkload(baseline, baseline, budget);
|
||||||
|
const candidateComparison = compareRouteWorkload(baseline, candidate, budget);
|
||||||
|
const mismatchedConditionsComparison = compareRouteWorkload(baseline, mismatchedConditionsCandidate, budget);
|
||||||
|
const repeatedCandidateComparison = compareRouteWorkload(baseline, candidate, budget);
|
||||||
|
const externalBoundary = Object.freeze({
|
||||||
|
browser: 'not-used',
|
||||||
|
ci: 'not-used',
|
||||||
|
trace: 'not-used',
|
||||||
|
network: 'not-used',
|
||||||
|
filesystem: 'not-used',
|
||||||
|
clock: 'not-used',
|
||||||
|
metricClass: 'training-ticks-not-web-vitals',
|
||||||
|
});
|
||||||
|
const assertions = Object.freeze({
|
||||||
|
baselineIsWithinEveryNamedBudget: baselineComparison.overall.status === 'pass'
|
||||||
|
&& Object.values(baselineComparison.componentChecks).every((check) => check.status === 'pass'),
|
||||||
|
candidateFailsNamedScriptBudget: candidateComparison.componentChecks.scriptTicks.status === 'fail'
|
||||||
|
&& candidateComparison.componentChecks.scriptTicks.component === 'scriptTicks',
|
||||||
|
candidateOverallFailsWhenOneNamedComponentFails: candidateComparison.overall.status === 'fail'
|
||||||
|
&& candidateComparison.overall.failedComponents.length === 1
|
||||||
|
&& candidateComparison.overall.failedComponents[0] === 'scriptTicks',
|
||||||
|
aggregatePassCannotHideNamedFailure: candidateComparison.aggregate.status === 'pass'
|
||||||
|
&& candidateComparison.overall.status === 'fail'
|
||||||
|
&& candidateComparison.aggregate.interpretation.includes('cannot turn a failed named component into pass'),
|
||||||
|
componentToleranceIsExplicit: candidateComparison.componentChecks.scriptTicks.limit === 28
|
||||||
|
&& candidateComparison.componentChecks.scriptTicks.tolerance === 2
|
||||||
|
&& candidateComparison.componentChecks.scriptTicks.allowed === 30,
|
||||||
|
aggregateToleranceIsExplicit: candidateComparison.aggregate.limit === 95
|
||||||
|
&& candidateComparison.aggregate.tolerance === 3
|
||||||
|
&& candidateComparison.aggregate.allowed === 98,
|
||||||
|
baselineAndCandidateShareDeclaredConditions: sameObject(baseline.conditions, candidate.conditions)
|
||||||
|
&& candidateComparison.validation.comparable === true
|
||||||
|
&& candidateComparison.measurementConditions.browserObservation === 'not-collected',
|
||||||
|
conditionMismatchBlocksComparison: mismatchedConditionsComparison.validation.comparable === false
|
||||||
|
&& mismatchedConditionsComparison.overall.reason === 'measurement-contract-invalid'
|
||||||
|
&& mismatchedConditionsComparison.aggregate.status === 'not-comparable',
|
||||||
|
allComponentsRemainIndividuallyVisible: Object.keys(candidateComparison.componentChecks).length === requiredTimingFields.length
|
||||||
|
&& requiredTimingFields.every((field) => candidateComparison.componentChecks[field].component === field),
|
||||||
|
diagnosisIsBoundedToNamedComponent: candidateComparison.diagnosis.kind === 'named-component-over-budget'
|
||||||
|
&& candidateComparison.diagnosis.component === 'scriptTicks'
|
||||||
|
&& candidateComparison.diagnosis.scope === 'scriptTicks only',
|
||||||
|
diagnosisLeadsToFocusedAction: candidateComparison.diagnosis.nextAction === 'inspect-script-input-and-one-route-boundary'
|
||||||
|
&& candidateComparison.diagnosis.notClaim.includes('not-a-global-speed-claim'),
|
||||||
|
arithmeticReconciles: baselineComparison.baseline.total === 85
|
||||||
|
&& candidateComparison.candidate.total === 93
|
||||||
|
&& candidateComparison.candidate.total - candidateComparison.baseline.total === 8,
|
||||||
|
invalidOrUnknownMeasurementIsNotSmuggledThrough: candidateComparison.validation.candidate.hasOnlyKnownFields === true
|
||||||
|
&& candidateComparison.validation.candidate.nonNegative === true
|
||||||
|
&& mismatchedConditionsComparison.validation.candidate.conditionsMatch === false,
|
||||||
|
fixtureHasNoExternalObservation: externalBoundary.browser === 'not-used'
|
||||||
|
&& externalBoundary.ci === 'not-used'
|
||||||
|
&& externalBoundary.trace === 'not-used'
|
||||||
|
&& externalBoundary.network === 'not-used'
|
||||||
|
&& externalBoundary.metricClass === 'training-ticks-not-web-vitals',
|
||||||
|
repeatedComparisonIsDeterministic: sameObject(candidateComparison, repeatedCandidateComparison),
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.freeze({
|
||||||
|
kind: 'deterministic-performance-budget-training-fixture',
|
||||||
|
budget,
|
||||||
|
baseline,
|
||||||
|
candidate,
|
||||||
|
baselineComparison,
|
||||||
|
candidateComparison,
|
||||||
|
mismatchedConditionsComparison,
|
||||||
|
externalBoundary,
|
||||||
|
assertions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const practiceArticle = createRevision(
|
||||||
|
{
|
||||||
|
slug: 'editorial-2022-02-practice-performance-budget',
|
||||||
|
title: 'Бюджет пользовательского пути: общий PASS больше не скрывает регрессию',
|
||||||
|
categories: ['Производительность', 'Frontend'],
|
||||||
|
cover: '/assets/editorial/2022/performance-budget-allocation-2022.svg',
|
||||||
|
excerpt: 'Практический маршрут: описать один пользовательский путь, зафиксировать условия и baseline, сравнить candidate по именованным частям и получить действие вместо общего PASS.',
|
||||||
|
readingMinutes: 11,
|
||||||
|
},
|
||||||
|
[
|
||||||
|
paragraph('Проблема начинается на странице оформления: пользователь ждёт, пока станет доступен следующий шаг. Изменения приходят маленькими: новый код, стиль, виджет, правило рендера. Каждое по отдельности проходит общий check. Через несколько релизов путь стал тяжелее, но CI всё ещё отдаёт зелёный PASS. Цена такого сигнала проста: он говорит, что не пересечён порог, но не говорит, какая часть пути забрала запас и где искать изменение.'),
|
||||||
|
paragraph('Не надо отвечать на это обещанием «ускорить сайт». Сначала нужен один воспроизводимый контракт для конкретного пути: route, сценарий, условия наблюдения, snapshot и именные части бюджета. Тогда проверка отвечает на скромный вопрос: candidate остаётся внутри заранее названного допуска или нет. Она не выдаёт учебный расчёт за браузерный trace, реальную пользовательскую метрику, SLA или результат существующего CI.'),
|
||||||
|
heading('Начинаем с пути, а не с общей оценки'),
|
||||||
|
paragraph('Бюджет полезен только у входа, который можно назвать. В fixture это <code>/training/checkout/review</code> и сценарий <code>anonymous-cart-with-one-item</code>. Название намеренно длиннее слова «главная»: при другом товаре, авторизации, locale или feature flag это уже может быть другой вход. Если один job смешивает такие варианты, изменение в одном из них способно спрятаться за средним числом другого.'),
|
||||||
|
paragraph('Дальше фиксируются условия. В учебной модели это version workload, единица <code>teaching-ticks</code>, а browser observation, CI observation, сеть, CPU, cache и trace имеют значение <code>not-collected</code> или <code>not-modelled</code>. Эти поля не делают проверку слабой. Они запрещают дорисовать доказательство задним числом: пока нет trace, нельзя из суммы ticks сделать вывод о браузере; пока нет CI run, нельзя назвать fixture результатом pipeline.'),
|
||||||
|
dataTable(
|
||||||
|
'Контракт учебного бюджета одного пользовательского пути',
|
||||||
|
['Элемент', 'Что хранит fixture', 'Для чего нужен', 'Чего не доказывает'],
|
||||||
|
[
|
||||||
|
['Route и scenario', '<code>/training/checkout/review</code>, одна корзина', 'не смешать разные входы', 'что путь уже прошёл в production'],
|
||||||
|
['Условия', 'version workload, ticks, <code>not-collected</code>', 'остановить сравнение при другой среде', 'свойства браузера или CI runner'],
|
||||||
|
['Компоненты', '<code>documentTicks</code>, <code>styleTicks</code>, <code>scriptTicks</code>, <code>renderTicks</code>', 'увидеть вклад по частям', 'стандартные Web Vitals'],
|
||||||
|
['Допуск', 'limit плюс tolerance у каждого имени', 'сделать правило явным', 'универсальную норму для любого продукта'],
|
||||||
|
['Aggregate', 'вторичный total guard', 'увидеть суммарный запас', 'право скрыть failed component'],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
heading('Разделите запас до того, как появится candidate'),
|
||||||
|
paragraph('Слово «бюджет» часто превращают в одну сумму. Для пользовательского пути это недостаточно. Один общий предел замечает только итог, а не перенос затрат между частями. В fixture у <code>scriptTicks</code> limit равен <code>28</code> и tolerance равен <code>2</code>; значит допустимо до <code>30</code>. У total limit <code>95</code> и tolerance <code>3</code>. Эти числа условны. Их смысл не в удачном размере, а в том, что limit и допуск существуют отдельно и печатаются в результате проверки.'),
|
||||||
|
paragraph('Почему допуск не стоит прятать в комментарии? Потому что без него нельзя понять, какой сдвиг команда заранее согласилась игнорировать и в каких условиях. Если tolerance нужен из-за нестабильности настоящего замера, эта нестабильность должна быть описана в measurement contract. В fixture она не измеряется: поэтому допуск — проектное правило учебного объекта, не статистическая модель и не утверждение о дисперсии браузера.'),
|
||||||
|
figure(
|
||||||
|
'/assets/editorial/2022/performance-budget-allocation-2022.svg',
|
||||||
|
'Вертикальная схема учебного бюджета пути checkout review: четыре именованные части document, style, script и render имеют свои limit и tolerance. Baseline укладывается во все части, candidate превышает допустимый scriptTicks, хотя общий total остаётся в пределах вторичного порога.',
|
||||||
|
'Распределение показывает порядок проверки: сначала сравниваются именованные части с явным допуском, затем читается общий total. Зелёная сумма не отменяет красный component check.',
|
||||||
|
),
|
||||||
|
heading('Snapshot и candidate должны быть сопоставимы'),
|
||||||
|
paragraph('Snapshot — не «последний хороший отчёт». Это сохранённый объект с route, scenario, conditions и четырьмя учебными полями. Baseline в fixture содержит <code>24, 15, 26, 20</code>; candidate меняет только <code>scriptTicks</code> на <code>34</code>. Сумма candidate равна <code>93</code>, поэтому aggregate остаётся PASS до allowed <code>98</code>. Но component rule разрешает максимум <code>30</code>, и общий результат обязан быть FAIL. Именно это защищает от красивой, но бесполезной зелёной суммы.'),
|
||||||
|
codeBlock(practiceExample),
|
||||||
|
paragraph('Код не вызывает Lighthouse, Chrome, PerformanceObserver или сетевой запрос. Он не получает timestamps. Его роль — зафиксировать invariant модели: named component сильнее aggregate. В рабочем проекте аналогичный object можно заполнить данными выбранного теста, но только после того, как команда назвала источник, версию инструмента, контролируемые условия и смысл каждого поля. Подмена этих границ «примерным CI» создаёт ту же ложную уверенность, от которой пытаемся уйти.'),
|
||||||
|
codeBlock(practiceGuardExample),
|
||||||
|
heading('Маршрут: симптом → причина → проверка → действие'),
|
||||||
|
orderedList([
|
||||||
|
'<strong>Симптом.</strong> Общая проверка стабильно зелёная, но конкретный пользовательский путь постепенно меняется и становится труднее объяснить.',
|
||||||
|
'<strong>Причина.</strong> Aggregate объединяет части пути и не показывает, какая из них съела запас; baseline и candidate могут быть собраны при разных условиях.',
|
||||||
|
'<strong>Проверка контракта.</strong> Назовите route, сценарий, version workload, единицу, наличие или отсутствие browser/CI observation. Если conditions отличаются, не сравнивайте числа.',
|
||||||
|
'<strong>Проверка snapshot.</strong> Сохраните baseline с именованными полями. У каждого поля должны быть limit, tolerance и owner, а не только одна сумма в документации.',
|
||||||
|
'<strong>Проверка candidate.</strong> Сначала прочитайте <code>componentChecks</code>, затем aggregate. В fixture <code>scriptTicks</code> = 34 больше allowed 30, даже когда total 93 меньше aggregate allowed 98.',
|
||||||
|
'<strong>Действие.</strong> Откройте один input и одну границу, принадлежащую failed component. Для <code>scriptTicks</code> fixture возвращает <code>inspect-script-input-and-one-route-boundary</code>; не начинайте глобальную оптимизацию без дополнительного факта.',
|
||||||
|
]),
|
||||||
|
heading('Как превратить модель в проверку без ложного PASS'),
|
||||||
|
paragraph('В проекте порядок важнее выбора файла. Сначала profile route в контролируемых условиях: один вариант сценария, одна версия приложения, явно записанная конфигурация измерения. Затем сохранить snapshot вместе с conditions. Только после этого compare candidate. И уже после compare назначить focused action. Если начать с порога в CI, получится gate без смысла: он может зелёнеть, потому что route заменили, cache стал другим или в результаты попала другая часть опыта.'),
|
||||||
|
heading('Что происходит при несовпадении условий'),
|
||||||
|
paragraph('Fixture создаёт также candidate с изменённым условием cache. У него не появляется новый PASS или FAIL по компонентам: comparison останавливается как <code>measurement-contract-invalid</code>. Это сознательное поведение. Сравнение разных условий не становится корректным от того, что оба объекта содержат числа. Сначала восстановите один scenario и controlled conditions, затем снова снимите candidate. Так в отчёте остаётся отдельная категория «данных для вывода нет», а не случайный технический verdict.'),
|
||||||
|
paragraph('Эта ветка особенно полезна для CI. У pipeline может быть хороший сигнал о том, что script завершился успешно, но этот факт не тождественен сопоставимому performance observation. Не нужно обесценивать CI: ему следует отдать узкое правило, которое он действительно проверяет. Например, сравнение serialized snapshot с candidate по одному маршруту. Но browser trace, real-user data и service-level решение остаются другими артефактами и требуют собственных условий.'),
|
||||||
|
heading('Факт платформы и учебная модель'),
|
||||||
|
paragraph('Исторический W3C Navigation Timing Level 2 от 17 января 2022 года описывает <code>PerformanceNavigationTiming</code> и timing information navigation. User Timing Level 3 от сентября 2021 года описывает именованные marks и measures. Performance Timeline Level 2 от августа 2021 года описывает хранение и получение performance entries. Это официальные смыслы платформенных интерфейсов, а не источник наших <code>documentTicks</code> и <code>scriptTicks</code>.'),
|
||||||
|
paragraph('Модель пакета намеренно уже: она учит не считать скорость, а правильно оформлять сравнение. Ее output можно читать как «в этих условных входах named rule нарушен». Из него нельзя получить LCP, TTFB, браузерный waterfall, real-user result, количество пользователей или SLA. Для фактической интеграции сначала сопоставьте реальные поля с официальной документацией используемого инструмента и добавьте отдельный тест на mapping.'),
|
||||||
|
heading('Критерий готовности и ограничение'),
|
||||||
|
paragraph('Работа закончена не при первом зелёном total. Готовность для одного budget change выглядит так: route и scenario названы; conditions сохранены; baseline и candidate имеют один schema; каждый component имеет limit/tolerance; failed component даёт bounded diagnosis; следующий шаг ограничен одной границей. Затем выбранное изменение проверяется повторно в тех же условиях. Если conditions уже другие, это новая карточка сравнения, а не повод переписать baseline.'),
|
||||||
|
paragraph('Fixture проверяет пятнадцать собственных assertions, включая baseline PASS, named failure candidate, явные tolerances, невозможность aggregate скрыть component, остановку при condition mismatch и отсутствие внешнего наблюдения. Это регрессия контракта текста и кода. Она не заменяет профиль настоящего route, потому что настоящий профиль остаётся отдельной инженерной работой с отдельной средой и доказательством.'),
|
||||||
|
heading('Историческая граница февраля 2022'),
|
||||||
|
paragraph('Статья ограничена документами, опубликованными не позднее января 2022 года. W3C sources ниже — рабочие drafts, а не современная рекомендация и не набор новых показателей. Здесь нет поздних метрик взаимодействия, нет текущих советов Lighthouse и нет заявлений об измеренном продукте. Это сохраняет честную дистанцию между исторической рамкой, официальным API и нашей учебной схемой бюджета.'),
|
||||||
|
],
|
||||||
|
commonSources,
|
||||||
|
);
|
||||||
|
|
||||||
|
const mechanismArticle = createRevision(
|
||||||
|
{
|
||||||
|
slug: 'editorial-2022-02-mechanism-performance-budget',
|
||||||
|
title: 'Почему общий PASS не равен бюджету пути: модель сравнения и допуск',
|
||||||
|
categories: ['Производительность', 'Frontend'],
|
||||||
|
cover: '/assets/editorial/2022/performance-budget-trend-2022.svg',
|
||||||
|
excerpt: 'Причинная модель бюджета пути: от controlled conditions и snapshot к component checks, aggregate и ограниченной диагностике без подмены учебного расчёта browser trace.',
|
||||||
|
readingMinutes: 12,
|
||||||
|
},
|
||||||
|
[
|
||||||
|
paragraph('Проблема появляется, когда общий PASS считают доказательством неизменности пути. Путь может получить лишнюю работу в одной части и сохранить приемлемую общую сумму. Цена — неверный выбор действия: команда смотрит на score или total, не видит сдвиг в компоненте и начинает менять всё подряд. Через несколько таких проверок бюджет превращается в табличку, которая ничего не запрещает.'),
|
||||||
|
paragraph('Причина не в том, что aggregate бесполезен. Он отвечает на другой вопрос: не вышла ли общая учебная сумма за свой предел. Бюджет пути отвечает сначала на вопрос о каждой именованной части и только потом о сумме. Чтобы ответ был воспроизводимым, рядом нужны controlled conditions, baseline snapshot, candidate и явный tolerance. Ни одно из этих слов не означает реальный browser observation: fixture строит только локальные objects с условными ticks.'),
|
||||||
|
heading('Четыре слоя одной проверки'),
|
||||||
|
paragraph('Первый слой — measurement contract. Он связывает route, user scenario, version workload и условия. Второй — snapshot baseline, который описывает известный вход именно в этом contract. Третий — component checks: каждый field сравнивается со своим limit плюс tolerance. Четвёртый — decision. Overall FAIL появляется, если хотя бы один named component не проходит; aggregate остаётся вторичным guard. Это простое правило убирает самую частую лазейку: «total зелёный, значит можно не смотреть красную строку». '),
|
||||||
|
paragraph('В fixture четыре поля называются <code>documentTicks</code>, <code>styleTicks</code>, <code>scriptTicks</code> и <code>renderTicks</code>. Они не названы <code>LCP</code>, <code>TTFB</code> или browser event, потому что не получены из браузера. Это project vocabulary учебной модели. Если настоящий проект берёт browser field, его официальное значение, supported browser и сбор нужно описать отдельно. Нельзя взять знакомое имя метрики и приклеить его к произвольному object только ради убедительного отчёта.'),
|
||||||
|
dataTable(
|
||||||
|
'Слои budget comparison и границы вывода',
|
||||||
|
['Слой', 'Вход', 'Выход', 'Правило', 'Запрещённый вывод'],
|
||||||
|
[
|
||||||
|
['Contract', 'route, scenario, declared conditions', 'comparable / invalid', 'любое несовпадение останавливает compare', '«числа сами по себе сопоставимы»'],
|
||||||
|
['Snapshot', 'baseline timingFields', 'точка отсчёта', 'хранит тот же schema', '«последний отчёт всегда baseline»'],
|
||||||
|
['Component', 'candidate field и limit+tolerance', 'pass/fail по имени', 'каждый field виден отдельно', '«общая сумма лечит частный fail»'],
|
||||||
|
['Aggregate', 'сумма fields и total rule', 'вторичный pass/fail', 'читается после component', '«PASS total доказывает путь»'],
|
||||||
|
['Diagnosis', 'failed named component', 'one bounded next action', 'не расширяет scope без факта', '«найдена глобальная причина скорости»'],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
heading('Допуск — часть контракта, а не скрытая скидка'),
|
||||||
|
paragraph('Если limit равен 28, а tolerance равен 2, то allowed равен 30. Такой расчёт должен быть виден в output, иначе reviewer не может отличить осознанный допуск от опечатки или смены правила. В fixture candidate имеет <code>scriptTicks: 34</code>. Он превышает allowed на четыре tick. Отдельное поле <code>deltaFromLimit</code> равно шести, потому что описывает расстояние до базового limit, а не до порога с допуском. Оба числа нужны, если заранее объяснить их смысл.'),
|
||||||
|
paragraph('Допуск нельзя переводить в процент без контекста. У одной части пути абсолютное изменение может быть осмысленным, у другой — нет. Настоящий способ выбрать tolerance зависит от источника наблюдения, повторов, среды и стоимости ложного FAIL. Учебная fixture не знает ни одной из этих вещей. Поэтому она не предлагает «правильные 2 ticks», а требует явного поля. Реальная команда добавляет обоснование рядом со своим mapping и не переносит учебные значения в конфигурацию молча.'),
|
||||||
|
heading('Почему линия aggregate может оставаться зелёной'),
|
||||||
|
paragraph('Baseline fixture суммируется до 85 ticks. Candidate суммируется до 93. Aggregate allowed равен 98, поэтому aggregate status — PASS. Однако рост находится в <code>scriptTicks</code>: baseline 26, candidate 34, allowed 30. Эта комбинация сделана специально. Если result позволил бы overall PASS, потому что 93 меньше 98, budget не защищал бы контракт пользователя: он прятал бы изменение в самой части, для которой владелец и limit были заведены.'),
|
||||||
|
figure(
|
||||||
|
'/assets/editorial/2022/performance-budget-trend-2022.svg',
|
||||||
|
'Сравнительная диаграмма baseline и candidate учебного маршрута: total растёт с 85 до 93 и остаётся ниже aggregate allowed 98, но scriptTicks растёт с 26 до 34 и превышает allowed 30. Итоговый verdict показан как fail по named component, а не как глобальная оценка скорости.',
|
||||||
|
'Линия aggregate нужна для контекста, а не для отмены component failure. На схеме значения подписаны как teaching ticks и отделены от браузерных метрик.',
|
||||||
|
),
|
||||||
|
heading('Механизм сравнения в одном объекте'),
|
||||||
|
codeBlock(mechanismExample),
|
||||||
|
paragraph('Функция <code>compareRouteWorkload</code> сначала вызывает validation для baseline и candidate. Она проверяет, что все четыре поля существуют, лишних нет, значения неотрицательные, route и scenario совпадают с contract, а conditions равны. Только после этого она строит component checks. Если conditions отличаются, component map пуст, aggregate становится <code>not-comparable</code>, а overall получает <code>measurement-contract-invalid</code>. Это предотвращает тихое смешение «до» и «после». '),
|
||||||
|
codeBlock(mechanismContractExample),
|
||||||
|
paragraph('Этот fragment не пытается «нормализовать» изменённый cache. Он оставляет compare недоступным, пока не восстановлен один declared contract. Так разница условий не превращается в число, которому дают performance-смысл.'),
|
||||||
|
heading('Сначала observation, потом архитектурный вывод'),
|
||||||
|
paragraph('M5-подход начинается с пользовательского сценария и заканчивается небольшим изменением, а не с тезиса «пакет тяжёлый». Candidate failure говорит только: в declared teaching model field <code>scriptTicks</code> больше allowed. Из этого нельзя вывести, что виновата конкретная библиотека, загрузчик, server response или пользовательское устройство. Чтобы назвать причину, нужно собрать следующий наблюдаемый артефакт на одной границе: dependency list, bundle diff, route trace или профиль — в зависимости от того, что реально доступно.'),
|
||||||
|
heading('Маршрут: симптом → причина → проверка → действие'),
|
||||||
|
orderedList([
|
||||||
|
'<strong>Симптом.</strong> В отчёте есть общий PASS, но изменения одной части пути продолжают накапливаться без понятного владельца.',
|
||||||
|
'<strong>Причина.</strong> Aggregate использовали как final verdict, хотя он не хранит распределение и может пройти при failed component.',
|
||||||
|
'<strong>Проверка contract.</strong> Сверьте route, scenario и все declared conditions baseline/candidate. Изменился хотя бы один обязательный элемент — верните <code>not-comparable</code>.',
|
||||||
|
'<strong>Проверка allocation.</strong> Для каждого component покажите actual, limit, tolerance, allowed и owner. Отдельно посчитайте total, но не смешивайте его с verdict component.',
|
||||||
|
'<strong>Проверка decision.</strong> Overall PASS допустим только когда все names PASS. Overall FAIL обязан перечислить failed component, даже если aggregate остаётся зелёным.',
|
||||||
|
'<strong>Действие.</strong> Возьмите первую failed name и сузьте следующую проверку до одного input и одной route boundary. Зафиксируйте, какой дополнительный факт нужен до архитектурного изменения.',
|
||||||
|
]),
|
||||||
|
heading('Откуда взять настоящие поля и где не подменять их'),
|
||||||
|
paragraph('Navigation Timing Level 2 уже к январю 2022 года описывал navigation timing information и <code>PerformanceNavigationTiming</code>. User Timing Level 3 описывал named marks/measures и их metadata. Performance Timeline Level 2 описывал получение entries. Эти документы полезны, когда проект выбирает platform source. Они не предлагают готовый budget для checkout, не говорят, что total должен быть 95, и не определяют связь наших четырех ticks с полями API.'),
|
||||||
|
paragraph('Поэтому корректная интеграция начинается с mapping-table. В ней для каждого production field указывают источник, version, значение, условия доступности, преобразование и owner. Затем отдельно документируют runner и repeatability. Пока этого нет, честнее оставить language учебным: <code>teaching-ticks</code>, <code>not-collected</code>, <code>not-modelled</code>. Это не уклонение от работы; это защита от визуально похожих, но невалидных цифр.'),
|
||||||
|
heading('Граница CI и browser observation'),
|
||||||
|
paragraph('CI может исполнить compare и упасть по возвратному коду. Но факт исполнения job не создаёт browser observation. Browser observation может быть полезным входом в budget, если его получали по определённому протоколу и сохранили вместе с conditions. У fixture оба значения прямо равны <code>not-collected</code>. Так output не позволяет сказать «CI замерил маршрут» или «Chrome подтвердил результат». '),
|
||||||
|
heading('Критерий корректной эволюции бюджета'),
|
||||||
|
paragraph('Budget change корректен, если меняется одна известная вещь: новый route scenario, новый component, limit/tolerance или способ получения поля. В каждом случае обновляется contract и baseline, а причина изменения остаётся рядом. Неправильно просто повысить общий total, когда <code>scriptTicks</code> красный: это снимает симптом, но не говорит, согласен ли владелец пути на более дорогую часть. Иногда повышение оправдано, но оно должно быть отдельным решением со стоимостью и ограничением.'),
|
||||||
|
paragraph('В fixture есть пятнадцать assertions: baseline проходит каждый named budget; candidate падает по <code>scriptTicks</code>; aggregate PASS не скрывает failure; tolerances явны; conditions несовместимы блокируют compare; diagnosis ограничена одним component; никаких browser, CI, trace или network observations не выполняется. Это техническая проверка нашей модели. Она не измеряет реальную страницу и не должна использоваться как её заменитель.'),
|
||||||
|
heading('Историческая граница февраля 2022'),
|
||||||
|
paragraph('Все ссылки ниже существовали до февраля 2022 года: Navigation Timing draft от 17 января 2022 года, User Timing draft от сентября 2021-го и Performance Timeline draft от августа 2021-го. Они процитированы как historical sources и сами обозначены W3C как working drafts. В статье нет поздних interaction metrics и нет современных рекомендаций, появившихся после этой рамки. Технический вывод относится только к структуре учебного comparison.'),
|
||||||
|
],
|
||||||
|
commonSources,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fieldArticle = createRevision(
|
||||||
|
{
|
||||||
|
slug: 'editorial-2022-02-field-performance-budget',
|
||||||
|
title: 'Красный component при зелёном total: как сузить диагностику бюджета пути',
|
||||||
|
categories: ['Производительность', 'Frontend'],
|
||||||
|
cover: '/assets/editorial/2022/performance-budget-diagnosis-2022.svg',
|
||||||
|
excerpt: 'Полевой маршрут для случая, когда aggregate остаётся PASS, а named component превышает допуск: проверить contract, сохранить факт и выбрать одно ограниченное действие без глобального performance verdict.',
|
||||||
|
readingMinutes: 11,
|
||||||
|
},
|
||||||
|
[
|
||||||
|
paragraph('На пользовательском пути есть неприятный случай: общий total остаётся зелёным, но одна именованная часть пересекает свой допуск. Его легко списать как «небольшое колебание», особенно если pipeline не упал. Цена такого решения — диагностический долг. Следующее изменение добавит ещё немного работы, а у команды останется только общий график и спор о том, когда именно путь начал меняться.'),
|
||||||
|
paragraph('Нужно не искать виноватого по одной строке, а правильно ограничить вывод. В учебной fixture candidate имеет total 93 при aggregate allowed 98, но <code>scriptTicks</code> равен 34 при allowed 30. Этот факт достаточен, чтобы заблокировать contract test, и недостаточен, чтобы объявить глобальную деградацию, проблему браузера, user metric или SLA. Хороший диагностический маршрут оставляет оба предложения рядом.'),
|
||||||
|
heading('Сначала отделите три разных состояния'),
|
||||||
|
paragraph('Первое состояние — comparable PASS: baseline и candidate имеют одинаковый contract, и все component checks проходят. Второе — comparable FAIL: contract тот же, но хотя бы один named component больше allowed. Третье — not comparable: scenario или conditions изменились, поэтому budget verdict не строится. Самая опасная ошибка — превратить третье состояние во второе. Тогда случайная смена cache, route data или config выглядит как performance regression, хотя данных для такого вывода нет.'),
|
||||||
|
paragraph('Fixture показывает все три ветки. Baseline сравнивается сам с собой и проходит. Candidate с теми же conditions падает по <code>scriptTicks</code>. Отдельный candidate меняет только <code>cache</code> с <code>not-modelled</code> на <code>changed-training-cache-state</code>; его comparison получает <code>measurement-contract-invalid</code>. В этой ветке нет component failure, потому что программа отказалась подменять различие условий числовым verdict.'),
|
||||||
|
dataTable(
|
||||||
|
'Три диагностических состояния budget comparison',
|
||||||
|
['Состояние', 'Что известно', 'Что не известно', 'Следующий шаг'],
|
||||||
|
[
|
||||||
|
['Comparable PASS', 'все names внутри allowed при одном contract', 'что путь быстрый для всех пользователей', 'сохранить snapshot и повторить только при том же contract'],
|
||||||
|
['Comparable FAIL', 'один named component превышает limit+tolerance', 'глобальная причина или browser effect', 'собрать один факт на границе failed component'],
|
||||||
|
['Not comparable', 'contract baseline/candidate различается', 'есть ли регрессия', 'восстановить scenario и conditions до нового compare'],
|
||||||
|
['Aggregate PASS + component FAIL', 'total в пределах, name не в пределах', 'что component можно игнорировать', 'считать overall FAIL и сузить действие'],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
heading('Прочитайте output в правильном порядке'),
|
||||||
|
paragraph('Первой строкой читается validation: comparable ли объекты. Второй — список <code>failedComponents</code>. Третьей — diagnosis, где field и scope явно ограничены. Aggregate читается последним: он объясняет общий запас, но не принимает решение. Такой порядок помогает reviewer не споткнуться о знакомое зелёное слово PASS. В fixture aggregate находится в <code>candidateComparison.aggregate</code>, а final decision — в <code>candidateComparison.overall</code>.'),
|
||||||
|
codeBlock(fieldExample),
|
||||||
|
paragraph('Поле <code>diagnosis.nextAction</code> сознательно скупо: <code>inspect-script-input-and-one-route-boundary</code>. Оно не говорит «удалить библиотеку», «переписать приложение» или «ускорить сервер». По одному учебному числу нельзя выбрать такую архитектуру. Следующее действие должно породить проверяемый факт: например, список route inputs, diff зависимостей или отдельный профиль, если он действительно доступен и собран по понятному протоколу.'),
|
||||||
|
codeBlock(fieldRecheckExample),
|
||||||
|
heading('Диаграмма: зелёный total не отменяет красную ветку'),
|
||||||
|
figure(
|
||||||
|
'/assets/editorial/2022/performance-budget-diagnosis-2022.svg',
|
||||||
|
'Диагностическая схема для budget comparison: сначала проверяются route, scenario и declared conditions; при несовпадении сравнение останавливается. При совпадении component checks идут раньше aggregate. Ветка scriptTicks 34 при allowed 30 ведёт к focused action, хотя total 93 при allowed 98 остаётся зелёным.',
|
||||||
|
'Схема показывает границы вывода: red component создаёт контрактный fail и ограниченную проверку, но не доказывает глобальную скорость, trace браузера или пользовательский результат.',
|
||||||
|
),
|
||||||
|
heading('Маршрут: симптом → причина → проверка → действие'),
|
||||||
|
orderedList([
|
||||||
|
'<strong>Симптом.</strong> Total budget зелёный, но на маршруте появляется новый красный component или его запас постепенно сокращается.',
|
||||||
|
'<strong>Причина.</strong> Итоговая сумма скрывает распределение. Либо baseline/candidate стали несопоставимыми, и цифры вообще нельзя читать вместе.',
|
||||||
|
'<strong>Проверка contract.</strong> Сверьте route, scenario, workloadVersion, unit и every declared condition. При любом различии получите <code>measurement-contract-invalid</code>, а не performance verdict.',
|
||||||
|
'<strong>Проверка component.</strong> Запишите actual, limit, tolerance и allowed. В этом case <code>scriptTicks</code> 34 против allowed 30; named fail остаётся виден независимо от total.',
|
||||||
|
'<strong>Проверка aggregate.</strong> Используйте 93 против allowed 98 только для контекста: total не вышел за вторичный guard, но он не может перевести overall в PASS.',
|
||||||
|
'<strong>Действие.</strong> Сохраните failed name и возьмите один input плюс одну boundary для следующего наблюдения. После изменения повторите тот же compare; если contract изменился, оформите новый snapshot.',
|
||||||
|
]),
|
||||||
|
heading('Как не сделать из диагностики глобальный performance verdict'),
|
||||||
|
paragraph('У diagnosis есть поле <code>notClaim</code>. Оно явно запрещает интерпретировать результат как global speed, browser trace, real-user result или SLA verdict. Такое ограничение кажется избыточным, пока в отчёте не появляется знакомое число. Именно тогда человек склонен достроить историю: «34 значит страница медленнее». Нет. В fixture 34 — значение одного условного property при локальном object comparison. Оно говорит ровно то, что проверено: rule именованной части не соблюдён.'),
|
||||||
|
paragraph('Нужен и обратный запрет. Aggregate PASS не даёт сказать «проблемы нет». Он лишь говорит, что учебный total не пересёк свой aggregate allowed. В реальном проекте этот total может быть полезен как guard против общего роста, но не как единственный gate. Если граница пользователя критична, ей нужен собственный named contract и owner. Если она не критична, тогда лучше не притворяться, что общий score защищает её.'),
|
||||||
|
heading('Выберите границу следующего наблюдения'),
|
||||||
|
paragraph('Для <code>scriptTicks</code> не стоит сразу открывать весь application. Начните с route boundary: entry, bundle boundary, dynamic input или другой реальный владелец, который выбран в mapping. Одного артефакта достаточно, чтобы сузить или отвергнуть гипотезу. Если видно, что changed input не относится к route, вернитесь к contract. Если input относится к route, тогда можно подготовить небольшое обратимое изменение и повторить compare. Оба пути лучше, чем широкая «оптимизация производительности». '),
|
||||||
|
paragraph('Подход сохраняет развитие автора 2022 года: сначала пользовательский опыт и наблюдаемый контракт, затем система границ, затем решение с ценой. Он не делает вид, что автор владеет production telemetry или знает реальную причину без trace. Любой более сильный вывод требует нового доказательства. Это не осторожность ради тона — это способ не закрепить неверную архитектуру после одной зелёной или красной строки.'),
|
||||||
|
heading('Источники помогают не перепутать поля'),
|
||||||
|
paragraph('W3C Navigation Timing Level 2 в историческом snapshot описывает platform interface для navigation data. User Timing Level 3 описывает marks/measures и metadata. Performance Timeline Level 2 описывает retrieval performance entries и observer mechanics. В каждом случае источник помогает понять, что значит фактическое поле платформы. Но он не говорит, что <code>scriptTicks</code> fixture равно одному из этих полей или что эти поля уже собраны в этом проекте.'),
|
||||||
|
paragraph('Перед переносом модели на настоящую страницу полезна маленькая таблица mapping: field учебного контракта, concrete source, data availability, transform, owner и known limitation. Если для одного поля mapping отсутствует, не подставляйте похожий термин. Либо оставьте field учебным, либо сначала соберите новый evidence. Такая дисциплина защищает не только от анахронизма, но и от обычной ошибки: неправильная метрика даёт очень точное число и очень плохое решение.'),
|
||||||
|
heading('Сигнал для CI и отдельный сигнал для исследования'),
|
||||||
|
paragraph('CI gate может работать как контрактный предохранитель: сравнить known snapshot и candidate, вывести failed components и завершиться с ошибкой. Но не стоит называть этот outcome «измерением браузерной производительности», если job не выполнял такое наблюдение. В fixture CI observation честно указано как <code>not-collected</code>. Так автоматизация остаётся полезной, но не получает чужой смысл.'),
|
||||||
|
paragraph('Исследование начинается после fail, а не до него. Для него нужны реальные артефакты, которые отсутствуют в fixture: выбранный browser profile, trace, resource list, application markers или другой источник. Их нельзя выводить из teaching ticks. Напротив, contract budget делает исследование дешевле: он уже назвал route, scenario, failed component, owner boundary и запретил смешать условия. Значит следующий сбор материала может быть меньше и точнее.'),
|
||||||
|
heading('Что проверить после focused action'),
|
||||||
|
paragraph('После небольшого изменения не повышайте limit первым движением. Сначала повторите same contract compare. Если <code>scriptTicks</code> вернулся в allowed, зафиксируйте, какой input изменился и что всё ещё не известно. Если не вернулся, следующий шаг должен быть ещё одним проверяемым узким экспериментом, а не каскадом правок. Если route or scenario изменились, не сравнивайте с исходным baseline: оформите новую версию workload и причину пересмотра.'),
|
||||||
|
heading('Историческая граница февраля 2022'),
|
||||||
|
paragraph('Материал не использует будущие показатели взаимодействия и не ссылается на актуальные страницы, которые могли поменять советы. Ссылки ниже ведут на dated W3C drafts: январь 2022 для Navigation Timing и 2021 для User Timing и Performance Timeline. Все три документа различают официальные platform interfaces и этап working draft. Учебная диагностика поверх них остаётся собственной моделью пакета.'),
|
||||||
|
],
|
||||||
|
commonSources,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const revisions = [practiceArticle, mechanismArticle, fieldArticle].map(({ proseLength, ...revision }) => revision);
|
||||||
|
export const february2022Revisions = revisions;
|
||||||
|
|
||||||
|
function verifyFixture() {
|
||||||
|
const fixture = runPerformanceBudgetFixture();
|
||||||
|
const failed = Object.entries(fixture.assertions)
|
||||||
|
.filter(([, passed]) => passed !== true)
|
||||||
|
.map(([name]) => name);
|
||||||
|
|
||||||
|
if (failed.length > 0) {
|
||||||
|
console.error('FAIL fixture: ' + failed.join(', '));
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('PASS fixture: ' + Object.keys(fixture.assertions).length + '/' + Object.keys(fixture.assertions).length + ' assertions');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv.includes('--verify-fixture')) {
|
||||||
|
verifyFixture();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv.includes('--print-revisions')) {
|
||||||
|
console.log(JSON.stringify(revisions));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user