@vernikr/size-report 2.5.0 → 2.7.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.7.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,41 @@ 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.7.0 is about the page and what a reader does with it, and its figures are measured rather than retold.
23
+ **The report's address stays clean:** a switch writes the record into the browser's memory and nothing into the
24
+ address bar the page no longer puts a `#size-report=…` tail on the report it was opened with — while a link
25
+ made by an earlier release is still read, and a foreign or broken one is still explained in words. **A test
26
+ is a category of its own**, right after Code: the files of `test`, `tests` and `fixtures` folders wherever
27
+ they stand, and every name carrying `.test` before its extension on this repository's own history Tests
28
+ holds **47 of 317 columns**, and the checkbox of the documentation is called **Docs**. **The tree opens
29
+ folded**, and the unfolding is what the memory keeps, under a key of its own; inside a level a hidden name —
30
+ one beginning with a dot — stands after every visible one. **The drawing of a switch is measured, and it is one
31
+ task**: the click is counted and answered, a drawing short enough to be over before the browser could paint is
32
+ done on the click, and a longer one happens in the next task with a stripe over the top edge of the window. The
33
+ price of a switch is the browser's own relayout of the table — 234.5 ms of layout for a *single* cell of this
34
+ repository's report (249 rows, 317 columns, 238 thousand cells), 287 ms for 750, 539 ms for 6 000 — while the
35
+ class changes are about 2 µs a node; on this page the whole Table of Contents of the report (73 columns) is
36
+ 867 ms blocked with a 742 ms task. A queue worked off in slices was written first and refused by measurement:
37
+ the same click in 37 slices paid that relayout 37 times — **169 layouts and 151.9 s of pure layout time against
38
+ 1.04 s**, with the tab growing to gigabytes of repaint and the reader waiting four minutes for a switch that
39
+ costs him less than a second unsliced. Within one task nothing can repaint, so a bar that filled would be a bar
40
+ nobody could read: the stripe travels and says the work is going on (`probes/step-12-columns.mjs`, the records in
41
+ `worklog/0207-page-choice-bar.md`). The order of the columns did not change there: what the last commit touched
42
+ still stands first, the rest after it in the settings' order.
43
+
44
+ 2.6.0 changes the numbers themselves, and they are measured rather than retold: on this repository's own
45
+ history the page this engine writes is **85 955 B against 1 899 370 B** written by 2.5.0 — 22.1 times
46
+ smaller, −95.5 % — because the data travels as one packed block (49 392 B, `base64+gzip`) instead of a list
47
+ of rows, the program is pasted without comments and indentation (28 007 B) and the styling is 6 172 B. The
48
+ page also stops rebuilding itself: a click on a filter rewrites the nodes in place and recomputes the
49
+ totals, the columns carry computed widths under a fixed layout, and two candidates (`content-visibility`,
50
+ `border-collapse: separate`) were measured dead here and left out, with the records in
51
+ `plans/2026-09-17-page-perf/`. **The contract loses fields:** `metrics[].accuracy` and the `approx` bit map
52
+ are gone and the split into exact and approximate numbers with them, so a reader of `--data` that still
53
+ asks for them gets nothing — while the two schema numbers (`1` for the contract, `2` for the packed block)
54
+ stay where they were. The checks grow with the work: 70 → **81** in the fast profile and 175 → **186** in
55
+ the full one. The note of 2.5.0 — the language the tool speaks — stands in the journal,
56
+ `worklog/0203-release-2.5.0.md`.
29
57
 
30
58
  **Parity with the implementation the move started from is proven, not asserted.** The command is
31
59
  `bin/size.js` and the package's entry point is `src/size-table.js` (a re-export only), with the
@@ -65,14 +93,55 @@ instruction below. The step with a key left it later, along with private access.
65
93
  **The data contract and the page.** The engine hands over absolute values and the shape of the table
66
94
  (`--data`), while deltas, totals, "now" and the filters are computed by the page — which is the
67
95
  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
