@vernikr/size-report 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +989 -1081
  2. package/bin/postinstall.js +17 -18
  3. package/bin/size.js +2 -2
  4. package/package.json +3 -4
  5. package/src/args.js +72 -72
  6. package/src/artifact.js +14 -14
  7. package/src/check.js +41 -42
  8. package/src/cli.js +26 -29
  9. package/src/config.js +87 -91
  10. package/src/css.js +14 -14
  11. package/src/data.js +26 -50
  12. package/src/derived.js +31 -35
  13. package/src/doctor.js +95 -99
  14. package/src/explain.js +46 -47
  15. package/src/git.js +66 -71
  16. package/src/history.js +74 -83
  17. package/src/hook.js +130 -149
  18. package/src/init.js +37 -37
  19. package/src/journal.js +17 -15
  20. package/src/locales.js +31 -22
  21. package/src/metrics.js +72 -89
  22. package/src/minify.js +28 -27
  23. package/src/modes.js +57 -60
  24. package/src/optional.js +13 -11
  25. package/src/page/app.css +76 -94
  26. package/src/page/app.js +124 -80
  27. package/src/page/build.js +193 -50
  28. package/src/page/dom.js +8 -9
  29. package/src/page/panel.js +157 -69
  30. package/src/page/payload.js +168 -0
  31. package/src/page/state.js +144 -104
  32. package/src/page/table.js +270 -86
  33. package/src/parse-worker.js +10 -10
  34. package/src/parse.js +43 -45
  35. package/src/project.js +100 -104
  36. package/src/refusal.js +75 -76
  37. package/src/size-table.js +41 -76
  38. package/src/strip/forms.js +5 -5
  39. package/src/strip/guard.js +28 -28
  40. package/src/strip/js.js +27 -27
  41. package/src/strip.js +17 -21
  42. package/src/table.css +54 -19
  43. package/src/tokens.js +27 -27
  44. package/src/tool.js +10 -11
  45. package/templates/README.md +71 -77
  46. package/templates/ci.yml +33 -33
  47. package/templates/size-report.config.json +3 -3
  48. package/CHANGELOG.md +0 -690
package/src/derived.js CHANGED
@@ -1,29 +1,27 @@
1
- /* Производные величины отчёта: из абсолютных значений получаются итоги, дельты,
2
- * содержимое клетки и подпись коммита.
1
+ /* The report's derived quantities: totals, deltas, the content of a cell and a commit's caption come out of
2
+ * the absolute values.
3
3
  *
4
- * Единственное место, где это считается. Оба вывода пользуются этим файлом:
5
- * статический артефакт импортирует его как обычный модуль, а страница получает
6
- * его текст вклеенным в свой единственный файл (внешних ссылок страница иметь не
7
- * может). Поэтому у этого файла два требования, и оба обязательны:
4
+ * The only place where they are counted. Both outputs use this file: the page gets its text pasted into its
5
+ * single file (which may hold no external reference), while the terminal answers import it as an ordinary
6
+ * module. Hence two requirements of this file, both binding:
8
7
  *
9
- * 1. Ни импортов, ни состояния модуляиначе текст нельзя вклеить;
10
- * 2. Один `import` на строку и экспорт объявлением (`export function`), а не
11
- * списком имён: модульный синтаксис при вклейке снимается построчно, и
12
- * непонятая строка не должна молча попасть в страницу (`pageScript`).
8
+ * 1. No imports and no module state or the text cannot be pasted in;
9
+ * 2. One `import` per line and exports as declarations (`export function`) rather than a list of names:
10
+ * module syntax is removed line by line when pasting, and an unparsed line must not slip into the page
11
+ * silently (`stripModules` / `pageScript`).
13
12
  *
14
- * Расхождение двух отчётов возможно только здесь, поэтому и стеречь его надо
15
- * здесь: `test/contract-derived.test.js` сверяет числа страницы с числами
16
- * артефакта, а `test/page-view.test.js` следит, чтобы у страницы не появилось
17
- * своего расчёта. */
13
+ * The two outputs can drift apart only here, so here is where it has to be guarded:
14
+ * `test/contract-derived.test.js` compares the page's numbers with the artifact's, while `test/page-view.test.js`
15
+ * watches that the page grows no calculation of its own. */
18
16
 
