agent-quality-kit 0.2.5 → 0.3.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 (37) hide show
  1. package/README.md +17 -0
  2. package/README.ru.md +16 -0
  3. package/kit/gates/_skip.sh +31 -3
  4. package/kit/gates/commit-explains-itself/gate.yml +1 -0
  5. package/kit/gates/complexity-limit/gate.yml +1 -0
  6. package/kit/gates/dead-code/gate.yml +1 -0
  7. package/kit/gates/deps-are-pinned/gate.yml +1 -0
  8. package/kit/gates/duplicate-code/gate.yml +1 -0
  9. package/kit/gates/entry-links-exist/gate.yml +1 -0
  10. package/kit/gates/file-size-limit/gate.yml +1 -0
  11. package/kit/gates/gate-has-samples/gate.yml +1 -0
  12. package/kit/gates/gates-are-runnable/gate.yml +1 -0
  13. package/kit/gates/gates-run-in-ci/gate.yml +1 -0
  14. package/kit/gates/lesson-has-outcome/gate.yml +1 -0
  15. package/kit/gates/no-print-in-prod/check.sh +3 -1
  16. package/kit/gates/no-print-in-prod/gate.yml +1 -0
  17. package/kit/gates/secrets-not-in-code/gate.yml +1 -0
  18. package/kit/gates/swallowed-error/gate.yml +1 -0
  19. package/kit/gates/todo-without-task/gate.yml +1 -0
  20. package/package.json +1 -1
  21. package/tool/commands/doctor.mjs +38 -41
  22. package/tool/commands/gates.mjs +103 -109
  23. package/tool/commands/project.mjs +84 -85
  24. package/tool/commands/report.mjs +194 -0
  25. package/tool/i18n/en.mjs +423 -0
  26. package/tool/i18n/index.mjs +33 -0
  27. package/tool/i18n/ru.mjs +424 -0
  28. package/tool/i18n/templates-en.mjs +164 -0
  29. package/tool/i18n/templates-ru.mjs +170 -0
  30. package/tool/lib/core.mjs +5 -1
  31. package/tool/lib/manifest.mjs +12 -31
  32. package/tool/lib/repo.mjs +45 -21
  33. package/tool/lib/templates.mjs +38 -182
  34. package/tool/program.mjs +31 -9
  35. package/tool/selfcheck/gates.sh +7 -1
  36. package/tool/selfcheck/smoke.sh +97 -0
  37. package/tool/selfcheck/units.mjs +80 -5
package/README.md CHANGED
@@ -104,6 +104,23 @@ let it grow. The rule applies from the day it is installed — the old code stay
104
104
  via `npx` the package is temporary, and tomorrow the command in your manifest would point at
105
105
  nothing.
106
106
 
107
+ ## Third-party code inside the repository
108
+
109
+ A reference copy, vendored code, generated clients — code that lives here but was not written
110
+ here. The scanning checks will skip it if you add `.aqkignore` in the root: one pattern per line,
111
+ `#` starts a comment, and `*` does not cross `/`.
112
+
113
+ ```
114
+ # brought in from another repository
115
+ third-party/
116
+ vendor/
117
+ *.generated.js
118
+ ```
119
+
120
+ `aqk report` prints the contents of this file as its own section. Hiding things silently is the
121
+ same class as a silent gate: a line here means there is no protection along that path, and will
122
+ not be.
123
+
107
124
  ## The catalogue of promises
108
125
 
109
126
  `doctor` inspects the repository — languages, existing gates — and shows **only what applies**:
package/README.ru.md CHANGED
@@ -101,6 +101,22 @@ aqk why "файл вырос до девяти тысяч строк"
101
101
  `add` **копирует проверку в репозиторий**, а не ссылается на пакет: при установке через `npx`
102
102
  пакет временный, и завтра команда в манифесте указывала бы в никуда.
103
103
 
104
+ ## Чужой код в репозитории
105
+
106
+ Референс, вендоринг, сгенерированные клиенты — код лежит здесь, но написан не здесь. Сканирующие
107
+ проверки по нему не пойдут, если завести `.aqkignore` в корне: по шаблону на строку, `#` —
108
+ комментарий, звёздочка не переходит через `/`.
109
+
110
+ ```
111
+ # принесено из другого репозитория
112
+ third-party/
113
+ vendor/
114
+ *.generated.js
115
+ ```
116
+
117
+ `aqk report` печатает содержимое этого файла отдельным разделом. Скрытое молча — тот же класс,
118
+ что молчащий гейт: строка здесь означает, что защиты по этому пути нет и не будет.
119
+
104
120
  ## Каталог обещаний
