@vernikr/size-report 2.8.1 → 2.8.2

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.8.1 (2026-09-18).** The tool lives as a package of its own: the registry name is
13
+ **Release 2.8.2 (2026-09-18).** 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,6 +19,25 @@ 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.8.2 is about the page's own sources and the checks that guard them, and its figures are measured rather than retold.
23
+ **The four page suites that went quiet with the `<table>` are alive again against the window:** `test/page-view.test.js`,
24
+ `page-cols`, `page-tree` and `page-choice` read the grid and the panel they guard rather than markup that no longer
25
+ exists, and what they promise is what they hold — the pasted program with no second calculation, the sparse block, the
26
+ recount by the choice, the order of the columns, the tree's folds and its memory, the link's refusals. **Their readers
27
+ are no longer each suite's own:** `tools/page-harness.js` keeps one set of them (a file's path at HEAD, the rows the
28
+ window built, the numbers of a row and its captions, a folder's and a category's switch, the place the shell stands at),
29
+ and the copies the suites carried left with it. **And the page's chapters shed the duplication the same request
30
+ found:** the choice's record and the folded tree share one reading and one writing of the browser's memory
31
+ (`appLoad`/`appKeep`), a folder's two figures come from one walk of its subtree (`appSub`), the three walks over the
32
+ metrics and the files of a record became the panels' one question asked twice (`appReach`), and the three paths of a
33
+ click became one drawing (`appChanged`) — while two rules that never took effect went with them (a dead `.cap` of the
34
+ styling, an undefined colour under the folding sign). Measured in bytes for the same history: the chapters **−745 B**
35
+ (37 lines), the suites **−18 462 B** (251 lines), the harness **+4 328 B** — the tree loses **14 879 B and 220 lines**,
36
+ and the artifact **85 730 → 84 991 B**, its program and its styling being exactly what the chapters are. The checks go
37
+ 89 → **86** in the fast profile and 194 → **191** in the full one: what a reader of the page does is one place now
38
+ rather than five, and a check that read the table it no longer has is not a check. The figures and the reasoning stand
39
+ in `worklog/0210-page-suites-on-the-window.md`.
40
+
22
41
  2.8.1 is the drawing of the page put right after its table became a window, and its figures are measured rather than
23
42
  retold. **The panel and the table were sharing one class:** the table's rows were styled by a bare `.row` while the
24
43
  panel's own rows are `.panel .row`, so the table's rule took the metrics row and the three method lines out of the flow
@@ -49,11 +68,10 @@ stripe over a drawing is gone with the freeze it was drawn for**: a switch costs
49
68
  for an indicator to indicate, and `src/page/work.js` left the tree with it. A virtualizer library was measured and not
50
69
  taken — `@tanstack/virtual-core` is ~6.7 kB gzip and headless, `virtua`'s grid is experimental and `Clusterize.js`
51
70
  knows rows and not columns — because each would be vendored into the artifact, which the report then measures as its own
52
- bytes. **The checks of the old table are deactivated while it is rebuilt:** `test/page-view.test.js`, `page-cols`,
53
- `page-tree` and `page-choice` skip every check with its reason at the top of the file, and this document names them
54
- where they are the promise-holders of what they described; `test/page-grid.test.js` is what guards the window meanwhile.
55
- The figures and the reasoning stand in `worklog/0208-table-window.md`. The checks grow with the work: 81 → **88** in the
56
- fast profile and 186 → **193** in the full one.
71
+ bytes. The checks of the old table went quiet with it, each naming its reason, and 2.8.2 brought them back against the
72
+ window; `test/page-grid.test.js` is what guards the window itself. The figures and the reasons stand in
73
+ `worklog/0208-table-window.md`. The checks grow with the work: 81 **88** in the fast profile and 186 **193**
74
+ in the full one.
57
75
 
58
76
  2.7.0 is about the page and what a reader does with it, and its figures are measured rather than retold.
59
77
  **The report's address stays clean:** a switch writes the record into the browser's memory and nothing into the
@@ -253,8 +271,8 @@ commits and installs hooks; the reason for each expensive file is named line by
253
271
 
