@vernikr/size-report 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +989 -1081
  2. package/bin/postinstall.js +17 -18
  3. package/bin/size.js +2 -2
  4. package/package.json +3 -4
  5. package/src/args.js +72 -72
  6. package/src/artifact.js +14 -14
  7. package/src/check.js +41 -42
  8. package/src/cli.js +26 -29
  9. package/src/config.js +87 -91
  10. package/src/css.js +14 -14
  11. package/src/data.js +26 -50
  12. package/src/derived.js +31 -35
  13. package/src/doctor.js +95 -99
  14. package/src/explain.js +46 -47
  15. package/src/git.js +66 -71
  16. package/src/history.js +74 -83
  17. package/src/hook.js +130 -149
  18. package/src/init.js +37 -37
  19. package/src/journal.js +17 -15
  20. package/src/locales.js +31 -22
  21. package/src/metrics.js +72 -89
  22. package/src/minify.js +28 -27
  23. package/src/modes.js +57 -60
  24. package/src/optional.js +13 -11
  25. package/src/page/app.css +76 -94
  26. package/src/page/app.js +124 -80
  27. package/src/page/build.js +193 -50
  28. package/src/page/dom.js +8 -9
  29. package/src/page/panel.js +157 -69
  30. package/src/page/payload.js +168 -0
  31. package/src/page/state.js +144 -104
  32. package/src/page/table.js +270 -86
  33. package/src/parse-worker.js +10 -10
  34. package/src/parse.js +43 -45
  35. package/src/project.js +100 -104
  36. package/src/refusal.js +75 -76
  37. package/src/size-table.js +41 -76
  38. package/src/strip/forms.js +5 -5
  39. package/src/strip/guard.js +28 -28
  40. package/src/strip/js.js +27 -27
  41. package/src/strip.js +17 -21
  42. package/src/table.css +54 -19
  43. package/src/tokens.js +27 -27
  44. package/src/tool.js +10 -11
  45. package/templates/README.md +71 -77
  46. package/templates/ci.yml +33 -33
  47. package/templates/size-report.config.json +3 -3
  48. package/CHANGELOG.md +0 -690
package/README.md CHANGED
@@ -1,836 +1,752 @@
1
1
  # @vernikr/size-report
2
2
 