105
121
 
106
122
  `doctor` смотрит на репозиторий — языки, наличие гейтов — и показывает **только применимое**:
@@ -21,11 +21,39 @@ migrations"
21
21
  # (red-team тесты, что угодно) становился невидим для secrets-not-in-code во всех проектах,
22
22
  # куда ставили гейт. Фильтр ниже смотрит на путь целиком: только `gates/<имя>/red|green/`,
23
23
  # а не голое имя каталога.
24
+ # Список исключений САМОГО ПРОЕКТА: файл .aqkignore в корне, по шаблону на строку, «#» —
25
+ # комментарий. Нужен для кода, который лежит в репозитории, но написан не здесь: референс,
26
+ # вендоринг, сгенерированные клиенты. Без него единственным способом настройки была правка
27
+ # КОПИИ этого файла в проекте — то есть настройка правкой чужого файла, которую затирает
28
+ # следующий `aqk add`. Найдено первым чужим прогоном.
29
+ #
30
+ # Шаблон — фрагмент пути: `vendor/`, `apps/legacy`, `*.generated.js`. Звёздочка не переходит
31
+ # через «/», как в .gitignore, — иначе «src/*» прятало бы весь проект.
32
+ aqkignore_re() {
33
+ F="${1:-.}/.aqkignore"
34
+ [ -f "$F" ] || return 1
35
+ sed -e 's/#.*$//' -e 's|/*[[:space:]]*$||' -e 's/^[[:space:]]*//' "$F" |
36
+ grep -v '^$' |
37
+ sed -e 's/[][(){}.^$+?|\\]/\\&/g' -e 's/\*/[^\/]*/g' |
38
+ tr '\n' '|' | sed 's/|$//'
39
+ }
40
+
41
+ # Один фильтр на два дела, и это осознанно: его зовут все семь сканирующих проверок, а их
42
+ # копии уже лежат в чужих проектах. Добавить сюда — значит, что обновление комплекта включает
43
+ # .aqkignore и у тех, кто ставил гейты раньше, без перекопирования их check.sh.
24
44
  own_samples_filter() {
25
- case "${1:-}" in
26
- */red|*/red/|*/green|*/green/) cat ;;
27
- *) grep -vE '/gates/[^/]+/(red|green)(/|$)' ;;
45
+ DIR0="${1:-}"
46
+ RE="$(aqkignore_re "$DIR0")"
47
+ case "$DIR0" in
48
+ # Цель проверки — сам образец: тогда прятать его нельзя, иначе гейт «пройдёт» на красном.
49
+ */red|*/red/|*/green|*/green/) SAMPLES="cat" ;;
50
+ *) SAMPLES="grep -vE /gates/[^/]+/(red|green)(/|\$)" ;;
28
51
  esac
52
+ if [ -n "$RE" ]; then
53
+ $SAMPLES | grep -vE "(^|/)($RE)(/|:|\$)"
54
+ else
55
+ $SAMPLES
56
+ fi
29
57
  }
30
58
 
31
59
  # Расширения, где `print` и маркеры долга — конструкции языка, а не текст. Проверять по ним
@@ -1,4 +1,5 @@
1
1
  intent: коммит несёт мини-отчёт — что сделано и в чём агент не уверен
2
+ intent_en: the commit carries a mini-report — what was done and what the agent is unsure about
2
3
 
3
4
  trigger:
4
5
  always: true
@@ -1,4 +1,5 @@
1
1
  intent: функция не вырастает до сложности, в которой её нельзя удержать в голове
2
+ intent_en: a function does not grow past the complexity you can hold in your head
2
3
 
3
4
  trigger:
4
5
  always: true
@@ -1,4 +1,5 @@
1
1
  intent: код, который никто не вызывает, не остаётся в проекте
2
+ intent_en: code nobody calls does not stay in the project
2
3
 
3
4
  # Только там, где есть готовый инструмент: переносимой проверки здесь быть не может, а
4
5
  # показывать запись тем, кому её нечем исполнить, — значит показывать работу, которую
@@ -1,4 +1,5 @@
1
1
  intent: версии зависимостей закреплены — сборка воспроизводима
2
+ intent_en: dependency versions are pinned — the build is reproducible
2
3
 
3
4
  trigger:
4
5
  has_deps: true
