@vernikr/size-report 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@ shows.
10
10
 
11
11
  ## Status
12
12
 
13
- **Release 2.5.0 (2026-09-17).** The tool lives as a package of its own: the registry name is
13
+ **Release 2.6.0 (2026-09-17).** The tool lives as a package of its own: the registry name is
14
14
  `@vernikr/size-report` (published by tag from CI, with no secret). A project may keep no settings at
15
15
  all: without a config file the tool derives them from the project itself and says so in one line,
16
16
  and `--init` pins what was derived into a file. The report is **one file**, the self-contained page
@@ -19,13 +19,19 @@ is installed and on the first run. The version is in the manifest, and every rel
19
19
  the journal — `worklog/` for today's entries, `worklog/archive/WORKLOG.md` for the earlier ones:
20
20
  what changes in the numbers is measured rather than retold.
21
21
 
22
- 2.5.0 changes nothing in the numbers; what it changes is **the language the tool speaks**: the help,
23
- the refusals, the diagnostics and the page are English, and a project with no config file now gets
24
- `locale: en` rather than `ru`. Nothing is thrown away for it the Russian dictionaries stay in the
25
- package and are chosen by `locale` in the settings, and the frozen layer keeps its Russian values,
26
- as `schema: 1` stays what it was. The note of 2.4.0 what that release made visible and in which
27
- order stands in the journal, `worklog/02*`: what changes in the numbers is measured rather than
28
- retold.
22
+ 2.6.0 changes the numbers themselves, and they are measured rather than retold: on this repository's own
23
+ history the page this engine writes is **85 955 B against 1 899 370 B** written by 2.5.0 — 22.1 times
24
+ smaller, −95.5 % because the data travels as one packed block (49 392 B, `base64+gzip`) instead of a list
25
+ of rows, the program is pasted without comments and indentation (28 007 B) and the styling is 6 172 B. The
26
+ page also stops rebuilding itself: a click on a filter rewrites the nodes in place and recomputes the
27
+ totals, the columns carry computed widths under a fixed layout, and two candidates (`content-visibility`,
28
+ `border-collapse: separate`) were measured dead here and left out, with the records in
29
+ `plans/2026-09-17-page-perf/`. **The contract loses fields:** `metrics[].accuracy` and the `approx` bit map
30
+ are gone and the split into exact and approximate numbers with them, so a reader of `--data` that still
31
+ asks for them gets nothing — while the two schema numbers (`1` for the contract, `2` for the packed block)
32
+ stay where they were. The checks grow with the work: 70 → **81** in the fast profile and 175 → **186** in
33
+ the full one. The note of 2.5.0 — the language the tool speaks — stands in the journal,
34
+ `worklog/0203-release-2.5.0.md`.
29
35
 
30
36
  **Parity with the implementation the move started from is proven, not asserted.** The command is
31
37
  `bin/size.js` and the package's entry point is `src/size-table.js` (a re-export only), with the
@@ -65,14 +71,54 @@ instruction below. The step with a key left it later, along with private access.
65
71
  **The data contract and the page.** The engine hands over absolute values and the shape of the table
66
72
  (`--data`), while deltas, totals, "now" and the filters are computed by the page — which is the
67
73
  report itself (`size-report.html`): without that split the filters and "the total over the
68
- selection" are impossible in principle. The contract carries the accuracy of a number as well, a
69
- row of `approx` marks per cell, because that is a fact of the measurement rather than a conclusion:
70
- the page shows what the engine said and keeps no rule of accuracy of its own. The page's panel is a
74
+ selection" are impossible in principle. The contract tells how each number was obtained (the metric's
75
+ `method`) and nothing else about it: the page shows what the engine said and judges no number — it keeps
76
+ no rule of counting of its own, and the split of numbers into exact and approximate was taken out of the
77
+ package. The page's panel is a
71
78
  tree of files by folder, with a switch per folder for the whole subtree; a reader's choice survives
72
79
  a revisit and travels in a link — the page's address is the link. The contract carries the **project
73
80
  catalogue** too: every path git sees, so the page's tree is the project's tree, while numbers exist
74
81
  only for the files that became columns (release 2.2.0).
75
82
 