3
- Инструмент учёта роста объёма кода и документов: показывает, насколько вырос или
4
- уменьшился проект в каждом изменении, в трёх разрезах «как написано» (raw),
5
- «в минифицированном виде» (min) и «в токенах для языковой модели» (tok).
6
-
7
- Отвечает на два вопроса: человеку «где проект распухает», ИИ-агенту «сколько
8
- весит моё изменение в его собственном контексте». Ничего не запрещает и не
9
- блокирует: только показывает.
10
-
11
- ## Статус
12
-
13
- **Выпуск 2.4.0 (2026-09-15).** Инструмент живёт отдельным пакетом: имя в
14
- реестре — `@vernikr/size-report` (публикуется тегом из CI, без секрета). Настроек
15
- проект может не заводить вовсе: без файла инструмент выводит их из самого проекта и
16
- говорит об этом строкой, а `--init` закрепляет выведенное файлом (чем этот шаг
17
- отличается от прежнего `CHANGELOG.md` 1.3.0). Отчёт **один файл**,
18
- самодостаточная страница `docs/size-report.html`, и он появляется сам: хук обновления
19
- ставится после установки пакета и при первом запуске (чем это отличается от двух
20
- файлов прежних выпусков `CHANGELOG.md` 2.0.0).
21
- Версия — в манифесте, а у выпуска есть `CHANGELOG.md` с разделом «Что изменится
22
- в числах»:
23
- таблица чисел в нём не пересказ, а замер на фикстуре, который сверяется с живым
24
- прогоном (`test/changelog.test.js`). Числа на фикстуре этим выпуском не меняются
25
- правки в том, **что и в каком порядке видно**: без файла настроек колонкой идёт
26
- каждый отслеживаемый файл не выборка из двенадцати), в дереве панели
27
- складываются папки, а всё, чего в отчёте нет, стоит после остальных и со снятой
28
- галочкой; в таблице колонки, которых коснулся последний коммит, идут впереди.
29
- `schema: 1` данных, замороженная 1.0.0, остаётся той же (выпуск добавил поле, а не
30
- поменял смысл прежних).
31
-
32
- **Шаг 1 плана пройден перенос без изменения поведения.** Команда
33
- `bin/size.js`, точка входа пакета`src/size-table.js` (только реэкспорт),
34
- механика разложена по модулям `src/`; паритет доказан автоматически:
35
- `pnpm test` сверяет пакет с эталоном побайтово на фикстуре и в четырёх заведомо
36
- чужих окружениях (настройки git машины, локаль), `pnpm run parity:live` на живой
37
- истории `safe-resets` в двух средах (95 строк × 27 колонок, артефакт байт в байт).
38
- Вывод не зависит от настроек машины настройки git, влияющие на разбор, закреплены
39
- в самом движке (`BLOCKERS.md` §B1). Сверка с рабочим деревом сравнивает содержимое,
40
- а не размеры, поэтому выкладка с переводами строк в CRLF (`.gitattributes`,
41
- `core.autocrlf`значение по умолчанию в установке Git для Windows) работе не
42
- мешает (`BLOCKERS.md` §B2).
43
-
44
- **История с удалениями больше не тупик** (`BLOCKERS.md` §B3). Колонка, чей файл жил
45
- в истории и был удалён до HEAD, роняла прогон **кодом 1** с текстом «файла нет
46
- вместо файла нет» — то есть на проекте с удалёнными файлами отчёта не было вовсе.
47
- Теперь отказом считается **расхождение** сторон сверки, а не пустота с обеих:
48
- потерянное создание, потерянное изменение и потерянное удаление по-прежнему роняют
49
- прогонно с настоящей причиной и готовой командой, а файл, удалённый до HEAD,
50
- просто пуст в таблице. Доказано числами, а не словом: размер колонки на каждом
51
- коммите сверяется с размером блоба из git (возврат файла даёт то же число, что его
52
- первое появление), а второй свидетель файл, который появляется только в слиянии,
53
- роняет прогон и называет обе стороны: «в дереве `src/only-in-merge.js` 77d3e2f, в
54
- состоянии файла нет». Заодно закрыта граница того же разбора (`BLOCKERS.md` §N8):
55
- путь для состояния выбирался по порядку настроек, а не по тому, что в коммите есть,
56
- поэтому при `diff.renames=false` когда git отдаёт в одном коммите и старое имя
57
- переименованного файла, и новое движок брал исчезнувшее и сверка отказывала на
58
- законном случае. Теперь берётся тот псевдоним колонки, для которого git отдал блоб.
59
- Прежние числа поехать не могли: обе логики совпадают всюду, где первый по порядку
60
- псевдоним в коммите существует, то есть в любом прогоне, который до сих пор
61
- заканчивался отчётом. Это проверено, а не заявлено: вывод движка до и после правки
62
- совпал побайтово на фикстуре при `diff.renames` в обоих значениях, оба эталона
63
- воспроизводятся байт в байт, живой отчёт те же 95 × 27 и 225 673 Б, а прогон на
64
- живой истории остался 1,56–1,58 с.
65
-
66
- **Подключение к проекту-потребителю сделано (2026-09-14, шаг 5 плана).**
67
- `safe-resets` ставит пакет из git по тегу выпуска и больше не держит своей копии
68
- инструмента: ни `tools/size-table.js`, ни его тестатаблицу собирает и
69
- проверяет команда `size` (`pnpm run test:sizes`), а её шаги в проекте одна
70
- строка в раннере (`WORKLOG.md` §18). Инструкция подключения оказалась верна, а
71
- двух вещей в ней не было: шага доступа к пакету в CI и порядка переезда с уже
72
- лежащей копии оба дописаны в её же раздел («Как подключить»); шаг с ключом
73
- оттуда потом ушёл вместе с приватностью (§1).
74
-
75
- **Шаг 2 начат первым срезом контрактом данных.** Движок отдаёт абсолютные
76
- значения и устройство таблицы (`--data`), а дельты, суммы, «сейчас» и фильтры
77
- считает страница (она же и есть отчёт — `size-report.html`): без этого
78
- фильтры и «итого по выбору» невозможны в принципе. В контракте едет и точность
79
- числа рядом пометок `approx` по клеткам,потому что это факт замера, а не
80
- вывод: страница показывает то, что сказал движок, и своего правила точности не
81
- заводит. Панель страницы дерево файлов по папкам, с переключателем у каждой
82
- папки на всё поддерево; выбор читателя переживает перезаход и передаётся
83
- ссылкой адрес страницы и есть ссылка. В контракте же едет **каталог проекта**
84
- все пути, которые видит git: дерево страницы дерево проекта, а числа есть только
85
- у тех файлов, что стали колонками (чем это отличается от прежнего дерева, где
86
- были одни колонки, `CHANGELOG.md` 2.2.0).
87
- Минификация и токены сделаны первыми срезами шагов 3 и 4 (ниже).
88
- Разбиение движка по файлам сделано (пункт D1 плана, R-1.3 `REFACTOR.md`).
89
- Источник инструмента скрипт `size-table.js` (1145 строк) в проекте
90
- [`safe-resets`](../figma/safe-resets) (метрики `raw` и «упрощение вместо
91
- минификации», статичный отчёт в git); в этом репозитории такого пути нет, и
92
- дальше он не упоминается без имени проекта.
93
- **Пункт R-1.2 волны 1** (`REFACTOR.md`): в пакете есть линтер правила те же, что у
94
- проекта-потребителя, плюс запрет склейки операторов в одну строку; всё настоящее
95
- дерево (102 файла) даёт ноль замечаний, `fixtures/` не линтуются там данные.
96
-
97
- **Пункты R-1.1 и R-2.1 волн 1–2** (`REFACTOR.md`): вычислительная часть отчёта одна
98
- (`src/derived.js`) страница исполняет тот же код, что считает статическую таблицу,
99
- и разметку для неё строят обычные исходники (`src/page/*.js`), а не строки внутри
100
- движка; артефакт и разметка страницы при этом совпали со старым выводом побайтово.
101
- Главы программы страницы разделены по предметамсостояние выбора, узлы, панель,
102
- таблица, сборка (`WORKLOG.md` §62): вклейка склеивает их подряд, поэтому собранная
103
- страница осталась той же побайтово, а у глав одна область видимости — это записано
104
- в `eslint.config.js`, потому что `import` между зовущими друг друга главами завёл
105
- бы кольцо связей.
106
-
107
- **Отчёт собирается и в проекте с модулями в `.js`** (`REFACTOR.md` R-4.6): гард
108
- стриппера понимает обе формы скрипт и модуль, поэтому подключение не требует
109
- ни одной правки настроек руками, а когда в графе и правда не JavaScript, отказ
110
- называет причину и команду. Подсказки, справка, умолчание команды починки и
111
- шаблоны называют **путь внутри проекта** (`node node_modules/@vernikr/size-report/bin/size.js`),
112
- а не имя пакета: `npx <имя>` запускает установленный пакет, только пока тот на
113
- месте, а в проекте без него это имя уходит в реестр и тянет пакет по сети
114
- (`R-4.7` прежнее решение, `R-4.21` почему оно отменено).
115
-
116
- **У обещаний документации есть сторож** (`REFACTOR.md` R-4.1): он разложен по
117
- обещаниям, поэтому у каждого свой дом `test/docs-paths.test.js` (пути из текста
118
- есть в дереве, таблица файлов сходится с ним в обе стороны),
119
- `test/docs-commands.test.js` (команды и ключи есть в справке, причины отказа
120
- совпадают с реестром движка, ссылки на разделы ведут в существующие),
121
- `test/docs-numbers.test.js` (числа проверок — факт) и
122
- `test/docs-pin.test.js` (пример установки ведёт на ревизию, чья справка знает
123
- названные команды), а с выпуском добавился пятый `test/changelog.test.js`
124
- (версия выпуска версия манифеста, а таблица «что изменится в числах» не
125
- пересказ, а замер на фикстуре, сверенный с живым прогоном); читатель фактов один
126
- `tools/docs-facts.js`. Заведённый сторож сразу
127
- нашёл четыре расхождения, и все починены: таблица файлов не называла
128
- `fixtures/live/README.md`, `test/runner.test.js` и сам файл сторожа, `README.md`
129
- обещал 92 проверки при 97, а две ссылки `PLAN.md` вели в разделы, которых в названных
130
- документах нет. Чего машиной не проверить формулировок, обещаний о будущем и
131
- верности описания роли файла сторож за собой не берёт и говорит об этом в шапке.
132
-
133
- **git читается через одну границу и в проверках тоже** (`REFACTOR.md` R-1.4).
134
- Список закреплений (`core.quotePath`, раскраска, подпись, кодировка) один на движок и
135
- обвязку: проверки и инструменты зовут git через общее место, а незакреплённое
136
- место стережёт `test/git-pins.test.js` и он же свидетелем показывает, что
137
- закрепление работает: то же чтение без него отдаёт не-английский путь кавычками, с
138
- ним как есть. Это тот же дефект, что B1, только найденный в обвязке: без
139
- закрепления проверка зелена на машине с нашими настройками и красна на машине с
140
- настройками по умолчанию. У проверки, которая измеряет само окружение,
141
- незакреплённое чтение осталось намеренно оно названо и стоит в отдельном списке.
142
-
143
- **Каждый отказ инструмента говорит правдуи это сторожится, а не подразумевается**
144
- (`REFACTOR.md` R-4.18). Ложную причину в тексте отказа находил живой прогон, и находил
145
- четыре раза подряд (B1, B3, разбор аргументов, `explain HEAD`) каждый раз случайно.
146
- Класс закрыт не пятым исправлением: в `tools/refusals.js` лежит по строке на каждый
147
- отказ что он обязан донести, каким кодом ответить и какие фразы в выводе обязаны
148
- остаться, а две проверки делят обе половины обещания. `test/refusals.test.js`
149
- **вызывает** тридцать два отказа (тридцать три запуска инструмента, включая чужие
150
- клоны для хука, обрезанной истории и ветки мимо отчёта) и сверяет код выхода и
151
- фразы; `test/refusals-catalog.test.js` читает исходники и требует, чтобы у каждого
152
- места отказа был свой пункт (карты `SITES` и `PRINTED` держат числа мест), а у
153
- каждого пункта случай в каталоге, то есть новый отказ не может появиться без
154
- проверки. Отказы, которых прогоном не поймать,
155
- названы явно: четыре стережёт своя проверка каталоге записаны её файл и фразы),
156
- а один не поймать вовсе «внутренняя ошибка»и там же сказано, почему. Чего
157
- каталог не берёт, сказано словами: формулировки вне перечисленных фраз, смысл, и
158
- знак «!» это примечание (приближение, смешанный коммит, выключенная автоматика),
159
- а не отказ, и код выхода у него нулевой.
160
-
161
- **И совет в отказе исполним это тоже проверяется.** Правда о причине — половина
162
- обещания: вторая — что предложенную команду можно выполнить. Поводом стал живой
163
- случай (R-4.21): подсказка звала по имени из реестра, где пакета с таким именем нет,
164
- и в проекте без установленного пакета запускала чужой код. Теперь у каждого случая
165
- каталога сказано, что отказ советует, и совет выполняется в том состоянии, которое
166
- его напечатало: сверяется код (включая «отказ ушёл» тот же зов после совета должен
167
- ответить другим), а совет-форма без значений проверяется по справке (те ли команды и
168
- ключи). Совет, который выполнить нечем, назван с причиной: правка настроек, коммит,
169
- установка зависимости за человеком, и это сказано там же, в каталоге. Новый совет
170
- в уже существующем отказе молча не пройдёт: совет вынимается из вывода по маркерам
171
- («починка:», «создайте его:», «соберите её:»), и у него обязано быть объявление.
172
-
173
- **Волна 3 чистки пройдена** (`REFACTOR.md`): обвязка проверок одна на пакет
174
- (`tools/harness.js`), прогоны на чтение не повторяются, клон фикстуры держится на
175
- набор, а тяжёлый паритетный файл распался по предметам и он, и команды CLI идут
176
- волной по ядрам. Проверок было 39 и все 39 сохранены (имена сверены), а новые
177
- добавлены только вместе с новым поведением страницы, подключения и разбора
178
- модулей, а последние о том, что записанное в манифестах сходится с файлами
179
- эталонов, и о склейке кусков вывода процесса, и о дереве файлов, и о памяти
180
- выбора и ссылке на странице, семь — о настоящем сжатии и семь — о токенах (§«Метрика
181
- `min` умеет считать по-настоящему», §«Метрика `tok` считает токены настоящим
182
- словарём», четыре о сверке с деревом и выборе пути для состояния (§«История с
183
- удалениями больше не тупик»), три о точности по клетке (§«Точность числа»);
184
- всего 80.
185
-
186
- **Разбор модуля перестал стоить запуска Node на клетку** (`REFACTOR.md` R-5.4).
187
- Гард понимает модуль через `vm.SourceTextModule`, а он живёт только под
188
- `--experimental-vm-modules`: раньше отсюда и брался отдельный процесс на каждую
189
- клетку. Теперь модуль разбирает один рабочий поток на прогон: на синтетической
190
- истории с модулями (31 коммит, 3 файла, файл меняется каждым коммитом) `--write`
191
- **5,59 0,47 с**, на той же истории со скриптами те же 0,39 с (скриптовый
192
- проект за поток не платит вовсе), а на проекте с одной изменённой клеткой
193
- 0,44 → 0,40 с. Цена не исчезла, а стала разовой: старт потока ≈ 54 мс вместо
194
- 86 мс на каждую клетку, плюс разбор опирается на экспериментальный API (без него
195
- гард отступает к прежнему `node --check` — медленнее, но не мягче).
196
- Прогон подешевел втрое: `pnpm test` 15,7 → **5,6 с** (тогда в наборе было 39
197
- проверок), `pnpm run parity:live` 23,8 → **8,3 с**. Набор стоит
198
- **23,4–29,9 с** при 124 проверках — в зависимости от загрузки машины: окна с
199
- загрузкой 18–70 несравнимы (в спокойном — 23,4–24,2 с, в занятых — 26,6–29,9 с;
200
- в среде без настроек git — 27,4 с, с `CI=1` — 29,9 с), и это свойство окна, а не
201
- набора: под той же загрузкой та же ревизия без нового сторожа идёт 24,4–26,3 с
202
- при 122 проверках. Вклад сторожа выпуска измерен **парным прогоном** с
203
- чередованием (124 → 122 проверки и обратно, два круга): **+2,0 и +2,2 с**, при этом
204
- сам он стоит 1,4 с собственным прогоном (`node --test test/changelog.test.js`: два
205
- запуска инструмента на фикстуре) и идёт параллельно прочим файлам.
206
- Числа разных окон несравнимы вовсе: тот же набор при 119 проверках шёл 25,5–25,7 с,
207
- хотя теперь проверок больше. Дороже всего в наборе — запуски инструмента: перебор
208
- режимов и правило `--json` в `test/cli.test.js` стоят по нескольку десятых секунды
209
- каждая, а разложение сторожа документации на четыре файла времени **не
210
- прибавило** — 23,2 с и до него, и после: обе новые проверки измерены отдельным
211
- прогоном, а не выведены из разброса. Из общего времени **+8,5 с** — десять проверок хука
212
- (`test/hook.test.js`: сам он идёт 17,9–18,5 с и становится самым долгим файлом
213
- набора, а та же ревизия без него — 15,8–16,6 с при 107 проверках). Интеграционные
214
- прогоны (клон, коммиты, слияние, отказы) дешевле не сделать, не ослабив проверку.
215
- `pnpm run parity:live` — 9,3 с в обеих средах. Замеры, машина и разброс —
216
- `REFACTOR.md` §5.
217
-
218
- **Прогонов два, и выбор между ними — по цене файла, а не по алфавиту**
219
- (`REFACTOR.md` R-5.5). Цена проверки в этом наборе — не объём файла, а сколько раз
220
- файл запускает инструмент и git: запуск — это процесс Node, а клон фикстуры и сборка
221
- артефакта — сотни миллисекунд. Поэтому быстрый прогон собирает то, что доказывает по
222
- прочитанному (исходники, дерево, справка, эталонные числа на общей фикстуре), а
223
- полный добавляет то, что гоняет инструмент по многу раз на своих клонах, коммитит и
224
- ставит хуки; причина для каждого дорогого файла названа построчно в
3
+ A tool that tracks how the volume of code and documents grows: every change shows how much the
4
+ project grew or shrank, in three measuresas written (`raw`), minified (`min`) and in tokens for a
5
+ language model (`tok`).
6
+
7
+ It answers two questions: for a person, "where is the project swelling"; for an AI agent, "how
8
+ much does my change weigh in its own context". It forbids nothing and blocks nothing: it only
9
+ shows.
10
+
11
+ ## Status
12
+
13
+ **Release 2.6.0 (2026-09-17).** The tool lives as a package of its own: the registry name is
14
+ `@vernikr/size-report` (published by tag from CI, with no secret). A project may keep no settings at
15
+ all: without a config file the tool derives them from the project itself and says so in one line,
16
+ and `--init` pins what was derived into a file. The report is **one file**, the self-contained page
17
+ `docs/size-report.html`, and it appears by itself: the updating hook is installed after the package
18
+ is installed and on the first run. The version is in the manifest, and every release is recorded in
19
+ the journal `worklog/` for today's entries, `worklog/archive/WORKLOG.md` for the earlier ones:
20
+ what changes in the numbers is measured rather than retold.
21
+
22
+ 2.6.0 changes the numbers themselves, and they are measured rather than retold: on this repository's own
23
+ history the page this engine writes is **85 955 B against 1 899 370 B** written by 2.5.0 — 22.1 times
24
+ smaller, −95.5 % because the data travels as one packed block (49 392 B, `base64+gzip`) instead of a list
25
+ of rows, the program is pasted without comments and indentation (28 007 B) and the styling is 6 172 B. The
26
+ page also stops rebuilding itself: a click on a filter rewrites the nodes in place and recomputes the
27
+ totals, the columns carry computed widths under a fixed layout, and two candidates (`content-visibility`,
28
+ `border-collapse: separate`) were measured dead here and left out, with the records in
29
+ `plans/2026-09-17-page-perf/`. **The contract loses fields:** `metrics[].accuracy` and the `approx` bit map
30
+ are gone and the split into exact and approximate numbers with them, so a reader of `--data` that still
31
+ asks for them gets nothing — while the two schema numbers (`1` for the contract, `2` for the packed block)
32
+ stay where they were. The checks grow with the work: 70 → **81** in the fast profile and 175 → **186** in
33
+ the full one. The note of 2.5.0 the language the tool speaks — stands in the journal,
34
+ `worklog/0203-release-2.5.0.md`.
35
+
36
+ **Parity with the implementation the move started from is proven, not asserted.** The command is
37
+ `bin/size.js` and the package's entry point is `src/size-table.js` (a re-export only), with the
38
+ mechanics laid out in modules under `src/`. `pnpm test` compares the package with the frozen
39
+ standard byte by byte on the fixture and in four deliberately hostile environments (the machine's
40
+ git settings, the locale); `pnpm run parity:live` does the same on the consumer project's live
41
+ history in two environments 95 rows × 27 columns, the artifact self-contained and passing its own
42
+ control mode. The output does not depend on the machine: the git settings that change what is
43
+ parsed are pinned inside the engine (`BLOCKERS.md` §B1). The comparison against the working tree
44
+ compares content rather than sizes, so a tree with CRLF newlines (`.gitattributes`; `core.autocrlf`,
45
+ the default of Git's installer for Windows) is no obstacle (`BLOCKERS.md` §B2).
46
+
47
+ **A history with deletions is no longer a dead end** (`BLOCKERS.md` §B3): a column whose file lived
48
+ in history and was deleted before HEAD used to fail the whole run with code 1 and the text "no file
49
+ instead of no file" that is, a project with deleted files got no report at all. Now only a
50
+ **disagreement** between the two sides of the comparison is a refusal: a lost creation, a lost edit
51
+ and a lost deletion still fail the run, but with the real cause and a ready command, while a file
52
+ deleted before HEAD is simply empty in the table. Proven by numbers rather than by a word: the
53
+ column's size at every commit is checked against the blob size from git — a returned file gives the
54
+ same number as its first appearance and a witness, the file that appears only in a merge, fails
55
+ the run naming both sides. The boundary of the same parse is closed as well (`BLOCKERS.md` §N8):
56
+ the path for the state was chosen by the order of the settings rather than by what the commit holds,
57
+ so with `diff.renames=false`when git returns the old name of a renamed file and the new one in a
58
+ single commit the engine took the vanished alias and the comparison refused on a legitimate case;
59
+ now it takes the alias git returned a blob for. The old numbers could not move: both logics agree
60
+ wherever the first alias in the commit exists, that is, in every run that ended with a report
61
+ before.
62
+
63
+ **The consumer project is connected** (2026-09-14). `safe-resets` installs the package from git by
64
+ the release tag and keeps no copy of the tool of its own — neither `tools/size-table.js` nor a test
65
+ for it: the table is built and checked by the `size` command (`pnpm run test:sizes`), and its part
66
+ in that project is one line of its runner (`worklog/archive/WORKLOG.md` §18). The connecting
67
+ instruction turned out to be right and incomplete in two places the step giving CI access to the
68
+ package and the order of moving off an already installed copy and both are written into the
69
+ instruction below. The step with a key left it later, along with private access.
70
+
71
+ **The data contract and the page.** The engine hands over absolute values and the shape of the table
72
+ (`--data`), while deltas, totals, "now" and the filters are computed by the page — which is the
73
+ report itself (`size-report.html`): without that split the filters and "the total over the
74
+ selection" are impossible in principle. The contract tells how each number was obtained (the metric's
75
+ `method`) and nothing else about it: the page shows what the engine said and judges no number — it keeps
76
+ no rule of counting of its own, and the split of numbers into exact and approximate was taken out of the
77
+ package. The page's panel is a
78
+ tree of files by folder, with a switch per folder for the whole subtree; a reader's choice survives
79
+ a revisit and travels in a linkthe page's address is the link. The contract carries the **project
80
+ catalogue** too: every path git sees, so the page's tree is the project's tree, while numbers exist
81
+ only for the files that became columns (release 2.2.0).
82
+
83
+ **The page's block is the contract in sparse form.** The file carries the history as changes rather than
84
+ as a snapshot per commit for every file the rows it appeared in (absolute numbers), moved in (deltas
85
+ against its own previous record) and disappeared in with the texts in a dictionary and the rows' links
86
+ cut by the part they share. The page's own chapter unrolls it back into exactly the contract
87
+ (`src/page/payload.js`), so the calculation and the table know nothing of the sparse form and there is no
88
+ second way to count a row; `--data` still answers with the dense contract, and the block's `schema: 2` is
89
+ what refuses a record written for the previous form. On this repository the data block is
90
+ 1 370 724 → 88 712 B, and its parse is 11.6 → 0.6 ms plus 3.3 ms of unrolling (`contract-data` holds the
91
+ round trip).
92
+
93
+ **The block travels packed, and that is the page's one asynchronous step.** It lies in the file gzipped and
94
+ base64 encoded the tag says so (`data-pack="base64+gzip"`) and the page unpacks it with the platform's own
95
+ `DecompressionStream`: no library travels in the page, nothing is fetched, and the whole artifact of this
96
+ repository goes 122 668 → 78 319 B (the block 88 786 → 42 856 B — 74 B more than step 05 measured, because the
97
+ report is itself a column of the report and its own size moved in between). The price is deliberate and twofold: the
98
+ block can no longer be read by eye or by `diff`, and the first drawing waits for a promise where it used to
99
+ happen during the parse. Everything after the first drawing is as synchronous as it was; a host that cannot
100
+ unpack is told in words rather than left with an empty table. The checks read the page in jsdom, which has no
101
+ such API, so the harness puts the platform's own implementations of it into the window and that seam is
102
+ tested from both sides: the ordinary path with them put in, and the message in words without them.
103
+
104
+ **What the page carries is squeezed, and only what the page carries.** The program and the styling are pasted
105
+ with their comments and indentation out — the same stripping the `min` metric counts — so the artifact holds
106
+ code without ballast while `src/derived.js`, `src/page/*.js`, `src/table.css` and `src/page/app.css` stay the
107
+ ordinary files a person reads: the squeeze lives in the paste and nowhere else. It is 58 922 → 24 885 B of
108
+ program and 15 505 5 905 B of styling (the artifact 166 305 → 122 668 B), and the assembled program is
109
+ guarded at build time by the stripper's own `assertCompilable` a squeeze that ate code stops the build
110
+ rather than the browser.
111
+
112
+ **Minification of what the page carries is decided, not defaulted: not taken — and measured.** esbuild would
113
+ take the pasted program 24 885 18 128 B and the styling 5 906 → 5 102 B, the artifact 122 668 → 115 107 B
114
+ (7 561 B), for 91 ms of every build. The price is not those bytes but the contract: esbuild is an **optional**
115
+ dependency and its absence is a different count rather than a refusal, while the artifact is rebuilt by the
116
+ post-commit hook on whatever machine made the commit a builder that minifies when it can would build **a
117
+ different file** there, and the report would stop being a fixed point. Buying determinism instead would mean
118
+ a pinned version and a page that cannot be assembled at all without esbuild (`src/optional.js`), for 6 % of
119
+ the file. What the decision rests on the bytes it would save, the fixed point it would cost, and what
120
+ reopens it is written down beside the plan the step belongs to.
121
+
122
+ The tool grew out of one script in the consumer project [`safe-resets`](../figma/safe-resets) — the
123
+ metrics `raw` and "a simplification instead of minification", a static report in git; that path
124
+ does not exist in this repository, and it is not named anywhere without the project.
125
+ **A project whose code is JavaScript modules in `.js` reports too**: the stripper's guard understands
126
+ both forms, a script and a module, so connecting needs no setting edited by hand, and when the graph
127
+ really is not JavaScript the refusal names the cause and the command. Hints, the help text, the
128
+ default fix command and the templates name the **path inside the project**
129
+ (`node node_modules/@vernikr/size-report/bin/size.js`) rather than the package name: `npx <name>`
130
+ runs an installed package only while it is there, and in a project without it the name goes to the
131
+ registry and pulls a package over the network.
132
+
133
+ **What the documentation promises is checked, not assumed**, and the promises are split one per file:
134
+ existence and completeness of paths (`test/docs-paths.test.js`), commands, refusal causes and section
135
+ links (`test/docs-commands.test.js`), the count of checks (`test/docs-numbers.test.js`) and the install
136
+ example leading to a revision whose help knows the named commands (`test/docs-pin.test.js`). One reader
137
+ of facts serves them all (`tools/docs-facts.js`). What a
138
+ machine cannot check wording, promises about the future, whether a file's role is described
139
+ correctly the guards do not take on, and they say so in their headers.
140
+
141
+ **git is read through one boundary, in the checks too**: the list of pins (`core.quotePath`,
142
+ colouring, the signature block, the encoding) is one for the engine and for the harness, so the
143
+ checks and the tools reach git through a common place and an unpinned place is guarded by
144
+ `test/git-pins.test.js`, which also shows by witness that a pin works: the same read without it
145
+ returns a non-English path quoted. It is the same defect as B1, only found in the harness: without
146
+ the pins a check is green on a machine with our settings and red on a machine with the default ones.
147
+ The check that measures the environment itself keeps its unpinned read deliberately it is named,
148
+ and it stands in a list of its own.
149
+
150
+ **Every refusal of the tool tells the truth, and that is guarded rather than assumed.** A false cause
151
+ in a refusal text was found by a live run, four times in a row, each time by accident — so the class
152
+ is closed not by a fifth fix: `tools/refusals.js` holds a line per refusal saying what it must
153
+ convey, which code to answer with and which phrases must stay in the output, and two checks split
154
+ that promise. `test/refusals.test.js` **calls** each refusal and compares the exit code and the
155
+ phrases; `test/refusals-catalog.test.js` reads the sources and requires a catalogue entry for every
156
+ refusal site the maps `SITES` and `PRINTED` hold the counts and a case in the catalogue for every
157
+ entry, so a new refusal cannot appear without a check. Refusals a run cannot reach are named
158
+ explicitly: four are guarded by a check of their own (the catalogue names the file and the phrases),
159
+ and one cannot be caught at all "internal error" — which is said where it stands. What the
160
+ catalogue does not take on is said in words: wording beyond the listed phrases, and meaning, andthe "!" sign, which is a note (another count, a mixed commit, the automation switched off) rather
161
+ than a refusal, with exit code zero.
162
+
163
+ **And the advice in a refusal is executable that is checked as well.** The truth about the cause is
164
+ half the promise; the other half is that the suggested command can be run. A live case started this:
165
+ a hint called out to a registry name no package carried, and in a project without that package
166
+ installed it ran someone else's code. Now every catalogue case says what the refusal advises, and the
167
+ advice is executed in the state that printed it: the exit code is compared (including "the refusal is
168
+ gone" the same call after the advice must answer differently), while an advice that is a form
169
+ without values is checked against the help output (the same commands and flags). Advice a person has
170
+ to carry out is named with its reason editing settings, committing, installing a dependency — and
171
+ that is said there too, in the catalogue. A new advice inside an existing refusal cannot pass
172
+ silently: the advice is taken out of the output by its markers, and it has to have a catalogue entry.
173
+
174
+ **A module no longer costs a Node process per cell**: the guard parses a module through
175
+ `vm.SourceTextModule`, which exists only under `--experimental-vm-modules`, and that is where the
176
+ per-cell process came from. Today one worker thread parses the modules for a whole run, and the price
177
+ has not disappeared but become one-time; the parse also rests on an experimental API (without it the
178
+ guard falls back to `node --check`: slower, no softer). What a run costs today the run prints itself
179
+ (`pnpm run suites:measure`).
180
+
181
+ **The checks' shared part lives in one place** (`tools/harness.js`): one clone of the fixture per
182
+ environment rather than one per check, a read-only run of the tool is not repeated, and a check that
183
+ edits files takes a clone of its own. The files go in a pool over the cores (`tools/run-tests.js`),
184
+ and the numbers add up: the run counts the checks of every file against the `test(` declarations in
185
+ it, so a file that did not run is a failure rather than fewer checks.
186
+
187
+ **There are two runs, and the choice between them follows the price of a file, not the alphabet.**
188
+ The cost of a check here is not the size of the file but how many times it launches the tool and git:
189
+ a launch is a Node process, while cloning the fixture or building the artifact takes hundreds of
190
+ milliseconds. So the fast run gathers what it proves from reading (sources, tree, help, reference
191
+ numbers on a shared fixture), and the full one adds what runs the tool many times on its own clones,
192
+ commits and installs hooks; the reason for each expensive file is named line by line in
225
193
  `tools/suites.js`.
226
194
 
227
- | Прогон | Команда | Проверок |
195
+ | Run | Command | Checks |
228
196
  |---|---|---|
229
- | Быстрыйкаждая правка | `pnpm test` | **72 из 177** |
230
- | Полныйвыкладка и CI | `pnpm test:all` | **177** |
231
-
232
- Ни одна проверка не потеряна и не ослаблена: полный прогон запускает все 177 теми же
233
- файлами, а быстрый берёт их часть. Умолчаниеполный: файл становится быстрым только
234
- явно и с причиной, поэтому новое дорогое не может тихо уехать в быстрый. Стерегут это
235
- объявление `test/suites.test.js` (полнота классификации и причина у каждого файла) и
236
- сторож документации `test/docs-numbers.test.js` (числа в таблице выше).
237
-
238
- **Целей по времени у прогонов нет, и это решение, а не пропуск.** Секунды зависят от
239
- окнамашина бывает под очень разной нагрузкой,поэтому ни набор, ни CI за время
240
- не валятся, и документ секунд не обещает: `pnpm run suites:measure` печатает
241
- длительность каждого файла отдельным прогоном (и сам прогон печатает её рядом с
242
- галочкой), но это измерение, а не порог. Разделение держится признаком файла чем он
243
- занят, а не сколько идёт. CI зовёт полный прогон дважды: обычной средой и без настроек
244
- машины (`GIT_CONFIG_GLOBAL=/dev/null`).
245
-
246
- **Обещанное пакетом сведено к факту.** Список поставки называл четыре пути,
247
- которых в репозитории нет (`dist/`, `templates/`, `CHANGELOG.md`, `LICENSE`):
248
- теперь он обещает только существующее (шаблоны и `CHANGELOG.md` вернулись в список
249
- вместе с файлами, а не раньше их), а `pnpm run pack:check` проверяет это с двух
250
- сторон в списке нет того, чего нет, и в тарболл не попадает то, чего список не
251
- обещает. Снятие обоих эталонов снова работает (`pnpm run parity`, `pnpm run
252
- fixture`) и больше не зависит ни от того, держит ли проект-потребитель свою копию
253
- инструмента, ни от настроек git на машине. Заодно поправлены два текста, которые
254
- это же обещали: подсказка `--init` (говорила «проверки едут вместе с пакетом», а
255
- сьют в пакет не входит) и умолчание `fixCommand` (называло несуществующее имя
256
- пакета `npx size-table --write`).
257
-
258
- **Проверки идут сами (шаг 6 плана, `.github/workflows/ci.yml`).** На каждый пуш и
259
- на каждый запрос правки один job `verify` зовёт **одну команду** `pnpm run verify`;
260
- список шагов живёт в одном месте (`tools/gates/run.js`) и совпадает с локальным,
261
- поэтому проверки, которой нет в профиле, в CI быть не может (это стережёт
262
- `test/gates-verify.test.js`). В профиле: строгий линтер, датчики раздувания,
263
- набор проверок, тот же набор в среде, где настроек машины нет вовсе
264
- (`GIT_CONFIG_GLOBAL=/dev/null`), работу из собранного тарболла, сверку с историей
265
- проекта-потребителя и воспроизводимость обоих эталонов. Покрытие под c8 дороже
266
- (полный набор под ним) и живёт в slow-профиле `pnpm run verify:slow`,
267
- `.github/workflows/verify-slow.yml` по расписанию.
268
- Секретов job не требует: история потребителя лежит в репозитории бандлом на той
269
- же ревизии, что записана в эталоне (`fixtures/live/`), а пересъём идёт во временный
270
- каталог и сверяется с закоммиченным рабочее дерево остаётся чистым. Матрицы по
271
- версиям Node нет намеренно: этот проход про контроль.
272
-
273
- **Выпуск это тег (`.github/workflows/release.yml`).** `git push origin v1.2.3`
274
- прогоняет тот же полный набор, сверяет версию манифеста с тегом, проверяет работу
275
- из собранного пакета и отправляет его в реестр без секрета и без кода из
276
- аутентификатора: публикация идёт по удостоверению GitHub Actions (trusted
277
- publishing), которое npm принимает вместо токена. Издатель заведён один раз и живёт
278
- на стороне npmjs.com, а не в репозитории: `npm trust github @vernikr/size-report
279
- --file release.yml --repo vernikr/size-report --allow-publish` (то же самое кнопка
280
- Trusted Publisher в настройках пакета), права **publish** и stage publish; проверить,
281
- что связь есть, — `npm trust list @vernikr/size-report`. Выпуск `1.2.0` прошёл именно
282
- так: `v1.2.0` 44 с, `+ @vernikr/size-report@1.2.0`, удостоверение подписано и
283
- записано в журнал прозрачности.
284
-
285
- Одна ловушка раннера стоила отдельной правки, и она не про этот пакет, а про
286
- `setup-node`: с `registry-url` действие пишет в `.npmrc` строку
287
- `_authToken=${NODE_AUTH_TOKEN}`, npm считает учётные данные заданными и за
288
- удостоверением OIDC **не идёт** публикация падает 404 при верно заведённом
289
- издателе. Поэтому `registry-url` здесь не указан (реестр и так по умолчанию тот же, а
290
- явный адрес живёт в `publishConfig`), и это стережёт `test/release.test.js`. Черновой
291
- прогон из Actions («Run workflow»: по умолчанию он ничего не публикует) проходит весь
292
- список до самого пути публикации: гоняет полный набор, проверяет работу из тарболла и
293
- собирает пакет на черновой версии (`1.2.0` `1.2.1-draft.0`, чтобы реестр не отказал
294
- в уже выпущенном номере). Настроен ли издатель, черновой прогон не показывает:
295
- `--dry-run` не обменивается удостоверением и проходит вообще без учётных данных
296
- (проверено в пустом каталоге: код 0 без токена) правду об этом даёт только настоящий
297
- тег, и он её дал.
298
-
299
- Первым же прогоном CI окупился: шаг живого паритета упал не на расхождении чисел,
300
- а на самой проверке вывод процессов собирался как строка, и многобайтовый символ,
301
- разорванный между кусками чтения, превращался в два символа-заменителя (местные
302
- прогоны этого не показывали: границы кусков зависят от того, как ядро вернуло
303
- чтение). Дефект починен, сторож `test/runner.test.js` (`WORKLOG.md` §21).
304
-
305
- Второй прогон нашёл ещё два дефекта, и оба про git по обе стороны границы
306
- вызова. Шаг воспроизводимости эталонов сверял бандл истории **побайтово**, а
307
- упаковку пишет git: её байты зависят от версии, и проверка была зелёной на одной
308
- машине и красной на другой (фикстура это и в README утверждает). Теперь у бандла
309
- сверяется содержимое ветки, верхушка и число коммитов, а побайтово только то,
310
- что пишем мы сами. Второй: бандл живой истории лежал **без `HEAD`**, и клон сам
311
- решает, какую ветку выложить, разные версии git решают по-разному (`hint: Using
312
- 'master' …`). Бандл пересобран с `HEAD`, `check:standards` это требует, а отказ
313
- инструмента печатается целиком, а не первой строкой (`WORKLOG.md` §22).
314
-
315
- **Страница отчёта выглядит и ведёт себя как инструмент** (`REFACTOR.md` R-2.2):
316
- один набор стилей таблицы на оба вывода (`src/table.css`) странице достались
317
- липкие шапка и колонка коммита, которые раньше были только у статического
318
- артефакта, и она больше не уезжает вбок в узком окне (до правки 1518px при
319
- окне 620). Добавились состояния «нечего показать» (сняты все метрики или все
320
- файлы) и переключатели, доступные с клавиатуры. Цвет
321
- deльт задан один раз и по артефакту: рост зелёный, спад красныйсмена это две
322
- строки в `src/table.css` плюс пересборка эталона артефакта, больше цвета нигде нет.
323
-
324
- **Левая панель страницы — дерево файлов проекта** (`REFACTOR.md` R-2.4, каталог
325
- путей `CHANGELOG.md` 2.2.0, складывание папок 2.3.0, вид того, чего в отчёте
326
- нет,2.4.0). Дерево строится по путям проекта, а не по одним
327
- колонкам, поэтому в нём видно и то, что в отчёт не попало: у такого листа у
328
- папки, где измерять нечего) галочка стоит на месте, но **снята и недоступна**
329
- включать нечего,а причина в всплывающей строке («колонкой быть не может»
330
- правило пакетаили «в набор колонок не попал» выбор проекта). Снятая, а не
331
- убранная: ряд строк остаётся ровным (глаз сравнивает одно с одним), а недоступность
332
- говорит, что это не выбор читателя. Заодно счётчик папки со смешанным
333
- составом написан долей («2/5»: два файла в отчёте из пяти в папке). Сам отчёт в
334
- дереве назван всегда: его отслеживаемость свойство момента, и от неё содержимое
335
- страницы не зависит. **Всё, чего в отчёте нет, стоит после того, что в нём есть**
336
- и папки, и листья: в списке, где половина строк не переключается, отчёт должен
337
- быть виден сразу, а не среди чужого (`test/page-tree.test.js`). У
338
- папки три состояния — все её файлы включены, часть, ни одного, — и переключатель
339
- папки ведёт за собой всё поддерево; рядом стоит число файлов. Своего состояния у
340
- папки и у быстрой кнопки категории нет: обе переставляют галочки файлов, поэтому
341
- дерево, кнопки и таблица не могут разойтись. У каждой папки есть ещё свой знак
342
- (▾/▸): он отвечает за то, сколько дерева видно, — это дело смотрящего, а не выбор
343
- читателя, поэтому знак помнится между заходами и в ссылку не идёт (у записи свой
344
- ключ и тот же паспорт отчёта; разворот всех папок её убирает, как возврат галочек
345
- запись выбора). Список файлов длиннее панели
346
- прокручивается, а не выталкивает таблицу, и прокрутка эта одна: на широком
347
- экране листается панель целиком (при окне 1440×900 страница укладывается в окно, а
348
- таблица берёт всю оставшуюся высоту), а на узком сам список, потому что там
349
- панель растёт вместе со страницей. Дерево стало длинным этом репозитории 148
350
- подписей), поэтому папки и складываются: иначе до его середины не добраться.
351
-
352
- **Складывание папки чистый вид, и оно не считает числа** (`CHANGELOG.md` 2.4.0).
353
- Поддерево лежит в разметке и прячется классом на строке: клик по знаку меняет три
354
- вещи, которые читатель и видит, класс, знак и запись в памяти. Пересборка здесь
355
- была бы честной работой впустую: она строит таблицу целиком этом репозитории
356
- 97 строк × 136 колонок, 39 576 клеток), то есть платит за числа, которых складывание
357
- не меняет, — и это было видно глазом как задержка. Замер в настоящем Chrome на этой
358
- же странице: клик по знаку папки `src/` (42 строки поддерева) **0,6 мс** в
359
- обработчике и 6 мс на перекладку против **107 + 380 мс** перерисовки, которую он
360
- вызывал раньше (столько же стоит переключение одного файла у той же страницы).
361
- Стережёт это проверка о том, что после складывания таблица осталась той же самой
362
- разметкой (`test/page-tree.test.js`), а не собранной заново.
363
-
364
- **Колонки, которых коснулся последний коммит, идут впереди** (`CHANGELOG.md` 2.4.0).
365
- Отчёт пересобирается после каждого коммита, и первый вопрос читателя что принесла
366
- эта правка. Знак приходит из истории, а не из чисел: правка без изменения размера —
367
- тоже правка. Берётся последний коммит, задевший хотя бы одну колонку, — считая от
368
- верхушки назад: коммиты мимо колонок (и, прежде всего, сам отчёт, который коммитит
369
- хук) пропускаются, иначе знак зависел бы от собственного коммита отчёта, а тот же
370
- прогон давал бы другие байты. Внутри каждой части порядок прежний, из настроек
371
- (`sort` устойчив): порядок колонок то, к чему читатель привык, и своим выбором
372
- файлов он его не переставляет (`test/page-view.test.js`, поле `last` контракта).
373
-
374
- **На широком экране панель стоит слева от таблицы и не вытесняет числа**
375
- (от 900 px, `src/page/app.css`). Это не украшение: на десктопе бокового места
376
- много, а вертикального мало переключатели, дерево и числа видны одновременно, и
377
- ни прокрутка чисел, ни прокрутка дерева не уводит управление за верх экрана.
378
- Раскладка сделана сеткой на `body`: обёртки в разметке нет, потому что страница
379
- собирается вклейкой глав и форма страницы должна жить в одном месте. Строк у сетки
380
- пять и они названы по предмету (заголовок, сообщение о ссылке, работа, сообщение
381
- пустоты, подпись), тянется только рабочая: таблица берёт всю оставшуюся высоту, а
382
- панель не больше неё. Цена прежнего поведения была видна глазом: высоту страницы
383
- задавал список файлов, и под таблицей оставалась пустота (замер до правки при окне
384
- 1440×900: панель 883 px, страница 1097 при окне 900, под таблицей 195 px).
385
- Измерено в настоящем Chrome после правки: при 1440×900 страница ровно в окно
386
- (900), панель колонка 300 px слева (103…888, содержимое 804 дальше она
387
- прокручивается), таблица 1060 × 735 на том же верху 103 (до правки 1060 × 602), а
388
- под ней остаётся только подпись под таблицей (62 px); при 1024×800 те же 300 и
389
- таблица 651 × 620; при 899 раскладка снимается и столбцы снова идут друг под
390
- другом (панель 871, таблица 871 × 442, страница прокручивается 972).
391
- Стрежет это проверка на числах таблицы, а не на разметке: выключение папки убирает
392
- ровно её колонки и ровно её объём из итога (`test/contract.test.js`). Проверено в
393
- настоящем Chrome: 9 папок до трёх уровней вложенности (`.github/workflows`,
394
- `tests/golden`), 25 файлов, ни одного внешнего запроса (сеть — только сам файл),
395
- ни одной ошибки в консоли, `docs/6` после выключения одного своего файла показала
396
- третье состояние, а после выключения целиком 52 колонки 40.
397
-
398
- **Галочка не отбирает ни прокрутку, ни место у чисел** (правка вида страницы
399
- 2026-09-15, `src/page/app.js`, `src/page/app.css`). Панель рисуется заново после
400
- каждого переключения, и вместе с ней терялось место, до которого читатель
401
- долистал: клик по галочке возвращал список к началу, а до нижних файлов дерева так
402
- и не добирались. Теперь прокрутка панели и спискачасть вида, как галочки: она
403
- запоминается перед пересборкой и ставится обратно после (фокус возвращается без
404
- прокрутки `preventScroll`), а поле «Файлы» больше не режет дерево своим
405
- потолком в 62vh. Строка категорий («Код», «Документация», «Служебные») в широкой
406
- раскладке липнет к верху панели — фон у неё тот же, что у панели, поэтому под ней
407
- не читаются проезжающие файлы; верхний отступ панели для этого переехал в первое
408
- поле (прокручиваемое видно и в отступе прокрутки замер: до него в полосе 13 px
409
- читались «parity/» и «data.json», послесама строка). Шрифт подписей файлов
410
- стал как у чисел таблицы (12,5 px), а расшифровка под деревом убрана: она
411
- отодвигала числа, а её смысл и так стоит у того, что объясняет (знак числа называет
412
- цвет дельты, способ и точность под переключателями метрик, знак пропуска
413
- в подсказке клетки). Заодно граница широкой раскладки стала 899 px: при ровно
414
- 900 px обе половины оформления применялись к одной странице, и от «узкой» в
415
- «широкой» оставался потолок высоты таблицы те же пустые 179 px под ней на одном
416
- единственном размере окна. Проверено в настоящем Chrome на этой странице: при
417
- окне 1440×500 (панель прокручивается) и прокрутке до последнего файла
418
- `panel.scrollTop` = 421 до клика и 421 после, строка категорий стоит на 1 px от
419
- верхнего края панели, а под ней на всех полосах прокрутки только её же подписи.
420
-
421
- **Панель помнит выбор читателя** (`REFACTOR.md` R-2.5). Запись хранится в памяти
422
- браузера, и она привязана к «паспорту отчёта» имя инструмента, схема данных,
423
- путь артефакта, заголовок и метки колонок; в ключ входит отпечаток паспорта,
424
- поэтому чужие отчёты живут порознь и не видят выбора друг друга (в браузере все
425
- страницы `file://` делят одну память, так что это не мелочь). Внутри записи выбор
426
- лежит **по именам** файл путём, метрика ключом, и хранится только выключенное:
427
- колонка, перенаправленная на другое, или метрика, убранная из настроек, просто
428
- ничего не значит, появившееся остаётся включённым, а «включил всё обратно»
429
- возвращает страницу к умолчанию и стирает запись. Первому читателю тому, чья
430
- запись испорчена или устарела) достаётся именно умолчание состояние на числа и
431
- разметку не влияет. Проверено перезаходом в настоящем Chrome с диска, без сети:
432
- после выключения метрики и одного файла следующий заход даёт те же **25 колонок
433
- вместо 52** и тот же итог **994 335 вместо 1 133 362**; два отчёта в одном браузере
434
- держат по своей записи (`size-report:4684b2b2` и `size-report:5dcd0db1`), и выбор
435
- одного не трогает другой.
436
-
437
- **Ту же выборку отдают ссылкой** (`REFACTOR.md` R-2.6). Адрес страницы это и есть
438
- ссылка: та же запись, что ложится в память браузера, ложится и в якорь
439
- (`#size-report=…`), поэтому отправитель просто копирует адрес, а получатель видит
440
- его выбор без единого действия. Ссылка старше памяти: она — явный выбор
441
- отправителя, а память читателя она не подменяет, пока тот сам чего-нибудь не
442
- поменяет. Чужой или испорченный адрес не применяется и не молчит: над таблицей
443
- появляется строка с причиной («ссылка собрана в другом отчёте» / «выбор в адресе
444
- нечитаем»), вид остаётся читательским, а присланный адрес не переписывается; о
445
- именах, которых в отчёте нет, сообщается числом, они пропускаются, остальное
446
- применяется. Ссылка работает и когда отчёт уже открыт: браузер на смену якоря
447
- документ не перезагружает, поэтому страница слушает адрес сама (без этого ссылка
448
- срабатывала бы только в новой вкладке этот разрыв нашёлся в браузерной
449
- проверке, а не в тестах). Проверено на живом отчёте в Chrome с диска: получатель с
450
- пустой памятью по ссылке видит те же **25 колонок и тот же итог 994 335**, что и
451
- отправитель; чужой адрес оставляет 52 колонки и 1 133 362 и объясняет отказ; на
452
- уже открытой странице ссылка меняет вид с 52 колонок на 25, а консоль остаётся
453
- пустой. Ни одного обращения в сеть в странице нет это отдельное утверждение
454
- проверки, а не обещание.
455
-
456
- **Метрика `min` умеет считать по-настоящему** (шаг 3 плана, срез 1). Способ
457
- выбирается в настройках: `"minify": {"engine": "esbuild"}` настоящее сжатие
458
- (JS/TS/CSS) необязательной зависимостью, `"engine": "strip"` прежнее снятие
459
- комментариев и отступов; умолчание не менялось, потому что под ним сняты оба
460
- замороженных эталона. На фикстуре сжатие меньше упрощения в **44 клетках и ни разу
461
- не больше**: `src/code.js` **276 185 Б**, `src/style.css` **55 43 Б**, по фикстуре
462
- **−1 372 Б**. Цена сжатия названа, а не спрятана: на живой истории (95 строк ×
463
- 27 колонок) прогон стал **1,48 1,71 с** это запуск минификатора и разбор тех
464
- файлов, которые он берёт. JSON минифицируется разбором и потому
465
- остаётся точным, а форматы, которых минификатор не берёт, честно названы в подписи
466
- метрики вместе с теми, которые он берёт. Точность объявлена дважды, и это не два
467
- ответа на один вопрос: подпись метрики говорит про **худшее в колонке** (один
468
- формат без минификатора делает метрику приближённой целиком, а не прячется за
469
- «точное» соседа), а каждая клетка про своё число, и приближённая помечена
470
- пунктиром с подписью способа. Худшее берётся у клеток, а не у названия способа:
471
- отчёт из одного JSON точен и под снятием балластаразбор теряет только
472
- незначащие пробелы, короче его не сделает никто, и подпись так и говорит.
473
- Оба ответа считаются одним правилом (`pointExact` в
474
- `src/metrics.js`), поэтому разойтись не могут. Минификатора нет (установка без необязательных
475
- зависимостей, платформа без него) метрика отступает к упрощению, способ говорит
476
- об этом словами, а прогон отдаёт **код 4**, а не молчание: числа при этом те же, что
477
- у прежнего способа,побайтово со эталоном. Выведенный профиль ведёт новые проекты
478
- сразу на сжатие `--init` закрепляет то же самое); цена названа прямо в его подсказке. Файл, который минификатор не
479
- разобрал (разметка в `.js`, чужой синтаксис), отказ кодом 2 с причиной от него
480
- самого и двумя готовыми выходами.
481
-
482
- **Метрика `tok` считает токены настоящим словарём** (шаг 4 плана, срез 1). Токены
483
- третье измерение отчёта: вес файла для языковой модели. Словарь выбирается в
484
- настройках (`"tokens": {"family": "openai", "encoding": "o200k_base"}`), и
485
- кодировка часть числа, а не подробность: на фикстуре `src/code.js` это **168
486
- токенов** в `o200k_base` и **196** в `cl100k_base`, поэтому кодировка называется
487
- рядом с семейством, а способ метрики цитирует ровно ту, что посчитана. Токены
488
- не байты и не сжатие, и расхождение видно, а не заглажено: та же клетка — **735 Б**
489
- `raw`, **276 Б** упрощением, **185 Б** настоящим сжатием и **168** токенов; байт на
490
- токен отличается по файлам в **2,5 раза** (от 2,56 у `package.json` до 6,30 у
491
- `crlf.txt`), то есть считается текст, а не отношение. Семейство в этой версии одно
492
- `openai`: у остальных нет словаря, который можно было бы назвать их собственным, а
493
- считать чужим и называть это семейством значило бы обещать то, чего нет.
494
- Переключателя словаря на странице нет намеренно: страница получает готовые числа и
495
- сама не считает ничего, а посчитать токены другим словарём ей нечем. Сосчитать все
496
- семейства на каждый прогон это платить временем за числа, о которых читатель,
497
- может быть, и не спросит, поэтому выбор семейства и кодировки живёт там, где стоит
498
- времени настройках запуска), а страница его **называет**: способ каждой метрики
499
- виден под переключателями текстом, а не только во всплывающей строке (решение
500
- плана §4.8.4 отменено осознанно — `PLAN.md`, шаг 4).
501
- Форматы без текста (картинка, шрифт, архив) названы в подписи метрики вместе с
502
- причиной: у них число идёт по байтам, и по тому же правилу помечена клетка такого
503
- файла, а подпись метрики берёт худшее в колонке — двум ответам разойтись нечем.
504
- Словаря нет (установка без необязательных зависимостей, платформа без него) — счёт
505
- идёт оценкой по длине с названным коэффициентом, а прогон отдаёт **код 4**; числа
506
- при этом те же, что у прежнего отчёта без токенов, а сам шов проверяется
507
- окружением `SIZE_REPORT_NO_OPTIONAL`. Прогон этим платит временем, и это честная
508
- цена словаря, а не разбор: таблицы словаря читаются **0,3 с на процесс**, а на
509
- живой истории (95 строк × 27 колонок, 1,23 МБ текста) тот же отчёт идёт
510
- **1,55 → 6,35 с** — умножается именно сбор истории, а не таблица: токенов в
511
- «сейчас» — **303 705**, то есть 4,05 Б на токен. Отсюда и цена набора проверок:
512
- **7,3–7,9 → 10,4 с** при 66 → 73 проверках (запас и новый бюджет — ниже). Выведенный
513
- профиль ведёт новые проекты сразу на токены.
514
-
515
- **Волна 0 чистки пройдена** (`REFACTOR.md`): у отказов командной строки появились
516
- коды выхода и справка вместо стека, `--help` отвечает, `--write`
517
- создаёт недостающий каталог, подсказка в отказе ведёт к работающей команде, а
518
- вывод настроек больше не предлагает колонкой саму таблицу — иначе первая же
519
- проверка настроек его отвергала.
520
-
521
- **Появились две команды: полнота и объяснение** (шаг 5 плана). `size check`
522
- отвечает, всё ли в истории попало в отчёт: каждый путь, тронутый коммитами,
523
- обязан быть колонкой или объявленным исключением, а непонятый путь — это код 1,
524
- путь, коммит, который его завёл, и готовая починка. Тем же ответом идут сводка по
525
- выпавшим коммитам (сколько и почему) и списки их sha — то есть «какая часть
526
- истории покрыта». `size explain <коммит>` отвечает про один коммит — назвать его
527
- можно и именем ревизии (`HEAD`, ветка, тег, `HEAD~1`), и sha, и началом sha:
528
- строка есть (и которая) либо причина, почему её нет, — тронут только отчёт, числа не сдвинулись
529
- при тронутых файлах колонок, коммит мимо колонок, слияние скрыто `rows.merges`.
530
- Обе берут причину у того же прохода, что и отчёты, а улики — из списка изменённых
531
- путей коммита: чего в истории нет, о том молчание вместо догадки. Полнота — из требований (§4.2: «ни одно изменение не
532
- просочилось мимо отчёта»), и она же заменяет контроль
533
- «артефакт ↔ история»: отчёт можно не хранить в git. Смысл `skip` в настройках от
534
- этого не изменился, но **значение расширилось**: это не только «пути, которые
535
- колонками быть не могут», но и объявленные исключения полноты — тот же список, и
536
- чеканить второй инструмент не стал. Цена названа: `check` — это проход по истории,
537
- как и любой отчёт (**1,5 с** на живой истории), а набор проверок подорожал на
538
- тринадцать запусков инструмента (бюджет — ниже). К ним добавился `size doctor` —
539
- диагностика одним ответом (ниже, в разделе про проверки).
540
-
541
- **Отчёт обновляется сам** (последний пункт шага 5 плана). `size install-hook`
542
- ставит два хука — `post-commit` и `post-merge` (`post-commit` при `git merge` не
543
- выполняется вовсе, поэтому одного файла мало), — и после каждого коммита и слияния
544
- отчёт пересобирается, а лежащий в git — ложится **отдельным коммитом**: ручного шага
545
- «код, потом таблица» больше нет. Коммит отчёта собирается плумбингом git
546
- (`commit-tree`): в него физически не могут попасть ни индекс, ни чужая
547
- незакоммиченная работа, и зацикливание невозможно по устройству, а не по флагу в
548
- окружении. Отказ инструмента коммит не роняет — причина печатается строкой и
549
- видна в `size doctor`.
550
-
551
- Перенос, доработка и оформление в пакет расписаны в `PLAN.md` по шагам, с
552
- приёмкой каждого.
553
-
554
- ## Что в репозитории
555
-
556
- | Файл | Роль |
197
+ | Fastevery edit | `pnpm test` | **81 of 186** |
198
+ | Fullrelease and CI | `pnpm test:all` | **186** |
199
+
200
+ No check is lost or weakened: the full run starts all 186 with the same files, the fast one takes part
201
+ of them. The default is the full run a file becomes fast only explicitly and with a reason — so new
202
+ expensive work cannot quietly move into the fast one. Two declarations guard that:
203
+ `test/suites.test.js` (every file classified, and a reason for each) and the documentation guard
204
+ `test/docs-numbers.test.js` (the numbers in the table above).
205
+
206
+ **The runs have no time targets, and that is a decision rather than an omission.** Seconds depend on
207
+ the window the machine is under very different load at different times so neither the suite nor CI
208
+ fails over time, and this document promises no seconds: `pnpm run suites:measure` prints every file's
209
+ duration in a run of its own (and a run prints it next to its tick), but that is a measurement, not a
210
+ threshold. The split rests on what a file is about rather than on how long it takes. CI calls the full
211
+ run twice: in the usual environment and with none of the machine's settings
212
+ (`GIT_CONFIG_GLOBAL=/dev/null`).
213
+
214
+ **What the package promises is down to fact.** The shipped-file list named four paths the repository
215
+ does not have (`dist/`, `templates/`, `LICENSE`): today it promises only what exists —
216
+ `templates/` came back into the list together with its files, not before them —
217
+ while `pnpm run pack:check` checks it from both sides, that the list names nothing absent and that the
218
+ tarball carries nothing the list does not promise. Taking both references works again (`pnpm run
219
+ parity`, `pnpm run fixture`) and no longer depends either on whether the consumer project keeps a copy
220
+ of the tool or on the machine's git settings. Two texts that promised the same were fixed as well: the
221
+ `--init` hint (it said the checks travel with the package, while the suite is not part of it) and the
222
+ default `fixCommand` (it named a package that does not exist, `npx size-table --write`).
223
+
224
+ **The checks run themselves** (`.github/workflows/ci.yml`). On every push and every pull request one
225
+ job `verify` calls **one command** — `pnpm run verify`; the list of steps lives in one place
226
+ (`tools/gates/run.js`) and matches the local one, so a check that is not in a profile cannot be in CI
227
+ (`test/gates-verify.test.js` watches that). The profile, in order: the strict linter, the bloat
228
+ sensors, the whole suite, parity with the history of the consumer project, reproducibility of both
229
+ references and the work from the assembled tarball. Two steps are dearer and live in the slow profile
230
+ instead the same suite in an environment with none of the machine's git settings
231
+ (`GIT_CONFIG_GLOBAL=/dev/null`) and coverage under c8: `pnpm run verify:slow`,
232
+ `.github/workflows/verify-slow.yml` on a schedule. The job needs no secrets: the consumer's history
233
+ lies in the repository as a bundle at the revision recorded in the reference (`fixtures/live/`), and a
234
+ re-take goes into a temporary directory and is compared with what is committed, so the working tree
235
+ stays clean. The job pins Node 22 and the actions by commit SHA, and there is deliberately no matrix
236
+ over Node versions: this pass is about control.
237
+
238
+ **A release is a tag** (`.github/workflows/release.yml`). Pushing `v<version>` runs the strict linter
239
+ and the whole suite, checks the work from the assembled package, compares the manifest version with the
240
+ tag and sends the package to the registry — no secret and no code from an authenticator: publishing
241
+ goes by the attestation GitHub Actions issues for that job (trusted publishing), which npm accepts
242
+ instead of a token. A prerelease goes to `next` rather than `latest`, so a draft is not what a default
243
+ install picks up. The publisher is set up once and lives on npmjs.com, not in the repository:
244
+ `npm trust github @vernikr/size-report --file release.yml --repo vernikr/size-report
245
+ --allow-publish` (the same is the Trusted Publisher button in the package's settings), and
246
+ `npm trust list @vernikr/size-report` shows whether the link is there. The job raises no version: a
247
+ person names it in the manifest, and it is compared with the tag rather than
248
+ derived from it.
249
+
250
+ One trap cost an edit of its own, and it is about `setup-node` rather than this package: with
251
+ `registry-url` the action writes `_authToken=${NODE_AUTH_TOKEN}` into `.npmrc`, npm then considers
252
+ credentials given and does **not** go for the OIDC attestation — publishing fails 404 with a correctly
253
+ set-up publisher. So `registry-url` is not given here: npmjs.org is the default registry anyway, and
254
+ `publishConfig` in the manifest carries `access: public` only. A draft run from Actions ("Run
255
+ workflow": nothing is published by default) goes the whole list up to the publishing step itself — the
256
+ strict linter and the whole suite, the work from the tarball, and a package built on a draft version
257
+ above the manifest's own, so that the registry does not refuse an already released number. Whether the
258
+ publisher is set up a draft run does not show: `--dry-run` exchanges no attestation and passes without
259
+ any credentials at all only a real tag tells the truth about that.
260
+
261
+ **Two rules came out of the first live runs of CI, and both are about the border of a call.** Process
262
+ output is collected by the harness rather than glued into a string: a multi-byte character torn at a
263
+ chunk border would turn into two replacement characters, and where those chunks fall is the kernel's
264
+ business a local run does not show it (`test/runner.test.js`). And of the references only what this
265
+ repository writes by itself is compared byte for byte: a history bundle is packed by git, whose bytes
266
+ depend on its version, so the bundle is compared by content — the branches, the tip and the number of
267
+ commits, that is, what makes it a replacement for the consumer project. The bundle also has to carry
268
+ `HEAD` and the branch `main` at the reference revision, or a clone decides on its own which branch to
269
+ lay out (`tools/check-standards.js`).
270
+
271
+ **The report page looks and behaves like a tool.** One set of table styles serves both outputs
272
+ (`src/table.css`), so the page took over the sticky header and commit column the static artifact
273
+ already had, and its commit column narrows in a narrow window instead of pushing the table sideways.
274
+ The page says so in words when there is nothing to assemble a table from — every metric or every file
275
+ switched off and its switches are labels around inputs, so a mouse, `Space` and assistive technology
276
+ all reach them. The colour of a delta is defined once: growth green, fall red — changing it is two
277
+ lines in `src/table.css` plus re-taking the artifact's reference, and no other place holds a colour.
278
+ The one place where the page departs from the shared geometry is its "adaptations" section, and every
279
+ departure stands there with its reason: the shared part is frozen by the artifact's bytes (`src/css.js`).
280
+
281
+ **The left panel is the project's file tree.** It is built from the catalogue — every path git sees —
282
+ rather than from the columns, so it also shows what did not make it into the report: such a leaf, and a
283
+ folder with nothing to measure in it, keeps its place with the checkbox off and unavailable, and the
284
+ tooltip names the reason the package's rule or the project's choice. Off rather than absent: the
285
+ rows stay even (the eye compares like with like) while unavailability says this is not the reader's
286
+ choice. The report itself is always in the catalogue, whether or not it is tracked: that is a property
287
+ of the moment, and the page must not depend on it, or the first rebuild in a fresh clone would give
288
+ different bytes. A folder whose files are only partly in the report writes its count as a fraction
289
+ ("2/5"), and everything outside the report stands after everything inside itfolders and leaves
290
+ alike so that the report is seen at once in a list where half the rows do not switch
291
+ (`test/page-tree.test.js`).
292
+
293
+ **A folder is a switch like a file, and its sign is a decision of its own.** The checkbox of a folder
294
+ carries its whole subtree and shows three states every file on, some, none with the number of files
295
+ next to it. Neither a folder nor a category button keeps state of its own: both flip the same file
296
+ checkboxes, so the tree, the buttons and the table cannot drift apart. The sign beside a folder answers
297
+ a different question how much of the tree is visible, which is the onlooker's business rather than the
298
+ reader's choiceso it is remembered between visits in a record of its own, under a key of its own and
299
+ the same report passport, and it never goes into the link; unfolding every folder removes that record,
300
+ just as turning the checkboxes back on removes the choice.
301
+
302
+ **The list scrolls, and there is one scroll.** On a narrow window it is the file list that scrolls —
303
+ the panel grows with the page there while on a wide one the whole panel does: otherwise the controls
304
+ would push the table off the screen. Folders fold because the tree is longer than the window; otherwise
305
+ its middle is out of reach.
306
+
307
+ **Folding is pure view, and it counts no numbers.** The subtree lies in the markup and a class on the
308
+ row hides it, so a click on the sign changes exactly the three things the reader sees — the class, the
309
+ sign and the note in the memory. What guards this is that after folding the table is the same markup
310
+ rather than a rebuilt one (`test/page-tree.test.js`).
311
+
312
+ **A click shows and hides rather than builds.** The table is assembled once, with every column of every
313
+ file, and a switch afterwards changes only what is visible: a metric is one class on the table plus the
314
+ `colSpan` of the group headings, a file's column is a class per node of it, and a folder or a category is
315
+ the same for each file of its subtree. The table's own nodes stay the objects the first drawing made:
316
+ `test/page-view.test.js` counts what a click appends (a metric: nothing at all; a file, a folder or a
317
+ category: at most the cells of the totals, rows × metrics) and checks that the rows and the cells are
318
+ still the very same objects. What makes this possible is that the order of the columns depends on the
319
+ files rather than on the choice — the last commit's first, then the settings' order — so a hidden column
320
+ keeps its place and the visible ones do not move. The totals are the only numbers a choice changes, and
321
+ they are carried rather than recounted: a sum is linear, so a file switched off subtracts exactly its own
322
+ values, which costs its own rows instead of rows × files. That arithmetic is the step's one new piece,
323
+ and it is held against the engine's own `rowModel` cell by cell for a mixed choice, so that counting a
324
+ row stays in one place (`test/page-view.test.js`).
325
+
326
+ **The columns the last commit touched come first.** The report is rebuilt after every commit, and a
327
+ reader's first question is what that edit brought. The mark comes from the history rather than from the
328
+ numbers an edit that changed no size is an edit too — and it is taken from the last commit that
329
+ touched at least one column, counting back from the top: a commit that went past the columns, above all
330
+ the report itself, which the hook commits, is skipped, or the mark would depend on the report's own
331
+ commit, the same run would give different bytes and the hook would commit the report a second time.
332
+ Inside each part the order stays as it comes from the settings (the sort is stable): the order of the
333
+ columns is what the reader is used to, and his choice of files does not rearrange it
334
+ (`test/page-view.test.js`, the contract's `last` field).
335
+
336
+ **On a wide window the panel stands to the left of the table and takes no room from the numbers** (from
337
+ 900px, `src/page/app.css`). That is not decoration: a desktop has much side room and little vertical
338
+ room, so the switches, the tree and the numbers are visible at once, and neither scrolling the numbers
339
+ nor scrolling the tree takes the controls off the top of the screen. The layout is a grid on `body`
340
+ rather than a wrapper in the markup — the page is assembled by pasting chapters, and the page's shape
341
+ should live in one place. The grid has five rows, named by subject (the heading, the message about a
342
+ link, the working row, the empty state, the note), and only the working row stretches: the table takes
343
+ all the remaining height and the panel no more than that, scrolling inside itself rather than pushing
344
+ the table off the screen. The narrow half starts at 899px rather than at 900px, so that at exactly
345
+ 900px the two halves cannot apply to one page they once did, and the table's height ceiling survived
346
+ from the narrow one, leaving empty space under the table at that single window size. What guards the
347
+ numbers behind the layout is the contract rather than the markup: switching a folder off removes exactly
348
+ its columns and exactly its volume from the total (`test/contract.test.js`).
349
+
350
+ **A checkbox takes away neither the numbers' room nor the reader's place in the list.** The panel is
351
+ built once and a switch writes only the fields it reached, so the reader's place is his still: there is
352
+ no rebuild that could lose the scroll of the panel or of the list, and the field under the keyboard keeps
353
+ its focus without being found again by hand (`test/page-tree.test.js`) with a rebuild every switch with
354
+ `Tab` and `Space` would mean walking the panel from the start. The file list has no ceiling of its own in a wide window: the panel
355
+ scrolls, and the list does not push the table. The row of categories sticks to the top of the panel,
356
+ with the panel's own background (or passing rows of the list would read through it), and the panel's own
357
+ top padding lives on its first field, which travels away with it. File captions use the table's font size
358
+ (12.5px), and the legend under the tree is gone on purpose: below the list it pushed the numbers away,
359
+ while what it explained already stands next to the thing it explains — the sign of a number names the
360
+ colour of a delta, the way each number was counted stands under the metric switches, and the mark of a gap
361
+ lives in the cell's own text.
362
+
363
+ **The panel remembers the reader's choice.** The record lives in the browser's memory, tied to the
364
+ report's passport the tool's name, the data schema, the artifact's path, the title and the column
365
+ labels, hashed into the record's key — so reports in one browser do not see each other's choice (all
366
+ `file://` pages share one memory, so this is no trifle). Inside the record the choice is held by names —
367
+ a file by its path, a metric by its key — and only what is switched off: a column pointed at another path
368
+ or a metric dropped from the settings simply matches nothing, what appeared stays switched on, and
369
+ turning everything back on returns the page to its default and removes the record. The first reader —
370
+ and a reader whose record is broken or outdated gets exactly the default, and the choice affects
371
+ neither the numbers nor the markup. The passport holds neither the tool's version nor the top of the
372
+ history, and on purpose: updating the tool does not change what a column means, while a grown history is
373
+ the very history the reader comes back to.
374
+
375
+ **The same choice travels as a link.** The page's address is the link: the record that goes into the
376
+ browser's memory goes into the anchor too (`#size-report=…`), so the sender copies the address and the
377
+ recipient sees that choice with no action at all. The link outranks the memory it is the sender's
378
+ explicit choice while it does not replace the reader's own until he changes something. A foreign or
379
+ broken address is not applied, and is not silent either: a line above the table names the reason ("the
380
+ link was made in another report" / "the choice in the address is unreadable"), the view stays the
381
+ reader's own, and the incoming address is not rewritten; names the report does not hold are reported by
382
+ count, skipped, and the rest is applied. The link works on an already open page as well: the browser does
383
+ not reload the document when the anchor changes, so the page reads the address itself, or a link would
384
+ only work in a new tab. The page makes no request to the network at all, and that is an assertion of a
385
+ check rather than a promise (`test/page-view.test.js`, `test/parity.test.js`).
386
+
387
+ **The `min` metric can count for real.** The way of counting is chosen in the settings:
388
+ `"minify": {"engine": "esbuild"}` minifies JS/TS/CSS for real through an optional dependency, while
389
+ `"engine": "strip"` is the earlier removal of comments and indentation. The default did not change,
390
+ because both frozen references were taken under it. Measured on the fixture: real minification is
391
+ smaller than stripping in **44 cells and never larger**; `src/code.js` **276 → 185 B**, `src/style.css`
392
+ **55 43 B**, and over the fixture's history **−1 372 B**. JSON is minified by parsing (parsing loses
393
+ only insignificant whitespace, and nobody would make it shorter), while the formats the minifier does not
394
+ take are named in the metric's caption **by extension**: `esbuild (minify, rename); other formats
395
+ (.md .toml) lose comments and indentation`. The split of numbers into exact and approximate is gone from
396
+ the package: how a column was counted is told once, in the metric's method, and no cell carries a mark of
397
+ it any more. The list comes from the columns rather than from the name of the method (`otherCountFormats`
398
+ in `src/metrics.js`), so a report made only of formats the minifier takes says nothing about other
399
+ formats. With no minifier
400
+ (an installation without optional dependencies, a platform without it) the metric falls back to
401
+ stripping, the method says so in words and the run answers **code 4** rather than staying silent, while
402
+ the numbers are the same as the earlier way of counting — byte for byte with the reference. The derived
403
+ profile leads new projects straight to minification (`--init` pins the same), its hint names that price,
404
+ and the report itself stays out of the columns there: a column that is the table is refused by the
405
+ settings check. A file the minifier could not parse (markup in `.js`, syntax it does not know) is a
406
+ refusal with code 2 whose text names the file, the minifier and its own cause, and whose advice gives a
407
+ ready way out assign simplification to that extension.
408
+
409
+ **The `tok` metric counts tokens with a real dictionary.** Tokens are the report's third measure: what a
410
+ file weighs for a language model. The dictionary is chosen in the settings
411
+ (`"tokens": {"family": "openai", "encoding": "o200k_base"}`), and the encoding is part of the number
412
+ rather than a detail: on the fixture `src/code.js` is **168 tokens** under `o200k_base` and **196** under
413
+ `cl100k_base`, which is why the encoding is named next to the family and the metric's method quotes
414
+ exactly the one that produced the number. Tokens are neither bytes nor minification, and the difference
415
+ is shown rather than smoothed over: the same cell is **735 B** `raw`, **276 B** stripped, **185 B** really
416
+ minified and **168** tokens, while bytes per token differ between files by **2.5 times** (from 2.56 in
417
+ `package.json` to 6.30 in `crlf.txt`) that is, the text is counted rather than a ratio. The family is
418
+ single in this version, `openai`: the others have no dictionary that could be called their own, and
419
+ counting with someone else's while calling that a family would promise what does not exist. There is no
420
+ dictionary switch on the page, and on purpose: the page gets ready numbers and counts nothing itself,
421
+ and it has nothing to count tokens with. Counting every family on every run would pay time for numbers
422
+ the reader may never ask about, so the choice of family and encoding lives where it costs time — in the
423
+ run's settings — while the page **names** it: the method of each metric stands under the switches as
424
+ text rather than only in a tooltip. Formats without text (a picture, a font, an archive) are named in
425
+ the metric's caption by extension: their number goes by bytes rather than text. With no dictionary (an installation without optional dependencies, a platform without it)
426
+ the count is an estimate by length with the coefficient named in the method, and the run answers **code
427
+ 4**; the other metrics stay what they were in a report without tokens, and that seam is checked in an
428
+ environment with no optional dependencies at all (`SIZE_REPORT_NO_OPTIONAL`). Counting tokens costs a
429
+ run time, and that is the honest price of the dictionary rather than of parsing: its tables are read
430
+ once per process while the counting is per file and per row, so the price grows with the history and not
431
+ with the dictionary. The derived profile leads new projects straight to tokens.
432
+
433
+ **Two conveniences of the command line are guarantees rather than accidents:** `--help` answers
434
+ wherever it is asked, and `--write` creates the report's directory when it is missing.
435
+
436
+ **Two commands answer about the history: completeness and explanation.** `size check` answers whether
437
+ everything in the history got into the report: every path the history touched has to be a column or a
438
+ declared exception, and a path that is neither is a violation code 1, the path, the commit that
439
+ introduced it and a ready fix. The same answer carries the summary of dropped commits how many and
440
+ why and their shas, that is, how much of the history is covered. Coverage is counted over the facts of
441
+ the history the union of the changed paths of every commit — rather than over the file list in the
442
+ tree: a file created and deleted before HEAD is invisible there while the history remembers it, and its
443
+ edits went into no number at all. What the tool does not claim is said in the same place: not that the
444
+ project picked the "right" columns, only that nothing went past them, and what exactly did not fit.
445
+ `size explain <commit>` answers about one commit named by a revision (`HEAD`, a branch, a tag,
446
+ `HEAD~1`), by a sha or by its beginning saying whether there is a row (and which) or why there is
447
+ none: only the report itself was touched, the numbers did not move although column files were touched,
448
+ no file of the commit is tracked as a column, or the commit is a merge and merges are hidden by
449
+ `rows.merges`. Both take the reason from the same run the reports come from, and the evidence from the
450
+ commit's list of changed paths: what the history does not hold, the answer is silent about instead of
451
+ guessing. Completeness comes from the requirements, and it also replaces the "artifact ↔ history"
452
+ control: the report need not be kept in git. The meaning of `skip` has not changed, but its **reach has
453
+ widened**: it is not only "paths that cannot be columns" but also the declared exceptions of
454
+ completeness one and the same list, and forging a second tool for it was not necessary. The price is
455
+ named: `check` costs a pass over the history, like any report. Next to them stands `size doctor`, the
456
+ diagnostics in one answer.
457
+
458
+ **The report updates itself.** `size install-hook` installs two hooks, `post-commit` and `post-merge`
459
+ (`post-commit` does not run for a merge at all, which is why one file is not enough), and after every
460
+ commit and merge the report is rebuilt, while the copy lying in git lands as **a commit of its own**:
461
+ the manual step "code, then the table" is gone. The report's commit is assembled with git's plumbing
462
+ (`commit-tree`), so neither the index nor someone else's uncommitted work can get into it, and a loop is
463
+ impossible by construction rather than through an environment flag. A refusal of the tool does not bring
464
+ the commit down: the cause is printed as one line and remembered — `size doctor` shows it.
465
+
466
+ The move, the refinement and the packaging are laid out step by step in `plans/archive/PLAN.md`, with
467
+ acceptance for each.
468
+
469
+ ## What is in the repository
470
+
471
+ | File | Role |
557
472
  |---|---|
558
- | `PLAN.md` | **Главный документ:** инвентаризация, границы, инварианты, архитектура, семь шагов переноса, приёмка, риски, открытые вопросы |
559
- | `docs/requirements.md` | Требования заказчика: что и зачем |
560
- | `docs/module-design.md` | Архитектурный проект выноса: как устроен модуль |
561
- | `docs/size-report.html` | Отчёт об объёме этого самого проекта: один самодостаточный файл, который обновляет хук после каждого коммита (отдельным коммитом) |
562
- | `WORKLOG.md` | Журнал запросов и сделанного |
563
- | `BLOCKERS.md` | Открытые блокеры и известные пробелы (обход обязан держаться проверкой) |
564
- | `REFACTOR.md` | Поканальный план чистки: объём кода, потом скорость; границы и чем доказывается, что поведение не изменилось |
565
- | `CHANGELOG.md` | История выпусков и, у каждого выпуска, раздел «Что изменится в числах»: у кого числа поедут и почему |
566
- | `tools/parity-freeze.js` | Снимает эталон паритета (`pnpm run parity`): замороженной копией, на ревизии проекта из манифеста `--json`, конфиг, хеш артефакта, хеш инструмента |
567
- | `tools/make-fixture.js` | Собирает синтетическую фикстуру (`pnpm run fixture`): детерминированную историю с ловушками плюс эталонные числа |
568
- | `tools/synthetic/` | Сюжеты той сборки по предметам: `repo.js` как говорим с git (закреплённые время, автор, настройки), `content.js` что лежит в файлах, `history.js` какие коммиты из этого получаются, `note.js` записка к фикстуре со списком ловушек |
569
- | `tools/parity-live.js` | Сверяет движок с живым проектом на клоне: числа и самодостаточный отчёт по пути из настроек потребителя (`pnpm run parity:live`) |
570
- | `tools/pack-check.js` | Собирает тарболл и проверяет, что из него всё работает: все исходники доехали, числа и отчёткак из репозитория (`pnpm run pack:check`) |
571
- | `tools/check-standards.js` | Проверяет, что оба эталона воспроизводятся: пересъём идёт в никуда и сверяется с закоммиченным (наши файлы побайтово, бандлпо содержимому) и что бандл живой истории несёт `HEAD` (`pnpm run check:standards`) |
572
- | `.github/workflows/ci.yml` | CI: работа `verify` на каждый пуш и запрос правки зовёт `pnpm run verify` тот же профиль, что локально; действия закреплены по SHA коммита |
573
- | `.github/workflows/verify-slow.yml` | Slow-профиль по расписанию: то же плюс покрытие под c8 дорогое не в каждом прогоне |
574
- | `tools/gates/run.js` | Профили проверок единственный список шагов: `fast` (каждая правка), `full` (перед отправкой и в CI), `slow` (+ покрытие); `--list` печатает команды |
575
- | `tools/gates/metrics.js` | Датчик раздувания: правила размера и сложности, вес проверок, пометки долгас храповиком подавлений ESLint (`.eslint-suppressions.json`) |
576
- | `tools/gates/dup.js` | Датчик дублей: отпечатки клонов по содержимому (`dup-baseline.json`), взгляд против файла базы и против дерева `origin/main` |
577
- | `tools/gates/deps.js` | Датчик связей: циклы, сироты, направление слоёв и неразрешимые импорты (`dependency-cruiser`) |
578
- | `tools/gates/coverage.js` | Датчик покрытия: храповик по файлам против `coverage-baseline.json`, а не процент по репозиторию |
579
- | `tools/gates/gatefiles.js` | Защита гейт-файлов: правка порогов, баз и обвязки без трейлера `Gate-Change:` красный (хук `commit-msg` и CI по диапазону) |
580
- | `tools/gates/common.js`, `tools/gate-probe.js` | Общее у датчиков (корень, разбор ключей, отчёты) и обвязка их проб: датчик зовётся командой, а не импортом |
581
- | `.githooks/commit-msg`, `.githooks/pre-commit`, `.githooks/pre-push` | Хуки: защита гейт-файлов, быстрый профиль на правку и перед отправкой; ставятся `pnpm run hooks:install` (свой менеджер хуков не заводится) |
582
- | `.githooks/post-commit` | Обновление отчёта после коммита: зов установленной копии пакета (строка вписана человекоминструмент чужие каталоги хуков не правит) |
583
- | `eslint.metrics.config.js`, `.eslint-suppressions.json` | Правила датчика раздувания и его база: пороги из замеров, всё, что выше, в базе и разбирается постепенно |
584
- | `.jscpd.json`, `dup-baseline.json` | Настройки и база датчика дублей: отпечаток считается по содержимому клона, поэтому база переносима |
585
- | `.dependency-cruiser.cjs`, `.c8rc.json`, `coverage-baseline.json` | Правила графа связей, настройки снятия покрытия и его база по файлам |
586
- | `AGENTS.md` | Короткая инструкция агенту репозитория: что запускать, что делать при красном, что нельзя менять |
587
- | `.github/workflows/release.yml` | Выпуск по тегу: тот же полный набор, сверка версии манифеста с тегом и публикация в реестр по удостоверению GitHub Actions без секрета и без кода из аутентификатора |
588
- | `templates/` | То, что проект берёт как есть: `size-report.config.json` (черновик настроек), `ci.yml` (описание проверки) и `README.md` (куда что кладётся и что в них менять); едут в поставке и стерегутся `pack:check` и `test/templates.test.js` |
589
- | `fixtures/parity/` | Эталон с `safe-resets` на коммите `bd6ef9d`: 95 строк × 27 колонок. Копия реализации, которой он снят, в дереве не лежит её байты живут в истории и берутся оттуда по требованию (`REFACTOR.md` R-1.5) |
590
- | `fixtures/synthetic/` | Бандл фикстуры на 16 коммитов, её конфиг, эталонные числа (`--json` прежней копии) и хеш её артефакта прежней формызапись того, с чем сверялся перенос |
591
- | `fixtures/live/history.bundle`, `fixtures/live/README.md` | История проекта-потребителя на ревизии эталона `bd6ef9d` и записка о том, какую ревизию бандл несёт и почему он лежит в репозитории: живая сверка работает без доступа к приватному проекту |
592
- | `bin/size.js` | Команда `size`: то, что ставит пакет (`package.json` `bin`); сама ничего не считает, только зовёт точку входа |
593
- | `LICENSE` | MIT: условия лицензии едут в поставке вместе с пакетом |
594
- | `.gitignore`, `pnpm-lock.yaml` | Что в репозиторий не идёт; lock-файл pnpm, а версия менеджера в поле `packageManager` (оттуда её берёт CI) |
595
- | `src/size-table.js` | Точка входа пакета: только реэкспорт публичного API (55 имён), ни одного расчёта |
596
- | `src/derived.js` | Общий расчёт отчёта: итоги, дельты, клетка, подпись коммита — один на движок и программу страницы |
597
- | `src/css.js` | Чтение оформления с диска: какие наборы стилей есть и какая у них роль |
598
- | `src/table.css` | Таблица отчёта: геометрия клеток, липкие шапка и колонка, цвет дельт |
599
- | `src/page/app.css` | Оформление страницы сверх общей части: панель с деревом файлов и липкой строкой категорий (на широком экране колонка слева, страница в окно), состояния пустоты, узкое окно |
600
- | `src/page/state.js` | Состояние страницы: данные отчёта, вид галочек, указатель «какой путь какая колонка», сложенные папки, паспорт записи, память браузера и обмен ссылкой — глава программы страницы |
601
- | `src/page/dom.js` | Узлы страницы: мелкие помощники разметки (`appEl`, `appBox`) одни на панель и таблицу |
602
- | `src/page/panel.js` | Панель выбора: галочки метрик и файлов, категории, дерево путей проекта (файлы вне отчёта снятой галочкой с причиной, после остальных; папки со знаком складывания, который прячет поддерево классом, а не пересборкой); перерисовку просит у главы сборки |
603
- | `src/page/table.js` | Таблица страницы: клетка, подпись коммита, шапка и состояния пустотыразметка поверх общего расчёта |
604
- | `src/page/app.js` | Сборка и запуск страницы: таблица целиком, перерисовка по выбору читателя возвратом фокуса и прокрутки), первая отрисовка и смена якоря; вклеивается в собранную страницу
605
- | `src/page/build.js` | Сборка страницы: данные, оформление и программа в одном файле без внешних ссылок |
606
- | `src/git.js` | Единственная граница вызова git: закрепления настроек, блобы пачкой, история, сверка с диском |
607
- | `src/strip.js` | Снятие балласта: какая форма к какому файлу (расширение, стратегия) и что считать точным числомвход разбора форм |
608
- | `src/strip/js.js` | Снятие комментариев и отступов в JS: проход по случаям (комментарий, регексп, строка, символ) строки и шаблоны насквозь |
609
- | `src/strip/forms.js` | Формы текста со своим снятием балласта: разметка, стили, строки файла и JSON |
610
- | `src/strip/guard.js` | Гард стриппера: снятое обязано компилироваться скриптом в процессе или модулем в рабочем потоке |
611
- | `src/parse.js` | Разбор модуля: рабочий поток на прогон и отступление к `node --check`, способ разбора последнего модуля |
612
- | `src/parse-worker.js` | Сам разбор внутри потока: разбирает текст без исполнения, сообщает, что модулей vm в Node нет |
613
- | `src/metrics.js` | Реестр метрик: что измеряется, нужен ли текст и насколько честна цифра; описание метрики для читателя в одном месте |
614
- | `src/minify.js` | Настоящий минификатор: необязательная зависимость, загружается один раз и не роняет прогон, если её нет |
615
- | `src/tokens.js` | Токены: словарь по семейству и кодировке, оценка по длине как запасной счёт, форматы без текста |
616
- | `src/optional.js` | Общее устройство необязательных зависимостей (минификатор и словарь): ленивая загрузка, версия пакета, шов отсутствия |
617
- | `src/history.js` | Обход истории: измерение по коммитам, сдвиг чисел, сборка, сверка с деревом, знак «какой колонки коснулся последний коммит» и причина пропуска у каждого выпавшего коммита |
618
- | `src/check.js` | Полнота покрытия (`size check`): настройки, история, пути, датчики что прошло мимо колонок и чем это чинится |
619
- | `src/explain.js` | Объяснение пропущенной строки (`size explain <коммит>`): причина, улики и готовая починка |
620
- | `src/doctor.js` | Диагностика одним ответом (`size doctor`): окружение, зависимости, настройки, покрытие, состояние хука сборкой из существующих кусков |
621
- | `src/hook.js` | Хуки автообновления: постановка сама (`autoInstall` из входа и `bin/postinstall.js`), снятие командой, коммит только отчёта, замок и запись о запуске |
622
- | `bin/postinstall.js` | Установка хука после постановки пакета: ищет проект-потребитель и молчит, если поставить негде |
623
- | `src/artifact.js` | Отчёт на диске: единственное место, где он превращается в файл (им пользуются и `--write`, и хук); отчёт — самодостаточная страница |
624
- | `src/journal.js` | Журнал и ссылки: к какому разделу относится коммит и куда ведёт описание |
625
- | `src/data.js` | Категории файлов и контракт со страницей (`--data`): числа, устройство таблицы и каталог путей проекта |
626
- | `src/config.js` | Настройки проекта-потребителя: умолчания, чтение, проверка |
627
- | `src/project.js` | Настройки, выведенные из самого проекта (дерево и история): колонки, журнал, исключения, каталог путей для дерева страницы. Без файла настроек он и есть настройки; `--init` закрепляет его файлом |
628
- | `src/locales.js`, `src/refusal.js`, `src/tool.js` | Тексты отчёта; коды выхода и справка; имя и версия пакета |
629
- | `src/cli.js` | Вход инструмента: разбор строки, чтение проекта и доставка запроса режиму; главный файл пакета |
630
- | `src/args.js` | Грамматика командной строки: режимы, ключи и команды плюс проверки их сочетаний — отказ называет виновника и готовую команду |
631
- | `src/modes.js` | Режимы: собрать отчёт, сверить его с историей, отдать данные, полноту покрытия и диагностику |
632
- | `src/init.js` | Закрепление настроек файлом (`--init`): то, что проект вывел о себе сам, ложится файлом и проходит ту же проверку, что первый запуск |
633
- | `test/api.test.js` | Публичный API пакета: список имён заморожен, разбиение не имеет права его менять |
634
- | `eslint.config.js` | Правила оформления: те же, что у проекта-потребителя, плюс запрет склейки операторов в строке (`pnpm run lint`, `pnpm run lint:strict`) |
635
- | `tools/harness.js` | Обвязка проверок: пути, клоны фикстуры том числе общий на набор и с CRLF), запуск инструмента, разбор отказов, хеши |
636
- | `tools/page-harness.js` | Обвязка проверок контракта и страницы: данные контракта, собранная страница, чтение её в настоящем DOM, переключатели панелиодна на четыре набора |
637
- | `tools/suites.js` | Разделение набора: какие файлы идут в быстрый прогон причиной для каждого), почему каждый дорогой в полном |
638
- | `tools/run-tests.js` | Прогон набора (`pnpm test`, `pnpm test:all`, `pnpm run suites:measure`): длительность каждого файла своим замером и сверка числа проверок |
639
- | `tools/docs-facts.js` | Чтение фактов из документации один слой на четыре проверки сторожа: что документ называет (пути, зовы, адреса разделов) против того, что есть в репозитории |
640
- | `tools/yaml.js` | Разбор подмножества YAML один разборщик на два сторожа описаний (`templates/ci.yml` и `.github/workflows/release.yml`): вне подмножества ошибка, а не молча пропущенная строка, включая двоеточие с пробелом в незакавыченном значении (именно оно делало описание выпуска неразбираемым, пока проверка искала подстроки) |
641
- | `tools/refusals.js` | Каталог отказов: по строке на каждый причина, код выхода, обязательные фразы вывода, **что отказ советует** (`advice`: `run` команда, `template` — форма с подстановкой, `manual` — действие человека с причиной, `coveredBy` — отдан другой проверке), а для непроверяемого почему; карты мест отказа (`SITES`, `PRINTED`) держат числа, чтобы новый отказ не появился молча, а маркеры совета — чтобы не появился молча новый совет |
642
- | `test/parity.test.js` | Паритет движка с эталоном: числа, самодостаточность отчёта, локаль |
643
- | `test/frozen.test.js` | Замороженная копия: та ли это ревизия, с которой снят эталон, и воспроизводит ли она его |
644
- | `test/environment.test.js` | Герметичность: вывод не зависит от настроек git машины и локали |
645
- | `test/crlf.test.js` | Выкладка с CRLF (`core.autocrlf`) не мешает сверке |
646
- | `test/disk.test.js` | Сверка с рабочим деревом: правка только на диске, три вида потери (правка, создание, удаление — все мутацией), файл, удалённый до HEAD, и переименование внутри псевдонимов — не потеря (`BLOCKERS.md` §B3, §N8) |
647
- | `test/cli.test.js`, `test/cli-paths.test.js` | Отказы командной строки: справка, настройки, коды выхода и куда инструмент пишет |
648
- | `test/refusals.test.js` | Отказы исполняются: каждый вызван прогоном, сверены код выхода и обещанные фразы (свои клоны для чужого хука, обрезанной истории и ветки мимо отчёта), и **совет выполняется** — команда даёт обещанный код, не падает стеком, а где объявлено «отказ ушёл», тот же зов после неё отвечает другим |
649
- | `test/refusals-catalog.test.js` | Сторож каталога отказов: у каждого места отказа в исходниках есть пункт, у каждого пункта — объявленный совет, а отказы, отданные другой проверке, ею в самом деле утверждаются (названные файл и строка проверяются) |
650
- | `test/contract-data.test.js` | Контракт данных: числа против эталона, состав полей против производных, пометки приближения против подписи метрики |
651
- | `test/contract-derived.test.js` | Производные против чисел артефакта: итоги строки, дельты клетки и дельта итогана коде, который лежит в дереве |
652
- | `test/page-view.test.js` | Собранная страница: вклейка без копий расчёта, самодостаточность, состояния пустоты, оформление и переключатели |
653
- | `test/page-tree.test.js` | Дерево файлов панели: папки по путям проекта, три состояния, поддерево, файлы и папки вне отчёта (снятая галочка и место после остальных), складывание без пересборки и прокрутка при пересборке |
654
- | `test/page-choice.test.js` | Память выбора и обмен ссылкой: перезаход, чужой отчёт, чужая и битая запись, смена адреса на открытой странице |
655
- | `test/module.test.js` | Модуль в расширении `.js`: измеряется без правок настроек; гард стриппера жив (доказано мутацией) и не обвиняет невиновного |
656
- | `test/guard.test.js` | Разбор модуля: идёт потоком, оба пути дают один вердикт, отступление работает без файла потока, сотни разборов дешевле запуска |
657
- | `test/runner.test.js` | Чтение вывода процесса: куски склеиваются буферами, а не приклеиваются к строке многобайтовый символ на границе кусков не превращается в два символа-заменителя |
658
- | `test/git-pins.test.js` | Сторож границы git: прямых вызовов git без общего списка закреплений нет, и незакреплённое чтение показывается свидетелем (путь кавычками) |
659
- | `test/docs-paths.test.js`, `test/docs-commands.test.js`, `test/docs-numbers.test.js`, `test/docs-pin.test.js` | Сторож документации, по файлу на обещание: пути и таблица файлов; зовы, причины отказа и адреса разделов; числа проверок; пин в примере установки |
660
- | `test/changelog.test.js` | Сторож выпуска: версия в `CHANGELOG.md` версия манифеста, а таблица «что изменится в числах» это замер на фикстуре, сверенный с живым прогоном |
661
- | `test/release.test.js` | Сторож выпуска из CI: он начинается тегом, версия берётся из манифеста, секрета и одноразового кода не требует, prerelease не уезжает в `latest`, перед публикацией идёт полный набор и подсказка на npmjs.com называет этот же файл |
662
- | `test/suites.test.js` | Сторож разделения набора: полнота классификации (быстрый явно, полныйс причиной), причина у каждого файла, что быстрый прогон остаётся частью набора |
663
- | `test/gates-metrics.test.js`, `test/gates-dup.test.js`, `test/gates-deps.test.js`, `test/gates-coverage.test.js`, `test/gates-files.test.js` | Пробы датчиков: искусственное нарушение датчик красный, снятие снова зелёный; прогон зовёт датчик командой, а не импортом, поэтому доказывает и код возврата |
664
- | `test/gates-verify.test.js` | Сторож единственного списка: команды профиля против рабочих процессов, хуков и `templates/ci.yml` проверки, которой нет в профиле, в CI быть не может |
665
- | `test/check.test.js` | Полнота и объяснение на настоящих коммитах фикстуры: непокрытый путь, «только отчёт», «число не сдвинулось», «мимо колонок», слияниеи что починка настроек не двигает числа |
666
- | `test/doctor.test.js` | Диагностика на пяти состояниях проекта: без настроек (2), полное покрытие (0), неполное (1), обрезанная история (3), нет датчика (4) и блок покрытия равен ответу `size check`, а не считается вторым разом |
667
- | `test/hook.test.js` | Хуки на свежем клоне: ставятся только командой, дают отдельный коммит отчёта том числе после слияния), повторный запуск молчит, чужая работа и индекс не тронуты, в CI и при отказе инструмента ничего не делают, снятие возвращает проект к прежнему |
668
- | `test/templates.test.js` | Шаблоны: черновик настроек проходит проверку инструмента и собирает настоящий отчёт; описание проверки разбирается и зовёт только существующие команды и ключи |
669
- | `test/minify.test.js`, `test/tokens.test.js` | Настоящее сжатие и токены: числа против упрощения, кодировка как часть числа, честность подписи, работа без необязательной зависимости (код 4) и шов `SIZE_REPORT_NO_OPTIONAL` |
670
- | `package.json` | Манифест пакета: имя `@vernikr/size-report`, версия `1.2.0`, список поставкитолько существующее |
671
-
672
- Оба каталога эталонов снимаются заново теми же инструментами: `pnpm run parity` и
673
- `pnpm run fixture` дают те же файлы. Побайтово сверяется наше конфиг, эталонные
674
- числа, хеш артефакта, описание; у бандла истории сверяется содержимое (ветки,
675
- верхушка, число коммитов), потому что упаковку пишет git и её байты зависят от его
676
- версии. Обе стороны пары закреплены инструмент это замороженная копия реализации,
677
- чьи байты лежат в истории (`fixtures/legacy/size-table.cjs`, `REFACTOR.md` R-1.5) и
678
- сверяются с записью о происхождении эталона, а ревизия проекта берётся из манифеста
679
- (`--at` сдвигает её осознанно), окружение снятия задано (`core.quotePath=false`).
680
- Без закрепления окружения эталон снимается другими числами: на машине с настройками
681
- git по умолчанию фикстура с не-английским именем файла теряет строку. Проверено
682
- тремя прогонами: повтор даёт те же байты и прогон без настроек машины
683
- (`GIT_CONFIG_GLOBAL=/dev/null`) тоже. Сходимость записанного в манифестах с
684
- файлами стережёт `test/frozen.test.js`.
685
-
686
- ## Чего ещё нет
473
+ | `plans/` | The plans of work on this repository: `plans/archive/` holds the earlier ones, `plans/2026-09-17-page-perf/` the plan of the page work — an index and one file per step of the report, each with why, what changes, acceptance and the risks (`plans/2026-09-17-page-perf/README.md`) |
474
+ | `plans/archive/PLAN.md` | **The main document of the move:** inventory, boundaries, invariants, architecture, the seven steps, acceptance, risks, open questions |
475
+ | `docs/requirements.md` | The customer's requirements: what and why |
476
+ | `docs/module-design.md` | The design of the extraction: how the module is put together |
477
+ | `docs/size-report.html` | The size report of this very project: one self-contained file, refreshed by the hook after every commit (as a commit of its own) |
478
+ | `worklog/` | The journal of requests and of what was done: an entry per portion of work, named `NNNN-slug.md`; `worklog/archive/WORKLOG.md` is the earlier journal in one file |
479
+ | `docs/plans/` | Plans of work: a folder `yyyy-mm-dd-name` per piece of work, holding the main plan and its subplans |
480
+ | `BLOCKERS.md` | Open blockers and known gaps (a workaround has to rest on a check) |
481
+ | `TODO.md` | Defects noticed in passing, one line each: where, what and how it showsfixed in a portion of their own |
482
+ | `plans/archive/REFACTOR.md` | The per-channel plan of the cleanup: size of the code first, speed after; the boundaries and what proves that the behaviour did not change |
483
+ | `tools/parity-freeze.js` | Takes the parity reference (`pnpm run parity`): with the frozen copy, at the project revision from the manifest `--json`, the config, the artifact's hash, the tool's hash |
484
+ | `tools/make-fixture.js` | Assembles the synthetic fixture (`pnpm run fixture`): a deterministic history with traps plus the reference numbers |
485
+ | `tools/synthetic/` | The subjects of that assembly, one per matter: `repo.js` how git is spoken to (pinned time, author, settings), `content.js` what the files hold, `history.js` which commits come of it, `note.js` — the fixture's note with the list of traps |
486
+ | `probes/` | The scripted measurements behind `plans/2026-09-17-page-perf/`: one file per step, run by hand against live Chrome at the debug port the fixed layout, where `content-visibility` acts at all, and the two border models with their pixels (`probes/README.md`). Outside the sensors' paths on purpose: they measure the product rather than being part of it, and a suite cannot see layout, paint or a browser's own skipping |
487
+ | `tools/parity-live.js` | Compares the engine with the live project on a clone: the numbers and the self-contained report at the path the consumer's settings give (`pnpm run parity:live`) |
488
+ | `tools/pack-check.js` | Assembles the tarball and checks that everything works from it: all sources arrived, the numbers and the report as from the repository (`pnpm run pack:check`) |
489
+ | `tools/check-standards.js` | Checks that both references reproduce: a re-take goes nowhere and is compared with what is committed (our files byte for byte, the bundle by content), and that the live-history bundle carries `HEAD` (`pnpm run check:standards`) |
490
+ | `.github/workflows/ci.yml` | CI: the job `verify` calls `pnpm run verify` on every push and every pull request the same profile as locally; the actions are pinned by commit SHA |
491
+ | `.github/workflows/verify-slow.yml` | The slow profile on a schedule: the same plus the same suite with no machine git settings and coverage under c8 — the dear steps, not in every run |
492
+ | `tools/gates/run.js` | The check profiles the single list of steps: `fast` (every edit), `full` (before pushing and in CI), `slow` (+ the hermetic suite and coverage); `--list` prints the commands |
493
+ | `tools/gates/metrics.js` | The bloat sensor: rules of size and complexity, the weight of checks, debt marks — with an ESLint suppression ratchet (`.eslint-suppressions.json`) |
494
+ | `tools/gates/dup.js` | The duplication sensor: clone fingerprints by content (`dup-baseline.json`), a view against the baseline file and one against the `origin/main` tree |
495
+ | `tools/gates/deps.js` | The dependency sensor: cycles, orphans, the direction of layers and unresolvable imports (`dependency-cruiser`) |
496
+ | `tools/gates/coverage.js` | The coverage sensor: a per-file ratchet against `coverage-baseline.json` rather than a percentage over the repository. The unit is **how much executed** lines, branches and functions, taken from c8's own numbers — so a file that merely grew does not move the ratchet while code that stopped being run does |
497
+ | `tools/gates/gatefiles.js` | The guard of the gate files: editing thresholds, baselines or the harness without the `Gate-Change:` trailer is red the `commit-msg` hook at commit time and the `pre-push` hook over a range, while CI reads no trailers at all |
498
+ | `tools/gates/common.js`, `tools/gate-probe.js` | What the sensors share (the root, argument parsing, reports) and the harness of their probes: a sensor is called as a command rather than imported |
499
+ | `.githooks/commit-msg`, `.githooks/pre-commit`, `.githooks/pre-push` | Hooks: the guard of the gate files, the fast profile on an edit and before a push; installed by `pnpm run hooks:install` (no hook manager of our own is started) |
500
+ | `.githooks/post-commit` | Refreshing the report after a commit: a call to the installed copy of the package (the line was written by a person — the tool does not edit someone else's hook directories) |
501
+ | `eslint.metrics.config.js`, `.eslint-suppressions.json` | The bloat sensor's rules and its baseline: thresholds taken from measurements, and everything above them lies in the baseline to be worked off gradually |
502
+ | `.jscpd.json`, `dup-baseline.json` | The duplication sensor's settings and baseline: a fingerprint is taken from a clone's content, which is why the baseline is portable |
503
+ | `.dependency-cruiser.cjs`, `.c8rc.json`, `coverage-baseline.json` | The rules of the dependency graph, the settings of the coverage run and its per-file baseline |
504
+ | `AGENTS.md` | A short instruction for an agent in this repository: what to run, what to do when a sensor is red, what must not be touched |
505
+ | `.github/workflows/release.yml` | A release by tag: the strict linter, the whole suite and the work from the assembled package, the manifest version compared with the tag, and publishing to the registry by the GitHub Actions attestation no secret and no code from an authenticator |
506
+ | `templates/` | What a project takes as it is: `size-report.config.json` (a draft of settings), `ci.yml` (a description of the check) and `README.md` (what goes where and what to change in them); they ship and are guarded by `pack:check` and `test/templates.test.js` |
507
+ | `fixtures/parity/` | The reference taken from `safe-resets` at commit `bd6ef9d`: 95 rows × 27 columns. The copy of the implementation it was taken with does not lie in the tree — its bytes live in the history and are taken from there on demand |
508
+ | `fixtures/synthetic/` | The fixture's bundle of 16 commits, its config, the reference numbers (the earlier copy's `--json`) and the hash of its artifact in its earlier shape — a record of what the move was checked against |
509
+ | `fixtures/live/history.bundle`, `fixtures/live/README.md` | The consumer project's history at the reference revision `bd6ef9d` and a note on which revision the bundle carries and why it lies in the repository: the live comparison works without access to the private project |
510
+ | `bin/size.js` | The `size` command: what the package installs (`package.json` → `bin`); it counts nothing itself and only calls the entry point |
511
+ | `LICENSE` | MIT: the licence terms travel in the package |
512
+ | `.gitignore`, `pnpm-lock.yaml` | What does not go into the repository; the pnpm lock file, while the manager's version lives in the `packageManager` field (which is where CI takes it from) |
513
+ | `src/size-table.js` | The package's entry point: a re-export of the public API (55 names) and no calculation of its own |
514
+ | `src/derived.js` | The report's shared calculation: totals, deltas, a cell, a commit's captionone for the engine and the page's program |
515
+ | `src/css.js` | Reading the styling from disk: which sets of styles exist and what role each has |
516
+ | `src/table.css` | The report's table: the geometry of a cell, the sticky header and commit column, the colour of deltas |
517
+ | `src/page/app.css` | The page's styling on top of the shared part: the panel with the file tree and its sticky row of categories (a column on the left on a wide screen, the page fitting the window), the empty states, a narrow window |
518
+ | `src/page/payload.js` | The page's block in sparse form, and the one place that unrolls it back: the history as changes (a file's appearance, its moves, its disappearance) turned into the snapshots the calculation and the table already speak a value that did not move is one object shared by the rows that hold it |
519
+ | `src/page/state.js` | The page's state: the report's data (the block unrolled by the payload chapter), the view of the checkboxes, the pointer "which path is which column", folded folders, the record's passport, the browser's memory and the exchange by link — a chapter of the page's program |
520
+ | `src/page/dom.js` | The page's nodes: the small helpers of markup (`appEl`, `appBox`) one set for the panel and the table alike |
521
+ | `src/page/panel.js` | The panel of choices: the switches of metrics and files, the categories, the tree of the project's paths (files outside the report keep a checkbox off with a reason and stand after the rest; folders carry a folding sign that hides the subtree by a class rather than by a rebuild); built once, with the fields of the switches and of the folders and categories written where they stand |
522
+ | `src/page/table.js` | The page's table, built once: a cell, a commit's caption, the header, the empty states and the cache of the nodes of every column markup over the shared calculation, with the totals carried rather than recounted |
523
+ | `src/page/app.js` | Assembling and starting the page: the first drawing, then a switch that shows, hides and rewrites the totals without making a node; an anchor change; pasted into the assembled page |
524
+ | `src/page/build.js` | Assembling the page: data, styling and program in one file with no external references — the pasted text is **squeezed** on the way in (comments and indentation out, the same stripping the `min` metric counts) while the sources keep them, and the result is guarded by the stripper's own `assertCompilable` |
525
+ | `src/git.js` | The only border where git is called: the pinned settings, blobs by the batch, the history, the comparison with the working tree |
526
+ | `src/strip.js` | Removing ballast: which form goes to which file (extension, strategy) and which strategies are minification itself — the entry to the parsing of forms |
527
+ | `src/strip/js.js` | Removing comments and indentation in JS: a pass over the cases (a comment, a regexp, a string, a character) — through strings and templates as well |
528
+ | `src/strip/forms.js` | The forms of text with a removal of their own: markup, styles, the lines of a file and JSON |
529
+ | `src/strip/guard.js` | The stripper's guard: what was stripped has to compile as a script in the process or as a module in a worker thread |
530
+ | `src/parse.js` | Parsing a module: one worker thread per run and a fallback to `node --check`, and the way the last module was parsed |
531
+ | `src/parse-worker.js` | The parsing itself inside the thread: it parses the text without executing it and reports that Node has no vm modules |
532
+ | `src/metrics.js` | The register of metrics: what is measured, whether the text is needed and how honest the number is; a metric's description for the reader lives in one place |
533
+ | `src/minify.js` | The real minifier: an optional dependency, loaded once, and it does not bring the run down when absent |
534
+ | `src/tokens.js` | Tokens: a dictionary by family and encoding, an estimate by length as the fallback count, the formats without text |
535
+ | `src/optional.js` | The shared handling of optional dependencies (the minifier and the dictionary): lazy loading, the package's version, the seam of absence |
536
+ | `src/history.js` | Walking the history: measuring commit by commit, shifting the numbers, assembling, comparing with the working tree, the mark "which column the last commit touched" and a reason for every dropped commit |
537
+ | `src/check.js` | Coverage (`size check`): settings, history, paths, sensors what went past the columns and how that is fixed |
538
+ | `src/explain.js` | Explaining a missing row (`size explain <commit>`): the reason, the evidence and a ready fix |
539
+ | `src/doctor.js` | Diagnostics in one answer (`size doctor`): the environment, the dependencies, the settings, the coverage and the hook's state — assembled from the pieces that already exist |
540
+ | `src/hook.js` | The hooks of self-updating: they install themselves (`autoInstall` — from the entry point and `bin/postinstall.js`), come off by a command, commit the report alone, and keep a lock and a record of the run |
541
+ | `bin/postinstall.js` | Installing the hook after the package is added: it looks for the consumer project and stays silent when there is nowhere to install |
542
+ | `src/artifact.js` | The report on disk: the only place where it becomes a file (both `--write` and the hook use it); the report is a self-contained page |
543
+ | `src/journal.js` | The journal and links: which section a commit belongs to and where a description leads |
544
+ | `src/data.js` | The file categories and the contract with the page (`--data`): the numbers, the shape of the table and the catalogue of the project's paths |
545
+ | `src/config.js` | The consumer project's settings: the defaults, reading them, checking them |
546
+ | `src/project.js` | The settings derived from the project itself (its tree and history): columns, the journal, the exceptions, the catalogue of paths for the page's tree. Without a settings file it *is* the settings; `--init` pins it as a file |
547
+ | `src/locales.js`, `src/refusal.js`, `src/tool.js` | The report's texts; the exit codes and the help; the package's name and version |
548
+ | `src/cli.js` | The tool's entry: parsing the command line, reading the project and handing the request to a mode; the package's main file |
549
+ | `src/args.js` | The grammar of the command line: modes, flags and commands plus the checks of their combinations a refusal names the culprit and a ready command |
550
+ | `src/modes.js` | The modes: assemble the report, compare it with the history, hand over the data, the coverage and the diagnostics |
551
+ | `src/init.js` | Pinning the settings as a file (`--init`): what the project derived about itself is written outand goes through the same check as the first run |
552
+ | `test/api.test.js` | The package's public API: the list of names is frozen, and splitting the engine may not change it |
553
+ | `eslint.config.js` | The rules of formatting: the same as the consumer project's, plus a ban on gluing operators into one line (`pnpm run lint`, `pnpm run lint:strict`) |
554
+ | `tools/harness.js` | The harness of the checks: paths, clones of the fixture (including one shared per suite and one with CRLF), running the tool, reading refusals, hashes |
555
+ | `tools/page-harness.js` | The harness of the contract and page checks: the contract data, the assembled page, reading it in a real DOM, the panel's switches, the page's calculation and its decoder evaluated from their sources, the block unpacked, and the platform's unpacker put into jsdom (which has none) — one for six suites |
556
+ | `tools/suites.js` | The split of the suite: which files go into the fast run (with a reason for each) and why every dear one is in the full run |
557
+ | `tools/run-tests.js` | Running the suite (`pnpm test`, `pnpm test:all`, `pnpm run suites:measure`): each file's duration measured on its own, and the counts of checks adding up |
558
+ | `tools/docs-facts.js` | Reading facts out of the documentation one layer for the four checks of the documentation guard: what a document names (paths, calls, section addresses) against what the repository holds |
559
+ | `tools/yaml.js` | Parsing a subset of YAML one parser for the two guards over descriptions (`templates/ci.yml` and `.github/workflows/release.yml`): anything outside the subset is an error rather than a silently skipped line, including a colon followed by a space in an unquoted value — which is what kept the release description unparsable while the check looked for substrings |
560
+ | `tools/refusals.js` | The catalogue of refusals: one line per refusal — its cause, its exit code, the phrases its output must carry, and **what it advises** (`advice`: `run` — a command, `template` — a form with substitutions, `manual` — a person's action with its reason, `coveredBy` — handed to another check), and for one that cannot be caught at all, why. The maps of refusal sites (`SITES`, `PRINTED`) hold the counts, so that a new refusal cannot appear in silence, and the markers of advice so that a new piece of advice cannot either |
561
+ | `test/parity.test.js` | The engine's parity with the reference: the numbers, the report's self-containedness, the locale |
562
+ | `test/frozen.test.js` | The frozen copy: that it is the revision the reference was taken at, and that it reproduces that reference |
563
+ | `test/environment.test.js` | Hermeticity: the output does not depend on the machine's git settings or on its locale |
564
+ | `test/crlf.test.js` | A checkout with CRLF (`core.autocrlf`) does not hinder the comparison |
565
+ | `test/disk.test.js` | The comparison with the working tree: an edit only on disk, three ways of losing a change (an edit, a creation, a deletion — all by mutation), a file deleted before HEAD, and a rename inside aliases is no loss |
566
+ | `test/cli.test.js`, `test/cli-paths.test.js` | The command line's refusals: the help, the settings, the exit codesand where the tool writes |
567
+ | `test/refusals.test.js` | The refusals are executed: each one is called by a run, its exit code and its promised phrases are compared (with clones of their own for someone else's hook, a shallow history and a branch past the report), and **the advice runs** — the command answers with the promised code and no stack, while where "the refusal is gone" is declared the same call answers differently after it |
568
+ | `test/refusals-catalog.test.js` | The guard of the refusal catalogue: every refusal site in the sources has an entry, every entry declares its advice, and refusals handed to another check are really accepted by it (the named file and line are checked) |
569
+ | `test/contract-data.test.js` | The data contract: the numbers against the reference, the set of fields against the derived quantities, the metric's method against the way the numbers were counted — and the round trip through the page's sparse block, which restores the contract whole and twice over the same bytes |
570
+ | `test/contract-derived.test.js` | The derived quantities against the artifact's numbers: a row's totals, a cell's delta and the delta of a total — on the code that lies in the tree |
571
+ | `test/page-view.test.js` | The assembled page: pasted with no copy of the calculation, self-contained, the empty states, the styling, the switches, a click that makes no table and the carried totals against the engine's own sums |
572
+ | `test/page-tree.test.js` | The panel's file tree: folders by the project's paths, three states, the subtree, files and folders outside the report (a checkbox off, a place after the rest), folding without a rebuild and a scroll a click does not touch |
573
+ | `test/page-choice.test.js` | The memory of the choice and the exchange by link: a revisit, someone else's report, a foreign and a broken record, an address change on an open page |
574
+ | `test/page-cols.test.js` | The fixed layout the page carries: a column's width is counted from the model rather than measured in a laid-out cell, the clip keeps a caption inside its column, the sticky header and commit column keep their edges, and the table names its own width (`width: auto` would hand the layout back to the automatic algorithm) |
575
+ | `test/module.test.js` | A module under a `.js` extension: measured without touching the settings; the stripper's guard is alive (proved by mutation) and does not accuse the innocent |
576
+ | `test/guard.test.js` | Parsing a module: it goes through a thread, both paths give one verdict, the fallback works with the thread's file away, and hundreds of parses are cheaper than a launch |
577
+ | `test/runner.test.js` | Reading a process's output: chunks are glued as buffers rather than appended to a string a multi-byte character at a chunk border does not turn into two replacement characters |
578
+ | `test/git-pins.test.js` | The guard of the git border: no direct calls to git outside the shared list of pins, and an unpinned read is shown by a witness (a quoted path) |
579
+ | `test/docs-paths.test.js`, `test/docs-commands.test.js`, `test/docs-numbers.test.js`, `test/docs-pin.test.js` | The documentation guard, one file per promise: the paths and the file table; the calls, the causes of refusal and the section addresses; the counts of checks; the pin in the install example |
580
+ | `test/release.test.js` | The guard of the release from CI: it begins with a tag, the version comes from the manifest, no secret and no one-time code are needed, a prerelease does not go to `latest`, the whole suite runs before publishing and the hint on npmjs.com names this same file |
581
+ | `test/suites.test.js` | The guard of the suite's split: the classification is complete (fast only explicitly, full with a reason), every file has its reason, and the fast run stays part of the suite |
582
+ | `test/gates-metrics.test.js`, `test/gates-dup.test.js`, `test/gates-deps.test.js`, `test/gates-coverage.test.js`, `test/gates-files.test.js` | The sensors' probes: an artificial violation the sensor is red, taking it away green again; the run calls a sensor as a command rather than importing it, which is why it proves the exit code too |
583
+ | `test/gates-verify.test.js` | The guard of the single list: the profile's commands against the workflows, the hooks and `templates/ci.yml` a check that is not in the profile cannot be in CI |
584
+ | `test/check.test.js` | Coverage and explanation on the fixture's real commits: an uncovered path, "only the report", "the number did not move", "past the columns", a merge — and that a fix of the settings does not move the numbers |
585
+ | `test/doctor.test.js` | Diagnostics over five states of a project: no settings (2), full coverage (0), incomplete (1), a shallow history (3), no sensor (4) and the coverage block equals the answer of `size check` rather than being counted a second time |
586
+ | `test/hook.test.js` | The hooks on a fresh clone: they are installed by a command only, give a commit of the report's own (after a merge as well), a repeated run stays silent, someone else's work and the index are untouched, nothing happens in CI or on a refusal of the tool, and removing them returns the project to what it was |
587
+ | `test/templates.test.js` | The templates: the draft of settings passes the tool's check and assembles a real report; the description of the check parses and calls only commands and flags that exist |
588
+ | `test/minify.test.js`, `test/tokens.test.js` | Real minification and tokens: the numbers against stripping, the encoding as part of the number, the honesty of a caption, work with no optional dependency (code 4) and the seam `SIZE_REPORT_NO_OPTIONAL` |
589
+ | `package.json` | The package's manifest: the name `@vernikr/size-report`, the version, and a shipped-file list that holds only what exists |
590
+
591
+ **Both references are taken anew by the same tools:** `pnpm run parity` and `pnpm run fixture` give
592
+ the same files. What is ours is compared byte for byte — the config, the reference numbers, the
593
+ artifact's hash, the description while the history bundle is compared by content (the branches, the
594
+ tip, the number of commits), because git does the packing and its bytes depend on git's version. Both
595
+ sides of the pair are pinned: the tool is a frozen copy of the implementation, whose bytes live in the
596
+ history (`fixtures/legacy/size-table.cjs`) and are compared against the record of the reference's
597
+ provenance, while the project's revision comes from the manifest (`--at` shifts it deliberately) and
598
+ the environment of the capture is set (`core.quotePath=false`). Without the pinned environment the
599
+ reference is taken with different numbers: on a machine with git's default settings the fixture loses
600
+ a row whose file name is not English. Repetition and hermeticity are not taken on trust either:
601
+ `test/git-pins.test.js` shows an unpinned read by a witness, `test/environment.test.js` keeps the
602
+ output independent of the machine, and the slow profile repeats the whole suite with none of the
603
+ machine's git settings at all. That what the manifests record agrees with the files is guarded by
604
+ `test/frozen.test.js`.
605
+
606
+ ## What is not here yet
687
607
 
688
608
  ```text
689
- dist/app.js пре-собранная программа отчёта для публикации
690
- size init/measure командами вместо флагов: сейчас командами стали только check, explain, doctor и хук
691
- блок для агентов инструкция агенту проекта: требования её не просят, поэтому в шаблонах её нет
692
- минификация HTML минификатор разметки: пока HTML считается упрощением (шаг 3)
693
- JSX и TSX выход зависит от настройки jsx самого проекта упрощение (шаг 3)
694
- семейства токенов кроме openai: у остальных нет своего словаря — считали бы чужим (шаг 4)
609
+ dist/app.js a pre-assembled report program: the page's program is pasted into the page
610
+ while the report is built, so the file would be a second copy of the same
611
+ size init / measure commands instead of flags: of the commands only check, explain, doctor and
612
+ the hook are here, and no command measures at all
613
+ a block for agents an instruction for the project's own agent: the requirements do not ask for
614
+ it, so the templates carry none
615
+ HTML minification a minifier of markup: HTML counts as stripping for now
616
+ JSX and TSX the output depends on the project's own jsx setting — stripping
617
+ token families anything but openai: the others have no dictionary of their own, and counting
618
+ with someone else's is not a family
695
619
  ```
696
620
 
697
- Швы между модулями проходят по границам данных: сверху то, что читает git и
698
- файловую систему (`git`, `strip`, `metrics`, `history`), ниже то, что работает на
699
- уже собранных значениях (`data`, `render`, `page`), а настройки, тексты и отказ
700
- по краям, потому что их знает любой и они не знают никого. Оба отчёта считаются на
701
- сборке: страница получает исходники общего расчёта и своей программы вклеенными
702
- (`src/derived.js`, `src/page/*.js`), потому что открывается она с диска, без
703
- сервера и без сети. Остальное — по шагам 2–6 (`PLAN.md` §5).
621
+ The seams between modules follow the borders of data: above sit the parts that read git and the file system
622
+ (`git`, `strip`, `metrics`, `history`), below the parts that work on values already collected (`data`,
623
+ `derived`, `page`), while the settings, the texts and the refusal stand at the edges, because everyone
624
+ knows them and they know no one. Both reports are counted at build time: the page gets the sources of the
625
+ shared calculation and of its own program pasted in (`src/derived.js`, `src/page/*.js`) and squeezed on the way
626
+ in, because it opens from disk, with no server and no network. The rest is planned step by step in
627
+ `plans/archive/PLAN.md`.
704
628
 
705
- ## Как подключить к своему проекту
629
+ ## Wiring it into your project
706
630
 
707
- Инструкция проверена покомандно на свежем проекте (три коммита, ESM в `src/`,
708
- `pnpm`): ниже ровно те команды, которые работают сегодня. Всё, что сегодня
709
- **не** работает, названо здесь же и с причиной, чтобы это не искали опытом;
710
- каждый такой случай — отдельный пункт `REFACTOR.md`.
631
+ The instruction was walked through command by command in a fresh project (the protocol is in
632
+ `worklog/archive/WORKLOG.md` §54): below are exactly the commands that work today. What does **not** work
633
+ today is named here too, with its reason, so that nobody has to find it out by trying.
711
634
 
712
- Эта же инструкциярецепт для шага 5 (`PLAN.md`): интеграция в проект-потребитель
713
- идёт по ней, а не по памяти.
635
+ What is needed: **a git repository with history** at least one commit, because the table is built from
636
+ commits (a repository with none ends in an internal error today: `BLOCKERS.md` §N16) — and
637
+ **Node ≥ 20.19** (`engines` of the package).
714
638
 
715
- Нужны: **git-репозиторий с историей** (хотя бы один коммит — таблица строится по
716
- коммитам) и **Node ≥ 20.19** (`engines` пакета).
717
-
718
- ### 1. Установка
639
+ ### 1. Installation
719
640
 
720
641
  ```bash
721
642
  pnpm add -D @vernikr/size-report
722
643
  ```
723
644
 
724
- Пакет **опубликован в реестре**, и публично: `npm view @vernikr/size-report
725
- version` отвечает `2.4.0`, `npm access get status @vernikr/size-report` `public`,
726
- а анонимный запрос тарболла код 200. `npm i -D` и `yarn add -D` принимают то же
727
- имя; ни ключа, ни ссылки на репозиторий не нужно.
645
+ The package is **published in the registry**, and publicly: `npm view @vernikr/size-report version`
646
+ answers the same version the manifest names, `npm access get status @vernikr/size-report` says `public`,
647
+ and an anonymous request for the tarball is a 200. `npm i -D` and `yarn add -D` take the same name; no key
648
+ and no link to the repository are needed.
728
649
 
729
- Тот же выпуск можно взять ссылкой на репозиторийтак установка не зависит от
730
- реестра, но остаётся привязанной к ревизии:
650
+ The same release can be taken by a reference to the repository installation then does not depend on the
651
+ registry, but stays tied to a revision:
731
652
 
732
653
  ```bash
733
- pnpm add -D github:vernikr/size-report#v2.4.0
654
+ pnpm add -D github:vernikr/size-report#v2.6.0
734
655
  ```
735
656
 
736
- Без сети (или если тянуть из codeload нечем) — тарболл: `pnpm pack` в клоне
737
- пакета, затем `pnpm add -D ./vernikr-size-report-2.4.0.tgz`.
738
-
739
- **Почему тег, а не sha.** Короткий sha pnpm разрешает только через видимые рефы, а
740
- `git ls-remote` отдаёт одни верхушки веток: пока ревизия верхушка, короткий sha
741
- работает, а как только ветка ушла вперёд, установка падает с `Could not resolve
742
- <sha> to a commit`. Это не рассуждение, а проба: короткий пин `6530237` ставился,
743
- пока `main` стоял на нём, и перестал на следующем же коммите, а тот же sha
744
- целиком поставился. Имя ветки (`#main`) или тег принимаются оба, но ветка
745
- движущаяся цель, а тег постоянен: этот выпуск стоит на теге `v2.4.0`, он же и в
746
- примере (сорок знаков тоже годятся, но их придётся брать глазами из истории).
747
-
748
- Ревизия в примере не украшение, а часть утверждения: она закреплена за тем, что
749
- описано ниже. Пин старше подкоманд (`check`, `doctor`, `explain`, `install-hook`)
750
- означал бы, что текст учит командам, которых в установленной ревизии нет, а
751
- лишнее слово там не отвергается, а молча пропускается то есть вместо отказа
752
- человек получил бы ноль и решил, что всё в порядке. Поэтому пин берётся не «какой был под рукой», а
753
- ревизией, в которой есть всё названное ниже включая отказы на незнакомое слово. За этим следит сторож документации (`test/docs-pin.test.js`): пин обязан вести
754
- на ревизию этого репозитория, и все названные в тексте команды обязаны быть в её
755
- справке.
756
-
757
- Репозиторий пакета **публичный** (приватным был до 2026-09-14), и это ровно то,
758
- что упрощает установку: ни ключа разработчика, ни шага в CI с доступом. Проверено
759
- прогоном в пустом проекте, где у git не было ни глобальных настроек, ни
760
- помощника учётных данных (`GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null
761
- GIT_SSH_COMMAND=false`): установка 3,4 с, дальше `size --write` и `size` работают
762
- (`WORKLOG.md` §44). Прежнее требование было ценой приватности: локально ключ, а
763
- в CI read-only deploy key перед `pnpm install` (то самое первое подключение,
764
- `WORKLOG.md` §18); шаг с ключом из шаблона ушёл вместе с приватностью. Публикация
765
- в npm сделана 2026-09-15, и у неё была цена: имя `size-report` в реестре занято чужим
766
- пакетом (2017 год, три версии), поэтому выкладка это ещё и смена имени на имя в
767
- области владельца (`@vernikr/size-report`), а не только отправка архива; что
768
- затронуло переименование — `PLAN.md` §10, чем доказана выкладка `WORKLOG.md` §53.
769
-
770
- ### 2. Настройки: их можно не заводить
657
+ With no network (or nothing to fetch from codeload) — the tarball: `pnpm pack` in the package clone, then
658
+ `pnpm add -D ./vernikr-size-report-<version>.tgz`, where the name is the one `pnpm pack` printed.
659
+
660
+ **Why a tag rather than a sha.** pnpm resolves a short sha only through visible refs, while
661
+ `git ls-remote` gives branch tips alone: while the revision is a tip, a short sha installs, and as soon as
662
+ the branch moves on the installation fails with `Could not resolve <sha> to a commit`. This is an
663
+ observation rather than reasoning: the short pin `6530237` installed while `main` stood on it and stopped
664
+ working at the very next commit, while the same sha in full installed. A branch name (`#main`) and a tag
665
+ are both accepted, but a branch is a moving target and a tag is constant: this release stands on the tag
666
+ `v2.6.0`, which is also the one in the example (forty characters work as well, but they have to be copied
667
+ out of the history by eye).
668
+
669
+ The revision in the example is a part of the claim rather than decoration: what is described below is
670
+ pinned to it. A pin older than the commands (`check`, `explain`, `doctor`, `install-hook`) would teach
671
+ commands the installed revision does not have, and an extra word there is not refused but silently
672
+ skipped that is, instead of a refusal the person gets a zero and concludes all is well. So the pin is
673
+ the revision that holds everything named below, refusals on an unknown word included.
674
+ `test/docs-pin.test.js` guards that: the pin has to lead to a revision of this repository, and every
675
+ command named in the text has to be in that revision's help.
676
+
677
+ The package repository is **public** (it was private until 2026-09-14), and that is exactly what makes the
678
+ installation simple: no developer key and no CI step with access. Checked by a run in an empty project
679
+ where git had neither global settings nor a credential helper (`GIT_CONFIG_GLOBAL=/dev/null
680
+ GIT_CONFIG_SYSTEM=/dev/null GIT_SSH_COMMAND=false`): `size --write` and `size` work there
681
+ (`worklog/archive/WORKLOG.md` §44). The earlier requirement was the price of privacy: a key locally and a
682
+ read-only deploy key before `pnpm install` in CI (that first wiring, `worklog/archive/WORKLOG.md` §18);
683
+ the template's key step went away together with the privacy. The publication to npm happened on
684
+ 2026-09-15, and it had a price: the name `size-report` in the registry is taken by someone else's package
685
+ (2017, three versions), so the release was also a renaming into the owner's scope (`@vernikr/size-report`)
686
+ rather than just an upload of an archive; what the renaming touched — `plans/archive/PLAN.md` §10, what proves the
687
+ publication`worklog/archive/WORKLOG.md` §53.
688
+
689
+ ### 2. Settings: you need not create them
771
690
 
772
691
  ```bash
773
- pnpm exec size --write # таблица; настроек нетих выведет сам инструмент
774
- pnpm exec size --init # закрепить выведенное в size-table.config.json
692
+ pnpm exec size --write # the table; no settings the tool derives them itself
693
+ pnpm exec size --init # pin what it derived into size-table.config.json
775
694
  ```
776
695
 
777
- Начинать с настроек не нужно: без файла инструмент выводит их из проектаколонками
778
- берёт **каждый отслеживаемый git файл, который можно измерить** (отчёт называет
779
- объём проекта, а не выборки из него; границы остались только у того, что колонкой
780
- быть не может: сам отчёт, замки зависимостей, собранное, незнакомый формат и файл
781
- сверх 512 КБ), а прочие называет в `skip`, журналом
782
- первый знакомый (`WORKLOG.md`, `CHANGELOG.md`, ), файлом отчёта `docs/`, если
783
- каталог есть, командой починки объявленный скрипт `sizes`, а без него путь
784
- к установленному пакету (его цитируют подпись отчёта и отказы, поэтому он обязан
785
- работать уже сейчас), ссылкой на коммит адрес `origin`, метриками `raw`, `min`,
786
- `tok`. Метрика `min` считается настоящим сжатием (`"minify": {"engine": "esbuild"}`),
787
- а `tok` словарём (`"tokens": {"family": "openai", "encoding": "o200k_base"}`): без
788
- этих необязательных зависимостей метрика честно отступает к другому счёту и прогон
789
- отдаёт код 4 — правки настроек и тут не требуются.
790
-
791
- Всё, что колонкой быть не может (сам отчёт, замки зависимостей, карты, собранное,
792
- незнакомый формат, слишком крупный файл) и чего git не отслеживает, называется
793
- в `skip` поэтому первый же `size check`
794
- полон, а не красен: «пути мимо колонок» появляются от новых правок, а не от того, что
795
- проект ещё не описан. О том, что настройки выведены, инструмент говорит строкой в
796
- stderr и называет команду, которая их закрепляет, `--init`; закреплённое проходит
797
- ту же проверку, что любой файл настроек, и дальше его правят глазами (сам `--init`
798
- печатает, что закрепил, и что делать дальше скрипты и проверку в CI). Без
799
- закрепления профиль выводится заново на каждом запуске: числа не «поедут», но
800
- повторить прежний замер в том числе хуком и проверкой можно только по файлу.
801
-
802
- Закрепляется **то же, чем проект работает без файла**: вывод из проекта поверх
803
- умолчаний. Поэтому в закреплённом файле видны и значения, которых в проекте никто не
804
- писал, — тогда смена умолчаний в новой версии пакета не поедет по уже настроенному
805
- проекту молча.
806
-
807
- > Subкоманды `size init` пока нет — CLI знает только флаги (`--init`, `--write`,
808
- > `--data`, `--json`, без флага проверка); полный список даёт `size --help`.
809
- > Subкоманды — шаг 5 плана (`REFACTOR.md` R-4.5).
810
-
811
- ### 3. Что правится в конфиге
812
-
813
- Вывод знает про проект только то, что видно в дереве и истории, — какие колонки важны,
814
- знает человек. Чаще всего правят:
815
-
816
- | Ключ | Что это |
696
+ There is no need to start with settings: without a file the tool derives them from the project the
697
+ columns are **every tracked file git can measure** (the report names the volume of the project rather than
698
+ of a sample of it, and the only limits are what cannot be a column at all: the report itself, dependency
699
+ locks, built output, an unknown format and a file above 512 KB), while everything else is named in `skip`;
700
+ the journal is the first familiar one (`WORKLOG.md`, `CHANGELOG.md`, …); the report file is
701
+ `docs/size-report.html` (the directory is created by the writer); the fix command is the declared `sizes`
702
+ script, or the path to the installed package without one (the report's signature and the refusals quote it,
703
+ so it has to work right here and now); the commit link comes from the `origin` address; the metrics are
704
+ `raw`, `min` and `tok`. The `min` metric is counted by real compression here (`"minify": {"engine":
705
+ "esbuild"}`), and `tok` by a dictionary (`"tokens": {"family": "openai", "encoding": "o200k_base"}`):
706
+ without those optional dependencies the metric honestly falls back to another count and the run returns
707
+ code 4 no settings need editing for that either.
708
+
709
+ Everything that cannot be a column (the report itself, dependency locks, maps, built output, an unknown
710
+ format, a file too large) and everything git does not track is named in `skip` — which is why the first
711
+ `size check` is complete rather than red: "paths past the columns" appear from new edits, not from a
712
+ project that has not been described yet. That the settings were derived, the tool says in a line on stderr
713
+ and names the command that pins them, `--init`; what is pinned passes the same check as any settings file,
714
+ and afterwards it is edited by hand (the `--init` itself prints what it pinned and what to do next — the
715
+ scripts and the CI check). Without pinning, the profile is derived anew on every run: the column set
716
+ changes from run to run (the tool says so with that very line), so repeating the same measurement — by the
717
+ hook and by the check includedis possible only from a file.
718
+
719
+ What is pinned is **the very thing the project runs on without a file**: the derivation from the project
720
+ on top of the defaults. That is why the pinned file holds values nobody wrote in the project — then a
721
+ change of the defaults in a new version of the package does not travel over an already configured project
722
+ in silence.
723
+
724
+ > `size init` as a command does not exist — `--init` is a mode: the commands are `check`, `explain`,
725
+ > `doctor` and the hook, and the full list is given by `size --help`.
726
+
727
+ ### 3. What is edited in the config
728
+
729
+ The derivation knows about the project only what the tree and the history show — which columns matter is
730
+ known to a person. What is edited most often:
731
+
732
+ | Key | What it is |
817
733
  |---|---|
818
- | `columns` | колонки таблицы: `{label, paths: [...]}`; **колонка это файл**: список путей её переименования (в ревизии берётся тот путь, который в ней есть), а не несколько файлов разом; `label` то, что увидит человек |
819
- | `metrics` | из чего состоит число: `raw` (размер объекта git), `min` (минифицированная формакакая именно, решает `minify.engine`), `tok` (токены), `gzip` |
820
- | `tokens.family`, `tokens.encoding` | словарь для `tok`: семейство (`openai`) и кодировка (`o200k_base` или `cl100k_base`) — кодировка меняет число, поэтому она и в настройках, и в подписи метрики |
821
- | `minify.engine` | чем считается `min`: `strip` (комментарии и отступы, точность не обещается) или `esbuild` (настоящее сжатие; форматы без минификатора упрощение, и это видно в подписи метрики) |
822
- | `output` | файл отчёта (в выведенном профиле `docs/size-report.html`; каталог создаётся сам, имя отчётаего имя) |
823
- | `journal` | где искать разделы журнала, на которые ссылаются строки |
824
- | `links.commitUrl` | шаблон ссылки на коммит, например `https://github.com/org/repo/commit/{sha}`; выводится из адреса `origin` у GitHub и GitLab (у остальных хозяевпусто, а не догадка) |
825
- | `skip` | пути, которые колонкой не стали: и те, что ею быть не могут (сам отчёт, замки зависимостей), и те, что в колонки не поместились (выведенный профиль объявляет исключениями всё остальноепоэтому первый `check` полон) |
826
- | `fixCommand` | команда, которую цитирует подпись отчёта и подсказывает отказ; в выведенном профиле ваш скрипт `sizes`, если он объявлен, иначе путь к установленному пакету внутри проекта (зов по имени пакета уходит в реестр — `REFACTOR.md` R-4.21) |
827
- | `locale`, `title`, `heading` | язык текстов отчёта и его заголовки; пустые `title`/`heading` значат «взять из локали» |
828
- | `minify.guard` | расширения, где результат стриппера проверяется разбором; модуль в `.js` гард понимает сам, трогать его не нужно |
829
- | `hooks.enabled` | выключатель хука автообновления (`false` — хук не ставится сам и молчит, если уже стоит; убирается он только `size uninstall-hook`) |
830
-
831
- Остальные ключи и умолчания `src/config.js` (`DEFAULT_CONFIG`).
832
-
833
- ### 4. Скрипты и первый отчёт
734
+ | `columns` | the table's columns: `{label, paths: [...]}`; **a column is a file**: the list of paths is its renames (a revision takes whichever of them it holds), not several files at once; `label` is what a person will see |
735
+ | `metrics` | what a number is made of: `raw` (the size of the git object), `min` (the minified form which one, `minify.engine` decides), `tok` (tokens), `gzip` |
736
+ | `tokens.family`, `tokens.encoding` | the dictionary for `tok`: the family (`openai`) and the encoding (`o200k_base` or `cl100k_base`) — the encoding changes the number, which is why it is both in the settings and in the metric's label |
737
+ | `minify.engine` | what counts `min`: `strip` (comments and indentation a simplification, and the method names it as one) or `esbuild` (real compression; a format the minifier does not take counts as stripping, and the method says so) |
738
+ | `output` | the report file (in the derived profile `docs/size-report.html`; the directory is created by the writer). The path enters the report's passport the key of the saved choice — so a changed path means a fresh choice |
739
+ | `journal` | where to look for the journal sections the rows refer to |
740
+ | `links.commitUrl` | the commit link template, for example `https://github.com/org/repo/commit/{sha}`; derived from the `origin` address for GitHub and GitLab (for other hostsempty rather than a guess) |
741
+ | `skip` | the paths that did not become columns: both those that cannot be (the report itself, dependency locks) and those that did not fit (the derived profile declares everything else an exception which is why the first `check` is complete) |
742
+ | `fixCommand` | the command the report's signature quotes and a refusal suggests; in the derived profile it is your `sizes` script if it is declared, and otherwise the path to the installed package inside the project (a call by package name goes to the registry — `REFACTOR.md` R-4.21) |
743
+ | `locale`, `title`, `heading` | the language of the report's texts and its headings; empty `title`/`heading` mean "take them from the locale" |
744
+ | `minify.guard` | the extensions whose stripper output is checked by parsing; a module in `.js` the guard understands by itself, and there is nothing to touch there |
745
+ | `hooks.enabled` | the switch of the self-updating hook (`false` — the hook is not installed by itself and keeps quiet if it is already there; it is removed only by `size uninstall-hook`) |
746
+
747
+ The other keys and defaults are in `src/config.js` (`DEFAULT_CONFIG`).
748
+
749
+ ### 4. Scripts and the first report
834
750
 
835
751
  ```jsonc
836
752
  // package.json
@@ -838,300 +754,292 @@ stderr и называет команду, которая их закрепля
838
754
  ```
839
755
 
840
756
  ```bash
841
- pnpm run sizes # → docs/size-report.html — отчёт: таблица, фильтры, ссылка
757
+ pnpm run sizes # → docs/size-report.html — the report: the table, the filters, the link
842
758
  ```
843
759
 
844
- Отчёт один самодостаточный файл: открывается двойным щелчком, без сервера и без
845
- сети (внешних ссылок в нём нет вовсе, данные, оформление и программа вклеены).
846
- Производные (дельты, итоги, фильтры) считает сама страницаиз абсолютных
847
- значений, которые даёт движок, и тем же кодом, что и его расчёт.
760
+ The report is one self-contained file: it opens with a double click, with no server and no network (it
761
+ holds no external references at all the data, the styling and the program are pasted in). What is
762
+ derived (the deltas, the totals, the filters) is counted by the page itself from the absolute values the
763
+ engine gives, and by the same code as the engine's own calculation.
848
764
 
849
- **Порядок правок:** код → `pnpm run sizes` → коммит с одной таблицей. Таблица
850
- обновляется **отдельным коммитом**, потому что строка коммита не может попасть в
851
- саму таблицу: обновили её вместе с кодом инструмент предупредит
852
- (`! таблицу обновляли вместе с кодом: <sha>`) и назовёт коммит, который выпал.
853
- Проверка `size` собирает таблицу заново и сверяет с файлом на диске, поэтому она
854
- же ловит и забытую пересборку. Убрать отчёт из git совсем шаг 5 плана
855
- (`PLAN.md` §5).
765
+ **The order of edits:** code → `pnpm run sizes` → a commit with the table alone. The table is updated in a
766
+ **commit of its own**, because a commit cannot have a row inside itself: update it together with the code
767
+ and the tool warns (the text is quoted as the tool prints it: `! the table was updated together with the code: <sha>`)
768
+ and names the commit that dropped out.
769
+ The `size` check rebuilds the table and compares it with the file on disk, so it catches a forgotten
770
+ rebuild too. Dropping the report from git altogether is possible as well: the completeness check exists
771
+ for that, and `templates/ci.yml` says which step to put in its place when the report is not in git.
856
772
 
857
- ### 5. Проверка в CI и перед коммитом
773
+ ### 5. The check in CI and before a commit
858
774
 
859
775
  ```bash
860
- pnpm run test:sizes # 0 — таблица сходится с историей
861
- pnpm exec size check # 0 — ни одно изменение не прошло мимо колонок
862
- pnpm exec size doctor # 0 — делать нечего; иначе первый по важности код
776
+ pnpm run test:sizes # 0 — the table agrees with the history
777
+ pnpm exec size check # 0 — not one change went past the columns
778
+ pnpm exec size doctor # 0 — nothing to do; otherwise the first code by importance
863
779
  ```
864
780
 
865
- `size check` отвечает на другой вопрос, чем сама команда `size`: та говорит
866
- «таблица совпадает с историей», а эта «история вся посчитана»: каждый путь,
867
- который трогали коммиты, должен быть либо колонкой, либо объявленным исключением
868
- (`skip` и сам файл отчёта), иначе это **код 1** со списком путей, коммитом,
869
- который путь завёл, и командой починки. Отчёт при этом не обязан лежать в git
870
- полнота и есть та проверка, которой заменяют «артефакт история».
871
- Если сомнение вызывает один коммит, `pnpm exec size explain <коммит>` объяснит,
872
- почему строки нет: тронут только отчёт, числа не сдвинулись, коммит мимо колонок
873
- или слияние скрыто настройкой с уликами и починкой, где она есть. Коммит можно
874
- назвать так, как его зовёт git: `HEAD`, `HEAD~1`, имя ветки или тега, полный sha
875
- или его начало. Если имя ведёт на коммит вне истории отчёта (другая ветка),
876
- инструмент скажет именно это и назовёт его sha — а не «нет такого коммита».
877
-
878
- `size doctor` собирает всю диагностику в один ответ: окружение и его влияние на
879
- числа (настройки машины на числа не влияют движок закрепляет их на границе
880
- вызова), состояние необязательных зависимостей и что оно значит для точности,
881
- годность настроек и полноту покрытия. Отвечает он теми же кусками, что и
882
- остальные команды: блок покрытия это ровно ответ `size check`, а не второй
883
- расчёт. Код выхода первый по важности, а не «что-то нашлось»: `2` настройки
884
- нечитаемы (читать больше нечего), `3` история обрезана, `1` покрытие неполно,
885
- `4` число приближённо, `0` делать нечего. Датчик, о котором настройки молчат,
886
- назван ненужным, а не отсутствующим, и не загружается: словарь весит мегабайты,
887
- а платить за строку ответа, которой у чисел не было, нечем.
888
-
889
- В шаблонный CI (`templates/ci.yml`) полнота намеренно **не** входит: колонки в
890
- шаблоне — пример, и на проекте, где колонки ещё не подобраны, такая проверка была
891
- бы красной не по делу. Когда колонки обрисуют проект, её добавляют одной строкой
892
- (`pnpm exec size check`).
893
-
894
- Готовая строка для CI: `pnpm run test:sizes` — больше ничего не нужно: проверка —
895
- это и есть команда `size`, своего набора тестов потребителю ставить не надо.
896
- Подсказка `--init` говорит то же самое: проверка — команда пакета, своих файлов в
897
- проект она не приносит.
898
-
899
- В поставке лежит и готовое описание этой проверки: `templates/ci.yml` из пакета
900
- (`node_modules/@vernikr/size-report/templates/ci.yml`) кладётся в
901
- `.github/workflows/size-report.yml` без правок сборка таблицы, сверка с файлом
902
- на диске, два снимка чисел (обычный и в среде без настроек git) и их сравнение.
903
- Секретов оно не требует. Для `npm`/`yarn` в самом файле сказано, какие две строки
904
- заменить. Рядом — `templates/size-report.config.json`, образец настроек: колонки в нём
905
- примерные (`README.md`, `package.json`), они есть почти в любом проекте, поэтому
906
- первый отчёт собирается сразу. Нужен он, только если хочется начать с правленого
907
- файла: без файла настройки выводятся из проекта (`--init` закрепляет выведенное).
908
-
909
- Свой CI у пакета `.github/workflows/ci.yml`: он гоняет у себя тот же список
910
- команд, что описан ниже, и его можно взять за образец для шага потребителя.
911
-
912
- | Код | Что случилось | Что делать |
781
+ `size check` answers a different question than the `size` command itself: that one says "the table agrees
782
+ with the history", while this one says "the whole history is counted": every path the commits touched has
783
+ to be either a column or a declared exception (`skip` and the report file itself), otherwise it is **code
784
+ 1** with the list of paths, the commit that introduced the path and a fix command. The report need not lie
785
+ in git for that completeness is exactly the check that replaces "artifact history". When a single
786
+ commit is in doubt, `pnpm exec size explain <commit>` explains why it has no row: the report alone was
787
+ touched, the numbers did not move, the commit went past the columns, or a merge is hidden by a setting —
788
+ with evidence and a fix where there is one. The commit may be named the way git names it: `HEAD`,
789
+ `HEAD~1`, a branch or a tag, a full sha or its beginning. If the name leads to a commit outside the
790
+ report's history (another branch), the tool says exactly that and names its sha — rather than "no such
791
+ commit".
792
+
793
+ `size doctor` gathers all the diagnostics into one answer: the environment and its influence on the numbers
794
+ (the machine's settings do not influence them the engine pins them at the call's border), the state of
795
+ the optional dependencies and what it means for the count, the validity of the settings and the completeness
796
+ of the coverage. It answers with the same pieces as the other commands: the coverage block is exactly the
797
+ answer of `size check` rather than a second calculation. The exit code is the first by importance rather
798
+ than "something was found": `2` the settings are unreadable (there is nothing else to read), `3` the
799
+ history is cut short, `1` the coverage is incomplete, `4` a sensor counted another way, `0` nothing to do. A
800
+ sensor the settings are silent about is named unneeded rather than missing, and it is not loaded: the
801
+ dictionary weighs megabytes, and there is nothing to pay with for an answer the numbers never needed.
802
+
803
+ The completeness check is deliberately **not** in the template CI (`templates/ci.yml`): the columns there
804
+ are an example, and in a project whose columns are not chosen yet such a check would be red for no reason.
805
+ Once the columns describe the project, it is added in one line (`pnpm exec size check`).
806
+
807
+ The ready line for CI: `pnpm run test:sizes` nothing else is needed: the check *is* the `size` command,
808
+ and a consumer has no test suite of its own to install. The `--init` prompt says the same: the check is a
809
+ command of the package and brings no files of its own into the project.
810
+
811
+ The package also ships a ready description of that check: `templates/ci.yml` from the package
812
+ (`node_modules/@vernikr/size-report/templates/ci.yml`) goes to `.github/workflows/size-report.yml` without
813
+ edits the table rebuilt and compared with the file on disk, two snapshots of the numbers (a plain one
814
+ and one in an environment without the machine's git settings) and their comparison. It needs no secrets.
815
+ For `npm`/`yarn` the file itself says which two lines to replace. Next to it is
816
+ `templates/size-report.config.json`, a sample of settings: its columns are examples (`README.md`,
817
+ `package.json`) that nearly any project has, so the first report is built at once. It is needed only to
818
+ start from an edited file: with no file the settings are derived from the project (`--init` pins the
819
+ derived ones), and the sample is copied to the project root as `size-table.config.json`.
820
+
821
+ The package's own CI is `.github/workflows/ci.yml`: it runs at home the same list of checks as a local run
822
+ (one command, `pnpm run verify`, whose list lives in `tools/gates/run.js`), while what a consumer's CI is
823
+ put together from are the templates above.
824
+
825
+ | Code | What happened | What to do |
913
826
  |---|---|---|
914
- | 0 | всё сходится | ничего |
915
- | 1 | таблица разошлась с историей (или правка на диске не закоммичена); у `size check` — путь истории не отслеживается и не исключён | `pnpm run sizes` и закоммитить таблицу; для `check` — дописать путь колонкой или в `skip` |
916
- | 2 | что-то в вызове или в проекте**командная строка** (незнакомый ключ, ключ без значения, повтор ключа, два режима сразу, лишнее слово, команда и режим, неизвестная команда, несовместимый ключ, нет ответа в JSON, два ответа сразу, нет коммита); **настройки и проект** (нет файла настроек, настройки не разобраны, настройки неверны, нет git, не git-репозиторий, конфиг уже есть); **история** (нет такого коммита, коммит назван неточно, коммит вне истории); **хук** (чужой хук, чужой core.hooksPath, нечем звать инструмент); **измерение** (файл не JavaScript, минификатор не разобрал) | текст отказа называет причину и готовую командуи она выполнима: это сторожит `test/refusals.test.js` |
917
- | 3 | неполная история (clone с `--depth`) | полный клон: `git fetch --unshallow` |
918
- | 4 | нет датчика | `minify.engine: "esbuild"`, а минификатора нет: числа получены упрощением. Отчёт собран, причина и починка в тексте; если при этом таблица расходится с историей, код остаётся **1** (нарушение старше приближения), а заметка о другом счёте печатается рядом |
919
- | 5 | внутренняя ошибка | это дефект инструмента: текст нужен нам, см. «Ловушки» ниже |
827
+ | 0 | everything agrees | nothing |
828
+ | 1 | the table diverged from the history (or an edit on disk is not committed); for `size check` — a path of the history is neither tracked nor excluded | `pnpm run sizes` and commit the table; for `check` — add the path as a column or to `skip` |
829
+ | 2 | something in the call or in the project the causes are quoted as the tool prints them: **command line** (unknown flag, flag without a value, repeated flag, two modes at once, extra word, command and mode, unknown command, incompatible flag, no JSON answer, two answers at once, no commit); **settings and the project** (no settings file, settings not parsed, settings invalid, git missing, not a git repository, config already exists); **history** (no such commit, ambiguous commit, commit outside the history); **hook** (foreign hook, foreign core.hooksPath, no way to invoke the tool); **measurement** (file is not JavaScript, minifier did not parse) | the refusal text names the reason and a ready command and it is executable: `test/refusals.test.js` guards that |
830
+ | 3 | a shallow history (a clone with `--depth`) | a full clone: `git fetch --unshallow` |
831
+ | 4 | no sensor | `minify.engine: "esbuild"` with no minifier: the numbers are stripped rather than minified. The report is built, and its text carries the reason and the fix; if the table also diverges from the history, the code stays **1** (a mismatch outranks the sensor note) while the note about the other count is printed next to it |
832
+ | 5 | an internal error | this is a defect of the tool: we are the ones who need the text — see "Traps worth testing the engine on" below |
833
+
834
+ The cell of code 2 quotes the tool rather than describing it: those are the names of the refusal registry
835
+ (`CONFIG_CAUSES` in `src/refusal.js`), and the documentation guard compares this table with it word by
836
+ word — which is why that one cell speaks the language of the command line, while the report's own texts
837
+ are translated by the `locale` key.
920
838
 
921
- ### 6. Отчёт обновляется сам после коммита
839
+ ### 6. The report updates itself after a commit
922
840
 
923
841
  ```bash
924
- pnpm exec size install-hook # поставить post-commit и post-merge
925
- pnpm exec size uninstall-hook # снять и вернуть проект к прежнему поведению
842
+ pnpm exec size install-hook # install post-commit and post-merge
843
+ pnpm exec size uninstall-hook # remove them and return the project to its previous behaviour
926
844
  ```
927
845
 
928
- Хуки ставятся **сами**, и это единственное, что проект замечает от установки пакета:
929
- после `npm i` скриптом установки, у pnpm 10 первым запуском инструмента (pnpm не
930
- исполняет скрипты зависимостей: «Ignored build scripts»; разрешить можно
931
- `pnpm.onlyBuiltDependencies: ["@vernikr/size-report"]` в своём манифесте). Ставшие
932
- файлы живут в `.git`, `git status` их не видит, снимаются командой выше. После
933
- каждого коммита и слияния отчёт пересобирается: каталог `docs` и файл `size-report.html`
934
- создаются, если их ещё нет, а **отслеживаемый** в git отчёт ложится отдельным коммитом
935
- с подписью `chore(report): отчёт пересобран после <sha>`. Коммитится только путь отчёта:
936
- чужой индекс и незакоммиченная работа не тронуты.
937
-
938
- Первый отчёт исключение из «сам»: файл создан, но не закоммичен, потому что новый
939
- файл в чужой истории — решение человека, а не услуга. Один `git add docs/size-report.html`
940
- (или обычный `git add -A`, если отчёт нужен в проекте) и дальше он едет коммитами сам.
941
- Слияние обрабатывается тем же входом, что обычный коммит, но другим файлом
942
- `post-merge`: git создаёт коммит слияния сам и `post-commit` при этом не зовёт.
943
-
944
- Зацикливания нет по устройству, а не по флагу: коммит отчёта собирается
945
- плумбингом (`commit-tree` хуков не зовёт), и сам отчёт строки не получает.
946
- Выключается автоматика двумя способами — `"hooks": {"enabled": false}` в
947
- настройках (хук остаётся, но молчит) или `size uninstall-hook`; в окружениях, где
948
- обновлять отчёт не нужно (CI, чужая машина, зависимости не поставлены), хук молчит
949
- сам и ничего не пишет в вывод коммита. Что он делает и чем кончился последний
950
- запуск, видно в `pnpm exec size doctor`; отказ инструмента коммит не роняет
951
- причина едет одной строкой и остаётся в записи о запуске.
952
-
953
- ### 7. Ловушки, найденные этой же инструкцией
954
-
955
- Две из них найдены прогоном и уже закрыты они оставлены здесь как объяснение
956
- поведения, а не как обходные пути:
957
-
958
- - **Модуль в расширении `.js`** (`import`/`export` в `.js` — обычное дело в
959
- проектах с бандлером) измеряется как любой другой файл, с `type: module` в
960
- манифесте или без него: гард разбирает результат и как скрипт, и как модуль.
961
- Раньше он пробовал только скрипт и падал кодом 5 на самом `export`, обвиняя
962
- стриппер; сегодня это невозможно, и правки в настройках не требуются
963
- (`REFACTOR.md` R-4.6);
964
- - **Не JavaScript в графе** (разметка или типы прямо в `.js`) это код 2 и
965
- отказ, который называет причину и что править. Причина берётся с того способа,
966
- которым файл считали: при `minify.engine: "esbuild"` отказ называет минификатор
967
- и даёт два выхода (расширению упрощение в `minify.ext` или способ `strip`), а
968
- при упрощении `minify.guard`. Стеком такой случай не выглядит ни там, ни там;
969
- - **Минификатора нет** (установка без необязательных зависимостей, платформа без
970
- `esbuild`)метрика честно отступает к упрощению: числа те же, что у `strip`,
971
- способ говорит об этом словами, а **сборка** (`--write`) отдаёт **код 4** с
972
- готовой починкой. У **проверки** в этом случае ответ из двух частей, и он назван
973
- здесь потому, что именно её советует CI: если отчёт на диске собран с настоящим
974
- минификатором, а прогон идёт без него, точность изменилась значит числа в
975
- таблице больше не совпадают с историей, и проверка скажет про расхождение
976
- (**код 1**), показав разошедшуюся строку подписи, **и тут же назовёт другой счёт**
977
- заметкой с готовой починкой. Вердикт при этом остаётся за расхождением: код 4
978
- утверждал бы, что разница объясняется датчиком, а это никто не проверял
979
- расхождение может быть и правкой мимо отчёта (тот же порядок, что у `size check` и
980
- у `doctor`: нарушение старше приближения). Починка в обоих случаях`pnpm run
981
- sizes`; на этом окружении она вернёт **код 4**.
982
- Проверить это без переустановки можно окружением `SIZE_REPORT_NO_OPTIONAL=1`
983
- тем же приёмом это делает `test/minify.test.js`;
984
- - **Разбор модуля рабочий поток, поднятый один раз на прогон** (`REFACTOR.md`
985
- R-5.4): сам разбор стоит ~0,1 мс, а платится за него стартовой ценой потока
986
- ( 54 мс) и только если в измеряемых файлах вообще есть модули. Отступление
987
- к `node --check` (≈ 86 мс на клетку) осталось на случай, когда файла потока нет
988
- в упаковке, поток не ответил или в Node нет модулей vm;
989
- - **Новый файл-колонка должен быть закоммичен** до запуска: иначе проверка
990
- состояния скажет «не совпало с деревом коммита» (сначала `git add` + коммит,
991
- потом `pnpm run sizes`);
992
- - **Доковая правкатоже правка.** Коммит, тронувший `WORKLOG.md` или любой
993
- файл-колонку, получает в таблице строку, поэтому после него таблицу собирают
994
- заново иначе проверка говорит «расходится с историей git» и называет строку.
995
- Незакоммиченная правка таблицу не двигает («сейчас» берётся из коммита), поэтому
996
- сборка не ломается от того, что рядом с ней правят доки.
997
- - **`--init` не правит `.gitignore`** (`REFACTOR.md` R-4.8) добавьте отчёты
998
- руками, если им не место в истории.
999
-
1000
- Проверено не на словах: раздел пройден покомандно на свежем репозитории (три
1001
- коммита, ESM в `src/`) протокол и найденные расхождения в `WORKLOG.md` §16, а
1002
- пути, которые README называет своими, сверены с деревом. За этим следит сторож
1003
- документации, и он падает вместе с документом, а не по желанию (`REFACTOR.md`
1004
- R-4.1): пути, таблица файлов, зовы и ключи инструкций, числа проверок и цели по
1005
- времени, ссылки на разделы и пин установки проверяются машинно. Формулировки,
1006
- смысл и обещания о будущем машиной не проверяются их держит человек.
1007
-
1008
- ### 8. Если в проекте уже лежит копия инструмента
1009
-
1010
- Порядок выше для проекта, который подключает инструмент впервые. Когда копия
1011
- уже лежит (свои `size-table.js` и его тесты), шаги идут в другом порядке; ниже
1012
- тот, которым переезжал `safe-resets` (`WORKLOG.md` §18):
1013
-
1014
- 1. **Установить, не удаляя копию**две реализации какое-то время сосуществуют,
1015
- и это даёт бесплатную сверку на одном дереве: команда пакета с конфигом проекта
1016
- обязана собрать тот же артефакт байт в байт (у `safe-resets` — 225 673 Б,
1017
- sha256 `1bdb27e1…`). Не совпалодальше не идём.
1018
- 2. **Перевести команды проекта на пакет:** `"test:sizes": "size"`,
1019
- `"sizes": "size --write"`.
1020
- 3. **Удалить копию** и инструмент, и его тест: те же утверждения проверяет
1021
- набор пакета, а в проекте остаётся одна команда. Если тест звался из общего
1022
- раннера, шаг раннера становится одним и зовёт команду пакета, а не файл
1023
- проекта `safe-resets` путь берётся из манифеста установленного пакета,
1024
- чтобы шаг не знал внутренних имён файлов).
1025
- 4. **Убрать колонки удалённых файлов из настроек** и пересобрать артефакт
1026
- **отдельным коммитом**: коммиты, трогавшие только эти файлы, без них не
1027
- двигают ни одного числа, а такие коммиты строк не получают `safe-resets`
1028
- 95 × 27 → 91 × 25).
1029
- 5. **Почистить документацию проекта:** ссылки на файлы инструмента заменяются
1030
- именем пакета и его командами, а описание внутренностей (стриппер, чтение
1031
- истории пачкой, вёрстка) из доков проекта уходит в доки пакета — иначе их две
1032
- копии и они разойдутся.
1033
-
1034
- Доступа к пакету не требуется ни локально, ни в CI репозиторий публичный (§1),
1035
- поэтому шага с ключом в этом порядке нет.
1036
-
1037
- Что при этом теряется: проверки, которые сверяли настройки проекта с ожиданиями
1038
- инструмента, отдельным набором больше не идут. Большую часть закрывает сама
1039
- команда (чужой ключ или незнакомая метрика в конфиге — отказ с объяснением, файл
1040
- таблицы не может быть колонкой), но _содержимое_ подписи (заголовок и команда
1041
- починки взяты из конфига) не проверяет никто: если это важно, это одна проверка
1042
- поверх `--data` в проекте.
1043
-
1044
- ## Гейт против раздувания
1045
-
1046
- **Список проверок один, и он же в CI.** Профиль проверок задан в одном месте
1047
- (`tools/gates/run.js`): `pnpm run verify:fast` (десятки секунд — каждая правка),
1048
- `pnpm run verify` (полный перед отправкой и в CI) и `pnpm run verify:slow`
1049
- (по расписанию то же плюс покрытие). CI зовёт эту же команду, а не свой список:
1050
- работа `verify` (`.github/workflows/ci.yml`) на каждый пуш и запрос правки, работа
1051
- `verify-slow` по расписанию. Совпадение стережёт `test/gates-verify.test.js`:
1052
- проверка, которой нет в профиле, в CI не пройдёт.
1053
-
1054
- **Датчики ловят раздувание, а не стиль** (стиль у линтера): размер и сложность
1055
- функций, размер модулей, дубли веток и функций (`sonarjs`), вес проверок (проверка
1056
- без утверждения, утверждение без сравнения, выключенная проверка), пометки долга,
1057
- клоны по токенам (`jscpd`), циклы и сироты связей (`dependency-cruiser`), просадка
1058
- покрытия против своей же базы (`c8`).
1059
-
1060
- **Порог взят из замера, а не из головы, и он храповик.** По исходному замеру:
1061
- сложность функции p50 1 / p90 4 / p99 11 / max 27 — порог 12 (в базе осталось 5
1062
- функций); длина функции p50 7 / p90 27 / p99 73 / max 118 порог 60 (9 в базе);
1063
- модуль p90 381 строка / max 907 порог 450 базе не осталось ни одного: три
1064
- толстых файла контракт, программа страницы и сборка фикстуры разделены,
1065
- `WORKLOG.md` §59–§61). Всё, что выше порога
1066
- сегодня, лежит в базе (`.eslint-suppressions.json`) и работе не мешает; новое валит
1067
- прогон. Дубли — 13 клонов / 84 строки (0,67 %), связи — 112 модулей / 468 связей и ни
1068
- одной находки.
1069
-
1070
- **Базы обновляет человек.** `pnpm run baseline:metrics`, `baseline:dup`,
1071
- `baseline:coverage` и только с трейлером `Gate-Change:` в сообщении коммита: правка
1072
- гейт-файла без него красна и локально (хук `commit-msg`), и в CI (по каждому коммиту
1073
- диапазона). Иначе гейт ослаблялся бы тем же коммитом, который он останавливает.
1074
- Таблица замеров, отвергнутые инструменты (knip, ast-grep, size-limit, gitleaks) и
1075
- действия человека в `WORKLOG.md` §58.
1076
-
1077
- ## Для ИИ-агента
1078
-
1079
- - `pnpm run verify:fast`перед каждой правкой, `pnpm run verify` перед отправкой;
1080
- что не так и что нельзя трогать при красном `AGENTS.md`.
1081
- - `size check --json` готово ли всё: какая часть истории покрыта, какие пути
1082
- мимо колонок (с коммитом-первопричиной) и какие коммиты выпали без строки.
1083
- - `size explain <коммит> --json` почему у конкретного коммита нет строки: причина,
1084
- тронутые файлы (колонки, исключённые, непокрытые) и готовая починка. Коммит
1085
- именем ревизии (`HEAD`, ветка, тег), полным sha или его началом.
1086
- - `size measure --json` — данные без вёрстки: строки, числа, суммы. Сегодня это
1087
- `--json` (прежняя форма, заморожена эталоном) и `--data` (контракт страницы).
1088
- - `--json` форма ответа, а не отдельный режим, и правило у него одно: ответ
1089
- бывает ровно у четырёх вызовов. Без команды это прежняя форма данных
1090
- (заморожена эталоном паритета), у `check`, `explain` и `doctor` их ответ.
1091
- У команды без ответа и рядом с режимом (`--write`, `--data`, `--init`)
1092
- он отказ, а не тишина: просить JSON там, где его не бывает, ошибка вызова.
1093
- - `size doctor --json` вся диагностика одним ответом: окружение, зависимости,
1094
- настройки, покрытие и находки с уровнем (`action` — делать, `note` — знать).
1095
- - Коды выхода: `0` всё хорошо · `1` расхождение с историей или неполнота ·
1096
- `2` настройки, окружение, неизвестное или лишнее слово, два режима сразу ·
1097
- `3` неполная история · `4` нет датчика · `5` внутренняя ошибка (таблица
1098
- `PLAN.md` §4.1). Действуют уже сейчас: отказ это код и одна строка с готовой
1099
- командой починки, без стека. `--help` печатает и то, и другое.
1100
- - Прогонов два, и оба названы: `pnpm test` — быстрый (каждая правка), `pnpm test:all` —
1101
- полный (выкладка и CI); что в каком и почему — `tools/suites.js`, печатает числа и
1102
- стоимости сам прогон.
1103
- - Разбор аргументов один на входе и до чтения проекта: режим либо один, либо
1104
- отказ с обоими названными; команда и режим вместе не работают; ключ без
1105
- значения и ключ, названный дважды, такой же отказ. Поэтому зов, который
1106
- инструмент не понял, нельзя спутать с исправным прогоном: вместо нуля придёт
1107
- код 2 и готовая команда.
1108
-
1109
- ## Ловушки, на которых стоит проверять движок
1110
-
1111
- Фикстура (`fixtures/synthetic/history.bundle`) — это история, в которой
1112
- собрано то, на чём ломаются такие инструменты: `//` внутри строки, регексп с
1113
- экранированным слэшем, шаблон с выражением, `.mjs` с `export`, не-английское имя
1114
- файла, CRLF, переименование файла, коммит «только отчёт», смешанный коммит,
1115
- слияние с правкой разрешения конфликта, замена символа без изменения объёма,
1116
- удаление и возврат файла, пустой файл, незнакомое расширение. Полный список — в
846
+ The hooks install themselves, and that is the only thing a project notices about installing the package:
847
+ after `npm i` by an install script, with pnpm 10 by the tool's first run (pnpm does not run dependency
848
+ scripts "Ignored build scripts"; it can be allowed with `pnpm.onlyBuiltDependencies:
849
+ ["@vernikr/size-report"]` in your manifest). The files land in `.git`, `git status` does not see them, and the
850
+ command above takes them away. It installs only where that is safe — an ordinary hooks directory, no hook of
851
+ someone else's, something to call the tool with and stays silent where it is not. After every commit and
852
+ merge the report is rebuilt: the `docs` directory and `size-report.html` are created if they are not there
853
+ yet, and a report **tracked** by git lands as a commit of its own signed `chore(report): report rebuilt
854
+ after <sha>` (the signature is quoted as the hook writes it, like every other line of the tool's output in
855
+ this document). Only the report's path is committed: the tree comes from HEAD with that one path replaced, so
856
+ neither someone's index nor uncommitted work can enter the commit.
857
+
858
+ The first report is the exception: while the report is untracked the hook rebuilds it and says so in words
859
+ instead of committing adding a new file to someone else's history is a person's decision. One `git add
860
+ docs/size-report.html` (or a plain `git add -A` if the report belongs in the project) and from then on it
861
+ travels by commits itself. A merge is the same case as an ordinary commit, with one correction to what git
862
+ does: the merge commit is made by git itself and does not run `post-commit`, hence the second file,
863
+ `post-merge` (checked on git 2.50).
864
+
865
+ There is no looping, and by construction rather than by a flag: the report's commit is assembled with
866
+ plumbing (`commit-tree` calls no hooks at all), and the report itself gets no row, so the same rebuild yields
867
+ the same bytes. A refusal by the tool does not bring the commit down — the commit has been made already: the
868
+ cause is printed as one line and remembered, and `pnpm exec size doctor` shows what the hook did and how the
869
+ last run ended. The automation is switched off in two ways — `"hooks": {"enabled": false}` in the settings
870
+ (the hook stays but keeps quiet) or `size uninstall-hook` — while in an environment where updating is not
871
+ wanted at all (CI, someone else's machine) the hook keeps quiet by itself: the hook file lies in `.git`
872
+ rather than in git, so every clone has one of its own, and the body checks whether there is anything to call
873
+ the tool with. `SIZE_REPORT_NO_HOOK` is the lever for one who would rather not edit the settings.
874
+
875
+ ### 7. Traps found by this very instruction
876
+
877
+ Two of them were found by the walkthrough and are closed already — they are kept here as an explanation of
878
+ behaviour rather than as workarounds:
879
+
880
+ - **A module in a `.js` extension** (`import`/`export` in `.js` is ordinary in projects with a bundler) is
881
+ measured like any other file, with `type: module` in the manifest or without it: the guard parses the
882
+ result both as a script and as a module. It used to try the script alone and fell with code 5 on the
883
+ `export` itself, blaming the stripper; that is impossible today and no settings need editing
884
+ (`REFACTOR.md` R-4.6).
885
+ - **Not JavaScript in a column** (markup or types straight in `.js`) is code 2 and a refusal naming the
886
+ reason and what to fix. The reason comes from the way the file was counted: with `minify.engine:
887
+ "esbuild"` the refusal names the minifier and its **one** way out (a simplification for that extension in
888
+ `minify.ext` — the `strip` way would hand the same file to the guard, whose verdict would be the same),
889
+ while with stripping it is the guard's refusal and **two** ways out (take the extension out of
890
+ `minify.guard`, or set `minify.ext`). Neither looks like a stack.
891
+ - **No minifier** (an installation without the optional dependencies, a platform without `esbuild`) — the
892
+ metric honestly falls back to stripping: the numbers are the same as `strip`, the label says so in words,
893
+ and a **build** (`--write`) returns **code 4** with a ready fix. A **check** answers in two parts in that
894
+ case, and it is named here because it is what CI advises: if the report on disk was built with the real
895
+ minifier while the run goes without it, the numbers were counted another way — the numbers in the table
896
+ no longer agree with the history, so the check says as much (**code 1**), showing the diverged signature
897
+ row and **naming the other count right there** in a note with a ready fix. The verdict stays with the
898
+ divergence: code 4 would claim the difference is explained by the sensor, and nobody checked that the
899
+ divergence may also be an edit that went past the report (the same order as `size check` and `doctor`: a
900
+ mismatch outranks the sensor note). The fix in both cases is `pnpm run sizes`; on this environment it returns
901
+ **code 4**. This can be checked without reinstalling by the `SIZE_REPORT_NO_OPTIONAL=1` environment — the
902
+ same way `test/minify.test.js` does it.
903
+ - **The module parse is one worker raised once per a run** (`REFACTOR.md` R-5.4): the fallback to
904
+ `node --check` (a Node run per cell) remains for when the worker's file is not in the package, the worker
905
+ does not answer, or the Node build has no vm modules; and the worker is raised only if the measured files
906
+ hold modules at all. The measured price of both is in `REFACTOR.md` R-5.4 rather than promised in numbers
907
+ here.
908
+ - **A new column file has to be committed** before the run: the table is built from commits, so a file git
909
+ does not track has nothing to measure and its column stays empty. The run itself does not complain — the
910
+ file is named by the settings rather than by the project it is the numbers that would be missing in
911
+ silence. So `git add` + commit first, then `pnpm run sizes`.
912
+ - **An edit to the journal is an edit too.** A commit that touched the journal or any column file gets a row
913
+ in the table, so the table is rebuilt after it — otherwise the check says "diverged from the git history"
914
+ and names the row. An uncommitted edit does not move the table ("now" comes from the commit), so a
915
+ rebuild is not broken by documentation being edited next to it.
916
+ - **`--init` does not edit `.gitignore`** (`REFACTOR.md` R-4.8) — add the report by hand if it has no place
917
+ in the history.
918
+
919
+ Not on words: the section was walked through command by command in a fresh repository, and the findings are
920
+ in `worklog/archive/WORKLOG.md` §16. What keeps it true is the documentation guard (`REFACTOR.md` R-4.1):
921
+ paths, the file table, the calls and flags of the instructions, the numbers of checks, references to sections
922
+ and the install pin are checked by machine. **No time target is declared anywhere** seconds depend on the
923
+ window, so there is nothing to check against (`tools/suites.js` says why). Wording, meaning and promises
924
+ about the future are not checked by machine; a person holds those.
925
+
926
+ ### 8. If a copy of the tool is already in the project
927
+
928
+ The order above is for a project wiring the tool in for the first time. When a copy is already there (its
929
+ own `size-table.js` and its tests), the steps go in another order; below is the one `safe-resets` migrated
930
+ by (`worklog/archive/WORKLOG.md` §18):
931
+
932
+ 1. **Install without removing the copy** two implementations live side by side for a while, and that
933
+ gives a free comparison on one tree: the package's command with the project's config has to assemble the
934
+ same artifact byte for byte (for `safe-resets` — 225 673 B, sha256 `1bdb27e1…`, and both are frozen in
935
+ the parity reference, `fixtures/parity/manifest.json`). No matchdo not go further.
936
+ 2. **Move the project's commands to the package:** `"test:sizes": "size"`, `"sizes": "size --write"`.
937
+ 3. **Remove the copy** — the tool and its test alike: the package's suite checks the same claims, and one
938
+ command stays in the project. If the test was called from a shared runner, the runner's step becomes a
939
+ single one calling the package's command rather than the project's file (in `safe-resets` the path comes
940
+ from the installed package's manifest, so the step knows no internal file names).
941
+ 4. **Take the deleted files' columns out of the settings** and rebuild the artifact in a **commit of its
942
+ own**: commits that touched only those files move no number without them, and such commits get no rows.
943
+ 5. **Clean the project's documentation:** references to the tool's files are replaced by the package's name
944
+ and its commands, while a description of the internals (the stripper, reading the history in batches,
945
+ the assembly) moves from the project's docs into the package's otherwise there are two copies and they
946
+ will drift apart.
947
+
948
+ No access to the package is needed either locally or in CI — the repository is public (§1), so there is no
949
+ key step in this order.
950
+
951
+ What is lost: the checks that compared the project's settings with the tool's expectations no longer run as
952
+ a suite of their own. Most of them are covered by the command itself (an unknown flag or an unfamiliar
953
+ metric in the config is a refusal with an explanation; the report file cannot be a column), but the
954
+ _content of the signature_ (the heading and the fix command taken from the config) is checked by nobody: if
955
+ that matters, it is one check on top of `--data` in the project.
956
+
957
+ ## The gate against bloat
958
+
959
+ **The list of checks is single, and it is the one CI runs.** The profiles live in one place
960
+ (`tools/gates/run.js`): `pnpm run verify:fast` (tens of seconds — every edit), `pnpm run verify` (the full
961
+ one — before pushing and in CI) and `pnpm run verify:slow` (on a schedule — the same plus the suite with
962
+ no machine git settings and coverage). CI calls that same command rather than a list of its own: the job
963
+ `verify` (`.github/workflows/ci.yml`) on every push and pull request, the job `verify-slow` on a schedule.
964
+ That they agree is guarded by `test/gates-verify.test.js`: a check that is not in a profile cannot pass in CI.
965
+
966
+ **The sensors catch bloat rather than style** (style is the linter's business): the size and complexity of
967
+ functions, the size of modules, duplicated branches and functions (`sonarjs`), the weight of checks (a
968
+ check with no assertion, an assertion with no comparison, a switched-off check), debt markers, token clones
969
+ (`jscpd`), cycles and orphans in the graph (`dependency-cruiser`), and coverage falling against its own
970
+ baseline (`c8`).
971
+
972
+ **A threshold comes from a measurement rather than from a guess, and it is a ratchet.** The thresholds
973
+ today: a function's complexity 12, its length 60, a module 450 lines, cognitive complexity 15 — each of
974
+ them cut in the tail of a measured distribution, not in its middle. **Nothing lies above them**: the
975
+ baseline (`.eslint-suppressions.json`) holds nothing at all, so a new overrun fails the run while the
976
+ tree as it stands needs no excuses. The table behind the thresholds is in
977
+ `worklog/archive/WORKLOG.md` §58.3, and its figures describe the tree of that day rather than this one;
978
+ the sensors print their own numbers on every run.
979
+
980
+ **A person updates the baselines.** `pnpm run baseline:metrics`, `baseline:dup` and `baseline:coverage`
981
+ and only with the `Gate-Change:` trailer in the commit message: a gate file edited without it is red both
982
+ locally (the `commit-msg` hook) and over a range (the `pre-push` hook, while CI reads no trailers at all).
983
+ Otherwise the gate would be weakened by the very commit it stops. The table of measurements and the
984
+ rejected tools (knip, ast-grep, size-limit, gitleaks) are in `worklog/archive/WORKLOG.md` §58.
985
+
986
+ ## For an AI agent
987
+
988
+ - `pnpm run verify:fast` before every edit, `pnpm run verify` before pushing; what is wrong and what
989
+ must not be touched when a sensor is red `AGENTS.md`.
990
+ - `size check --json` whether everything is in: how much of the history is covered, which paths went
991
+ past the columns (with the commit that introduced them) and which commits dropped out without a row.
992
+ - `size explain <commit> --json` why one commit has no row: the reason, the files it touched (columns,
993
+ excluded, untracked) and a ready fix. The commit is named by a revision (`HEAD`, a branch, a tag), by a
994
+ full sha or by its beginning.
995
+ - The data without the markup — the rows, the numbers, the totals — is `--json` (the earlier form, frozen
996
+ byte for byte by the parity reference) and `--data` (the contract: absolute values and the shape of the
997
+ table, with nothing derivedwhatever the page can count itself is not there; the page's own block is the
998
+ same data in sparse form). A `size measure` command does not exist yet.
999
+ - `--json` is a form of answer rather than a mode of its own, and it has one rule: exactly four calls have
1000
+ an answer. With no command it is the earlier form of the data (frozen by the parity reference), and for
1001
+ `check`, `explain` and `doctor` it is their answer. For a command with no answer, and next to a mode
1002
+ (`--write`, `--data`, `--init`), it is a refusal rather than silence: asking for JSON where there is none
1003
+ is an error of the call.
1004
+ - `size doctor --json` — all the diagnostics in one answer: the environment, the dependencies, the
1005
+ settings, the coverage and findings with their level (`action` to be done, `note` to be known).
1006
+ - Exit codes: `0` all is well · `1` a mismatch with the history or incomplete coverage · `2` the settings,
1007
+ the environment, an unknown or extra word, two modes at once · `3` a shallow history · `4` no sensor ·
1008
+ `5` an internal error. They work already: a refusal is a code and one line with a ready fix, with no
1009
+ stack. `--help` prints both.
1010
+ - There are two runs, and both are named: `pnpm test` is the fast one (every edit), `pnpm test:all` the
1011
+ full one (a release and CI); what is in which and why is in `tools/suites.js`, while the numbers and the
1012
+ durations are printed by the run itself.
1013
+ - Arguments are parsed once, on the way in and before the project is read: either one mode or a refusal
1014
+ naming both; a command and a mode do not work together; a flag with no value and a flag named twice are
1015
+ such refusals too. So a call the tool did not understand cannot be confused with a healthy run: instead
1016
+ of zero comes code 2 and a ready command.
1017
+
1018
+ ## Traps worth testing the engine on
1019
+
1020
+ The fixture (`fixtures/synthetic/history.bundle`) is a history holding what breaks tools of this kind:
1021
+ `//` inside a string, a regexp with an escaped slash, a template with an expression, `.mjs` with `export`,
1022
+ a file name that is not English, CRLF, a file renamed, a commit that touched only the report, a mixed
1023
+ commit, a merge with a conflict-resolution edit, a character replaced without changing the volume, a file
1024
+ deleted and returned, an empty file, an unknown extension. The full list is in
1117
1025
  `fixtures/synthetic/README.md`.
1118
1026
 
1119
1027
  ```bash
1120
- pnpm test # быстрый прогон (каждая правка): паритет на фикстуре,
1121
- # контракт данных и страница, сторож документации и выпуска
1122
- pnpm test:all # полный прогон (выкладка и CI): то же плюс интеграционные
1123
- # сборка на дисках, сверка с деревом, хуки, метрики
1124
- pnpm run suites:measure # замерить длительность каждого файла набора
1125
- pnpm run parity:live # паритет с живым проектом на клоне, две среды
1126
- node bin/size.js --data # контракт данных: отчёт и агент
1127
- node bin/size.js --write # минимальный отчёт
1128
- node bin/size.js --help # справка и коды выхода
1129
- pnpm run parity # переснять эталон паритета: проект и ревизия из манифеста
1130
- pnpm run fixture # пересобрать фикстуру и её эталон
1131
- pnpm run pack:check # работает ли движок из собранного тарболла
1132
- pnpm run check:standards # эталоны воспроизводятся, а дерево остаётся чистым
1028
+ pnpm test # the fast run (every edit): parity on the fixture,
1029
+ # the data contract and the page, the documentation and release guards
1030
+ pnpm test:all # the full run (a release and CI): the same plus the integration ones
1031
+ # assembling on disk, the comparison with the tree, the hooks, the sensors
1032
+ pnpm run suites:measure # measure every file of the suite
1033
+ pnpm run parity:live # parity with the live project on a clone, two environments
1034
+ node bin/size.js --data # the data contract: the report and an agent
1035
+ node bin/size.js --write # the smallest report
1036
+ node bin/size.js --help # the help and the exit codes
1037
+ pnpm run parity # re-take the parity reference: the project and the revision from the manifest
1038
+ pnpm run fixture # rebuild the fixture and its reference
1039
+ pnpm run pack:check # does the engine work from the assembled tarball
1040
+ pnpm run check:standards # both references reproduce and the tree stays clean
1133
1041
  git clone fixtures/synthetic/history.bundle /tmp/size-report-fixture
1134
1042
  ```
1135
1043
 
1136
- Открытые блокеры и известные пробелы в `BLOCKERS.md`; там же таблица настроек,
1137
- которые проверены и оказались инертными (чтобы не проверять их заново).
1044
+ Open blockers and known gaps are in `BLOCKERS.md`, and next to them the note about the settings that were
1045
+ checked and turned out inert, so as not to check them again.