@@ -1,4 +1,5 @@
1
1
  intent: один и тот же код не размножается по проекту копиями
2
+ intent_en: the same code does not multiply into copies across the project
2
3
 
3
4
  trigger:
4
5
  files_gt: 20
@@ -2,6 +2,7 @@
2
2
  # Читается программой; всё, что нельзя выполнить, живёт в README.md рядом.
3
3
 
4
4
  intent: файлы, на которые ссылается точка входа, существуют на диске
5
+ intent_en: files the entry point links to actually exist on disk
5
6
 
6
7
  # Когда запись показывается человеку. Отсутствие триггера сделало бы её шумом
7
8
  # для тех, кого она не касается.
@@ -1,4 +1,5 @@
1
1
  intent: файл не вырастает до размера, в котором агент теряется
2
+ intent_en: a file does not grow to a size where the agent gets lost
2
3
 
3
4
  trigger:
4
5
  always: true
@@ -1,4 +1,5 @@
1
1
  intent: у каждого объявленного гейта есть красный и зелёный образец
2
+ intent_en: every declared gate has a red and a green sample
2
3
 
3
4
  trigger:
4
5
  has_gates: true
@@ -1,4 +1,5 @@
1
1
  intent: каждый гейт, объявленный в манифесте, существует и запускается
2
+ intent_en: every gate declared in the manifest exists and runs
2
3
 
3
4
  trigger:
4
5
  has_gates: true
@@ -1,4 +1,5 @@
1
1
  intent: каждый объявленный гейт запускается конвейером, а не только руками
2
+ intent_en: every declared gate runs in the pipeline, not only by hand
2
3
 
3
4
  # Условия складываются: запись касается только тех, у кого есть и гейты, и конвейер.
4
5
  # Нет конвейера — сначала он, а не эта проверка.
@@ -1,4 +1,5 @@
1
1
  intent: каждая записанная шишка кончается решением, а не рассказом
2
+ intent_en: every recorded bruise ends in a decision, not a story
2
3
 
3
4
  # Проверять нечего там, где журнала нет: запись сама скажет об этом и промолчит.
4
5
  trigger:
@@ -14,7 +14,9 @@ DIR="${1:-.}"
14
14
  #
15
15
  # На настоящем проекте без этого различия 285 находок из 330 пришли из scripts/ и оснастки.
16
16
  # Гейт, который на 86% состоит из ложных сработок, выключают целиком.
17
- TOOLING="--exclude-dir=scripts --exclude-dir=tools --exclude-dir=bin --exclude-dir=examples --exclude-dir=notebooks --exclude-dir=docs --exclude-dir=.claude --exclude-dir=gates --exclude-dir=gates-reference"
17
+ TOOLING="--exclude-dir=scripts --exclude-dir=tools --exclude-dir=bin --exclude-dir=examples --exclude-dir=notebooks --exclude-dir=docs --exclude-dir=.claude --exclude-dir=gates"
18
+ # `gates-reference` отсюда убран: это имя каталога ОДНОГО проекта, зашитое в общий
19
+ # инструмент. Такому место в .aqkignore самого проекта — он появился позже этой строки.
18
20
 
19
21
  # Проект называет СВОИ каталоги, где печать — интерфейс, а не отладка: у программы командной
20
22
  # строки это её исходники целиком. Объявляется в манифесте, рядом с командой, и потому видно
@@ -1,4 +1,5 @@
1
1
  intent: отладочная печать не доезжает до прод-кода
2
+ intent_en: debug printing does not reach production code
2
3
 
3
4
  # Запись касается только языков, где есть эта конструкция. В проекте на Go или
4
5
  # Rust она не показывается вовсе.
@@ -1,4 +1,5 @@
1
1
  intent: ключи, пароли и приватные ключи не попадают в код
2
+ intent_en: keys, passwords and private keys do not end up in the code
2
3
 
3
4
  trigger:
4
5
  always: true
@@ -1,4 +1,5 @@
1
1
  intent: ошибка не глушится молча — она обработана и записана либо проброшена
2
+ intent_en: an error is not silently swallowed — it is handled and logged, or re-raised
2
3
 
3
4
  trigger:
4
5
  always: true
@@ -1,4 +1,5 @@
1
1
  intent: маркеров «доделать потом» нет в готовом коде — вместо них заведённая задача
2
+ intent_en: "fix later" markers are absent from finished code — a filed task replaces them
2
3
 
3
4
  trigger:
4
5
  always: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-quality-kit",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "AQK — Agent Quality Kit: переносимый комплект, приводящий проект в состояние, пригодное для работы агентов. Правила, механические упоры, накопленные уроки. Одна команда, любой инструмент.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,6 +6,7 @@ import { spawnSync } from "node:child_process";
6
6
  import { CWD, PKG_ROOT, TARGET_DIR, SELF, c, exists } from "../lib/core.mjs";
7
7
  import { readManifest, assessLevel } from "../lib/manifest.mjs";
8
8
  import { detectFacts, readCatalog, triggerVerdict, recipeFor } from "../lib/repo.mjs";
9
+ import { L } from "../i18n/index.mjs";
9
10
 
10
11
  async function reportCatalog(man, facts) {
11
12
  const catalog = await readCatalog();
@@ -19,27 +20,27 @@ async function reportCatalog(man, facts) {
19
20
  else todo.push(rec);
20
21
  }
21
22
 
22
- console.log(c.bold(`\n Гейты\n`));
23
+ console.log(c.bold(`\n ${L.doctor.gatesHeading}\n`));
23
24
  const marks = ["has_ci", "has_db", "has_docker", "has_tests", "has_deps"]
24
25
  .filter((k) => facts[k])
25
26
  .map((k) => k.replace("has_", ""));
26
27
  console.log(
27
- c.dim(` языки: ${[...facts.langs].join(", ") || "не определены"} · файлов: ${facts.files}` +
28
- (marks.length ? ` · есть: ${marks.join(", ")}` : "") + "\n")
28
+ c.dim(` ${L.doctor.langs}: ${[...facts.langs].join(", ") || L.doctor.langsUnknown} · ${L.doctor.files}: ${facts.files}` +
29
+ (marks.length ? ` · ${L.doctor.hasThings}: ${marks.join(", ")}` : "") + "\n")
29
30
  );
30
31
 
31
32
  for (const rec of held) console.log(` ${c.green("✔")} ${rec.slug.padEnd(22)} ${c.dim(rec.intent || "")}`);
32
33
  for (const rec of todo) {
33
34
  console.log(` ${c.yellow("✘")} ${rec.slug.padEnd(22)} ${rec.intent || ""}`);
34
- console.log(c.dim(` поставить: ${SELF} add ${rec.slug}`));
35
+ console.log(c.dim(` ${L.doctor.install(`${SELF} add ${rec.slug}`)}`));
35
36
  }
36
37
  if (skip.length) {
37
- console.log(c.dim(`\n Не применимо к этому репозиторию (${skip.length}):`));
38
+ console.log(c.dim(`\n ${L.doctor.notApplicable(skip.length)}`));
38
39
  for (const [rec, why] of skip) console.log(c.dim(` · ${rec.slug.padEnd(22)} ${why}`));
39
40
  }
40
41
  console.log(
41
- `\n ${c.bold("Итого:")} держит машина ${held.length}, применимо но не поставлено ${c.yellow(todo.length)}, ` +
42
- c.dim(`скрыто ${skip.length}`) + "\n"
42
+ `\n ${c.bold(L.doctor.total)} ${L.doctor.totalHeld(held.length)}, ${L.doctor.totalTodo(c.yellow(todo.length))}, ` +
43
+ c.dim(L.doctor.totalSkip(skip.length)) + "\n"
43
44
  );
44
45
  }
45
46
 