96
+ selection" are impossible in principle. The contract tells how each number was obtained (the metric's
97
+ `method`) and nothing else about it: the page shows what the engine said and judges no number — it keeps
98
+ no rule of counting of its own, and the split of numbers into exact and approximate was taken out of the
99
+ package. The page's panel is a
71
100
  tree of files by folder, with a switch per folder for the whole subtree; a reader's choice survives
72
- a revisit and travels in a linkthe page's address is the link. The contract carries the **project
101
+ a revisit in the browser's memory, and the address keeps a clean tail an address with a link of its
102
+ own is read and never written (release 2.7.0). The contract carries the **project
73
103
  catalogue** too: every path git sees, so the page's tree is the project's tree, while numbers exist
74
104
  only for the files that became columns (release 2.2.0).
75
105
 
106
+ **The page's block is the contract in sparse form.** The file carries the history as changes rather than
107
+ as a snapshot per commit — for every file the rows it appeared in (absolute numbers), moved in (deltas
108
+ against its own previous record) and disappeared in — with the texts in a dictionary and the rows' links
109
+ cut by the part they share. The page's own chapter unrolls it back into exactly the contract
110
+ (`src/page/payload.js`), so the calculation and the table know nothing of the sparse form and there is no
111
+ second way to count a row; `--data` still answers with the dense contract, and the block's `schema: 2` is
112
+ what refuses a record written for the previous form. On this repository the data block is
113
+ 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
114
+ round trip).
115
+
116
+ **The block travels packed, and that is the page's one asynchronous step.** It lies in the file gzipped and
117
+ base64 encoded — the tag says so (`data-pack="base64+gzip"`) — and the page unpacks it with the platform's own
118
+ `DecompressionStream`: no library travels in the page, nothing is fetched, and the whole artifact of this
119
+ repository goes 122 668 → 78 319 B (the block 88 786 → 42 856 B — 74 B more than step 05 measured, because the
120
+ report is itself a column of the report and its own size moved in between). The price is deliberate and twofold: the
121
+ block can no longer be read by eye or by `diff`, and the first drawing waits for a promise where it used to
122
+ happen during the parse. Everything after the first drawing is as synchronous as it was; a host that cannot
123
+ unpack is told in words rather than left with an empty table. The checks read the page in jsdom, which has no
124
+ such API, so the harness puts the platform's own implementations of it into the window — and that seam is
125
+ tested from both sides: the ordinary path with them put in, and the message in words without them.
126
+
127
+ **What the page carries is squeezed, and only what the page carries.** The program and the styling are pasted
128
+ with their comments and indentation out — the same stripping the `min` metric counts — so the artifact holds
129
+ code without ballast while `src/derived.js`, `src/page/*.js`, `src/table.css` and `src/page/app.css` stay the
130
+ ordinary files a person reads: the squeeze lives in the paste and nowhere else. It is 58 922 → 24 885 B of
131
+ program and 15 505 → 5 905 B of styling (the artifact 166 305 → 122 668 B), and the assembled program is
132
+ guarded at build time by the stripper's own `assertCompilable` — a squeeze that ate code stops the build
133
+ rather than the browser.
134
+
135
+ **Minification of what the page carries is decided, not defaulted: not taken — and measured.** esbuild would
136
+ take the pasted program 24 885 → 18 128 B and the styling 5 906 → 5 102 B, the artifact 122 668 → 115 107 B
137
+ (7 561 B), for 91 ms of every build. The price is not those bytes but the contract: esbuild is an **optional**
138
+ dependency and its absence is a different count rather than a refusal, while the artifact is rebuilt by the
139
+ post-commit hook on whatever machine made the commit — a builder that minifies when it can would build **a
140
+ different file** there, and the report would stop being a fixed point. Buying determinism instead would mean
141
+ a pinned version and a page that cannot be assembled at all without esbuild (`src/optional.js`), for 6 % of
142
+ the file. What the decision rests on — the bytes it would save, the fixed point it would cost, and what
143
+ reopens it — is written down beside the plan the step belongs to.
144
+
76
145
  The tool grew out of one script in the consumer project [`safe-resets`](../figma/safe-resets) — the