254
272
  | Run | Command | Checks |
255
273
  |---|---|---|
256
- | Fast — every edit | `pnpm test` | **89 of 194** |
257
- | Full — release and CI | `pnpm test:all` | **194** |
274
+ | Fast — every edit | `pnpm test` | **86 of 191** |
275
+ | Full — release and CI | `pnpm test:all` | **191** |
258
276
 
259
277
  No check is lost or weakened: the full run starts every file, the fast one takes part of them. The default is the full run — a file becomes fast only explicitly and with a reason — so new
260
278
  expensive work cannot quietly move into the fast one. Two declarations guard that:
@@ -637,7 +655,7 @@ acceptance for each.
637
655
  | `test/api.test.js` | The package's public API: the list of names is frozen, and splitting the engine may not change it |
638
656
  | `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`) |
639
657
  | `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 |
640
- | `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 seven suites |
658
+ | `tools/page-harness.js` | The harness of the contract and page checks: the contract data, the assembled page, reading it in a real DOM, one set of readers for every page suite (a file's path at HEAD, the rows the window built and their numbers, the captions over them, the order of the columns, a folder's and a category's switch, the place the shell stands at), the page's calculation and its decoder evaluated from their sources, the block unpacked, and the platform's unpacker put into jsdom (which has none) — one for seven suites |
641
659
  | `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 |
642
660
  | `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 |
643
661
  | `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 |
@@ -654,10 +672,10 @@ acceptance for each.
654
672
  | `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 |
655
673
  | `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 |
656
674
  | `test/page-grid.test.js` | The grid of the page as a window: what the reader sees is built and no more, the geometry of the styling against the script's, the rows and the columns that a scroll builds and drops, a number under its own caption, a file and a metric switched off, every row of the window against the engine's own calculation, and the drawing's own rules — the order of the columns by the numbers, one border, the header's rows of one height, and the panel out of the shared styling's reach |
657
- | `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 **deactivated** while the table is rebuilt, every check skipped with its reason in the file |
658
- | `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 — **deactivated** with the table, the checks skipped with their reason |
659
- | `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 — **deactivated**: the choice and the link are the same, while the counts and the stripe are not |
660
- | `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 **deactivated** with the table: the columns are one fixed width now |
675
+ | `test/page-view.test.js` | The assembled page: pasted with no copy of the calculation, self-contained, the sparse block, the recount by the choice, every total of the window against the engine's own sums, no mark of precision left on a cell, the shared styling pasted once and the delta colour set once, the empty states, and the switches' focus |
676
+ | `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 |
677
+ | `test/page-choice.test.js` | The reader's choice: the memory and a revisit, someone else's report, a foreign and a broken record, an address that stays clean, a link read at opening and on an open page, and a host without an unpacker that is told so in words |
678
+ | `test/page-cols.test.js` | The columns of the grid: their order by the newest row a file's numbers moved in, a caption over exactly the metrics of its group, and a choice that moves no other column |
661
679
  | `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 |
662
680
  | `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 |
663
681
  | `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 |
@@ -737,7 +755,7 @@ The same release can be taken by a reference to the repository — installation
737
755
  registry, but stays tied to a revision:
738
756
 
739
757
  ```bash
740
- pnpm add -D github:vernikr/size-report#v2.8.1
758
+ pnpm add -D github:vernikr/size-report#v2.8.2
741
759
  ```
742
760
 
743
761
  With no network (or nothing to fetch from codeload) — the tarball: `pnpm pack` in the package clone, then
@@ -749,7 +767,7 @@ the branch moves on the installation fails with `Could not resolve <sha> to a co
749
767
  observation rather than reasoning: the short pin `6530237` installed while `main` stood on it and stopped
750
768
  working at the very next commit, while the same sha in full installed. A branch name (`#main`) and a tag
751
769
  are both accepted, but a branch is a moving target and a tag is constant: this release stands on the tag
752
- `v2.8.1`, which is also the one in the example (forty characters work as well, but they have to be copied
770
+ `v2.8.2`, which is also the one in the example (forty characters work as well, but they have to be copied
753
771
  out of the history by eye).
754
772
 
755
773
  The revision in the example is a part of the claim rather than decoration: what is described below is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vernikr/size-report",
3
- "version": "2.8.1",
3
+ "version": "2.8.2",
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.8.0",
68
+ "@vernikr/size-report": "2.8.1",
69
69
  "c8": "10",
70
70
  "dependency-cruiser": "17",
71
71
  "eslint": "^9.18.0",
package/src/page/app.css CHANGED
@@ -45,7 +45,7 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
45
45
  }
46
46
  .panel fieldset { margin: 0; padding: 0; border: 0; }
47
47
  .panel fieldset + fieldset { margin-top: 10px; }
48
- .panel legend, .panel .cap {
48
+ .panel legend {
49
49
  padding: 0;
50
50
  color: var(--muted);
51
51
  font-size: 11px;
@@ -53,7 +53,6 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
53
53
  text-transform: uppercase;
54
54
  letter-spacing: .07em;
55
55
  }
56
- .panel .cap { display: block; margin-bottom: 4px; }
57
56
  .panel .row { display: flex; flex-wrap: wrap; gap: 3px 6px; align-items: center; }
58
57
  /* The way of counting is visible text under the switches: the token dictionary and the way of compression come from the
59
58
  * settings of the run rather than from a checkbox, so pointing a mouse is not enough — the reader has to see what produced
@@ -91,7 +90,7 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
91
90
  cursor: pointer;
92
91
  user-select: none;
93
92
  }
94
- .panel .tree li > .fold:hover { color: var(--ink); }
93
+ .panel .tree li > .fold:hover { color: CanvasText; }
95
94
 
96
95
  /* A switch is a label around an input: one label and one click target, which is why a mouse, the keyboard (`Space` on the
97
96
  * input) and assistive technology all reach it. */
@@ -131,11 +130,7 @@ h1 { margin: 0 0 3px; font-size: 21px; font-weight: 650; letter-spacing: -.012em
131
130
  /* Numbers are denser than the page's text: there are more of them, they are shorter, and they are read by their digits. */
132
131
  #grid { font-size: 12.5px; }
133
132
 
134
- /* There is no stripe of progress over the page any longer, and that is a measurement rather than an omission: it stood
135
- * over the browser's relayout of the whole table, and the table is built as a window now — a switch costs a few
136
- * milliseconds (`src/page/table.js`), so an indicator would be a promise the page does not keep.
137
- *
138
- * The message about a link that came in stands above the table, so that it cannot be missed, while it does not push the
133
+ /* The message about a link that came in stands above the table, so that it cannot be missed, while it does not push the
139
134
  * table away: one line in the place of the page. Its colour is the system one (the accent), for the page keeps no colours
140
135
  * of its own. */
141
136
  .notice {
package/src/page/app.js CHANGED
@@ -4,15 +4,11 @@ import { appState, appTable, appWindow } from './table.js';
4
4
  import { appPanel, appPanelAll, appPanelState } from './panel.js';
5
5
 
6
6
  /* Assembling the report: the table is a window of the grid (`appWindow` of the table chapter) and nothing is built
7
- * that the reader cannot see. Two paths and no third, as before: `appPaint` draws the whole view (the first drawing, a
8
- * record from the browser's memory, a link in the address), while a click on a box does the same work for the choice
9
- * it made — and the difference between them is only how much of the view moved.
10
- *
11
- * What a click costs now: the window of rows and columns is built again — a few hundred cells, measured at 1–5 ms on
12
- * this repository's report — instead of a class on every node of a column of a table the browser lays out whole. The
13
- * stripe that used to stand over a long drawing (`src/page/work.js`) is gone with the reason for it: a switch no
14
- * longer has anything to wait for, and an indicator over work that is over before it could be painted would be a
15
- * promise the page does not keep.
7
+ * that the reader cannot see. Two paths and no third: `appPaint` draws the whole view (the first drawing, a record from
8
+ * the browser's memory, a link in the address), while a click on a box does the same work for the choice it made — the
9
+ * difference between them is only how much of the view moved. Neither has anything to wait for: a switch builds the
10
+ * window of the grid, a few hundred cells and milliseconds, so the page needs no indicator and keeps no promise it
11
+ * cannot measure.
16
12
  */
17
13
 
18
14
  // The table's window: made once, at the first drawing.
@@ -42,38 +38,36 @@ export function appPaint() {
42
38
  appWrite();
43
39
  }
44
40
 
45
- /* One file switched by the reader: the view, the window (its column is simply not among the columns that are built),
46
- * the fields it shows in. The message about a link fades here: by this action the reader has read it. */
47
- export function appSwitch(i, on) {
48
- if (appView.files[i] === on) return;
49
- appView.files[i] = on;
41
+ /* One drawing after a click, whatever was clicked: the window of the grid (the columns of what is switched off are
42
+ * simply not among the columns that are built), the counts of the empty states, the fields the choice really reached
43
+ * (`touched` — a whole group is one click, and a field that did not move is not written) and the memory. The message
44
+ * about a link fades here: by this action the reader has read it. */
45
+ function appChanged(touched) {
50
46
  appWindow(appCache, true);
51
47
  appCounts();
52
- appPanelState([i]);
48
+ if (touched.length > 0) appPanelState(touched);
53
49
  appWrite();
54
50
  appNotice('');
55
51
  }
56
52
 
57
- /* A group switched at once a folder or a category: the same work per file, then the fields of the files the choice
58
- * really reached (switching a folder on when a part of it was already on touches only the rest, and a field that did
59
- * not move is not written). The columns of the whole group leave or enter the window together. */
53
+ // One file switched by the reader. A switch that changes nothing is not a change: the click costs no drawing.
54
+ export function appSwitch(i, on) {
55
+ if (appView.files[i] === on) return;
56
+ appView.files[i] = on;
57
+ appChanged([i]);
58
+ }
59
+
60
+ /* A group switched at once — a folder or a category: the same work per file, and the fields of the files the choice
61
+ * really reached (switching a folder on when a part of it was already on touches only the rest). */
60
62
  export function appSwitchGroup(indexes, on) {
61
63
  const touched = indexes.filter((i) => appView.files[i] !== on);
62
64
  touched.forEach((i) => { appView.files[i] = on; });
63
- appWindow(appCache, true);
64
- appCounts();
65
- appPanelState(touched);
66
- appWrite();
67
- appNotice('');
65
+ appChanged(touched);
68
66
  }
69
67
 
70
- /* One metric switched: the columns of that metric are not among the columns that are built any longer, so this is the
71
- * same drawing as a file's switch. The metric's own field is the box the reader just clicked. */
68
+ /* One metric switched. Only the panel's metric box shows it, and that box is the one the reader just clicked. */
72
69
  export function appSwitchMetric() {
73
- appWindow(appCache, true);
74
- appCounts();
75
- appWrite();
76
- appNotice('');
70
+ appChanged([]);
77
71
  }
78
72
 
79
73
  /* The first drawing: the choice is already in the view (the link and the memory are applied above), the panel is
package/src/page/panel.js CHANGED
@@ -22,19 +22,18 @@ function appUnmeasuredBox(entry) {
22
22
  + (entry.why === 'rule' ? appUi.notMeasuredRule : appUi.notMeasuredChoice));
23
23
  }
24
24
 
25
- /* Every measured file of a subtree what a folder's switch controls: a file outside the report has nothing to
26
- * switch. */
27
- function appIndexes(node) {
28
- const out = node.files.slice();
29
- node.dirs.forEach((sub) => { out.push(...appIndexes(sub)); });
30
- return out;
31
- }
32
-
33
- /* How many files a subtree holds — including the ones that made it into no report. */
34
- function appCount(node) {
35
- let n = node.files.length + node.others.length;
36
- node.dirs.forEach((sub) => { n += appCount(sub); });
37
- return n;
25
+ /* A subtree in the two figures its folder shows: the measured files its checkbox controls, and how many files the
26
+ * folder holds at all — including the ones that made it into no report. One walk, because the two are asked together
27
+ * (a folder of five files with two measured reads "2/5"). */
28
+ function appSub(node) {
29
+ const idx = node.files.slice();
30
+ let total = node.files.length + node.others.length;
31
+ node.dirs.forEach((sub) => {
32
+ const inner = appSub(sub);
33
+ idx.push(...inner.idx);
34
+ total += inner.total;
35
+ });
36
+ return { idx: idx, total: total };
38
37
  }
39
38
 
40
39
  /* A tree node: the measured files (columns), the project's other files and the subfolders. */
@@ -48,8 +47,9 @@ function appNode() {
48
47
  * without a single measured file stays in place with its checkbox off and unavailable: there is nothing to switch on
49
48
  * in it. */
50
49
  function appDirHead(name, here, sub) {
51
- const idx = appIndexes(sub);
52
- const total = appCount(sub);
50
+ const own = appSub(sub);
51
+ const idx = own.idx;
52
+ const total = own.total;
53
53
  const label = name + '/';
54
54
  let head;
55
55
  if (idx.length === 0) {
@@ -141,13 +141,14 @@ function appLeaf(leaf) {
141
141
  function appTreeList(node, prefix) {
142
142
  const list = appEl('ul', 'tree');
143
143
  const dirs = [...node.dirs.keys()].sort(appName)
144
- .map((name) => ({ name: name, sub: node.dirs.get(name), inReport: appIndexes(node.dirs.get(name)).length > 0 }));
144
+ .map((name) => ({ name: name, sub: node.dirs.get(name), inReport: appSub(node.dirs.get(name)).idx.length > 0 }));
145
145
  const leaves = appLeaves(node);
146
146
  const inside = leaves.filter((leaf) => leaf.entry === null);
147
+ const outside = leaves.filter((leaf) => leaf.entry !== null);
147
148
  dirs.filter((d) => d.inReport).forEach((d) => list.appendChild(appDir(d.name, d.sub, prefix)));
148
149
  inside.forEach((leaf) => list.appendChild(appLeaf(leaf)));
149
150
  dirs.filter((d) => !d.inReport).forEach((d) => list.appendChild(appDir(d.name, d.sub, prefix)));
150
- leaves.filter((leaf) => leaf.entry !== null).forEach((leaf) => list.appendChild(appLeaf(leaf)));
151
+ outside.forEach((leaf) => list.appendChild(appLeaf(leaf)));
151
152
  return list;
152
153
  }
153
154
 
@@ -237,10 +238,10 @@ function appRowBoxes(box) {
237
238
  }
238
239
 
239
240
  // A folder's field from its files: all on — checked, some — the third state, none — simply unchecked.
240
- function appDirState(box) {
241
- const boxes = appRowBoxes(box);
241
+ function appDirState(path) {
242
+ const boxes = appRowBoxes(appFields.dir[path]);
242
243
  const on = boxes.filter((b) => b.checked).length;
243
- const input = box.querySelector('input');
244
+ const input = appFields.dir[path].querySelector('input');
244
245
  input.checked = on === boxes.length;
245
246
  input.indeterminate = on > 0 && on < boxes.length;
246
247
  }
@@ -267,29 +268,17 @@ function appDirPath(path) {
267
268
  return parts.slice(0, -1).map((_part, i) => parts.slice(0, i + 1).join('/'));
268
269
  }
269
270
 
270
- /* A click reaches a folder's field through the files below it: every folder on the path of a switched file shows the
271
- * share of what is left on. A folder without measured files has a field of its own too it is off and unavailable,
272
- * which is not the reader's state and must not be overwritten here. */
273
- function appDirsOf(indexes) {
274
- const seen = {};
275
- indexes.forEach((i) => {
276
- appDirPath(appFileAt(i)).forEach((path) => {
277
- if (seen[path] === true) return;
278
- seen[path] = true;
279
- if (appFields.dir[path] !== undefined) appDirState(appFields.dir[path]);
280
- });
281
- });
282
- }
283
-
284
- // The same for the quick buttons of the categories: every category one of the switched files belongs to.
285
- function appCatsOf(indexes) {
271
+ /* A click reaches the fields of a folder and of a category through the files below them one walk for both, because
272
+ * it is one question: which groups the reader sees hold a switched file, and what do they show now. A field that is not
273
+ * there is left alone: a folder without measured files has a box of its own, off and unavailable, which is not the
274
+ * reader's state. */
275
+ function appReach(indexes, fields, namesOf, stateOf) {
286
276
  const seen = {};
287
- indexes.forEach((i) => {
288
- const key = appData.files[i].category;
289
- if (seen[key] === true || appFields.cat[key] === undefined) return;
290
- seen[key] = true;
291
- appCatState(key);
292
- });
277
+ indexes.forEach((i) => namesOf(i).forEach((name) => {
278
+ if (seen[name] === true || fields[name] === undefined) return;
279
+ seen[name] = true;
280
+ stateOf(name);
281
+ }));
293
282
  }
294
283
 
295
284
  /* What a click changed, written where it stands: the files' own boxes, then the fields of the folders and categories
@@ -299,8 +288,8 @@ export function appPanelState(indexes) {
299
288
  const input = appFields.file[i];
300
289
  if (input !== undefined) input.checked = appView.files[i] === true;
301
290
  });
302
- appDirsOf(indexes);
303
- appCatsOf(indexes);
291
+ appReach(indexes, appFields.dir, (i) => appDirPath(appFileAt(i)), appDirState);
292
+ appReach(indexes, appFields.cat, (i) => [appData.files[i].category], appCatState);
304
293
  }
305
294
 
306
295
  /* The whole panel from the view: what a link, a record from the memory and the first drawing need. The metric boxes
@@ -309,5 +298,5 @@ export function appPanelAll() {
309
298
  appData.categories.forEach((c) => appCatState(c.key));
310
299
  appData.metrics.forEach((m) => { appFields.metric[m.key].checked = appView.metrics[m.key] === true; });
311
300
  appData.files.forEach((_f, i) => { appFields.file[i].checked = appView.files[i] === true; });
312
- Object.keys(appFields.dir).forEach((path) => appDirState(appFields.dir[path]));
301
+ Object.keys(appFields.dir).forEach((path) => appDirState(path));
313
302
  }
package/src/page/state.js CHANGED
@@ -106,15 +106,16 @@ function appPassport() {
106
106
  return appPassportValue;
107
107
  }
108
108
 
109
- /* One record of the choice for everything: it goes both into the memory and into the address, so there are no two formats
110
- * of one state. Only what is switched off is kept, by name: "switched on" and "no record" are the same state, which is
111
- * why turning every checkbox back on removes the record instead of leaving a trace indistinguishable from a choice. */
109
+ /* One record of the choice: only what is switched off is kept, by name. "Switched on" and "no record" are the same
110
+ * state, hence `null` rather than an empty record turning every checkbox back on removes the record instead of leaving
111
+ * a trace indistinguishable from a choice. */
112
112
  function appRecord() {
113
113
  const metrics = {};
114
114
  const files = {};
115
115
  appData.metrics.forEach((m) => { if (!appView.metrics[m.key]) metrics[m.key] = false; });
116
116
  appData.files.forEach((_f, i) => { if (!appView.files[i]) files[appFileAt(i)] = false; });
117
- return { v: 1, passport: appPassport(), metrics: metrics, files: files };
117
+ const empty = Object.keys(metrics).length === 0 && Object.keys(files).length === 0;
118
+ return empty ? null : { v: 1, passport: appPassport(), metrics: metrics, files: files };
118
119
  }
119
120
 
120
121
  // Whether a record is ours and of the right format — one rule for the memory and the address alike.
@@ -122,24 +123,40 @@ function appRecordOk(rec) {
122
123
  return rec !== null && typeof rec === 'object' && rec.v === 1 && rec.passport === appPassport();
123
124
  }
124
125
 
125
- /* The record goes into the browser's memory and nowhere else: it is written on the click itself, which is what survives
126
- * a closing, and the page's address keeps a clean tail the report is a local page whose address is copied as it is,
127
- * and a reader's choice belongs in the browser that made it rather than in the tab's title bar. What a link sent from an
128
- * earlier release holds is still read (`appLinkUse`), and it is not written into the reader's memory: what came in is
129
- * not his choice until he changes something. */
130
- export function appWrite() {
131
- if (appTransient) return;
132
- const rec = appRecord();
133
- const empty = Object.keys(rec.metrics).length === 0 && Object.keys(rec.files).length === 0;
126
+ /* A record into the browser's memory or out of it, and what is read back from it: the two halves of the memory, written
127
+ * once, because the page keeps two records of the same kind under two keys the choice and the unfolded tree — and
128
+ * their rules are one. Nothing but the browser's memory is written: it is written on the click itself, which is what
129
+ * survives a closing, and the page's address keeps a clean tail the report is a local page whose address is copied as
130
+ * it is, and a reader's choice belongs in the browser that made it rather than in the tab's title bar. What a link sent
131
+ * from an earlier release holds is still read (`appLinkUse`), and it is not written into the reader's memory: what came
132
+ * in is not his choice until he changes something. */
133
+ function appKeep(key, rec) {
134
134
  try {
135
- if (empty) window.localStorage.removeItem(appKey);
136
- else window.localStorage.setItem(appKey, JSON.stringify(rec));
135
+ if (rec === null) window.localStorage.removeItem(key);
136
+ else window.localStorage.setItem(key, JSON.stringify(rec));
137
137
  } catch (_e) {
138
138
  /* There is no memory (the browser grants this page none): the choice will not survive a closing, while the numbers
139
139
  * and the markup do not depend on it. */
140
140
  }
141
141
  }
142
142
 
143
+ export function appWrite() {
144
+ if (!appTransient) appKeep(appKey, appRecord());
145
+ }
146
+
147
+ /* What the browser saved under a key: its absence, a foreign record, a broken one and a record of another report amount
148
+ * to the same thing here — nothing to read. `JSON.parse` of a missing value answers `null`, so the two absences need no
149
+ * telling apart. */
150
+ function appLoad(key) {
151
+ let rec = null;
152
+ try {
153
+ rec = JSON.parse(window.localStorage.getItem(key));
154
+ } catch (_e) {
155
+ return null;
156
+ }
157
+ return appRecordOk(rec) ? rec : null;
158
+ }
159
+
143
160
  /* A reset to "everything on": the border between "this is no longer in the report" and "switched off" is the record
144
161
  * rather than a missing value. A link carries the sender's whole choice, which is why it is applied to a clean view
145
162
  * rather than on top of someone else's. */
@@ -174,15 +191,12 @@ function appLinkRead() {
174
191
  }
175
192
 
176
193
  /* How many names in the link are unknown to this report: the reader has to be told about them — otherwise he would look
177
- * in the table for something that was never there. */
194
+ * in the table for something that was never there. The two tables the model already holds answer it: a file by its path
195
+ * at HEAD (`appMeasured`) and a metric by its key (`appMetric`), the same names a record is written in (`appFileAt`). */
178
196
  function appUnknown(rec) {
179
- const known = {};
180
- const metricKeys = {};
181
- appData.files.forEach((_f, i) => { known[appFileAt(i)] = true; });
182
- appData.metrics.forEach((m) => { metricKeys[m.key] = true; });
183
197
  let n = 0;
184
- Object.keys(rec.metrics || {}).forEach((k) => { if (!metricKeys[k]) n++; });
185
- Object.keys(rec.files || {}).forEach((k) => { if (!known[k]) n++; });
198
+ Object.keys(rec.metrics || {}).forEach((k) => { if (appMetric[k] === undefined) n++; });
199
+ Object.keys(rec.files || {}).forEach((k) => { if (appMeasured[k] === undefined) n++; });
186
200
  return n;
187
201
  }
188
202
 
@@ -213,23 +227,9 @@ export function appLinkUse() {
213
227
  return 'none';
214
228
  }
215
229
 
216
- /* Reading: only a record of ours of our format version and our passport. Another report's record lies under another key,
217
- * while a foreign, outdated or broken one amounts to its absence. */
230
+ // Reading: the reader's own choice, as he left it another report's record lies under another key.
218
231
  export function appRead() {
219
- let text = null;
220
- try {
221
- text = window.localStorage.getItem(appKey);
222
- } catch (_e) {
223
- return null;
224
- }
225
- if (text === null) return null;
226
- let rec = null;
227
- try {
228
- rec = JSON.parse(text);
229
- } catch (_e) {
230
- return null;
231
- }
232
- return appRecordOk(rec) ? rec : null;
232
+ return appLoad(appKey);
233
233
  }
234
234
 
235
235
  /* Applying goes by name: a file is recognised by its path, a metric by its key. A name the report does not hold matches
@@ -248,37 +248,22 @@ export function appApply(rec) {
248
248
  * what the onlooker looks at rather than about which numbers are read, which is why it never goes into a link and never
249
249
  * leaves the browser. The tree opens folded, so **the unfolded folders are what is kept** (`true`) — the default is the
250
250
  * absence of the name, the same way "switched on" is the absence of a choice. A folder's name is its path ("src/page"),
251
- * so a vanished name simply means nothing, and unfolding nothing is the state the page opens in: then the record is not
252
- * kept at all rather than being kept empty. `appFoldKey` is set with the model (`appBoot`), for the reason the key
253
- * itself is. */
251
+ * so a vanished name simply means nothing. `appFoldKey` is set with the model (`appBoot`), for the reason the key itself
252
+ * is. */
254
253
  export function appFoldRead() {
255
- let text = null;
256
- try {
257
- text = window.localStorage.getItem(appFoldKey);
258
- } catch (_e) {
259
- return;
260
- }
261
- if (text === null) return;
262
- let rec = null;
263
- try {
264
- rec = JSON.parse(text);
265
- } catch (_e) {
266
- return;
267
- }
268
- if (!appRecordOk(rec)) return;
254
+ const rec = appLoad(appFoldKey);
255
+ if (rec === null) return;
269
256
  const open = rec.open || {};
270
257
  Object.keys(open).forEach((p) => { if (open[p] === true) appView.open[p] = true; });
271
258
  }
272
259
 
260
+ /* What a folder's sign does with the memory: the unfolded folders are what is kept (`true`), so a tree folded whole is
261
+ * the absence of the record — the default — and the record goes away with the last unfolded folder. */
273
262
  export function appFoldSet(path, open) {
274
263
  if (open) appView.open[path] = true;
275
264
  else delete appView.open[path];
276
- const rec = { v: 1, passport: appPassport(), open: Object.assign({}, appView.open) };
277
- try {
278
- if (Object.keys(rec.open).length === 0) window.localStorage.removeItem(appFoldKey);
279
- else window.localStorage.setItem(appFoldKey, JSON.stringify(rec));
280
- } catch (_e) {
281
- /* There is no memory: what is unfolded will not survive a closing, while the view does not depend on it — the tree
282
- * is unfolded exactly the way the reader unfolded it just now. */
283
- }
265
+ const names = Object.keys(appView.open);
266
+ appKeep(appFoldKey, names.length === 0
267
+ ? null
268
+ : { v: 1, passport: appPassport(), open: appView.open });
284
269
  }
package/src/page/table.js CHANGED
@@ -268,11 +268,11 @@ export function appWindow(cache, redraw) {
268
268
  const was = cache.win;
269
269
  cache.win = span;
270
270
  if (redraw === true || was === null || was.c0 !== span.c0 || was.c1 !== span.c1) {
271
- const head = cache.grid.querySelector('.head');
272
- if (head !== null) head.remove();
273
- cache.grid.insertBefore(appHead(cache, span), cache.grid.firstChild);
271
+ /* The grid holds the header and the rows of the window and nothing else, so the whole of it is thrown away rather
272
+ * than tracked piece by piece — and the rows that are made below say in the map that they are there. */
273
+ cache.grid.textContent = '';
274
274
  cache.rows.clear();
275
- [...cache.grid.querySelectorAll('.row')].forEach((row) => row.remove());
275
+ cache.grid.appendChild(appHead(cache, span));
276
276
  for (let r = span.r0; r <= span.r1; r++) appPlace(cache, r, span);
277
277
  return;
278
278
  }