83
+ **The page's block is the contract in sparse form.** The file carries the history as changes rather than
84
+ as a snapshot per commit — for every file the rows it appeared in (absolute numbers), moved in (deltas
85
+ against its own previous record) and disappeared in — with the texts in a dictionary and the rows' links
86
+ cut by the part they share. The page's own chapter unrolls it back into exactly the contract
87
+ (`src/page/payload.js`), so the calculation and the table know nothing of the sparse form and there is no
88
+ second way to count a row; `--data` still answers with the dense contract, and the block's `schema: 2` is
89
+ what refuses a record written for the previous form. On this repository the data block is
90
+ 1 370 724 → 88 712 B, and its parse is 11.6 → 0.6 ms plus 3.3 ms of unrolling (`contract-data` holds the
91
+ round trip).
92
+
93
+ **The block travels packed, and that is the page's one asynchronous step.** It lies in the file gzipped and
94
+ base64 encoded — the tag says so (`data-pack="base64+gzip"`) — and the page unpacks it with the platform's own
95
+ `DecompressionStream`: no library travels in the page, nothing is fetched, and the whole artifact of this
96
+ repository goes 122 668 → 78 319 B (the block 88 786 → 42 856 B — 74 B more than step 05 measured, because the
97
+ report is itself a column of the report and its own size moved in between). The price is deliberate and twofold: the
98
+ block can no longer be read by eye or by `diff`, and the first drawing waits for a promise where it used to
99
+ happen during the parse. Everything after the first drawing is as synchronous as it was; a host that cannot
100
+ unpack is told in words rather than left with an empty table. The checks read the page in jsdom, which has no
101
+ such API, so the harness puts the platform's own implementations of it into the window — and that seam is
102
+ tested from both sides: the ordinary path with them put in, and the message in words without them.
103
+
104
+ **What the page carries is squeezed, and only what the page carries.** The program and the styling are pasted
105
+ with their comments and indentation out — the same stripping the `min` metric counts — so the artifact holds
106
+ code without ballast while `src/derived.js`, `src/page/*.js`, `src/table.css` and `src/page/app.css` stay the
107
+ ordinary files a person reads: the squeeze lives in the paste and nowhere else. It is 58 922 → 24 885 B of
108
+ program and 15 505 → 5 905 B of styling (the artifact 166 305 → 122 668 B), and the assembled program is
109
+ guarded at build time by the stripper's own `assertCompilable` — a squeeze that ate code stops the build
110
+ rather than the browser.
111
+
112
+ **Minification of what the page carries is decided, not defaulted: not taken — and measured.** esbuild would
113
+ take the pasted program 24 885 → 18 128 B and the styling 5 906 → 5 102 B, the artifact 122 668 → 115 107 B
114
+ (7 561 B), for 91 ms of every build. The price is not those bytes but the contract: esbuild is an **optional**
115
+ dependency and its absence is a different count rather than a refusal, while the artifact is rebuilt by the
116
+ post-commit hook on whatever machine made the commit — a builder that minifies when it can would build **a
117
+ different file** there, and the report would stop being a fixed point. Buying determinism instead would mean
118
+ a pinned version and a page that cannot be assembled at all without esbuild (`src/optional.js`), for 6 % of
119
+ the file. What the decision rests on — the bytes it would save, the fixed point it would cost, and what
120
+ reopens it — is written down beside the plan the step belongs to.
121
+
76
122
  The tool grew out of one script in the consumer project [`safe-resets`](../figma/safe-resets) — the
77
123
  metrics `raw` and "a simplification instead of minification", a static report in git; that path
78
124
  does not exist in this repository, and it is not named anywhere without the project.
@@ -111,8 +157,7 @@ refusal site — the maps `SITES` and `PRINTED` hold the counts — and a case i
111
157
  entry, so a new refusal cannot appear without a check. Refusals a run cannot reach are named
112
158
  explicitly: four are guarded by a check of their own (the catalogue names the file and the phrases),
113
159
  and one cannot be caught at all — "internal error" — which is said where it stands. What the
114
- catalogue does not take on is said in words: wording beyond the listed phrases, and meaning, and the
115
- "!" sign, which is a note (an approximation, a mixed commit, the automation switched off) rather
160
+ catalogue does not take on is said in words: wording beyond the listed phrases, and meaning, andthe "!" sign, which is a note (another count, a mixed commit, the automation switched off) rather
116
161
  than a refusal, with exit code zero.
117
162
 
118
163
  **And the advice in a refusal is executable — that is checked as well.** The truth about the cause is
@@ -149,10 +194,10 @@ commits and installs hooks; the reason for each expensive file is named line by
149
194
 
150
195
  | Run | Command | Checks |
151
196
  |---|---|---|
152
- | Fast — every edit | `pnpm test` | **70 of 175** |
153
- | Full — release and CI | `pnpm test:all` | **175** |
197
+ | Fast — every edit | `pnpm test` | **81 of 186** |
198
+ | Full — release and CI | `pnpm test:all` | **186** |
154
199
 
155
- No check is lost or weakened: the full run starts all 177 with the same files, the fast one takes part
200
+ No check is lost or weakened: the full run starts all 186 with the same files, the fast one takes part
156
201
  of them. The default is the full run — a file becomes fast only explicitly and with a reason — so new
157
202
  expensive work cannot quietly move into the fast one. Two declarations guard that:
158
203
  `test/suites.test.js` (every file classified, and a reason for each) and the documentation guard
@@ -261,9 +306,22 @@ its middle is out of reach.
261
306
 
262
307
  **Folding is pure view, and it counts no numbers.** The subtree lies in the markup and a class on the
263
308
  row hides it, so a click on the sign changes exactly the three things the reader sees — the class, the
264
- sign and the note in the memory. A rebuild here would be honest work for nothing: it counts the whole
265
- table, every row by every column, and so pays for numbers folding does not change. What guards this is
266
- that after folding the table is the same markup rather than a rebuilt one (`test/page-tree.test.js`).
309
+ sign and the note in the memory. What guards this is that after folding the table is the same markup
310
+ rather than a rebuilt one (`test/page-tree.test.js`).
311
+
312
+ **A click shows and hides rather than builds.** The table is assembled once, with every column of every
313
+ file, and a switch afterwards changes only what is visible: a metric is one class on the table plus the
314
+ `colSpan` of the group headings, a file's column is a class per node of it, and a folder or a category is
315
+ the same for each file of its subtree. The table's own nodes stay the objects the first drawing made:
316
+ `test/page-view.test.js` counts what a click appends (a metric: nothing at all; a file, a folder or a
317
+ category: at most the cells of the totals, rows × metrics) and checks that the rows and the cells are
318
+ still the very same objects. What makes this possible is that the order of the columns depends on the
319
+ files rather than on the choice — the last commit's first, then the settings' order — so a hidden column
320
+ keeps its place and the visible ones do not move. The totals are the only numbers a choice changes, and
321
+ they are carried rather than recounted: a sum is linear, so a file switched off subtracts exactly its own
322
+ values, which costs its own rows instead of rows × files. That arithmetic is the step's one new piece,
323
+ and it is held against the engine's own `rowModel` cell by cell for a mixed choice, so that counting a
324
+ row stays in one place (`test/page-view.test.js`).
267
325
 
268
326
  **The columns the last commit touched come first.** The report is rebuilt after every commit, and a
269
327
  reader's first question is what that edit brought. The mark comes from the history rather than from the
@@ -290,17 +348,17 @@ numbers behind the layout is the contract rather than the markup: switching a fo
290
348
  its columns and exactly its volume from the total (`test/contract.test.js`).