77
146
  metrics `raw` and "a simplification instead of minification", a static report in git; that path
78
147
  does not exist in this repository, and it is not named anywhere without the project.
@@ -111,8 +180,7 @@ refusal site — the maps `SITES` and `PRINTED` hold the counts — and a case i
111
180
  entry, so a new refusal cannot appear without a check. Refusals a run cannot reach are named
112
181
  explicitly: four are guarded by a check of their own (the catalogue names the file and the phrases),
113
182
  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
183
+ 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
184
  than a refusal, with exit code zero.
117
185
 
118
186
  **And the advice in a refusal is executable — that is checked as well.** The truth about the cause is
@@ -149,10 +217,10 @@ commits and installs hooks; the reason for each expensive file is named line by
149
217
 
150
218
  | Run | Command | Checks |
151
219
  |---|---|---|
152
- | Fast — every edit | `pnpm test` | **70 of 175** |
153
- | Full — release and CI | `pnpm test:all` | **175** |
220
+ | Fast — every edit | `pnpm test` | **81 of 186** |
221
+ | Full — release and CI | `pnpm test:all` | **186** |
154
222
 
155
- No check is lost or weakened: the full run starts all 177 with the same files, the fast one takes part
223
+ No check is lost or weakened: the full run starts all 186 with the same files, the fast one takes part
156
224
  of them. The default is the full run — a file becomes fast only explicitly and with a reason — so new
157
225
  expensive work cannot quietly move into the fast one. Two declarations guard that:
158
226
  `test/suites.test.js` (every file classified, and a reason for each) and the documentation guard
@@ -242,7 +310,9 @@ choice. The report itself is always in the catalogue, whether or not it is track
242
310
  of the moment, and the page must not depend on it, or the first rebuild in a fresh clone would give
243
311
  different bytes. A folder whose files are only partly in the report writes its count as a fraction
244
312
  ("2/5"), and everything outside the report stands after everything inside it — folders and leaves
245
- alike — so that the report is seen at once in a list where half the rows do not switch
313
+ alike — so that the report is seen at once in a list where half the rows do not switch. The name of a level
314
+ holds a second rule: a hidden name — one beginning with a dot — stands after every visible one, the alphabet
315
+ deciding the rest, so the service files of a project do not stand in the reader's way
246
316
  (`test/page-tree.test.js`).
247
317
 
248
318
  **A folder is a switch like a file, and its sign is a decision of its own.** The checkbox of a folder
@@ -251,8 +321,10 @@ next to it. Neither a folder nor a category button keeps state of its own: both
251
321
  checkboxes, so the tree, the buttons and the table cannot drift apart. The sign beside a folder answers
252
322
  a different question — how much of the tree is visible, which is the onlooker's business rather than the
253
323
  reader's choice — so it is remembered between visits in a record of its own, under a key of its own and
254
- the same report passport, and it never goes into the link; unfolding every folder removes that record,
255
- just as turning the checkboxes back on removes the choice.
324
+ the same report passport, and it never goes into a link. The tree opens **folded**: a project's tree is
325
+ longer than the window, and the reader looks at a short list first, so what the record keeps is the
326
+ unfolding rather than the folding — folding every folder back removes the record, just as turning the
327
+ checkboxes back on removes the choice.
256
328
 
257
329
  **The list scrolls, and there is one scroll.** On a narrow window it is the file list that scrolls —
258
330
  the panel grows with the page there — while on a wide one the whole panel does: otherwise the controls
@@ -261,9 +333,38 @@ its middle is out of reach.
261
333
 
262
334
  **Folding is pure view, and it counts no numbers.** The subtree lies in the markup and a class on the