19
- // Разряды тонкими пробелами: toLocaleString зависит от ICU сборки Node, а строка
20
- // таблицы обязана совпадать побайтово на любой машине.
17
+ // Thousands split by thin spaces: toLocaleString depends on the Node build's ICU, while the report has to be
18
+ // byte-identical on any machine.
21
19
  export function group(n) {
22
20
  return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, '\u2009');
23
21
  }
24
22
 
25
- /* Итог: сумма по включённым файлам. Выключенный файл не участвует ни в таблице,
26
- * ни в сумме, иначе «итого» отвечало бы не про то, что видно. */
23
+ /* The total: the sum over the switched-on files. A switched-off file joins neither the table nor the sum —
24
+ * otherwise "total" would answer about something other than what is visible. */
27
25
  export function totalsOf(values, metrics, on) {
28
26
  const out = {};
29
27
  metrics.forEach((m) => { out[m] = 0; });
@@ -34,17 +32,17 @@ export function totalsOf(values, metrics, on) {
34
32
  return out;
35
33
  }
36
34
 
37
- /* Дельта к предыдущему коммиту. Появление файла рост на весь его объём: иначе
38
- * сумма дельт по колонке не сходилась бы с текущим размером. */
35
+ /* The delta to the previous commit. A file's appearance is a growth by its whole volume: otherwise the sum
36
+ * of a column's deltas would not add up to the current size. */
39
37
  export function deltaOf(now, before) {
40
38
  return before === null || before === undefined ? now : now - before;
41
39
  }
42
40
 
43
- /* Содержимое клетки строки-коммита: что в ней написано и каким цветом. Разметку
44
- * из этого делает каждый вывод сам (строка HTML или узел DOM), а правила одни.
45
- * Пустая клетка «не менялось», `—` — файла в ревизии нет.
46
- * `minus` знак минуса: у артефакта он заморожен эталоном побайтово, страница
47
- * ставит типографский. */
41
+ /* The content of a cell of a commit row: what it says and in which colour. Each output turns this into markup
42
+ * itself (an HTML string or a DOM node), while the rules are one. An empty cell means "no change", `—` that
43
+ * the file is absent in the revision.
44
+ * `minus` is passed in rather than chosen here: the page draws the typographic one, and the signature is part
45
+ * of the package's frozen API (`test/api.test.js` holds the list of names). */
48
46
  export function cellParts(value, delta, minus) {
49
47
  if (value === null) return { text: '—', dir: null, miss: true };
50
48
  if (!delta) return { text: '', dir: null, miss: false };
@@ -55,14 +53,13 @@ export function cellParts(value, delta, minus) {
55
53
  };
56
54
  }
57
55
 
58
- // Клетка верхней строки: абсолютный размер, без дельты.
56
+ // A cell of the top row: the absolute size, with no delta.
59
57
  export function valueParts(value) {
60
58
  return value === null ? { text: '—', miss: true } : { text: group(value), miss: false };
61
59
  }
62
60
 
63
- /* Строка-коммит: блок «общий объём» и по блоку на включённый файл, в каждом
64
- * клетка на метрику. Отбор включённых файлов происходит здесь, поэтому и таблица,
65
- * и суммы считаются от одного выбора. */
61
+ /* A commit row: a "total volume" block and one block per switched-on file, each with a cell per metric. The
62
+ * choosing of the switched-on files happens here, so both the table and the totals come out of one choice. */
66
63
  export function rowModel(values, prev, metrics, on) {
67
64
  const total = totalsOf(values, metrics, on);
68
65
  const prevTotal = prev === null ? null : totalsOf(prev, metrics, on);
@@ -85,8 +82,8 @@ export function rowModel(values, prev, metrics, on) {
85
82
  return out;
86
83
  }
87
84
 
88
- /* Верхняя строка абсолютные размеры на HEAD: абсолютное число стоит в таблице
89
- * один раз, и именно с ним сходятся все дельты под ним. */
85
+ /* The top row is the absolute sizes at HEAD: an absolute number stands in the table once, and it is the one
86
+ * every delta below it adds up to. */
90
87
  export function nowModel(values, metrics, on) {
91
88
  const total = totalsOf(values, metrics, on);
92
89
  const files = [];
@@ -97,10 +94,9 @@ export function nowModel(values, metrics, on) {
97
94
  return { total: metrics.map((m) => total[m]), files: files };
98
95
  }
99
96
 
100
- /* Подпись коммита в терминах данных: что показать, чем подписать и куда вести.
101
- * Ссылку считает `rowHref` движка то же место, откуда её берёт контракт для
102
- * страницы, поэтому оба вывода ведут туда же. Подпись всплывающей строки тоже
103
- * здесь: два вывода не должны подписывать один коммит по-разному. */
97
+ /* A commit's caption in terms of data: what to show, how to caption it and where to lead. The link is counted
98
+ * by the engine's `rowHref` — the same place the page's contract takes it from, so both outputs lead there. The
99
+ * caption of the tooltip is here too: two outputs must not caption one commit differently. */
104
100
  export function commitParts(row, showSha, href) {
105
101
  const short = showSha ? row.sha.slice(0, 7) : '';
106
102
  return {
package/src/doctor.js CHANGED
@@ -8,26 +8,25 @@ import { derivedSummary } from './project.js';
8
8
  import { minifier } from './minify.js';
9
9
  import { tokenizer } from './tokens.js';
10
10
 
11
- /* Диагностика одним ответом (`size doctor`): отвечает ли машина за числа, чем
12
- * считаются метрики здесь и сейчас, годятся ли настройки, всё ли из истории
13
- * покрыто. Ничего своего он не считает: покрытие тот же ответ, что даёт
14
- * `size check` (`coverage`), окружение факты этой машины, зависимости те же
15
- * загрузчики, которыми пользуются датчики. Второго расчёта в пакете нет.
11
+ /* Diagnostics in a single answer (`size doctor`): does the machine stand behind the numbers, what counts
12
+ * the metrics here and now, are the settings usable, is everything from the history covered. It computes
13
+ * nothing of its own: coverage is the same answer `size check` gives (`coverage`), the environment is this
14
+ * machine's facts, and the dependencies are the very loaders the sensors use. There is no second calculation
15
+ * in the package.
16
16
  *
17
- * Правило ответа: `ok` значит «делать нечего», а у находки назван уровень.
18
- * `action` что-то надо сделать (и, где возможно, названа команда починки);
19
- * `note` наблюдение: знать полезно, делать нечего. Код выхода считает один
20
- * `verdictOf` в конце: шаги чтения только называют вид обстоятельства (`troubles`),
21
- * а и порядок видов, и код каждого один список `WEIGHT`. Своего кода у шага нет,
22
- * поэтому разойтись эти два ответа не могут.
17
+ * The rule of the answer: `ok` means "nothing to do", and every finding names its level. `action` means
18
+ * something has to be done (with a fix command where one exists); `note` is an observation — useful to know,
19
+ * nothing to do. One `verdictOf` at the end counts the exit code: the reading steps only name the kind of
20
+ * trouble (`troubles`), while both the order of the kinds and each of their codes come from one list,
21
+ * `WEIGHT`. A step has no code of its own, so those two answers cannot drift apart.
23
22
  *
24
- * Чего ответ не делает: не говорит, «правильно» ли выбраны колонки (это знает
25
- * проект), и не угадывает там, где данных нет,отсутствие ответа называется
26
- * словами (`coverage: null` и находка с причиной).
23
+ * What the answer does not do: it does not say whether the columns are chosen "correctly" (the project knows
24
+ * that), and it does not guess where there are no data a missing answer is named in words (`coverage: null`
25
+ * and a finding carrying the reason).
27
26
  */
28
27
 
29
- /* Окружение: что за машина и что она говорит о числах. Без ответа git ответ
30
- * честно неполон (`git: null`), а не выдуман. */
28
+ /* The environment: what machine this is and what it says about the numbers. With no answer from git the
29
+ * answer is honestly incomplete (`git: null`) rather than invented. */
31
30
  function environment(root) {
32
31
  const env = {
33
32
  node: process.version,
@@ -42,26 +41,23 @@ function environment(root) {
42
41
  env.git = git(root, ['--version']).trim();
43
42
  env.shallow = git(root, ['rev-parse', '--is-shallow-repository']).trim() === 'true';
44
43
  } catch (_e) {
45
- // git не ответилоб этом скажет находка, а не выдуманное значение.
44
+ // git did not answer the finding will say so, rather than an invented value.
46
45
  }
47
46
  return env;
48
47
  }
49
48
 
50
- /* Зависимости: чем метрики считаются здесь и сейчас. Спрашиваются те же
51
- * загрузчики, что и у датчиков (`minifier`, `tokenizer`), поэтому ответ не может
52
- * разойтись с числом: без минификатора `min` считает упрощением, без словаря
53
- * `tok` — оценкой.
49
+ /* Dependencies: what counts the metrics here and now. The very loaders the sensors use are asked
50
+ * (`minifier`, `tokenizer`), so the answer cannot drift from the number: without the minifier `min` counts
51
+ * by simplification, without the dictionary `tok` by estimate.
54
52
  *
55
- * Загружается только то, о чём проект действительно спросил: словарь весит
56
- * мегабайты, и трогать его ради строки «есть» значило бы заплатить за ответ,
57
- * которого у чисел не было (то же правило, что у отчёта: `test/tokens.test.js`).
58
- * Ненужный датчик назван не «неизвестным», а ненужным на точность он не влияет,
59
- * и это и есть ответ; «неизвестно» остаётся там, где настройки нечитаемы и
60
- * спросить не у кого. */
61
- const UNREADABLE = 'неизвестно: настройки нечитаемы';
53
+ * Only what the project actually asked for is loaded: the dictionary weighs megabytes, and touching it for
54
+ * the sake of an "installed" line would mean paying for an answer the numbers never needed (the same rule as
55
+ * in the report: `test/tokens.test.js`). An unwanted sensor is named unneeded rather than unknown — it does
56
+ * not affect the count, and that is the answer; "unknown" stays for the case where the settings are unreadable
57
+ * and there is nobody to ask. */
58
+ const UNREADABLE = 'unknown: the settings cannot be read';
62
59
 
63
- /* Спрошеноспрашиваем загрузчик; не спрошеноговорим об этом словами и не
64
- * платим за него. */
60
+ /* Askedask the loader; not asked say so in words and do not pay for it. */
65
61
  function entry(asked, name, metric, load, note) {
66
62
  if (asked !== true) return { name: name, metric: metric, present: null, note: note };
67
63
  const { tool, version } = load();
@@ -73,20 +69,20 @@ function dependencies(cfg) {
73
69
  const asksTokens = cfg === null ? null : cfg.metrics.indexOf('tok') >= 0;
74
70
  return [
75
71
  entry(asksMinify, 'esbuild', 'min', () => minifier(),
76
- asksMinify === null ? UNREADABLE : 'не спрашивается: «minify» считает снятием балласта'),
72
+ asksMinify === null ? UNREADABLE : 'not asked for: "minify" counts by stripping'),
77
73
  entry(asksTokens, 'gpt-tokenizer', 'tok', () => tokenizer(cfg.tokens),
78
- asksTokens === null ? UNREADABLE : 'не спрашивается: метрики ' + cfg.metrics.join(' '))
74
+ asksTokens === null ? UNREADABLE : 'not asked for: the metrics are ' + cfg.metrics.join(' '))
79
75
  ];
80
76
  }
81
77
 
82
- /* Настройки: при нечитаемых ответ честно неполон (покрытие считать нечем), а
83
- * причинане отказ, а находка: диагностика затем и нужна, чтобы назвать причину
84
- * и починку, их и несёт текст отказа. Вес обстоятельства шаг только **называет**
85
- * (`troubles`), а важнее оно или нет не его дело: решает `verdictOf`.
78
+ /* Settings: with unreadable ones the answer is honestly incomplete (there is nothing to count coverage with),
79
+ * and the cause is a finding rather than a refusal diagnostics exist to name the cause and its fix, and the
80
+ * refusal's text carries both. A step only **names** the weight of a trouble (`troubles`); whether it outranks
81
+ * another is not its business — `verdictOf` decides.
86
82
  *
87
- * Настроек, выведенных из проекта, тут не обстоятельство, а наблюдение: проект
88
- * работает, но числа его отчёта зависят от того, что инструмент о нём угадал,
89
- * поэтому находка уровня `note` (код выхода она не несёт) и поле `derived` в ответе. */
83
+ * Settings derived from the project are an observation here rather than a trouble: the project works, but the
84
+ * numbers in its report depend on what the tool guessed about it hence a `note` finding (it carries no exit
85
+ * code) and the `derived` field in the answer. */
90
86
  function readConfig(root, configFile) {
91
87
  try {
92
88
  const cfg = loadConfig(configFile, root);
@@ -98,7 +94,7 @@ function readConfig(root, configFile) {
98
94
  columns: cfg.columns.length, metrics: cfg.metrics
99
95
  },
100
96
  findings: derived
101
- ? [{ level: 'note', what: derivedSummary(cfg), fix: 'закрепите их файлом: ' + cliCommand('--init') }]
97
+ ? [{ level: 'note', what: derivedSummary(cfg), fix: 'make them a file of their own: ' + cliCommand('--init') }]
102
98
  : [],
103
99
  troubles: []
104
100
  };
@@ -113,41 +109,41 @@ function readConfig(root, configFile) {
113
109
  }
114
110
  }
115
111
 
116
- /* Хук: две находки, у каждой своя починка. Веса у них нет и своего кода выхода тоже
117
- * отчёт собирается и без хука, поэтому сломанный хук меняет только вердикт `ok`. */
112
+ /* The hook: two findings, each with a fix of its own. They carry neither weight nor an exit code of their
113
+ * own the report is built without the hook too, so a broken hook changes only the `ok` verdict. */
118
114
  function hookFindings(hooks) {
119
115
  const found = [];
120
116
  if (!hooks.installed) return found;
121
117
  if (hooks.enabled === false) {
122
118
  found.push({
123
119
  level: 'action',
124
- what: 'хук установлен, но автоматика выключена настройкой hooks.enabled: отчёт обновляется руками',
125
- fix: 'верните «"hooks": {"enabled": true}» в файл настроек или снимите хук: ' + cliCommand('uninstall-hook')
120
+ what: 'the hook is installed, but the automation is switched off by the setting hooks.enabled: the report is updated by hand',
121
+ fix: 'put "hooks": {"enabled": true} back in the settings file, or take the hook off: ' + cliCommand('uninstall-hook')
126
122
  });
127
123
  }
128
124
  if (hooks.last !== null && HOOK_BAD.indexOf(hooks.last.result) >= 0) {
129
125
  found.push({
130
126
  level: 'action',
131
- what: 'хук: последний запуск не пересобрал отчёт — ' + hooks.last.why,
132
- fix: 'починьте то, на что жалуется причина, и пересоберите отчёт: ' + cliCommand('--write')
127
+ what: 'hook: the last run did not rebuild the report — ' + hooks.last.why,
128
+ fix: 'fix what the reason complains about and rebuild the report: ' + cliCommand('--write')
133
129
  });
134
130
  }
135
131
  return found;
136
132
  }
137
133
 
138
- /* Покрытие тот же ответ, что даёт `size check`, плюс вид обстоятельства, если оно есть:
139
- * неполнота пути или приближение датчика (вес у видов разный — `WEIGHT`); неполнота
140
- * старше, потому что без неё чисел нет вовсе.
141
- * Что попадает в находки, а что нет: в отчёте целиком стоит блок покрытия (тот же текст,
142
- * что у `size check`), поэтому неполнота второй раз не пересказываетсяона весит. А по
143
- * датчикам находка есть: `size check` печатает их строкой `!`, здесь они часть ответа. */
134
+ /* Coverage is the same answer `size check` gives, plus the kind of trouble if there is one: an incomplete path
135
+ * or a sensor counting another way (the kinds weigh differently — `WEIGHT`), with incompleteness outranking,
136
+ * because without it there are no numbers at all.
137
+ * What becomes a finding and what does not: the report holds the whole coverage block (the same text as
138
+ * `size check`), so incompleteness is not retold a second time it weighs. Sensors do get a finding:
139
+ * `size check` prints them as a `!` line, while here they are part of the answer. */
144
140
  function readCoverage(cfg, root, configFile) {
145
141
  if (cfg === null) {
146
142
  return {
147
143
  report: null,
148
144
  findings: [{
149
145
  level: 'note',
150
- what: 'покрытие не считалось: настройки нечитаемыпочините их и спросите снова'
146
+ what: 'coverage was not counted: the settings cannot be read fix them and ask again'
151
147
  }],
152
148
  troubles: []
153
149
  };
@@ -161,10 +157,10 @@ function readCoverage(cfg, root, configFile) {
161
157
  };
162
158
  } catch (e) {
163
159
  if (!(e instanceof Refusal)) throw e;
164
- /* Внутри покрытия отказывают двое, и род у них разный: обрезанной истории
165
- * свой вид (`assertFullHistory`), а неразобранному файлунастройки: числа
166
- * нет из-за них, и починка у него настройками же. Вид выбирается по коду
167
- * отказа эти два кода и есть весь выбор. */
160
+ /* Two things refuse inside coverage, of different kinds: a truncated history gets a kind of its own
161
+ * (`assertFullHistory`), while an unparsed file counts as settings the number is missing because of them,
162
+ * and its fix is in the settings too. The kind is picked by the refusal's code: those two codes are the
163
+ * whole choice. */
168
164
  return {
169
165
  report: null,
170
166
  findings: [{ level: 'action', what: e.message }],
@@ -173,11 +169,11 @@ function readCoverage(cfg, root, configFile) {
173
169
  }
174
170
  }
175
171
 
176
- /* Вес обстоятельстввот и весь порядок важности, и он один на весь модуль: ключи идут
177
- * по важности, значения код каждого вида. Код выхода берётся у самого важного из
178
- * найденного, а не у того, что нашлось позже: сначала чем считать нечем (настройки,
179
- * история), потом неполное покрытие, потом оговорка о счёте.
180
- * Хук в список не входит: отчёт собирается и без него (см. `hookFindings`). */
172
+ /* The weight of troubles the whole order of importance, and one for the entire module: the keys run by
173
+ * importance, the values are each kind's code. The exit code comes from the most important thing found rather
174
+ * than from the last one found: first what leaves nothing to count with (settings, history), then incomplete
175
+ * coverage, then the caveat about the count.
176
+ * The hook is not in this list: the report is built without it too (see `hookFindings`). */
181
177
  const WEIGHT = {
182
178
  config: EXIT.CONFIG,
183
179
  history: EXIT.SHALLOW,
@@ -185,9 +181,9 @@ const WEIGHT = {
185
181
  sensor: EXIT.SENSOR
186
182
  };
187
183
 
188
- /* Вердиктодно место, где обстоятельства превращаются в код выхода и в `ok`.
189
- * «Делать нечего» это ни одной находки-действия и сосчитанное полное покрытие: без
190
- * покрытия вердикта нет, потому что считать больше нечего (см. заметку в `readCoverage`). */
184
+ /* The verdict the one place where troubles turn into an exit code and into `ok`. "Nothing to do" means no
185
+ * action finding and counted complete coverage: with no coverage there is no verdict, because there is nothing
186
+ * left to count (see the note in `readCoverage`). */
191
187
  function verdictOf(rep, troubles) {
192
188
  const found = Object.keys(WEIGHT).filter((kind) => troubles.indexOf(kind) >= 0);
193
189
  rep.exit = found.length === 0 ? EXIT.OK : WEIGHT[found[0]];
@@ -215,22 +211,22 @@ export function doctor(root, configFile) {
215
211
  return verdictOf(rep, config.troubles.concat(cov.troubles));
216
212
  }
217
213
 
218
- /* Итог последнего запуска хука словами: по нему человек понимает, что произошло
219
- * после коммита, не заглядывая в `.git`. */
214
+ /* The outcome of the hook's last run in words: it tells a person what happened after a commit without looking
215
+ * into `.git`. */
220
216
  const HOOK_RESULT = {
221
- committed: 'отчёт пересобран и закоммичен',
222
- rebuilt: 'отчёт пересобран без коммита',
223
- unchanged: 'менять было нечего',
224
- refused: 'отказ',
225
- failed: 'ошибка',
226
- skipped: 'пропущен'
217
+ committed: 'the report was rebuilt and committed',
218
+ rebuilt: 'the report was rebuilt without a commit',
219
+ unchanged: 'there was nothing to change',
220
+ refused: 'a refusal',
221
+ failed: 'an error',
222
+ skipped: 'skipped'
227
223
  };
228
- // Итоги, которые требуют действий: отказ инструмента и его собственная ошибка.
224
+ // The outcomes that call for action: a refusal by the tool and its own error.
229
225
  const HOOK_BAD = ['refused', 'failed'];
230
226
 
231
- /* Состояние хука: установлен ли, включён ли настройкой и чем кончился последний
232
- * запуск. «Не установлен» не находка: автоматика ставится явной командой,
233
- * и её отсутствие решение проекта, а не забывчивость. */
227
+ /* The state of the hook: whether it is installed, switched on by the settings, and how its last run ended.
228
+ * "Not installed" is not a finding: the automation is installed by an explicit command, and its absence is the
229
+ * project's decision rather than forgetfulness. */
234
230
  function hooksReport(root, cfg) {
235
231
  const status = hookStatus(root);
236
232
  return {
@@ -242,40 +238,40 @@ function hooksReport(root, cfg) {
242
238
  }
243
239
 
244
240
  function hookLine(hooks) {
245
- if (!hooks.installed) return 'не установлен (ставится командой ' + cliCommand('install-hook') + ')';
241
+ if (!hooks.installed) return 'not installed (installed with the command ' + cliCommand('install-hook') + ')';
246
242
  const last = hooks.last === null
247
- ? 'ещё не запускался'
248
- : 'последний запуск ' + hooks.last.at + ' — ' + (HOOK_RESULT[hooks.last.result] || hooks.last.result)
243
+ ? 'it has not run yet'
244
+ : 'the last run ' + hooks.last.at + ' — ' + (HOOK_RESULT[hooks.last.result] || hooks.last.result)
249
245
  + (hooks.last.commit ? ' (' + hooks.last.commit + ')' : '')
250
246
  + (hooks.last.why ? ': ' + hooks.last.why.split('\n')[0] : '');
251
- return hooks.files.join(', ') + (hooks.enabled === false ? ' (выключен настройкой)' : '') + '; ' + last;
247
+ return hooks.files.join(', ') + (hooks.enabled === false ? ' (switched off by the settings)' : '') + '; ' + last;
252
248
  }
253
249
 
254
- /* Текст для человека. Покрытие печатает `coverageText` — тот же, что у
255
- * `size check`: два ответа об одном не должны разойтись формулировками. */
250
+ /* The text for a person. Coverage is printed by `coverageText` — the same one `size check` uses: two answers
251
+ * about one thing must not drift apart in wording. */
256
252
  export function doctorText(rep) {
257
253
  const env = rep.environment;
258
254
  const lines = [];
259
- lines.push((rep.ok ? '✓ ' : '✗ ') + rep.tool.name + ' ' + rep.tool.version + ': диагностика ' + env.root);
260
- lines.push(' окружение: Node ' + env.node + ', ' + env.platform + ', '
261
- + (env.git === null ? 'git недоступен' : env.git)
262
- + (env.shallow === null ? '' : env.shallow ? ', история обрезана' : ', история полная'));
263
- // Закрепления механизм, а не украшение: движок ставит их сам на границе вызова,
264
- // поэтому настройки машины на числа не влияют (проверка `test/environment.test.js`).
265
- lines.push(' git читается с закреплениями: ' + env.pins.join(', ') + '; локаль ' + env.locale
266
- + ' (настройки машины на числа не влияют)');
267
- lines.push(' настройки: ' + (rep.config.ok
268
- ? (rep.config.derived ? 'выводятся из проекта (файла нет)' : rep.config.file)
269
- + ' — ' + rep.config.columns + ' колонок, метрики ' + rep.config.metrics.join(' ')
270
- : rep.config.file + ' — нечитаемы'));
271
- lines.push(' зависимости: ' + rep.dependencies.map((d) => d.name
272
- + (d.present === null ? ' — ' + d.note : d.present ? ' ' + d.version + ' есть' : ' нет')
255
+ lines.push((rep.ok ? '✓ ' : '✗ ') + rep.tool.name + ' ' + rep.tool.version + ': diagnostics for ' + env.root);
256
+ lines.push(' environment: Node ' + env.node + ', ' + env.platform + ', '
257
+ + (env.git === null ? 'git is unavailable' : env.git)
258
+ + (env.shallow === null ? '' : env.shallow ? ', the history is truncated' : ', the history is complete'));
259
+ // The pins are a mechanism rather than decoration: the engine sets them itself at the call boundary, so the
260
+ // machine's settings do not reach the numbers (guarded by `test/environment.test.js`).
261
+ lines.push(' git is read with the pins: ' + env.pins.join(', ') + '; locale ' + env.locale
262
+ + ' (the settings of the machine do not reach the numbers)');
263
+ lines.push(' settings: ' + (rep.config.ok
264
+ ? (rep.config.derived ? 'derived from the project (no file)' : rep.config.file)
265
+ + ' — ' + rep.config.columns + ' columns, metrics ' + rep.config.metrics.join(' ')
266
+ : rep.config.file + ' — unreadable'));
267
+ lines.push(' dependencies: ' + rep.dependencies.map((d) => d.name
268
+ + (d.present === null ? ' — ' + d.note : d.present ? ' ' + d.version + ' present' : ' absent')
273
269
  + ' (' + d.metric + ')').join(', '));
274
- lines.push(' хук: ' + hookLine(rep.hooks));
270
+ lines.push(' hook: ' + hookLine(rep.hooks));
275
271
  if (rep.coverage) lines.push(coverageText(rep.coverage));
276
272
  rep.findings.forEach((f) => {
277
273
  lines.push((f.level === 'action' ? '✗ ' : '· ') + f.what);
278
- if (f.fix) lines.push(' починка: ' + f.fix);
274
+ if (f.fix) lines.push(' fix: ' + f.fix);
279
275
  });
280
276
  return lines.join('\n');
281
277
  }