@vernikr/size-report 2.4.0 → 2.5.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.
@@ -1,28 +1,27 @@
1
1
  #!/usr/bin/env node
2
- /* Постановка хука после установки пакета: чтобы отчёт обновлялся с первого же
3
- * коммита, не требуя ни запуска инструмента, ни файла настроек.
2
+ /* Hook installation after the package is installed: the report is rebuilt from the very
3
+ * first commit, with neither the tool run nor a settings file.
4
4
  *
5
- * Здесь только поиск проекта-потребителя: сам хук ставит `autoInstall`
6
- * (`src/hook.js`) — то же место, что и при первом запуске, иначе «поставлено при
7
- * установке» и «поставлено при запуске» могли бы разойтись содержимым файла.
5
+ * Only the consumer project is located here: the hook itself is installed by
6
+ * `autoInstall` (`src/hook.js`) — the same place as on the first run, so that "installed
7
+ * by the installer" and "installed by the first run" cannot diverge in the file content.
8
8
  *
9
- * Код выхода всегда 0: установка зависимостей не должна падать из-за того, что
10
- * услугу не удалось оказать (нет git, нет прав, чужой хук, CI). Причина не
11
- * печатается: у фоновой работы нет читателя, а точная причина есть у команды
12
- * `install-hook`.
9
+ * The exit code is always 0: installing dependencies must not fail because a service
10
+ * could not be rendered (no git, no permissions, a foreign hook, CI), and the reason is
11
+ * not printed background work has no reader, while `install-hook` has the exact one.
13
12
  *
14
- * Отдельная тонкость: платформы, где скрипты зависимостей по умолчанию не
15
- * исполняются (pnpm 10, yarn berry), зовут этот файл не всегда тогда хук
16
- * ставится при первом запуске инструмента в проекте. Оба пути ведут в одно место. */
13
+ * One subtlety: platforms that skip dependency scripts by default (pnpm 10, yarn berry)
14
+ * do not always call this file; there the hook is installed by the first run of the tool
15
+ * in the project. Both paths lead to the same place. */
17
16
 
18
17
  import fs from 'fs';
19
18
  import path from 'path';
20
19
  import { autoInstall } from '../src/hook.js';
21
20
 
22
- /* Каталог проекта-потребителя ищется в порядке убывания точности: `INIT_CWD`
23
- * (его ставят npm и pnpm, запуская скрипт пакета), `npm_config_local_prefix`,
24
- * затем подъём от текущего каталога вверх до ближайшего `.git`. Подъём нужен,
25
- * потому что сам скрипт исполняется из `node_modules`, где репозитория нет. */
21
+ /* The consumer project directory, most precise candidate first: `INIT_CWD` (set by npm
22
+ * and pnpm when they run a package script), `npm_config_local_prefix`, and finally a walk
23
+ * up from the current directory to the nearest `.git` the script itself runs from
24
+ * `node_modules`, where there is no repository. */
26
25
  function projectRoot() {
27
26
  const candidates = [process.env.INIT_CWD, process.env.npm_config_local_prefix, process.cwd()];
28
27
  for (const start of candidates) {
@@ -46,6 +45,6 @@ function gitRootOf(start) {
46
45
  const root = projectRoot();
47
46
  const files = root === null ? null : autoInstall(root, null);
48
47
  if (files !== null && process.env.SIZE_REPORT_QUIET !== '1') {
49
- console.error('· size-report: хук поставлен (' + files.join(', ') + ') — отчёт обновляется после'
50
- + ' каждого коммита; снять: size uninstall-hook');
48
+ console.error('· size-report: hook installed (' + files.join(', ') + ') — the report refreshes after'
49
+ + ' every commit; remove it: size uninstall-hook');
51
50
  }
package/bin/size.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- /* Точка входа команды `size`. Разбор аргументов и режимы живут в движке: держать
3
- * их здесь значило бы, что движок нельзя импортировать, не исполнив команду. */
2
+ /* The `size` entry point. Arguments and modes live in the engine: keeping them here
3
+ * would mean the engine could not be imported without running the command. */
4
4
  import { main } from '../src/size-table.js';
5
5
 
6
6
  process.exitCode = main();
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@vernikr/size-report",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "author": "vernikr",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/vernikr/size-report.git"
8
8
  },
9
- "description": "Учёт роста объёма кода и документов по истории git: raw / min / токены; отчёт один самодостаточный файл, обновляется хуком сам",
9
+ "description": "Tracking how the volume of code and documents grows across git history: raw / min / tokens; the report is one self-contained file, refreshed by the hook on its own",
10
10
  "type": "module",
11
11
  "license": "MIT",
12
12
  "packageManager": "pnpm@10.6.1",
@@ -27,7 +27,6 @@
27
27
  "src",
28
28
  "templates",
29
29
  "README.md",
30
- "CHANGELOG.md",
31
30
  "LICENSE"
32
31
  ],
33
32
  "sideEffects": false,
@@ -66,7 +65,7 @@
66
65
  "report"
67
66
  ],