263
335
  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`).
336
+ sign and the note in the memory. What guards this is that after folding the table is the same markup
337
+ rather than a rebuilt one (`test/page-tree.test.js`).
338
+
339
+ **A switch draws its columns in one task, with a stripe over the page while it is going on.** The class
340
+ changes are cheap — about 2 µs a node, so a category of this repository's report (73 columns, 55 042 nodes) is
341
+ 120 ms of them — while the browser lays the whole table out again for any change of a column's visibility, and
342
+ that is where the seconds are (`probes/step-12-columns.mjs`). So `src/page/work.js` asks for the drawing once:
343
+ work short enough to be over before the browser could paint goes on the click itself, and a longer one goes to
344
+ the next task with the stripe over the top edge of the window. **The stripe carries no share, and that is the
345
+ measurement rather than a shortcut**: inside one task the browser cannot repaint, so a length would be a length
346
+ nobody could keep; what moves is an indeterminate highlight. A queue worked off between timeouts was written
347
+ first and refused by the same probe — 37 slices paid the table's relayout 37 times (2.5–4 s each, 169 layouts
348
+ against 1) — and the numbers stay out of all of it, because the totals are counted on the click itself, where
349
+ they cost arithmetic rather than nodes. Nothing is asked for a column that is already right either
350
+ (`appColumnStale`): a report opened with everything switched on has nothing to draw, and a record from the
351
+ memory or a link queues exactly the columns that differ from it. What guards the two halves is
352
+ `test/page-choice.test.js` — a long drawing that leaves the click's line at once, runs in the next task and
353
+ takes the stripe away, and a short one that is over before the click returns.
354
+
355
+ **A click shows and hides rather than builds.** The table is assembled once, with every column of every
356
+ file, and a switch afterwards changes only what is visible: a metric is one class on the table plus the
357
+ `colSpan` of the group headings, a file's column is a class per node of it, and a folder or a category is
358
+ the same for each file of its subtree. The table's own nodes stay the objects the first drawing made:
359
+ `test/page-view.test.js` counts what a click appends (a metric: nothing at all; a file, a folder or a
360
+ category: at most the cells of the totals, rows × metrics) and checks that the rows and the cells are
361
+ still the very same objects. What makes this possible is that the order of the columns depends on the
362
+ files rather than on the choice — the last commit's first, then the settings' order — so a hidden column
363
+ keeps its place and the visible ones do not move. The totals are the only numbers a choice changes, and
364
+ they are carried rather than recounted: a sum is linear, so a file switched off subtracts exactly its own
365
+ values, which costs its own rows instead of rows × files. That arithmetic is the step's one new piece,
366
+ and it is held against the engine's own `rowModel` cell by cell for a mixed choice, so that counting a
367
+ row stays in one place (`test/page-view.test.js`).
267
368
 
268
369
  **The columns the last commit touched come first.** The report is rebuilt after every commit, and a
269
370
  reader's first question is what that edit brought. The mark comes from the history rather than from the
@@ -290,17 +391,17 @@ numbers behind the layout is the contract rather than the markup: switching a fo
290
391
  its columns and exactly its volume from the total (`test/contract.test.js`).
291
392
 
292
393
  **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
394
+ built once and a switch writes only the fields it reached, so the reader's place is his still: there is
395
+ no rebuild that could lose the scroll of the panel or of the list, and the field under the keyboard keeps
396
+ its focus without being found again by hand (`test/page-tree.test.js`) with a rebuild every switch with
397
+ `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
398
  scrolls, and the list does not push the table. The row of categories sticks to the top of the panel,
298
399
  with the panel's own background (or passing rows of the list would read through it), and the panel's own
299
400
  top padding lives on its first field, which travels away with it. File captions use the table's font size
300
401
  (12.5px), and the legend under the tree is gone on purpose: below the list it pushed the numbers away,
301
402
  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.
403
+ colour of a delta, the way each number was counted stands under the metric switches, and the mark of a gap
404
+ lives in the cell's own text.
304
405
 
305
406
  **The panel remembers the reader's choice.** The record lives in the browser's memory, tied to the
306
407
  report's passport — the tool's name, the data schema, the artifact's path, the title and the column