@@ -61,7 +62,7 @@ function runGates(man) {
61
62
  const gates = declaredGates(man);
62
63
  if (!gates.length) return { failed: 0, ran: 0, results: [] };
63
64
 
64
- console.log(c.bold("\n Прогон объявленных гейтов\n"));
65
+ console.log(c.bold(`\n ${L.doctor.runHeading}\n`));
65
66
  let failed = 0;
66
67
  const results = [];
67
68
 
@@ -71,9 +72,9 @@ function runGates(man) {
71
72
  const secs = (Math.max(0, Date.now() - t0) / 1000).toFixed(1);
72
73
 
73
74
  if (r.error && r.error.code === "ETIMEDOUT") {
74
- console.log(` ${c.red("✘")} ${name.padEnd(14)} ${c.red("не уложился в 5 минут")}`);
75
+ console.log(` ${c.red("✘")} ${name.padEnd(14)} ${c.red(L.doctor.timeout)}`);
75
76
  failed++;
76
- results.push({ name, cmd, ok: false, secs, note: "не уложился в 5 минут" });
77
+ results.push({ name, cmd, ok: false, secs, note: L.doctor.timeout });
77
78
  continue;
78
79
  }
79
80
  const code = r.status;
@@ -83,9 +84,9 @@ function runGates(man) {
83
84
  } else {
84
85
  failed++;
85
86
  const out = `${r.stdout || ""}${r.stderr || ""}`.trim().split("\n").filter(Boolean);
86
- console.log(` ${c.red("✘")} ${name.padEnd(14)} ${c.red(`код ${code}`)} ${c.dim(`· ${secs}s · ${cmd}`)}`);
87
+ console.log(` ${c.red("✘")} ${name.padEnd(14)} ${c.red(L.doctor.exitCode(code))} ${c.dim(`· ${secs}s · ${cmd}`)}`);
87
88
  for (const line of out.slice(0, 3)) console.log(c.dim(` ${line.slice(0, 100)}`));
88
- if (out.length > 3) console.log(c.dim(` … и ещё ${out.length - 3} строк`));
89
+ if (out.length > 3) console.log(c.dim(` ${L.doctor.moreLines(out.length - 3)}`));
89
90
  results.push({ name, cmd, ok: false, secs, code });
90
91
  }
91
92
  }
@@ -100,13 +101,13 @@ async function writeRunReport({ version, reached, results }) {
100
101
  const stamp = new Date().toISOString().replace("T", " ").slice(0, 16);
101
102
  const ok = results.filter((r) => r.ok).length;
102
103
  const lines = [
103
- `# aqk doctor --run — ${stamp}`,
104
- version ? `версия: ${version}` : null,
105
- `уровень: AQK-${reached < 0 ? "нет" : reached}`,
104
+ `# ${L.report.title} — ${stamp}`,
105
+ version ? `${L.report.version}: ${version}` : null,
106
+ `${L.report.level}: AQK-${reached < 0 ? L.doctor.levelNone : reached}`,
106
107
  "",
107
- ...results.map((r) => `${r.ok ? "✔" : "✘"} ${r.name} — ${r.secs}s${r.ok ? "" : ` (${r.note || `код ${r.code}`})`}`),
108
+ ...results.map((r) => `${r.ok ? "✔" : "✘"} ${r.name} — ${r.secs}s${r.ok ? "" : ` (${r.note || L.doctor.exitCode(r.code)})`}`),
108
109
  "",
109
- `итого: ${ok} из ${results.length} зелёных`,
110
+ L.report.summary(ok, results.length),
110
111
  ].filter((l) => l !== null);
111
112
 
112
113
  const dst = join(CWD, TARGET_DIR, "last-run.md");
@@ -129,11 +130,11 @@ async function cmdDoctor() {
129
130
  // репозитории и требовал разложить комплект в комплект.
130
131
  const inKit = resolve(CWD) === resolve(PKG_ROOT);
131
132
  const checks = [
132
- inKit ? ["kit/docs", "методички — здесь оригиналы, а не копия"] : [".aqk/docs", "методички"],
133
- inKit ? ["kit/rules", "стандарты — здесь оригиналы, а не копия"] : [".aqk/rules", "стандарты"],
134
- ["AGENTS.md", "точка входа для агентов"],
135
- [".gitignore", "гигиена репозитория"],
136
- [".git", "проект под контролем версий"],
133
+ inKit ? ["kit/docs", L.doctor.docsKit] : [".aqk/docs", L.doctor.docs],
134
+ inKit ? ["kit/rules", L.doctor.rulesKit] : [".aqk/rules", L.doctor.rules],
135
+ ["AGENTS.md", L.doctor.agents],
136
+ [".gitignore", L.doctor.gitignore],
137
+ [".git", L.doctor.git],
137
138
  ];
138
139
 
139
140
  let missing = 0;
@@ -150,8 +151,8 @@ async function cmdDoctor() {
150
151
  const emptyCommands = (text.match(/^- [^:]+: ``$/gm) || []).length;
151
152
  if (emptyCommands) {
152
153
  console.log(
153
- `\n ${c.yellow("!")} В AGENTS.md ${emptyCommands} незаполненных команд. ` +
154
- c.dim("Агент не может выполнить пустую строку.")
154
+ `\n ${c.yellow("!")} ${L.doctor.emptyCommands(emptyCommands)} ` +
155
+ c.dim(L.doctor.emptyCommandsWhy)
155
156
  );
156
157
  }
157
158
  }
@@ -159,7 +160,7 @@ async function cmdDoctor() {
159
160
  const man = await readManifest();
160
161
  const { reached, steps } = await assessLevel(man);
161
162
 
162
- console.log(c.bold("\n Уровень соответствия AQK\n"));
163
+ console.log(c.bold(`\n ${L.doctor.levelHeading}\n`));
163
164
  for (const s of steps) {
164
165
  const mark = s.ok ? c.green("✔") : reached + 1 === s.level ? c.yellow("→") : c.dim("·");
165
166
  console.log(` ${mark} AQK-${s.level} ${s.title}`);
@@ -171,25 +172,21 @@ async function cmdDoctor() {
171
172
  // работающих проверок и без манифеста стоит на нуле — и это сообщение обязано это объяснить,
172
173
  // иначе человек услышит «у тебя плохо» и закроет.
173
174
  if (reached < 0 && !man) {
174
- console.log(c.yellow("\n Уровень: стандарт в этом репозитории не заведён.\n"));
175
- console.log(
176
- c.dim(" Это не оценка проекта. Уровень мерит не зрелость практики, а то, можно ли\n") +
177
- c.dim(" прочитать её машиной. Проверки могут стоять и работать — но пока они не\n") +
178
- c.dim(" объявлены в .aqk.yml, ни агент, ни конвейер, ни новый человек о них не знают.\n")
179
- );
175
+ console.log(c.yellow(`\n ${L.doctor.levelNotSet}\n`));
176
+ console.log(L.doctor.levelNotSetWhy.map((line) => c.dim(` ${line}`)).join("\n") + "\n");
180
177
  } else {
181
178
  console.log(
182
179
  reached < 0
183
- ? c.yellow(`\n Уровень: манифест есть, но AQK-0 не пройден.\n`)
184
- : c.green(`\n Уровень: AQK-${reached}.\n`)
180
+ ? c.yellow(`\n ${L.doctor.levelManifestNoZero}\n`)
181
+ : c.green(`\n ${L.doctor.level(reached)}\n`)
185
182
  );
186
183
  }
187
184
 
188
185
  if (next) {
189
- console.log(` ${c.bold(`Чтобы достичь AQK-${next.level}:`)} ${next.need}`);
190
- console.log(c.dim(` Что это даст: ${next.gives}\n`));
186
+ console.log(` ${c.bold(L.doctor.toReach(next.level))} ${next.need}`);
187
+ console.log(c.dim(` ${L.doctor.gives(next.gives)}\n`));
191
188
  } else {
192
- console.log(c.green(" Все ступени пройдены.\n"));
189
+ console.log(c.green(` ${L.doctor.allDone}\n`));
193
190
  }
194
191
 
195
192
  const facts = await detectFacts(man);
@@ -205,8 +202,8 @@ async function cmdDoctor() {
205
202
  await writeRunReport({ version, reached, results: run.results });
206
203
  } else if (gates.length) {
207
204
  console.log(
208
- c.yellow(` ${gates.length} гейтов объявлено, но не запускалось.`) +
209
- c.dim(` «Объявлен» и «работает» — разные утверждения: ${SELF} doctor --run\n`)
205
+ c.yellow(` ${L.doctor.declaredNotRun(gates.length)}`) +
206
+ c.dim(L.doctor.declaredNotRunWhy(`${SELF} doctor --run`) + "\n")
210
207
  );
211
208
  }
212
209
 
@@ -217,8 +214,8 @@ async function cmdDoctor() {
217
214
  const pass = reached >= min && gateFailed === 0;
218
215
  console.log(
219
216
  pass
220
- ? c.green(` Порог AQK-${min} пройден.\n`)
221
- : c.red(` Порог AQK-${min} НЕ пройден: сейчас AQK-${reached < 0 ? "нет" : reached}.\n`)
217
+ ? c.green(` ${L.doctor.thresholdPass(min)}\n`)
218
+ : c.red(` ${L.doctor.thresholdFail(min, reached < 0 ? L.doctor.levelNone : reached)}\n`)
222
219
  );
223
220
  process.exit(pass ? 0 : 1);
224
221
  }
@@ -227,4 +224,4 @@ async function cmdDoctor() {
227
224
 
228
225
  // Наружу — только команда. Остальное здесь же и используется: экспорт, который никто не
229
226
  // импортирует, читается как «это часть договора» и мешает менять внутренности.
230
- export { cmdDoctor };
227
+ export { cmdDoctor, runGates, declaredGates };