68
67
  "devDependencies": {
69
- "@vernikr/size-report": "2.3.0",
68
+ "@vernikr/size-report": "2.4.0",
70
69
  "c8": "10",
71
70
  "dependency-cruiser": "17",
72
71
  "eslint": "^9.18.0",
package/src/args.js CHANGED
@@ -1,24 +1,24 @@
1
1
  import { cliCommand, advicePath, refuseCause } from './refusal.js';
2
2
 
3
- /* Грамматика командной строки: что человек назвал и законно ли это сочетание.
3
+ /* Command-line grammar: what the user named, and whether that combination is legal.
4
4
  *
5
- * Один разбор на входе, до чтения проекта. Он решает всё сразу какой режим
6
- * запрошен, совместим ли он с командой и с остальными ключами, все ли ключи
7
- * получили значение, нет ли лишних слов,и отвечает либо планом, либо отказом.
8
- * Поэтому порядок ветвлений в `main` ничего не решает: правило «так нельзя» — это
9
- * значение, а не место в коде, и наружу оно выходит одним способомотказом с
10
- * названным виновником и готовой командой.
5
+ * One parse, at the entry, before the project is read. It settles everything at once —
6
+ * which mode was asked for, whether it fits the command and the other flags, whether every
7
+ * flag got a value, whether there are extra words and answers with either a plan or a
8
+ * refusal. That is why the order of branches in `main` decides nothing: "you cannot do
9
+ * that" is a value, not a place in the code, and it surfaces in one way only as a refusal
10
+ * naming the culprit and the command that fixes it.
11
11
  *
12
- * Ключи трёх родов: режимы (взаимоисключающиеони задают, что делать), ключи со
13
- * значением (забирают следующий аргумент) и переключатели. Команда первое слово
14
- * вне ключей: её чтение не зависит от места в строке, поэтому и `size check
15
- * --config x`, и `size --config x check` одно и то же.
12
+ * Flags come in three kinds: modes (mutually exclusive they say what to do), value flags
13
+ * (they take the next argument) and switches. A command is the first word outside the
14
+ * flags, so its position in the line does not matter: `size check --config x` and
15
+ * `size --config x check` are the same.
16
16
  *
17
- * Файл отдельный от `cli.js`, потому что вопросы у них разные: здесь «что
18
- * запрошено и можно ли так», там «что по запросу делать». Отсюда и цена ошибки
19
- * в каждом: неверное правило здесь ломает все режимы сразу, поэтому проверки
20
- * разложены по группам (режим, слово, форма ответа), а каталог отказов считает их
21
- * места и требует проверки на каждое.
17
+ * The file is separate from `cli.js` because the questions differ: here "what was asked and
18
+ * may it be so", there "what to do about it". Hence the price of a wrong rule here — it
19
+ * breaks every mode at once and hence the checks split by subject, with the refusal
20
+ * catalogue (`tools/refusals.js`) counting their sites in the sources and demanding a test
21
+ * for each.
22
22
  */
23
23
 
24
24
  const MODES = ['--init', '--write', '--data'];
@@ -28,23 +28,23 @@ const COMMANDS = ['check', 'explain', 'doctor', 'install-hook', 'uninstall-hook'
28
28
  const ANSWER_COMMANDS = ['check', 'explain', 'doctor'];
29
29
  export const HOOK_COMMANDS = ['install-hook', 'uninstall-hook', 'hook-run'];
30
30
 
31
- /* Ключ со значением: забирает следующий аргумент и возвращает, сколько съел. У
32
- * `--init` и `--write` пустое значение законное «по умолчанию», а у `--config`
33
- * это молчаливый пропуск: настройки были бы взяты не те, что назвал человек. */
31
+ /* A value flag takes the next argument and reports how many it consumed. An empty value is
32
+ * a legitimate "default" for `--init` and `--write`, but for `--config` it would be a
33
+ * silent skip: the settings read would not be the ones the user named. */
34
34
  function takeValue(flag, args, i, values) {
35
35
  const next = args[i + 1];
36
36
  const none = next === undefined || next[0] === '-';
37
37
  if (none && flag === '--config') {
38
- refuseCause('ключ без значения', 'у ключа «' + flag + '» нет значения: нужен файл настроек'
39
- + '\n починка: ' + cliCommand(flag + ' <файл>'));
38
+ refuseCause('flag without a value', 'the flag "' + flag + '" has no value: a settings file is needed'
39
+ + '\n fix: ' + cliCommand(flag + ' <file>'));
40
40
  }
41
41
  values[flag] = none ? null : next;
42
42
  return none ? 0 : 1;
43
43
  }
44
44
 
45
- /* Один проход по строке: слова, режимы и значения ключей. Отказы здесь только
46
- * про сам ключ (незнакомый, повторённый, без значения): сочетания разбирает
47
- * `checkArgs`, потому что они про названное вместе, а не про отдельный аргумент. */
45
+ /* One pass over the line: words, modes, flag values. Refusals here are about a single flag
46
+ * only (unknown, repeated, missing a value); combinations belong to `checkArgs`, because
47
+ * they are about what was named together rather than about one argument. */
48
48
  function scan(args) {
49
49
  const seen = new Set();
50
50
  const modes = [];
@@ -54,10 +54,10 @@ function scan(args) {
54
54
  const a = args[i];
55
55
  if (a[0] !== '-') { words.push(a); continue; }
56
56
  if (FLAGS.indexOf(a) < 0) {
57
- refuseCause('незнакомый ключ', 'незнакомый ключ «' + a + '»\n починка: ' + cliCommand('--help'));
57
+ refuseCause('unknown flag', 'unknown flag "' + a + '"\n fix: ' + cliCommand('--help'));
58
58
  }
59
59
  if (seen.has(a)) {
60
- refuseCause('повтор ключа', 'ключ «' + a + '» назван дважды\n починка: ' + cliCommand('--help'));
60
+ refuseCause('repeated flag', 'the flag "' + a + '" is named twice\n fix: ' + cliCommand('--help'));
61
61
  }
62
62
  seen.add(a);
63
63
  if (VALUE_FLAGS.indexOf(a) >= 0) i += takeValue(a, args, i, values);
@@ -66,98 +66,98 @@ function scan(args) {
66
66
  return { seen: seen, modes: modes, values: values, words: words };
67
67
  }
68
68
 
69
- /* Совет повторяет настройки, которые человек назвал: без `--config` команда ищет
70
- * файл под умолчательным именем, в проекте с другим именем не находит его и уводит
71
- * человека во второй отказпро файл настроек, которого у него нет. */
69
+ /* The advice repeats the settings the user named: without it a command looks for the
70
+ * default file name, does not find it in a project that named the file otherwise, and sends
71
+ * the user into a second refusal about a settings file they do not have. */
72
72
  function advisor(values) {
73
73
  const given = typeof values['--config'] === 'string'
74
74
  ? '--config ' + advicePath(values['--config']) + ' ' : '';
75
75
  return (rest) => cliCommand(given + rest);
76
76
  }
77
77
 
78
- /* Режим против ключей: сами режимы взаимоисключающие, а `--force` и `--config`
79
- * работают не с каждым из них. */
78
+ /* A mode against the flags: the modes exclude each other, and `--force` and `--config` do
79
+ * not go with every one of them. */
80
80
  function checkModes(plan) {
81
81
  const { modes, seen, mode, advice } = plan;
82
82
  if (modes.length > 1) {
83
- refuseCause('два режима сразу', 'два режима сразу: «' + modes[0] + '» и «' + modes[1] + '» — режим один'
84
- + '\n починка: ' + advice(modes[0]));
83
+ refuseCause('two modes at once', 'two modes at once: "' + modes[0] + '" and "' + modes[1]
84
+ + '" — it is one mode' + '\n fix: ' + advice(modes[0]));
85
85
  }
86
86
  if (seen.has('--force') && mode !== '--init') {
87
- refuseCause('несовместимый ключ', 'ключ «--force» работает только с «--init»'
88
- + '\n починка: ' + cliCommand('--init --force'));
87
+ refuseCause('incompatible flag', 'the flag "--force" works only with "--init"'
88
+ + '\n fix: ' + cliCommand('--init --force'));
89
89
  }
90
90
  if (seen.has('--config') && mode === '--init') {
91
- refuseCause('несовместимый ключ', 'у «--init» свой файл, а «--config» называет настройки проекта'
92
- + '\n починка: ' + cliCommand('--init <файл>'));
91
+ refuseCause('incompatible flag', '"--init" has a file of its own, while "--config" names the'
92
+ + ' settings of the project' + '\n fix: ' + cliCommand('--init <file>'));
93
93
  }
94
94
  }
95
95
 
96
- /* Слово, которого команда не знает. Отдельным вопросом, потому что виновников
97
- * тут двое: опечатка в команде и лишнее значение режима, который своё значение
98
- * уже забрал (у `--init` и `--write` оно одно). Оба случая обязаны назвать своего
99
- * виновника: у `--config` остаток именно команда, и зов её разбирается как
100
- * команда, а не как лишнее слово. */
96
+ /* A word the command does not know. Its own question, because there are two possible
97
+ * culprits: a typo in the command, or an extra value for a mode that already took one
98
+ * (`--init` and `--write` take a single value). Either way the culprit has to be named: a
99
+ * leftover after `--config` is the command itself, and its call is parsed as a command
100
+ * rather than as an extra word. */
101
101
  function checkUnknownWord(plan) {
102
102
  const { verb, values, advice } = plan;
103
103
  if (verb === null || COMMANDS.indexOf(verb) >= 0) return;
104
104
  const valued = MODES.find((f) => VALUE_FLAGS.indexOf(f) >= 0 && typeof values[f] === 'string');
105
105
  if (valued !== undefined) {
106
- refuseCause('лишнее слово', 'лишнее слово «' + verb + '»: «' + valued + '» принимает одно значение'
107
- + '\n починка: ' + advice(valued + ' ' + advicePath(values[valued])));
106
+ refuseCause('extra word', 'the extra word "' + verb + '": "' + valued + '" takes one value'
107
+ + '\n fix: ' + advice(valued + ' ' + advicePath(values[valued])));
108
108
  }
109
- refuseCause('неизвестная команда', 'неизвестная команда «' + verb + '»\n починка: ' + cliCommand('--help'));
109
+ refuseCause('unknown command', 'unknown command "' + verb + '"\n fix: ' + cliCommand('--help'));
110
110
  }
111
111
 
112
- /* Слово и режим вместе: команда задаёт, что ответить, режим что записать, и
113
- * одновременно они не работают. */
112
+ /* A word and a mode together: the command says what to answer, the mode what to write, and
113
+ * the two do not work at once. */
114
114
  function checkWordAgainstMode(plan) {
115
115
  const { verb, mode, advice } = plan;
116
116
  if (verb === null || mode === null) return;
117
- refuseCause('команда и режим', 'команда «' + verb + '» и режим «' + mode + '» — разное, вместе они не работают'
118
- + '\n починка: ' + advice(verb));
117
+ refuseCause('command and mode', 'the command "' + verb + '" and the mode "' + mode
118
+ + '" are different things and do not work together' + '\n fix: ' + advice(verb));
119
119
  }
120
120
 
121
- /* Сколько слов принято: у `explain` коммит один и он обязателен, у остальных
122
- * команд аргументов нет вовсе. Коммит либо не назван, либо назван не один раз
123
- * тупика два, а починка одна и та же. */
121
+ /* How many words are accepted: `explain` takes exactly one commit and requires it, the
122
+ * other commands take no argument at all. A missing commit and several commits are two
123
+ * dead ends with one and the same fix. */
124
124
  function checkWordCount(plan) {
125
125
  const { verb, arg, advice } = plan;
126
126
  if (verb === 'explain' && arg.length === 0) {
127
- refuseCause('нет коммита', 'команде «explain» нужен коммит: имя ревизии (HEAD, ветка, тег),'
128
- + ' sha или его начало'
129
- + '\n починка: ' + advice('explain <коммит>'));
127
+ refuseCause('no commit', 'the command "explain" needs a commit: a revision name (HEAD, a branch,'
128
+ + ' a tag), a sha or its beginning'
129
+ + '\n fix: ' + advice('explain <commit>'));
130
130
  }
131
131
  if (verb === 'explain' && arg.length > 1) {
132
- refuseCause('лишнее слово', 'команда «explain» принимает один коммит, а не ' + arg.length
133
- + ': «' + arg.slice(1).join('», «') + '» лишние\n починка: ' + advice('explain <коммит>'));
132
+ refuseCause('extra word', 'the command "explain" takes one commit, not ' + arg.length
133
+ + ': "' + arg.slice(1).join('", "') + '" are extra\n fix: ' + advice('explain <commit>'));
134
134
  }
135
135
  if (verb !== null && verb !== 'explain' && arg.length > 0) {
136
- refuseCause('лишнее слово', 'команда «' + verb + '» аргументов не принимает: «' + arg[0] + '» лишний'
137
- + '\n починка: ' + advice(verb));
136
+ refuseCause('extra word', 'the command "' + verb + '" takes no arguments: "' + arg[0] + '" is extra'
137
+ + '\n fix: ' + advice(verb));
138
138
  }
139
139
  }
140
140
 
141
- /* Форма ответа против всего остального. `--json` не режим, а форма, и правило у
142
- * него одно: ответ бывает ровно у трёх команд и у запуска без команды (прежняя
143
- * форма данных она заморожена эталоном паритета, убрать её нельзя). У команды
144
- * без ответа просить нечего, а у режима ответ уже один запись; оба случая
145
- * отказ, и каждый называет своего виновника. */
141
+ /* The shape of the answer against everything else. `--json` is a shape, not a mode, and it
142
+ * has one rule: exactly three commands and a bare run have an answer — the bare run is the
143
+ * older data form, frozen by the parity fixture and therefore not removable. A command
144
+ * without an answer has nothing to ask for, and a mode already has one answer, the write;
145
+ * both are refusals, and each names its culprit. */
146
146
  function checkAnswer(plan) {
147
147
  const { verb, mode, seen, advice } = plan;
148
148
  if (seen.has('--json') && verb !== null && ANSWER_COMMANDS.indexOf(verb) < 0) {
149
- refuseCause('нет ответа в JSON', 'у команды «' + verb + '» нет ответа в JSON'
150
- + '\n починка: ' + advice(verb));
149
+ refuseCause('no JSON answer', 'the command "' + verb + '" has no answer in JSON'
150
+ + '\n fix: ' + advice(verb));
151
151
  }
152
152
  if (seen.has('--json') && verb === null && mode !== null) {
153
- refuseCause('два ответа сразу', '«--json» и режим «' + mode + '» разное: данные или запись, но не оба'
154
- + '\n починка: ' + advice(mode));
153
+ refuseCause('two answers at once', '"--json" and the mode "' + mode + '" are different: the data or'
154
+ + ' the write, not both' + '\n fix: ' + advice(mode));
155
155
  }
156
156
  }
157
157
 
158
- /* Проверки сочетаний тремя группами и одним планом на всех: план это то, что
159
- * разбор уже знает (слово, аргумент, режим, названные ключи и совет), а группы
160
- * отвечают каждая за своё. Возврат готовый план запуска. */
158
+ /* Combination checks, in groups over one plan: the plan holds what the parse already knows
159
+ * (the word, the argument, the mode, the named flags and the advice), each group answers for
160
+ * its own subject, and what comes back is a ready plan to run. */
161
161
  function checkArgs(scanned) {
162
162
  const { words, seen, values, modes } = scanned;
163
163
  const plan = {
@@ -184,7 +184,7 @@ function checkArgs(scanned) {
184
184
  }
185
185
 
186
186
  export function parseArgs(args) {
187
- // Справка отвечает всегда и первой: она и есть выход из любой опечатки.
187
+ // Help always answers, and answers first: it is the way out of any typo.
188
188
  if (args.indexOf('--help') >= 0 || args.indexOf('-h') >= 0) return { help: true };
189
189
  const scanned = scan(args);
190
190
  return Object.assign({ help: false, values: scanned.values }, checkArgs(scanned));
package/src/artifact.js CHANGED
@@ -3,28 +3,28 @@ import path from 'path';
3
3
  import { reportData } from './data.js';
4
4
  import { pageHtml } from './page/build.js';
5
5
 
6
- /* Отчёт один файл: самодостаточная страница. Она и есть артефакт, потому что
7
- * несёт всё сама (данные, оформление, программу), а второй формы того же отчёта не
8
- * существует: два вывода одной истории разошлись бы молча, и выбрать, какой из них
9
- * верный, было бы нечем.
6
+ /* The report is one file: a self-sufficient page. It is the artifact because it carries everything itself
7
+ * (data, styling, program), and a second form of the same report does not exist: two outputs of one history
8
+ * would drift apart silently, with nothing to tell which of them is right.
10
9
  *
11
- * Через это место проходят оба потребителя режим записи (`--write`) и хук после
12
- * коммита (`src/hook.js`), поэтому «что записано в файл» не может разойтись между
13
- * ними: в коммит хук кладёт ровно те байты, которые показывает `--write`.
10
+ * Both consumers pass through herethe writing mode (`--write`) and the hook after a commit
11
+ * (`src/hook.js`), so "what went into the file" cannot drift between them: the hook commits exactly the bytes
12
+ * `--write` shows.
14
13
  *
15
- * Каталог создаётся здесь же: `--write docs/size-report.html` в свежем проекте
16
- * обычный запуск, а не ошибка пользователя. Тем же путём пишется черновик настроек
17
- * (`--init`), поэтому он один на пакет. */
14
+ * The directory is created here as well: `--write docs/size-report.html` in a fresh project is an ordinary run
15
+ * rather than a user's mistake. The settings draft (`--init`) is written the same way, which is why there is one
16
+ * such place in the package. */
18
17
 
19
- /* Запись файла с созданием каталога: путь может не существовать ни одной своей
20
- * частью это не ошибка того, кто его назвал. */
18
+ /* Writing a file while creating the directory: not a single part of the path may exist, and that is not a
19
+ * mistake of whoever named it. */
21
20
  export function writeFileEnsured(file, text) {
22
21
  fs.mkdirSync(path.dirname(file), { recursive: true });
23
22
  fs.writeFileSync(file, text);
24
23
  }
25
24
 
26
- /* Собранные байты отчёта без записи: они же нужны проверке (`таблица совпадает с
27
- * историей`), и собирать их вторым способом значило бы сверять не то, что пишется. */
25
+ /* The report's bytes built without writing: the check needs them too (it compares the file on disk with exactly
26
+ * these bytes and reports it as diverging from the history), and building them a second way would compare
27
+ * something other than what gets written. */
28
28
  export function artifact(cfg, root) {
29
29
  const data = reportData(cfg, root);
30
30
  return { data: data, html: pageHtml(data, cfg), file: path.join(root, cfg.output) };
package/src/check.js CHANGED
@@ -4,29 +4,27 @@ import { sensorGaps } from './metrics.js';
4
4
  import { outsideFix, pathRoles } from './config.js';
5
5
  import { cliCommand } from './refusal.js';
6
6
 
7
- /* Полнота покрытия ответ на вопрос «всё ли в истории попало в отчёт». Правило
8
- * из требований (§4.2) одно: каждый путь, тронутый историей, обязан быть либо
9
- * колонкой, либо объявленным исключением (`skip` и сам файл отчёта), а путь,
10
- * который не то и не другое, — это нарушение: изменение прошло мимо отчёта.
7
+ /* Coveragethe answer to "did everything in the history get into the report". The project's rule of
8
+ * completeness is one: every path the history touched has to be either a column or a declared exception
9
+ * (`skip` and the report file itself), and a path that is neither is a violation — an edit went past the
10
+ * report.
11
11
  *
12
- * Поэтому полнота считается по фактам истории (объединение изменённых путей всех
13
- * коммитов), а не по списку файлов в дереве: файл, появившийся и удалённый до
14
- * HEAD, в дереве не виден, а история его помнит и его правки не посчитаны
15
- * ничем. Отсюда же и то, чего инструмент не утверждает: он не говорит, «правильные»
16
- * ли колонки выбрал проект, он говорит только, что мимо них ничего не прошло и
17
- * что именно не поместилось.
12
+ * Hence coverage is counted over the facts of the history (the union of the changed paths of every commit)
13
+ * rather than over the file list in the tree: a file created and deleted before HEAD is invisible there
14
+ * while the history remembers it, and its edits went into no number at all. The same place explains what
15
+ * the tool does **not** claim: it does not say whether the project picked the "right" columns — only that
16
+ * nothing went past them, and what exactly did not fit.
18
17
  *
19
- * Сводка по строкам идёт тем же проходом, что и отчёты (`measureHistory`), по
20
- * тем же причинам, что и везде: второго расчёта в пакете нет. Отсюда цена: ответ
21
- * стоит одного прохода по истории, как и любой отчёт. */
18
+ * The rows are counted by the same run as the reports (`measureHistory`), for the usual reason: there is no
19
+ * second calculation in the package. That is the price the answer costs one pass over the history, like
20
+ * any report. */
22
21
 
23
- // Сколько путей и коммитов показывать человеку в тексте: остальное числом.
22
+ // How many paths and commits to show a person in the text: the rest as a number.
24
23
  const SHOW = 8;
25
24
 
26
- /* Список коммитов одной причиныстроками не длиннее экрана: у большой истории
27
- * одних пропущенных коммитов набирается полсотни, и одна строка на всех не
28
- * читается вовсе. Все они остаются в ответе целиком в `--json`), но перенос
29
- * делает его читаемым. */
25
+ /* The list of commits of one reason lines no longer than a screen: a large history gathers fifty dropped
26
+ * commits alone, and one line for all of them is not readable at all. Every one of them stays in the answer
27
+ * (in full in `--json`), while the wrapping is what makes it readable. */
30
28
  function wrapped(head, items) {
31
29
  const lines = [];
32
30
  let line = head;
@@ -39,15 +37,15 @@ function wrapped(head, items) {
39
37
  return lines;
40
38
  }
41
39
 
42
- const REASON_WORD = { merge: 'слияние', report: 'только таблица', flat: 'без изменения объёма' };
40
+ const REASON_WORD = { merge: 'merges', report: 'report only', flat: 'no change of volume' };
43
41
 
44
42
  function short(sha) {
45
43
  return sha.slice(0, 7);
46
44
  }
47
45
 
48
- /* Пути, тронутые историей, разложенные на три части: отслеживаемые колонками,
49
- * исключённые объявлением и незнакомые. Само суждение одно и живёт в настройках
50
- * (`pathRoles`), а здесь только его применение ко всей истории. */
46
+ /* The paths the history touched, split in three: tracked by columns, excluded by declaration, and unknown.
47
+ * The judgement itself is one and lives with the settings (`pathRoles`); here it is only applied to the
48
+ * whole history. */
51
49
  function pathCoverage(cfg, commits) {
52
50
  const role = pathRoles(cfg);
53
51
  const seen = { covered: new Set(), excluded: new Set() };
@@ -57,9 +55,8 @@ function pathCoverage(cfg, commits) {
57
55
  const kind = role(f);
58
56
  if (kind === 'columns') { seen.covered.add(f); return; }
59
57
  if (kind === 'excluded') { seen.excluded.add(f); return; }
60
- /* Коммит, заведший путь, первый по истории (порядок чтения от старых к
61
- * новым), и показывается он человеку как улика: по нему видно, чья это была
62
- * правка. */
58
+ /* The commit that introduced the path is the first one in the history (reads run oldest first), and it
59
+ * is shown to a person as evidence: it tells whose edit it was. */
63
60
  if (!unknown.has(f)) unknown.set(f, { path: f, since: c.sha, subject: c.subject });
64
61
  });
65
62
  });
@@ -70,9 +67,9 @@ function pathCoverage(cfg, commits) {
70
67
  };
71
68
  }
72
69
 
73
- /* Полный ответ: настройки, история, пути, датчикии вердикт. `ok` это только
74
- * «мимо отчёта не прошло»: датчик, считающий приближённо, вердикта не меняет (это
75
- * отдельный код выхода, как и у остальных режимов). */
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,
72
+ * as in every other mode). */
76
73
  export function coverage(cfg, root, configFile) {
77
74
  assertFullHistory(root);
78
75
  const commits = readHistory(root);
@@ -104,26 +101,28 @@ export function coverage(cfg, root, configFile) {
104
101
  };
105
102
  }
106
103
 
107
- /* Текст для человека: короткое «да» или список того, что не поместилось,с
108
- * путём, коммитом, который его завёл, и готовой командой. Печатает `cli`, а не
109
- * этот модуль: у модуля нет и не должно быть вывода. */
104
+ /* The text for a person: a short "yes", or the list of what did not fit with the path, the commit that
105
+ * introduced it, and a ready command. `cli` does the printing rather than this module: a module has no
106
+ * output and must not have one. */
110
107
  export function coverageText(rep) {
111
108
  const lines = [];
112
109
  const unknown = rep.paths.unknown;
113
- const counts = ' история: ' + rep.history.commits + ' коммитов, ' + rep.history.rows + ' строк, без строки '
110
+ const counts = ' history: ' + rep.history.commits + ' commits, ' + rep.history.rows + ' rows, without a row '
114
111
  + rep.history.dropped.length;
115
112
  if (!rep.ok) {
116
- lines.push('✗ покрытие: ' + unknown.length + (unknown.length === 1 ? ' путь' : ' путей')
117
- + ' истории не отслеживается и не исключён');
113
+ lines.push('✗ coverage: ' + unknown.length
114
+ + (unknown.length === 1
115
+ ? ' path of the history is neither tracked nor excluded'
116
+ : ' paths of the history are neither tracked nor excluded'));
118
117
  unknown.slice(0, SHOW).forEach((u) => {
119
- lines.push(' ' + u.path + ' — с ' + short(u.since) + ' «' + u.subject.slice(0, 60) + '»');
118
+ lines.push(' ' + u.path + ' — since ' + short(u.since) + ' "' + u.subject.slice(0, 60) + '"');
120
119
  });
121
- if (unknown.length > SHOW) lines.push(' … ещё ' + (unknown.length - SHOW));
122
- lines.push(' починка: ' + outsideFix(unknown.map((u) => u.path))
123
- + '; черновик колонок по расширениям даёт ' + cliCommand('--init draft.json'));
120
+ if (unknown.length > SHOW) lines.push(' … and ' + (unknown.length - SHOW) + ' more');
121
+ lines.push(' fix: ' + outsideFix(unknown.map((u) => u.path))
122
+ + '; a draft of columns by extension: ' + cliCommand('--init draft.json'));
124
123
  } else {
125
- lines.push('✓ покрытие: ' + rep.history.commits + ' коммитов истории, ' + rep.history.rows
126
- + ' строк, тронутые пути отслеживаются или исключены');
124
+ lines.push('✓ coverage: ' + rep.history.commits + ' commits of the history, ' + rep.history.rows
125
+ + ' rows, every touched path is tracked or excluded');
127
126
  }
128
127
  lines.push(counts + ' (' + Object.keys(rep.history.byReason)
129
128
  .filter((k) => rep.history.byReason[k] > 0)
@@ -134,7 +133,7 @@ export function coverageText(rep) {
134
133
  if (shas.length > 0) lines.push(...wrapped(' ' + REASON_WORD[key] + ' (' + shas.length + '): ', shas));
135
134
  });
136
135
  }
137
- lines.push(' пути: ' + rep.paths.covered + ' отслеживаются, ' + rep.paths.excluded + ' исключены'
138
- + (rep.ok ? '' : ', ' + unknown.length + ' незнакомы'));
136
+ lines.push(' paths: ' + rep.paths.covered + ' tracked, ' + rep.paths.excluded + ' excluded'
137
+ + (rep.ok ? '' : ', ' + unknown.length + ' unknown'));
139
138
  return lines.join('\n');
140
139
  }