@@ -314,32 +415,32 @@ neither the numbers nor the markup. The passport holds neither the tool's versio
314
415
  history, and on purpose: updating the tool does not change what a column means, while a grown history is
315
416
  the very history the reader comes back to.
316
417
 
317
- **The same choice travels as a link.** The page's address is the link: the record that goes into the
318
- browser's memory goes into the anchor too (`#size-report=…`), so the sender copies the address and the
319
- recipient sees that choice with no action at all. The link outranks the memory it is the sender's
320
- explicit choice while it does not replace the reader's own until he changes something. A foreign or
321
- broken address is not applied, and is not silent either: a line above the table names the reason ("the
322
- link was made in another report" / "the choice in the address is unreadable"), the view stays the
323
- reader's own, and the incoming address is not rewritten; names the report does not hold are reported by
324
- count, skipped, and the rest is applied. The link works on an already open page as well: the browser does
325
- not reload the document when the anchor changes, so the page reads the address itself, or a link would
326
- only work in a new tab. The page makes no request to the network at all, and that is an assertion of a
327
- check rather than a promise (`test/page-view.test.js`, `test/parity.test.js`).
418
+ **The link is read and never written.** The page's address is the report's own: a choice lives in the
419
+ browser's memory, and the page puts nothing into the address bar a report opened from disk keeps the
420
+ address it was opened with, and not one switch lands in the tab's title bar. What an earlier release sent
421
+ as a link is still read, though the record in `#size-report=…`, in the same shape the memory holds it —
422
+ and such a link outranks the memory while it does not replace the reader's own until he changes something.
423
+ A foreign or broken address is not applied, and is not silent either: a line above the table names the
424
+ reason ("the link was made in another report" / "the choice in the address is unreadable"), the view stays
425
+ the reader's own, and the incoming address is not rewritten; names the report does not hold are reported
426
+ by count, skipped, and the rest is applied. The link works on an already open page as well: the browser
427
+ does not reload the document when the anchor changes, so the page reads the address itself, or a link
428
+ would only work in a new tab. The page makes no request to the network at all, and that is an assertion of
429
+ a check rather than a promise (`test/page-view.test.js`, `test/parity.test.js`).
328
430
 
329
431
  **The `min` metric can count for real.** The way of counting is chosen in the settings:
330
432
  `"minify": {"engine": "esbuild"}` minifies JS/TS/CSS for real through an optional dependency, while
331
433
  `"engine": "strip"` is the earlier removal of comments and indentation. The default did not change,
332
434
  because both frozen references were taken under it. Measured on the fixture: real minification is
333
435
  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
436
+ **55 → 43 B**, and over the fixture's history **−1 372 B**. JSON is minified by parsing (parsing loses
437
+ only insignificant whitespace, and nobody would make it shorter), while the formats the minifier does not
438
+ take are named in the metric's caption **by extension**: `esbuild (minify, rename); other formats
439
+ (.md .toml) lose comments and indentation`. The split of numbers into exact and approximate is gone from
440
+ the package: how a column was counted is told once, in the metric's method, and no cell carries a mark of
441
+ it any more. The list comes from the columns rather than from the name of the method (`otherCountFormats`
442
+ in `src/metrics.js`), so a report made only of formats the minifier takes says nothing about other
443
+ formats. With no minifier
343
444
  (an installation without optional dependencies, a platform without it) the metric falls back to
344
445
  stripping, the method says so in words and the run answers **code 4** rather than staying silent, while
345
446
  the numbers are the same as the earlier way of counting — byte for byte with the reference. The derived
@@ -365,9 +466,7 @@ and it has nothing to count tokens with. Counting every family on every run woul
365
466
  the reader may never ask about, so the choice of family and encoding lives where it costs time — in the
366
467
  run's settings — while the page **names** it: the method of each metric stands under the switches as
367
468
  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)
469
+ 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
470
  the count is an estimate by length with the coefficient named in the method, and the run answers **code
372
471
  4**; the other metrics stay what they were in a report without tokens, and that seam is checked in an
373
472
  environment with no optional dependencies at all (`SIZE_REPORT_NO_OPTIONAL`). Counting tokens costs a
@@ -415,6 +514,7 @@ acceptance for each.
415
514
 
416
515
  | File | Role |
417
516
  |---|---|
517
+ | `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
518
  | `plans/archive/PLAN.md` | **The main document of the move:** inventory, boundaries, invariants, architecture, the seven steps, acceptance, risks, open questions |