291
349
 
292
350
  **A checkbox takes away neither the numbers' room nor the reader's place in the list.** The panel is
293
- drawn anew after every switch, so its scroll and the list's are part of the view like the checkboxes:
294
- both are saved before the rebuild and set back after, and the field under the keyboard comes back with
295
- its focus (without scrolling `preventScroll`), or switching with `Tab` and `Space` would mean walking
296
- the panel from the start again. The file list has no ceiling of its own in a wide window: the panel
351
+ built once and a switch writes only the fields it reached, so the reader's place is his still: there is
352
+ no rebuild that could lose the scroll of the panel or of the list, and the field under the keyboard keeps
353
+ its focus without being found again by hand (`test/page-tree.test.js`) with a rebuild every switch with
354
+ `Tab` and `Space` would mean walking the panel from the start. The file list has no ceiling of its own in a wide window: the panel
297
355
  scrolls, and the list does not push the table. The row of categories sticks to the top of the panel,
298
356
  with the panel's own background (or passing rows of the list would read through it), and the panel's own
299
357
  top padding lives on its first field, which travels away with it. File captions use the table's font size
300
358
  (12.5px), and the legend under the tree is gone on purpose: below the list it pushed the numbers away,
301
359
  while what it explained already stands next to the thing it explains — the sign of a number names the
302
- colour of a delta, accuracy stands under the metric switches, and the mark of a gap lives in the cell's
303
- tooltip.
360
+ colour of a delta, the way each number was counted stands under the metric switches, and the mark of a gap
361
+ lives in the cell's own text.
304
362
 
305
363
  **The panel remembers the reader's choice.** The record lives in the browser's memory, tied to the
306
364
  report's passport — the tool's name, the data schema, the artifact's path, the title and the column
@@ -331,15 +389,14 @@ check rather than a promise (`test/page-view.test.js`, `test/parity.test.js`).
331
389
  `"engine": "strip"` is the earlier removal of comments and indentation. The default did not change,
332
390
  because both frozen references were taken under it. Measured on the fixture: real minification is
333
391
  smaller than stripping in **44 cells and never larger**; `src/code.js` **276 → 185 B**, `src/style.css`
