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.
- package/README.md +17 -0
- package/README.ru.md +16 -0
- package/kit/gates/_skip.sh +31 -3
- package/kit/gates/commit-explains-itself/gate.yml +1 -0
- package/kit/gates/complexity-limit/gate.yml +1 -0
- package/kit/gates/dead-code/gate.yml +1 -0
- package/kit/gates/deps-are-pinned/gate.yml +1 -0
- package/kit/gates/duplicate-code/gate.yml +1 -0
- package/kit/gates/entry-links-exist/gate.yml +1 -0
- package/kit/gates/file-size-limit/gate.yml +1 -0
- package/kit/gates/gate-has-samples/gate.yml +1 -0
- package/kit/gates/gates-are-runnable/gate.yml +1 -0
- package/kit/gates/gates-run-in-ci/gate.yml +1 -0
- package/kit/gates/lesson-has-outcome/gate.yml +1 -0
- package/kit/gates/no-print-in-prod/check.sh +3 -1
- package/kit/gates/no-print-in-prod/gate.yml +1 -0
- package/kit/gates/secrets-not-in-code/gate.yml +1 -0
- package/kit/gates/swallowed-error/gate.yml +1 -0
- package/kit/gates/todo-without-task/gate.yml +1 -0
- package/package.json +1 -1
- package/tool/commands/doctor.mjs +38 -41
- package/tool/commands/gates.mjs +103 -109
- package/tool/commands/project.mjs +84 -85
- package/tool/commands/report.mjs +194 -0
- package/tool/i18n/en.mjs +423 -0
- package/tool/i18n/index.mjs +33 -0
- package/tool/i18n/ru.mjs +424 -0
- package/tool/i18n/templates-en.mjs +164 -0
- package/tool/i18n/templates-ru.mjs +170 -0
- package/tool/lib/core.mjs +5 -1
- package/tool/lib/manifest.mjs +12 -31
- package/tool/lib/repo.mjs +45 -21
- package/tool/lib/templates.mjs +38 -182
- package/tool/program.mjs +31 -9
- package/tool/selfcheck/gates.sh +7 -1
- package/tool/selfcheck/smoke.sh +97 -0
- 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` смотрит на репозиторий — языки, наличие гейтов — и показывает **только применимое**:
|
package/kit/gates/_skip.sh
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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: code nobody calls does not stay in the project
|
|
2
3
|
|
|
3
4
|
# Только там, где есть готовый инструмент: переносимой проверки здесь быть не может, а
|
|
4
5
|
# показывать запись тем, кому её нечем исполнить, — значит показывать работу, которую
|
|
@@ -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: every declared gate runs in the pipeline, not only by hand
|
|
2
3
|
|
|
3
4
|
# Условия складываются: запись касается только тех, у кого есть и гейты, и конвейер.
|
|
4
5
|
# Нет конвейера — сначала он, а не эта проверка.
|
|
@@ -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
|
|
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
|
# строки это её исходники целиком. Объявляется в манифесте, рядом с командой, и потому видно
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-quality-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "AQK — Agent Quality Kit: переносимый комплект, приводящий проект в состояние, пригодное для работы агентов. Правила, механические упоры, накопленные уроки. Одна команда, любой инструмент.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/tool/commands/doctor.mjs
CHANGED
|
@@ -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
|
|
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(`
|
|
28
|
-
(marks.length ? ` ·
|
|
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(`
|
|
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
|
|
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(
|
|
42
|
-
c.dim(
|
|
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(
|
|
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(
|
|
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:
|
|
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(
|
|
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(`
|
|
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
|
-
`#
|
|
104
|
-
version ?
|
|
105
|
-
|
|
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 ||
|
|
108
|
+
...results.map((r) => `${r.ok ? "✔" : "✘"} ${r.name} — ${r.secs}s${r.ok ? "" : ` (${r.note || L.doctor.exitCode(r.code)})`}`),
|
|
108
109
|
"",
|
|
109
|
-
|
|
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",
|
|
133
|
-
inKit ? ["kit/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("!")}
|
|
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(
|
|
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(
|
|
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
|
|
184
|
-
: c.green(`\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(
|
|
190
|
-
console.log(c.dim(`
|
|
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(
|
|
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(
|
|
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(`
|
|
221
|
-
: c.red(`
|
|
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 };
|