419
519
  | `docs/requirements.md` | The customer's requirements: what and why |
420
520
  | `docs/module-design.md` | The design of the extraction: how the module is put together |
@@ -427,6 +527,7 @@ acceptance for each.
427
527
  | `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
528
  | `tools/make-fixture.js` | Assembles the synthetic fixture (`pnpm run fixture`): a deterministic history with traps plus the reference numbers |
429
529
  | `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 |
530
+ | `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
531
  | `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
532
  | `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
533
  | `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 +559,16 @@ acceptance for each.
458
559
  | `src/css.js` | Reading the styling from disk: which sets of styles exist and what role each has |
459
560
  | `src/table.css` | The report's table: the geometry of a cell, the sticky header and commit column, the colour of deltas |
460
561
  | `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 |
562
+ | `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 |
563
+ | `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 |
462
564
  | `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 |
565
+ | `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 |
566
+ | `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, and the count answerable for how many nodes a column holds |
567
+ | `src/page/work.js` | The page's long drawing: the columns of a switch in one task, on the click when they are few and in the next task with an indeterminate stripe over the top edge of the window when they are many — one task because the browser's relayout of the table is the price, and it is paid once (this is where the measurement that refused the slices stands) |
568
+ | `src/page/app.js` | Assembling and starting the page: the first drawing, then a switch that counts the numbers on the click, hands the columns to the queue and writes the fields it reached without making a node; an anchor change; pasted into the assembled page |
569
+ | `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
570
  | `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 |
571
+ | `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
572
  | `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
573
  | `src/strip/forms.js` | The forms of text with a removal of their own: markup, styles, the lines of a file and JSON |
471
574
  | `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 |
@@ -483,7 +586,7 @@ acceptance for each.
483
586
  | `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 |
484
587
  | `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 |
485
588
  | `src/journal.js` | The journal and links: which section a commit belongs to and where a description leads |
486
- | `src/data.js` | The file categories and the contract with the page (`--data`): the numbers, the shape of the table and the catalogue of the project's paths |
589
+ | `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 |
487
590
  | `src/config.js` | The consumer project's settings: the defaults, reading them, checking them |
488
591
  | `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 |
489
592
  | `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 |
@@ -494,7 +597,7 @@ acceptance for each.
494
597
  | `test/api.test.js` | The package's public API: the list of names is frozen, and splitting the engine may not change it |
495
598
  | `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
599
  | `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 |
600
+ | `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
601
  | `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
602
  | `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
603
  | `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 +611,12 @@ acceptance for each.
508
611
  | `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
612
  | `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
613
  | `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 |
614
+ | `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
615
  | `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 |
515
- | `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 |
616
+ | `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 |
617
+ | `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 |
618
+ | `test/page-choice.test.js` | The reader's choice and the work a click starts: 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 the drawing of a long switch in a task of its own behind the stripe |
619
+ | `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
620
  | `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
621
  | `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
622
  | `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 +667,9 @@ The seams between modules follow the borders of data: above sit the parts that r
563
667
  (`git`, `strip`, `metrics`, `history`), below the parts that work on values already collected (`data`,
564
668
  `derived`, `page`), while the settings, the texts and the refusal stand at the edges, because everyone