334
- **55 → 43 B**, and over the fixture's history **−1 372 B**. JSON is minified by parsing and so stays
335
- exact, while the formats the minifier does not take are named in the metric's caption together with the
336
- ones it does take. Accuracy is declared twice, and that is not two answers to one question: the caption
337
- speaks of **the worst in the column** one format without minification makes the metric approximate as
338
- a whole rather than hiding behind an exact neighbour while each cell speaks of its own number, and an
339
- approximate one is marked with a dashed line and the method in its tooltip. The worst is taken from the
340
- cells rather than from the engine's name: a report of one JSON is exact even under stripping — parsing
341
- loses only insignificant whitespace, and nobody would make it shorter — and the caption says so. Both
342
- answers come from one rule (`pointExact` in `src/metrics.js`), so they cannot diverge. With no minifier
392
+ **55 → 43 B**, and over the fixture's history **−1 372 B**. JSON is minified by parsing (parsing loses
393
+ only insignificant whitespace, and nobody would make it shorter), while the formats the minifier does not
394
+ take are named in the metric's caption **by extension**: `esbuild (minify, rename); other formats
395
+ (.md .toml) lose comments and indentation`. The split of numbers into exact and approximate is gone from
396
+ the package: how a column was counted is told once, in the metric's method, and no cell carries a mark of
397
+ it any more. The list comes from the columns rather than from the name of the method (`otherCountFormats`
398
+ in `src/metrics.js`), so a report made only of formats the minifier takes says nothing about other
399
+ formats. With no minifier
343
400
  (an installation without optional dependencies, a platform without it) the metric falls back to
344
401
  stripping, the method says so in words and the run answers **code 4** rather than staying silent, while
345
402
  the numbers are the same as the earlier way of counting — byte for byte with the reference. The derived
@@ -365,9 +422,7 @@ and it has nothing to count tokens with. Counting every family on every run woul
365
422
  the reader may never ask about, so the choice of family and encoding lives where it costs time — in the
366
423
  run's settings — while the page **names** it: the method of each metric stands under the switches as
367
424
  text rather than only in a tooltip. Formats without text (a picture, a font, an archive) are named in
368
- the metric's caption together with the reason: their number goes by bytes, the cell of such a file is
369
- marked by the same rule, and the caption takes the worst in the column — two answers have nothing to
370
- diverge with. With no dictionary (an installation without optional dependencies, a platform without it)
425
+ the metric's caption by extension: their number goes by bytes rather than text. With no dictionary (an installation without optional dependencies, a platform without it)
371
426
  the count is an estimate by length with the coefficient named in the method, and the run answers **code
372
427
  4**; the other metrics stay what they were in a report without tokens, and that seam is checked in an
373
428
  environment with no optional dependencies at all (`SIZE_REPORT_NO_OPTIONAL`). Counting tokens costs a
@@ -415,6 +470,7 @@ acceptance for each.
415
470
 
416
471
  | File | Role |
417
472
  |---|---|
473
+ | `plans/` | The plans of work on this repository: `plans/archive/` holds the earlier ones, `plans/2026-09-17-page-perf/` the plan of the page work — an index and one file per step of the report, each with why, what changes, acceptance and the risks (`plans/2026-09-17-page-perf/README.md`) |
418
474
  | `plans/archive/PLAN.md` | **The main document of the move:** inventory, boundaries, invariants, architecture, the seven steps, acceptance, risks, open questions |
419
475
  | `docs/requirements.md` | The customer's requirements: what and why |
420
476
  | `docs/module-design.md` | The design of the extraction: how the module is put together |
@@ -427,6 +483,7 @@ acceptance for each.
427
483
  | `tools/parity-freeze.js` | Takes the parity reference (`pnpm run parity`): with the frozen copy, at the project revision from the manifest — `--json`, the config, the artifact's hash, the tool's hash |
428
484
  | `tools/make-fixture.js` | Assembles the synthetic fixture (`pnpm run fixture`): a deterministic history with traps plus the reference numbers |
429
485
  | `tools/synthetic/` | The subjects of that assembly, one per matter: `repo.js` — how git is spoken to (pinned time, author, settings), `content.js` — what the files hold, `history.js` — which commits come of it, `note.js` — the fixture's note with the list of traps |
486
+ | `probes/` | The scripted measurements behind `plans/2026-09-17-page-perf/`: one file per step, run by hand against live Chrome at the debug port — the fixed layout, where `content-visibility` acts at all, and the two border models with their pixels (`probes/README.md`). Outside the sensors' paths on purpose: they measure the product rather than being part of it, and a suite cannot see layout, paint or a browser's own skipping |
430
487
  | `tools/parity-live.js` | Compares the engine with the live project on a clone: the numbers and the self-contained report at the path the consumer's settings give (`pnpm run parity:live`) |
431
488
  | `tools/pack-check.js` | Assembles the tarball and checks that everything works from it: all sources arrived, the numbers and the report as from the repository (`pnpm run pack:check`) |
432
489
  | `tools/check-standards.js` | Checks that both references reproduce: a re-take goes nowhere and is compared with what is committed (our files byte for byte, the bundle by content), and that the live-history bundle carries `HEAD` (`pnpm run check:standards`) |
@@ -458,14 +515,15 @@ acceptance for each.
458
515
  | `src/css.js` | Reading the styling from disk: which sets of styles exist and what role each has |
459
516
  | `src/table.css` | The report's table: the geometry of a cell, the sticky header and commit column, the colour of deltas |
460
517
  | `src/page/app.css` | The page's styling on top of the shared part: the panel with the file tree and its sticky row of categories (a column on the left on a wide screen, the page fitting the window), the empty states, a narrow window |
461
- | `src/page/state.js` | The page's state: the report's data, the view of the checkboxes, the pointer "which path is which column", folded folders, the record's passport, the browser's memory and the exchange by link — a chapter of the page's program |
518
+ | `src/page/payload.js` | The page's block in sparse form, and the one place that unrolls it back: the history as changes (a file's appearance, its moves, its disappearance) turned into the snapshots the calculation and the table already speak — a value that did not move is one object shared by the rows that hold it |
519
+ | `src/page/state.js` | The page's state: the report's data (the block unrolled by the payload chapter), the view of the checkboxes, the pointer "which path is which column", folded folders, the record's passport, the browser's memory and the exchange by link — a chapter of the page's program |
462
520
  | `src/page/dom.js` | The page's nodes: the small helpers of markup (`appEl`, `appBox`) — one set for the panel and the table alike |
463
- | `src/page/panel.js` | The panel of choices: the switches of metrics and files, the categories, the tree of the project's paths (files outside the report keep a checkbox off with a reason and stand after the rest; folders carry a folding sign that hides the subtree by a class rather than by a rebuild); a redraw is asked of the assembling chapter |
464
- | `src/page/table.js` | The page's table: a cell, a commit's caption, the header and the empty states — markup over the shared calculation |
465
- | `src/page/app.js` | Assembling and starting the page: the whole table, a redraw on the reader's choice (with the focus and the scroll put back), the first drawing and an anchor change; pasted into the assembled page |
466
- | `src/page/build.js` | Assembling the page: data, styling and program in one file with no external references |
521
+ | `src/page/panel.js` | The panel of choices: the switches of metrics and files, the categories, the tree of the project's paths (files outside the report keep a checkbox off with a reason and stand after the rest; folders carry a folding sign that hides the subtree by a class rather than by a rebuild); built once, with the fields of the switches and of the folders and categories written where they stand |
522
+ | `src/page/table.js` | The page's table, built once: a cell, a commit's caption, the header, the empty states and the cache of the nodes of every column — markup over the shared calculation, with the totals carried rather than recounted |
523
+ | `src/page/app.js` | Assembling and starting the page: the first drawing, then a switch that shows, hides and rewrites the totals without making a node; an anchor change; pasted into the assembled page |
524
+ | `src/page/build.js` | Assembling the page: data, styling and program in one file with no external references — the pasted text is **squeezed** on the way in (comments and indentation out, the same stripping the `min` metric counts) while the sources keep them, and the result is guarded by the stripper's own `assertCompilable` |
467
525
  | `src/git.js` | The only border where git is called: the pinned settings, blobs by the batch, the history, the comparison with the working tree |
468
- | `src/strip.js` | Removing ballast: which form goes to which file (extension, strategy) and what counts as an exact number — the entry to the parsing of forms |
526
+ | `src/strip.js` | Removing ballast: which form goes to which file (extension, strategy) and which strategies are minification itself — the entry to the parsing of forms |
469
527
  | `src/strip/js.js` | Removing comments and indentation in JS: a pass over the cases (a comment, a regexp, a string, a character) — through strings and templates as well |
470
528
  | `src/strip/forms.js` | The forms of text with a removal of their own: markup, styles, the lines of a file and JSON |
471
529
  | `src/strip/guard.js` | The stripper's guard: what was stripped has to compile — as a script in the process or as a module in a worker thread |
@@ -494,7 +552,7 @@ acceptance for each.
494
552
  | `test/api.test.js` | The package's public API: the list of names is frozen, and splitting the engine may not change it |
495
553
  | `eslint.config.js` | The rules of formatting: the same as the consumer project's, plus a ban on gluing operators into one line (`pnpm run lint`, `pnpm run lint:strict`) |
496
554
  | `tools/harness.js` | The harness of the checks: paths, clones of the fixture (including one shared per suite and one with CRLF), running the tool, reading refusals, hashes |
497
- | `tools/page-harness.js` | The harness of the contract and page checks: the contract data, the assembled page, reading it in a real DOM, the panel's switches — one for five suites |
555
+ | `tools/page-harness.js` | The harness of the contract and page checks: the contract data, the assembled page, reading it in a real DOM, the panel's switches, the page's calculation and its decoder evaluated from their sources, the block unpacked, and the platform's unpacker put into jsdom (which has none) — one for six suites |
498
556
  | `tools/suites.js` | The split of the suite: which files go into the fast run (with a reason for each) and why every dear one is in the full run |
499
557
  | `tools/run-tests.js` | Running the suite (`pnpm test`, `pnpm test:all`, `pnpm run suites:measure`): each file's duration measured on its own, and the counts of checks adding up |
500
558
  | `tools/docs-facts.js` | Reading facts out of the documentation — one layer for the four checks of the documentation guard: what a document names (paths, calls, section addresses) against what the repository holds |
@@ -508,11 +566,12 @@ acceptance for each.
508
566
  | `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 |
