@vernikr/size-report 2.8.3 → 2.8.5

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