565
669
  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`.
670
+ shared calculation and of its own program pasted in (`src/derived.js`, `src/page/*.js`) and squeezed on the way
671
+ in, because it opens from disk, with no server and no network. The rest is planned step by step in
672
+ `plans/archive/PLAN.md`.
568
673
 
569
674
  ## Wiring it into your project
570
675
 
@@ -591,7 +696,7 @@ The same release can be taken by a reference to the repository — installation
591
696
  registry, but stays tied to a revision:
592
697
 
593
698
  ```bash
594
- pnpm add -D github:vernikr/size-report#v2.5.0
699
+ pnpm add -D github:vernikr/size-report#v2.7.0
595
700
  ```
596
701
 
597
702
  With no network (or nothing to fetch from codeload) — the tarball: `pnpm pack` in the package clone, then
@@ -603,7 +708,7 @@ the branch moves on the installation fails with `Could not resolve <sha> to a co
603
708
  observation rather than reasoning: the short pin `6530237` installed while `main` stood on it and stopped
604
709
  working at the very next commit, while the same sha in full installed. A branch name (`#main`) and a tag
605
710
  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
711
+ `v2.7.0`, which is also the one in the example (forty characters work as well, but they have to be copied
607
712
  out of the history by eye).
608
713
 
609
714
  The revision in the example is a part of the claim rather than decoration: what is described below is
@@ -674,7 +779,7 @@ known to a person. What is edited most often:
674
779
  | `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
780
  | `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
781
  | `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) |
782
+ | `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
783
  | `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
784
  | `journal` | where to look for the journal sections the rows refer to |
680
785
  | `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 +837,11 @@ commit".
732
837
 
733
838
  `size doctor` gathers all the diagnostics into one answer: the environment and its influence on the numbers
734
839
  (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
840
+ the optional dependencies and what it means for the count, the validity of the settings and the completeness
736
841
  of the coverage. It answers with the same pieces as the other commands: the coverage block is exactly the
737
842
  answer of `size check` rather than a second calculation. The exit code is the first by importance rather
738
843
  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
844
+ history is cut short, `1` the coverage is incomplete, `4` a sensor counted another way, `0` nothing to do. A
740
845
  sensor the settings are silent about is named unneeded rather than missing, and it is not loaded: the
741
846
  dictionary weighs megabytes, and there is nothing to pay with for an answer the numbers never needed.
742
847
 
@@ -768,7 +873,7 @@ put together from are the templates above.
768
873
  | 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
874
  | 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
875
  | 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 |
876
+ | 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
877
  | 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
878
 
774
879
  The cell of code 2 quotes the tool rather than describing it: those are the names of the refusal registry
@@ -832,12 +937,12 @@ behaviour rather than as workarounds:
832
937
  metric honestly falls back to stripping: the numbers are the same as `strip`, the label says so in words,
833
938
  and a **build** (`--write`) returns **code 4** with a ready fix. A **check** answers in two parts in that
834
939
  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
940
+ minifier while the run goes without it, the numbers were counted another way — the numbers in the table
941
+ no longer agree with the history, so the check says as much (**code 1**), showing the diverged signature
942
+ row and **naming the other count right there** in a note with a ready fix. The verdict stays with the
943
+ divergence: code 4 would claim the difference is explained by the sensor, and nobody checked that — the
944
+ divergence may also be an edit that went past the report (the same order as `size check` and `doctor`: a
945
+ mismatch outranks the sensor note). The fix in both cases is `pnpm run sizes`; on this environment it returns
841
946
  **code 4**. This can be checked without reinstalling by the `SIZE_REPORT_NO_OPTIONAL=1` environment — the
842
947
  same way `test/minify.test.js` does it.
843
948
  - **The module parse is one worker raised once per a run** (`REFACTOR.md` R-5.4): the fallback to
@@ -933,9 +1038,9 @@ rejected tools (knip, ast-grep, size-limit, gitleaks) are in `worklog/archive/WO
933
1038
  excluded, untracked) and a ready fix. The commit is named by a revision (`HEAD`, a branch, a tag), by a
934
1039
  full sha or by its beginning.
935
1040
  - 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.
1041
+ byte for byte by the parity reference) and `--data` (the contract: absolute values and the shape of the
1042
+ table, with nothing derived — whatever the page can count itself is not there; the page's own block is the
1043
+ same data in sparse form). A `size measure` command does not exist yet.
939
1044
  - `--json` is a form of answer rather than a mode of its own, and it has one rule: exactly four calls have
940
1045
  an answer. With no command it is the earlier form of the data (frozen by the parity reference), and for
941
1046
  `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.7.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.6.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
@@ -10,19 +10,35 @@ import { TOOL_PKG } from './tool.js';
10
10
  * derived quantity at all. Everything the page counts itself begins where this module ends. */