509
567
  | `test/refusals.test.js` | The refusals are executed: each one is called by a run, its exit code and its promised phrases are compared (with clones of their own for someone else's hook, a shallow history and a branch past the report), and **the advice runs** — the command answers with the promised code and no stack, while where "the refusal is gone" is declared the same call answers differently after it |
510
568
  | `test/refusals-catalog.test.js` | The guard of the refusal catalogue: every refusal site in the sources has an entry, every entry declares its advice, and refusals handed to another check are really accepted by it (the named file and line are checked) |
511
- | `test/contract-data.test.js` | The data contract: the numbers against the reference, the set of fields against the derived quantities, the marks of approximation against a metric's caption |
569
+ | `test/contract-data.test.js` | The data contract: the numbers against the reference, the set of fields against the derived quantities, the metric's method against the way the numbers were counted — and the round trip through the page's sparse block, which restores the contract whole and twice over the same bytes |
512
570
  | `test/contract-derived.test.js` | The derived quantities against the artifact's numbers: a row's totals, a cell's delta and the delta of a total — on the code that lies in the tree |
513
- | `test/page-view.test.js` | The assembled page: pasted with no copy of the calculation, self-contained, the empty states, the styling and the switches |
514
- | `test/page-tree.test.js` | The panel's file tree: folders by the project's paths, three states, the subtree, files and folders outside the report (a checkbox off, a place after the rest), folding without a rebuild and the scroll across a rebuild |
571
+ | `test/page-view.test.js` | The assembled page: pasted with no copy of the calculation, self-contained, the empty states, the styling, the switches, a click that makes no table and the carried totals against the engine's own sums |
572
+ | `test/page-tree.test.js` | The panel's file tree: folders by the project's paths, three states, the subtree, files and folders outside the report (a checkbox off, a place after the rest), folding without a rebuild and a scroll a click does not touch |
515
573
  | `test/page-choice.test.js` | The memory of the choice and the exchange by link: a revisit, someone else's report, a foreign and a broken record, an address change on an open page |
574
+ | `test/page-cols.test.js` | The fixed layout the page carries: a column's width is counted from the model rather than measured in a laid-out cell, the clip keeps a caption inside its column, the sticky header and commit column keep their edges, and the table names its own width (`width: auto` would hand the layout back to the automatic algorithm) |
516
575
  | `test/module.test.js` | A module under a `.js` extension: measured without touching the settings; the stripper's guard is alive (proved by mutation) and does not accuse the innocent |
517
576
  | `test/guard.test.js` | Parsing a module: it goes through a thread, both paths give one verdict, the fallback works with the thread's file away, and hundreds of parses are cheaper than a launch |
518
577
  | `test/runner.test.js` | Reading a process's output: chunks are glued as buffers rather than appended to a string — a multi-byte character at a chunk border does not turn into two replacement characters |
@@ -563,8 +622,9 @@ The seams between modules follow the borders of data: above sit the parts that r
563
622
  (`git`, `strip`, `metrics`, `history`), below the parts that work on values already collected (`data`,
564
623
  `derived`, `page`), while the settings, the texts and the refusal stand at the edges, because everyone
565
624
  knows them and they know no one. Both reports are counted at build time: the page gets the sources of the
566
- shared calculation and of its own program pasted in (`src/derived.js`, `src/page/*.js`), because it opens
567
- from disk, with no server and no network. The rest is planned step by step in `plans/archive/PLAN.md`.
625
+ shared calculation and of its own program pasted in (`src/derived.js`, `src/page/*.js`) and squeezed on the way
626
+ in, because it opens from disk, with no server and no network. The rest is planned step by step in
627
+ `plans/archive/PLAN.md`.
568
628
 
569
629
  ## Wiring it into your project
570
630
 
@@ -591,7 +651,7 @@ The same release can be taken by a reference to the repository — installation
591
651
  registry, but stays tied to a revision:
592
652
 
593
653
  ```bash
594
- pnpm add -D github:vernikr/size-report#v2.5.0
654
+ pnpm add -D github:vernikr/size-report#v2.6.0
595
655
  ```
596
656
 
597
657
  With no network (or nothing to fetch from codeload) — the tarball: `pnpm pack` in the package clone, then
@@ -603,7 +663,7 @@ the branch moves on the installation fails with `Could not resolve <sha> to a co
603
663
  observation rather than reasoning: the short pin `6530237` installed while `main` stood on it and stopped
604
664
  working at the very next commit, while the same sha in full installed. A branch name (`#main`) and a tag
605
665
  are both accepted, but a branch is a moving target and a tag is constant: this release stands on the tag
606
- `v2.5.0`, which is also the one in the example (forty characters work as well, but they have to be copied
666
+ `v2.6.0`, which is also the one in the example (forty characters work as well, but they have to be copied
607
667
  out of the history by eye).
608
668
 
609
669
  The revision in the example is a part of the claim rather than decoration: what is described below is
@@ -674,7 +734,7 @@ known to a person. What is edited most often:
674
734
  | `columns` | the table's columns: `{label, paths: [...]}`; **a column is a file**: the list of paths is its renames (a revision takes whichever of them it holds), not several files at once; `label` is what a person will see |
675
735
  | `metrics` | what a number is made of: `raw` (the size of the git object), `min` (the minified form — which one, `minify.engine` decides), `tok` (tokens), `gzip` |
676
736
  | `tokens.family`, `tokens.encoding` | the dictionary for `tok`: the family (`openai`) and the encoding (`o200k_base` or `cl100k_base`) — the encoding changes the number, which is why it is both in the settings and in the metric's label |
677
- | `minify.engine` | what counts `min`: `strip` (comments and indentation, with no accuracy promised) or `esbuild` (real compression; a format without a minifier counts as stripping, and the metric's label says so) |
737
+ | `minify.engine` | what counts `min`: `strip` (comments and indentation — a simplification, and the method names it as one) or `esbuild` (real compression; a format the minifier does not take counts as stripping, and the method says so) |
678
738
  | `output` | the report file (in the derived profile `docs/size-report.html`; the directory is created by the writer). The path enters the report's passport — the key of the saved choice — so a changed path means a fresh choice |
679
739
  | `journal` | where to look for the journal sections the rows refer to |
680
740
  | `links.commitUrl` | the commit link template, for example `https://github.com/org/repo/commit/{sha}`; derived from the `origin` address for GitHub and GitLab (for other hosts — empty rather than a guess) |
@@ -732,11 +792,11 @@ commit".
732
792
 
733
793
  `size doctor` gathers all the diagnostics into one answer: the environment and its influence on the numbers
734
794
  (the machine's settings do not influence them — the engine pins them at the call's border), the state of
735
- the optional dependencies and what it means for accuracy, the validity of the settings and the completeness
795
+ the optional dependencies and what it means for the count, the validity of the settings and the completeness
736
796
  of the coverage. It answers with the same pieces as the other commands: the coverage block is exactly the
737
797
  answer of `size check` rather than a second calculation. The exit code is the first by importance rather
738
798
  than "something was found": `2` the settings are unreadable (there is nothing else to read), `3` the
739
- history is cut short, `1` the coverage is incomplete, `4` a number is approximate, `0` nothing to do. A
799
+ history is cut short, `1` the coverage is incomplete, `4` a sensor counted another way, `0` nothing to do. A
740
800
  sensor the settings are silent about is named unneeded rather than missing, and it is not loaded: the
741
801
  dictionary weighs megabytes, and there is nothing to pay with for an answer the numbers never needed.
742
802
 
@@ -768,7 +828,7 @@ put together from are the templates above.
768
828
  | 1 | the table diverged from the history (or an edit on disk is not committed); for `size check` — a path of the history is neither tracked nor excluded | `pnpm run sizes` and commit the table; for `check` — add the path as a column or to `skip` |
769
829
  | 2 | something in the call or in the project — the causes are quoted as the tool prints them: **command line** (unknown flag, flag without a value, repeated flag, two modes at once, extra word, command and mode, unknown command, incompatible flag, no JSON answer, two answers at once, no commit); **settings and the project** (no settings file, settings not parsed, settings invalid, git missing, not a git repository, config already exists); **history** (no such commit, ambiguous commit, commit outside the history); **hook** (foreign hook, foreign core.hooksPath, no way to invoke the tool); **measurement** (file is not JavaScript, minifier did not parse) | the refusal text names the reason and a ready command — and it is executable: `test/refusals.test.js` guards that |
770
830
  | 3 | a shallow history (a clone with `--depth`) | a full clone: `git fetch --unshallow` |
771
- | 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 approximation) while the note about the other count is printed next to it |
831
+ | 4 | no sensor | `minify.engine: "esbuild"` with no minifier: the numbers are stripped rather than minified. The report is built, and its text carries the reason and the fix; if the table also diverges from the history, the code stays **1** (a mismatch outranks the sensor note) while the note about the other count is printed next to it |
772
832
  | 5 | an internal error | this is a defect of the tool: we are the ones who need the text — see "Traps worth testing the engine on" below |
773
833
 
774
834
  The cell of code 2 quotes the tool rather than describing it: those are the names of the refusal registry
@@ -832,12 +892,12 @@ behaviour rather than as workarounds:
832
892
  metric honestly falls back to stripping: the numbers are the same as `strip`, the label says so in words,
833
893
  and a **build** (`--write`) returns **code 4** with a ready fix. A **check** answers in two parts in that
834
894
  case, and it is named here because it is what CI advises: if the report on disk was built with the real
835
- minifier while the run goes without it, the accuracy has changed — the numbers in the table no longer
836
- agree with the history, so the check says as much (**code 1**), showing the diverged signature row and
837
- **naming the other count right there** in a note with a ready fix. The verdict stays with the divergence:
838
- code 4 would claim the difference is explained by the sensor, and nobody checked that — the divergence
839
- may also be an edit that went past the report (the same order as `size check` and `doctor`: a mismatch
840
- outranks an approximation). The fix in both cases is `pnpm run sizes`; on this environment it returns
895
+ minifier while the run goes without it, the numbers were counted another way — the numbers in the table
896
+ no longer agree with the history, so the check says as much (**code 1**), showing the diverged signature
897
+ row and **naming the other count right there** in a note with a ready fix. The verdict stays with the
898
+ divergence: code 4 would claim the difference is explained by the sensor, and nobody checked that — the
899
+ divergence may also be an edit that went past the report (the same order as `size check` and `doctor`: a
900
+ mismatch outranks the sensor note). The fix in both cases is `pnpm run sizes`; on this environment it returns
841
901
  **code 4**. This can be checked without reinstalling by the `SIZE_REPORT_NO_OPTIONAL=1` environment — the
842
902
  same way `test/minify.test.js` does it.
843
903
  - **The module parse is one worker raised once per a run** (`REFACTOR.md` R-5.4): the fallback to
@@ -933,9 +993,9 @@ rejected tools (knip, ast-grep, size-limit, gitleaks) are in `worklog/archive/WO
933
993
  excluded, untracked) and a ready fix. The commit is named by a revision (`HEAD`, a branch, a tag), by a
934
994
  full sha or by its beginning.
935
995
  - The data without the markup — the rows, the numbers, the totals — is `--json` (the earlier form, frozen
936
- byte for byte by the parity reference) and `--data` (the page's contract: absolute values and the shape of
937
- the table, with nothing derived — whatever the page can count itself is not there). A `size measure`
938
- command does not exist yet.
996
+ byte for byte by the parity reference) and `--data` (the contract: absolute values and the shape of the
997
+ table, with nothing derived — whatever the page can count itself is not there; the page's own block is the
998
+ same data in sparse form). A `size measure` command does not exist yet.
939
999
  - `--json` is a form of answer rather than a mode of its own, and it has one rule: exactly four calls have
940
1000
  an answer. With no command it is the earlier form of the data (frozen by the parity reference), and for
941
1001
  `check`, `explain` and `doctor` it is their answer. For a command with no answer, and next to a mode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vernikr/size-report",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "author": "vernikr",
5
5
  "repository": {
6
6
  "type": "git",
@@ -65,7 +65,7 @@
65
65
  "report"
66
66
  ],
67
67
  "devDependencies": {
68
- "@vernikr/size-report": "2.4.0",
68
+ "@vernikr/size-report": "2.5.0",
69
69
  "c8": "10",
70
70
  "dependency-cruiser": "17",
71
71
  "eslint": "^9.18.0",
package/src/check.js CHANGED
@@ -68,7 +68,7 @@ function pathCoverage(cfg, commits) {
68
68
  }
69
69
 
70
70
  /* The full answer: settings, history, paths, sensors — and the verdict. `ok` means only "nothing went past
71
- * the report": a sensor counting approximately does not change the verdict (it has an exit code of its own,
71
+ * the report": a sensor counting another way does not change the verdict (it has an exit code of its own,
72
72
  * as in every other mode). */
73
73
  export function coverage(cfg, root, configFile) {
74
74
  assertFullHistory(root);
package/src/data.js CHANGED
@@ -75,28 +75,10 @@ export function reportData(cfg, root) {
75
75
  * changed just now rather than the rest. This is a fact from the history rather than a derived quantity:
76
76
  * it does not count anything, it says what the edit touched. */
77
77
  last: last,
78
- approx: approxMarks(rows, state, cfg),
79
78
  skipped: dropped.map(skipLine)
80
79
  };
81
80
  }
82
81
 
83
- /* Marks of approximate cells — one record per metric: a string of marks over the row cells and one over the
84
- * "now" row. '1' means the number came out of stripping or an estimate, '0' that it is exact. A metric with
85
- * no mark at all does not appear in the report: all numbers exact — silence.
86
- *
87
- * The engine sets a mark where it counts the number, by the same rule as the metric's label. Hence the page
88
- * derives nothing about paths and formats: it only shows what was said and keeps no second rule of accuracy. */
89
- function approxMarks(rows, state, cfg) {
90
- const out = {};
91
- cfg.metrics.forEach((m) => {
92
- const mark = (flags) => (flags !== null && flags[m] ? '1' : '0');
93
- const inRows = rows.map((r) => r.approx.map(mark).join('')).join('');
94
- const now = state.map((s) => mark(s === null ? null : s.approx)).join('');
95
- if (inRows.indexOf('1') >= 0 || now.indexOf('1') >= 0) out[m] = { rows: inRows, now: now };
96
- });
97
- return out;
98
- }
99
-
100
82
  /* The part of a row shared by both answers: the page's contract (`--data`) and the older `--json` form (frozen
101
83
  * by the parity fixture). One place, because these two answers can drift apart exactly here, and **the order
102
84
  * of the fields is the bytes of the answer**: the object holds them in the order the frozen form prints. */
package/src/doctor.js CHANGED
@@ -48,12 +48,12 @@ function environment(root) {
48
48
 
49
49
  /* Dependencies: what counts the metrics here and now. The very loaders the sensors use are asked
50
50
  * (`minifier`, `tokenizer`), so the answer cannot drift from the number: without the minifier `min` counts
51
- * by approximation, without the dictionary `tok` by estimate.
51
+ * by simplification, without the dictionary `tok` by estimate.
52
52
  *
53
53
  * Only what the project actually asked for is loaded: the dictionary weighs megabytes, and touching it for
54
54
  * the sake of an "installed" line would mean paying for an answer the numbers never needed (the same rule as
55
55
  * in the report: `test/tokens.test.js`). An unwanted sensor is named unneeded rather than unknown — it does
56
- * not affect accuracy, and that is the answer; "unknown" stays for the case where the settings are unreadable
56
+ * not affect the count, and that is the answer; "unknown" stays for the case where the settings are unreadable
57
57
  * and there is nobody to ask. */
58
58
  const UNREADABLE = 'unknown: the settings cannot be read';
59
59
 
@@ -132,8 +132,8 @@ function hookFindings(hooks) {
132
132
  }
133
133
 
134
134
  /* Coverage is the same answer `size check` gives, plus the kind of trouble if there is one: an incomplete path
135
- * or an approximating sensor (the kinds weigh differently — `WEIGHT`), with incompleteness outranking, because
136
- * without it there are no numbers at all.
135
+ * or a sensor counting another way (the kinds weigh differently — `WEIGHT`), with incompleteness outranking,
136
+ * because without it there are no numbers at all.
137
137
  * What becomes a finding and what does not: the report holds the whole coverage block (the same text as
138
138
  * `size check`), so incompleteness is not retold a second time — it weighs. Sensors do get a finding:
139
139
  * `size check` prints them as a `!` line, while here they are part of the answer. */
package/src/history.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { assertFullHistory, diskForm, diskHashes, git, headTree, readBlobs, readHistory } from './git.js';
4
- import { METRICS, measureBlob, pointExact } from './metrics.js';
4
+ import { METRICS, measureBlob } from './metrics.js';
5
5
  import { touchedSection } from './journal.js';
6
6
  import { EXIT, refuse } from './refusal.js';
7
7
 
@@ -82,15 +82,10 @@ function applyPicks(pass, c, picks) {
82
82
  }
83
83
  const blob = pass.blobs.get(pick.spec);
84
84
  const cells = {};
85
- /* Approximation is a property of the path rather than of the blob: whether the format goes to
86
- * the minifier depends on its extension. It is computed here along with the measurement, and
87
- * so it does not enter the content cache. */
88
- const approx = {};
89
85
  pass.metrics.forEach((m) => {
90
86
  cells[m] = pass.measure(m, blob, pick.path, c.sha);
91
- approx[m] = !pointExact(m, pick.path, pass.cfg);
92
87
  });
93
- pass.state[i] = { path: pick.path, sha: blob.sha, cells: cells, approx: approx };
88
+ pass.state[i] = { path: pick.path, sha: blob.sha, cells: cells };
94
89
  });
95
90
  }
96
91
 
@@ -127,8 +122,7 @@ function stepCommit(pass, c, ci) {
127
122
  when: c.when,
128
123
  subject: c.subject,
129
124
  section: section,
130
- cells: pass.state.map((s) => (s === null ? null : s.cells)),
131
- approx: pass.state.map((s) => (s === null ? null : s.approx))
125
+ cells: pass.state.map((s) => (s === null ? null : s.cells))
132
126
  });
133
127
  }
134
128
 
package/src/init.js CHANGED
@@ -14,7 +14,8 @@ import { packageManager } from './project.js';
14
14
  * pass the very check the first run will apply**, or the advice leads a person into a new dead end.
15
15
  */
16
16
 
17
- /* What to say after writing: what was written, what will replace the approximations, and what to do
17
+ /* What to say after writing: what was written, what counts the same numbers when the optional
18
+ * dependency is missing, and what to do
18
19
  * next. The lines are assembled into a list rather than printed as they come, so that "what was said"
19
20
  * can be read as a whole. */
20
21
  function draftLines(root, target, cfg) {
package/src/locales.js CHANGED
@@ -37,12 +37,6 @@ export const LOCALES = {
37
37
  categoryByExtension: 'по расширению',
38
38
  all: 'все',
39
39
  sub: '{tool} {version} · {artifact}',
40
- /* Words of precision: a metric label speaks about the worst in its column, a cell
41
- * about its own number. One word serves both the label and the cell hint, so the two
42
- * cannot drift or sound differently. */
43
- exact: 'точное число',
44
- approximate: 'приближение',
45
- approximateCell: 'приближённое число: ',
46
40
  panelMethod: 'способ:',
47
41
  emptyMetrics: 'Ни одна метрика не выбрана — таблицу не из чего собрать. '
48
42
  + 'Включите хотя бы одну в панели выше: без метрик нет ни одной колонки с числом.',
@@ -54,6 +48,11 @@ export const LOCALES = {
54
48
  + 'что выбрано в этом браузере.',
55
49
  linkExtra: 'Часть ссылки пропущена: в этом отчёте нет {n} названных имён. '
56
50
  + 'Остальное применено.',
51
+ /* Данные отчёта лежат в файле сжатыми, и распаковывает их браузер. Текст называет причину
52
+ * (браузера нет в списке тех, что умеют `DecompressionStream`) и что с этим делать — пустая
53
+ * таблица не сказала бы читателю ничего. */
54
+ unpack: 'Данные отчёта сжаты, а браузер не умеет их распаковать: в нём нет DecompressionStream. '
55
+ + 'Обновите браузер — таблицу показывать не из чего.',
57
56
  note: 'Абсолютные размеры стоят один раз — в строке «{now}». Строк: {rows}: коммиты без '
58
57
  + 'изменения объёма и коммиты, тронувшие только выключенные файлы, строк не получают. '
59
58
  + 'Собрано из истории git: {command}.'
@@ -99,9 +98,6 @@ export const LOCALES = {
99
98
  categoryByExtension: 'by extension',
100
99
  all: 'all',
101
100
  sub: '{tool} {version} · {artifact}',
102
- exact: 'an exact number',
103
- approximate: 'an approximation',
104
- approximateCell: 'an approximate number: ',
105
101
  panelMethod: 'method:',
106
102
  emptyMetrics: 'No metric is selected — there is nothing to build the table from. '
107
103
  + 'Turn on at least one above: without metrics there is no column holding a number.',
@@ -113,6 +109,10 @@ export const LOCALES = {
113
109
  + 'has.',
114
110
  linkExtra: 'Part of the link is skipped: this report has none of the {n} names '
115
111
  + 'it mentions. The rest is applied.',
112
+ /* The report's data lies in the file compressed and the browser unpacks it; this text is what a
113
+ * browser without `DecompressionStream` gets instead of an empty table. */
114
+ unpack: 'The report\u2019s data is compressed and this browser cannot unpack it: there is no '
115
+ + 'DecompressionStream. Update the browser — there is nothing to build the table from.',
116
116
  note: 'Absolute sizes appear once — in the “{now}” row. Rows: {rows}: commits that change '
117
117
  + 'no number, and commits touching only the disabled files, get no row. '
118
118
  + 'Collected from git history: {command}.'