11
11
 
12
12
  /* A file's category is only for the page's quick on/off buttons for a group: it does not reach the
13
- * numbers. The rule is one: the extension gives the category, everything else counts as code; a category
14
- * set in the column's settings outranks that rule, and the data says where it came from (`categoryBy`) — a
15
- * manual decision is explainable, while a table of extensions is a guess by file name. */
13
+ * numbers. The rule is: a category set in the column's settings outranks everything, a test is
14
+ * recognised by its path rather than by its extension, and for the rest the extension gives the
15
+ * category while everything else counts as code. The data says where the category came from
16
+ * (`categoryBy`) — a manual decision is explainable, while a table of extensions is a guess by file
17
+ * name. */
16
18
  export const CATEGORY_EXTS = {
17
19
  docs: ['.md', '.markdown', '.rst', '.txt', '.adoc'],
18
20
  chore: ['.json', '.yaml', '.yml', '.toml', '.ini', '.cfg', '.conf', '.lock', '.editorconfig'],
19
21
  assets: ['.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.woff', '.woff2', '.ttf', '.otf']
20
22
  };
21
- export const CATEGORY_ORDER = ['code', 'docs', 'chore', 'assets'];
23
+ /* Tests stand next to code: they are files of the same kind (`*.test.js` is JavaScript too), and a reader
24
+ * switching code off wants to see what the tests weigh separately. */
25
+ export const CATEGORY_ORDER = ['code', 'tests', 'docs', 'chore', 'assets'];
26
+
27
+ /* What makes a file a test: the folder it lies in, wherever that folder is in the project, or a name
28
+ * carrying `.test` before its extension. Both are read off the path rather than off the extension, which is
29
+ * how a test is recognised in practice — a test may be written in any language the tool can measure. */
30
+ const TEST_DIRS = ['test', 'tests', 'fixtures'];
31
+ const TEST_NAME = /\.test\.[^./]+$/;
32
+
33
+ function isTest(p) {
34
+ return p.split('/').some((part) => TEST_DIRS.indexOf(part) >= 0) || TEST_NAME.test(path.basename(p));
35
+ }
22
36
 
23
37
  export function categoryOf(col) {
24
38
  if (col.category) return { key: col.category, by: 'config' };
25
- const ext = path.extname(col.paths[col.paths.length - 1]).toLowerCase();
39
+ const last = col.paths[col.paths.length - 1];
40
+ if (isTest(last)) return { key: 'tests', by: 'auto' };
41
+ const ext = path.extname(last).toLowerCase();
26
42
  const known = CATEGORY_ORDER.find((key) => (CATEGORY_EXTS[key] || []).indexOf(ext) >= 0);
27
43
  return { key: known === undefined ? 'code' : known, by: 'auto' };
28
44
  }
@@ -75,28 +91,10 @@ export function reportData(cfg, root) {
75
91
  * changed just now rather than the rest. This is a fact from the history rather than a derived quantity:
76
92
  * it does not count anything, it says what the edit touched. */
77
93
  last: last,
78
- approx: approxMarks(rows, state, cfg),
79
94
  skipped: dropped.map(skipLine)
80
95
  };
81
96
  }
82
97
 
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
98
  /* The part of a row shared by both answers: the page's contract (`--data`) and the older `--json` form (frozen
101
99
  * by the parity fixture). One place, because these two answers can drift apart exactly here, and **the order
102
100
  * of the fields is the bytes of the answer**: the object holds them in the order the frozen form prints. */