agent-quality-kit 0.6.0 → 0.8.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 +61 -2
- package/README.ru.md +61 -2
- package/kit/docs/ai/agent-harness-playbook.md +1 -1
- package/kit/docs/ready-made-rules.md +29 -4
- package/kit/gates/README.md +40 -0
- package/kit/gates/_skip.sh +61 -1
- package/kit/gates/ci-actually-fails/README.md +12 -0
- package/kit/gates/ci-actually-fails/check.sh +26 -3
- package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +16 -0
- package/kit/gates/ci-actually-fails/red/.github/workflows/soft.yml +15 -0
- package/kit/gates/ci-not-hijackable/README.md +56 -0
- package/kit/gates/ci-not-hijackable/check.sh +73 -0
- package/kit/gates/ci-not-hijackable/gate.yml +19 -0
- package/kit/gates/ci-not-hijackable/green/.github/workflows/triage.yml +19 -0
- package/kit/gates/ci-not-hijackable/red/.github/workflows/triage.yml +18 -0
- package/kit/gates/color-from-token/check.sh +19 -3
- package/kit/gates/color-from-token/green/Button.tsx +2 -0
- package/kit/gates/commit-explains-itself/README.md +13 -3
- package/kit/gates/commit-explains-itself/check.sh +8 -4
- package/kit/gates/complexity-limit/README.md +5 -0
- package/kit/gates/complexity-limit/check.sh +27 -9
- package/kit/gates/complexity-limit/green/test_fixtures.py +14 -0
- package/kit/gates/deps-are-pinned/README.md +14 -1
- package/kit/gates/deps-are-pinned/check.sh +6 -1
- package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/pyproject.toml +12 -0
- package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/requirements.txt +3 -0
- package/kit/gates/deps-are-pinned/red/pyproject-loose/pyproject.toml +12 -0
- package/kit/gates/deps-are-pinned/red/pyproject-loose/requirements.txt +3 -0
- package/kit/gates/duplicate-code/README.md +11 -2
- package/kit/gates/duplicate-code/check.sh +36 -5
- package/kit/gates/duplicate-code/gate.yml +8 -0
- package/kit/gates/duplicate-code/green/imports_a.go +20 -0
- package/kit/gates/duplicate-code/green/imports_b.go +19 -0
- package/kit/gates/entry-links-exist/README.md +5 -0
- package/kit/gates/entry-links-exist/check.sh +10 -1
- package/kit/gates/entry-links-exist/green/AGENTS.md +5 -0
- package/kit/gates/file-size-limit/README.md +9 -2
- package/kit/gates/file-size-limit/check.sh +14 -2
- package/kit/gates/gate-not-weakened/check.sh +13 -1
- package/kit/gates/hook-actually-fires/README.md +74 -0
- package/kit/gates/hook-actually-fires/check.sh +183 -0
- package/kit/gates/hook-actually-fires/gate.yml +15 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/hooks.json +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/settings.json +74 -0
- package/kit/gates/hook-actually-fires/green/.claude/settings.local.json +74 -0
- package/kit/gates/hook-actually-fires/red/.claude/hooks/hooks.json +4 -0
- package/kit/gates/hook-actually-fires/red/.claude/settings.json +53 -0
- package/kit/gates/no-phantom-package/README.md +84 -0
- package/kit/gates/no-phantom-package/check.sh +161 -0
- package/kit/gates/no-phantom-package/gate.yml +20 -0
- package/kit/gates/no-phantom-package/green/AGENTS.md +15 -0
- package/kit/gates/no-phantom-package/red/AGENTS.md +15 -0
- package/kit/gates/no-print-in-prod/README.md +33 -39
- package/kit/gates/no-print-in-prod/gate.yml +14 -6
- package/kit/gates/personal-config-not-shared/README.md +66 -0
- package/kit/gates/personal-config-not-shared/check.sh +103 -0
- package/kit/gates/personal-config-not-shared/gate.yml +16 -0
- package/kit/gates/personal-config-not-shared/green/.aqk-tracked +9 -0
- package/kit/gates/personal-config-not-shared/red/.aqk-tracked +6 -0
- package/kit/gates/secrets-not-in-code/check.sh +29 -4
- package/kit/gates/secrets-not-in-code/green/testdata/certificate/key.pem +3 -0
- package/kit/gates/swallowed-error/README.md +36 -18
- package/kit/gates/swallowed-error/gate.yml +13 -3
- package/kit/gates/test-has-assertion/check.sh +13 -1
- package/kit/gates/test-not-adjusted/README.md +79 -0
- package/kit/gates/test-not-adjusted/check.sh +136 -0
- package/kit/gates/test-not-adjusted/gate.yml +19 -0
- package/kit/gates/test-not-adjusted/green/after/calc.py +6 -0
- package/kit/gates/test-not-adjusted/green/after/tests/test_calc.py +9 -0
- package/kit/gates/test-not-adjusted/green/before/calc.py +2 -0
- package/kit/gates/test-not-adjusted/green/before/tests/test_calc.py +5 -0
- package/kit/gates/test-not-adjusted/red/after/calc.py +2 -0
- package/kit/gates/test-not-adjusted/red/after/tests/test_calc.py +5 -0
- package/kit/gates/test-not-adjusted/red/before/calc.py +2 -0
- package/kit/gates/test-not-adjusted/red/before/tests/test_calc.py +7 -0
- package/kit/gates/todo-without-task/README.md +6 -0
- package/kit/gates/todo-without-task/check.sh +14 -2
- package/kit/gates/todo-without-task/green/app.py +1 -0
- package/kit/ratchet/ratchet.sh +70 -2
- package/kit/rules/general.md +9 -0
- package/kit/rules-en/general.md +82 -0
- package/kit/rules-en/security.md +33 -0
- package/kit/rules-en/testing.md +48 -0
- package/llms.txt +22 -1
- package/package.json +6 -2
- package/tool/commands/badge.mjs +7 -1
- package/tool/commands/context.mjs +260 -0
- package/tool/commands/doctor.mjs +72 -25
- package/tool/commands/gates.mjs +10 -4
- package/tool/commands/learn.mjs +159 -0
- package/tool/commands/project.mjs +9 -1
- package/tool/commands/prove.mjs +67 -0
- package/tool/commands/report.mjs +37 -2
- package/tool/i18n/en-docs.mjs +154 -0
- package/tool/i18n/en.mjs +70 -90
- package/tool/i18n/ru-docs.mjs +156 -0
- package/tool/i18n/ru.mjs +69 -90
- package/tool/i18n/templates-en.mjs +1 -1
- package/tool/i18n/templates-ru.mjs +1 -1
- package/tool/lib/core.mjs +37 -2
- package/tool/lib/evidence.mjs +124 -0
- package/tool/lib/manifest.mjs +63 -5
- package/tool/lib/prove.mjs +172 -0
- package/tool/lib/repo.mjs +31 -2
- package/tool/lib/scope.mjs +46 -2
- package/tool/lib/templates.mjs +3 -0
- package/tool/program.mjs +23 -22
- package/tool/selfcheck/gates.sh +66 -0
- package/tool/selfcheck/mutation.sh +21 -1
- package/tool/selfcheck/smoke.sh +519 -39
- package/tool/selfcheck/units-context.mjs +186 -0
- package/tool/selfcheck/units-evidence.mjs +83 -0
- package/tool/selfcheck/units-learn.mjs +88 -0
- package/tool/selfcheck/units-level.mjs +122 -0
- package/tool/selfcheck/units.mjs +114 -2
- package/kit/gates/no-print-in-prod/check.sh +0 -38
- package/kit/gates/no-print-in-prod/green/docs.ts +0 -15
- package/kit/gates/no-print-in-prod/green/main.go +0 -8
- package/kit/gates/no-print-in-prod/green/main.rs +0 -4
- package/kit/gates/no-print-in-prod/red/main.go +0 -8
- package/kit/gates/no-print-in-prod/red/main.rs +0 -4
- package/kit/gates/swallowed-error/check.sh +0 -54
- package/kit/gates/swallowed-error/green/run.js +0 -8
- package/kit/gates/swallowed-error/red/run.js +0 -3
- /package/kit/gates/commit-explains-itself/green/{COMMIT_MSG → .aqk-commit-msg} +0 -0
- /package/kit/gates/commit-explains-itself/red/{COMMIT_MSG → .aqk-commit-msg} +0 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// tool/lib/prove.mjs — доказательство гейтов проекта: краснеет ли гейт на своём красном
|
|
2
|
+
// образце и молчит ли на зелёном.
|
|
3
|
+
//
|
|
4
|
+
// ЗАЧЕМ ЭТО ВООБЩЕ СУЩЕСТВУЕТ. Уровень AQK до 2026-09-07 считался наличием файлов: есть папка
|
|
5
|
+
// правил, объявлен хоть один гейт, существуют папки образцов и храповиков, есть журнал. Проект
|
|
6
|
+
// с тремя гейтами `true` — командой, которая всегда отвечает «ок», — проходил порог AQK-3 и
|
|
7
|
+
// получал значок. Проверено прогоном на пустой папке; три «гейта», ноль защиты, высший уровень.
|
|
8
|
+
//
|
|
9
|
+
// Это ровно тот класс, против которого весь комплект: `pytest || true`, только на уровне всего
|
|
10
|
+
// стандарта. Наше же правило гласит: «если утверждение не проверяется машиной — его в стандарте
|
|
11
|
+
// нет». Уровень был самым громким нашим утверждением, и машина проверяла у него `exists()`.
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { readFile } from "node:fs/promises";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { CWD, exists } from "./core.mjs";
|
|
16
|
+
import { parseManifest } from "./manifest.mjs";
|
|
17
|
+
|
|
18
|
+
// Гейт можно доказать, если у него есть оба образца. Признак по образцам, а не по тексту
|
|
19
|
+
// команды: запись, делегирующая готовому инструменту (`npx knip --directory .`), каталог
|
|
20
|
+
// образцов в команде не упоминает, но образцы у неё есть — их кладёт `aqk add`.
|
|
21
|
+
async function samplesFor(samplesDir, name) {
|
|
22
|
+
if (!samplesDir) return null;
|
|
23
|
+
const red = join(CWD, samplesDir, name, "red");
|
|
24
|
+
const green = join(CWD, samplesDir, name, "green");
|
|
25
|
+
if (!(await exists(red)) || !(await exists(green))) return null;
|
|
26
|
+
return { red: join(samplesDir, name, "red"), green: join(samplesDir, name, "green") };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Команда записи всегда кончается каталогом проверки: рецепт пишется как `… {dir}`, и при
|
|
30
|
+
// установке `{dir}` превращается в «.». Чтобы прогнать гейт по образцу, подменяем ПОСЛЕДНЕЕ
|
|
31
|
+
// слово команды. Делается это только для гейтов, у которых образцы есть, — то есть для
|
|
32
|
+
// установленных из каталога, где форма команды наша и известна.
|
|
33
|
+
// Обёртки снимаются ПЕРЕД тем, как решать что-либо о команде. Их две, и обе меняют смысл
|
|
34
|
+
// первых слов:
|
|
35
|
+
//
|
|
36
|
+
// `bash …/ratchet.sh <реестр> <команда…>` — мера постепенности. Без снятия доказательство
|
|
37
|
+
// гоняет гейт ВМЕСТЕ с реестром долга, и реестр перезаписывается находками из ОБРАЗЦА:
|
|
38
|
+
// на живом проекте это стёрло бы долг целиком. Поймано прогоном комплекта.
|
|
39
|
+
//
|
|
40
|
+
// `bash gates/_native.sh <каталог> <команда…>` — фильтр образцов для родного инструмента.
|
|
41
|
+
// Его первый аргумент обязан ехать вместе с каталогом проверки: фильтр прячет пути
|
|
42
|
+
// `gates/*/red|green`, и оставленный «.» спрятал бы ровно то, что образец обязан показать, —
|
|
43
|
+
// красный образец прошёл бы зелёным.
|
|
44
|
+
function unwrap(cmd) {
|
|
45
|
+
let parts = String(cmd).trim().split(/\s+/);
|
|
46
|
+
let nativeAt = -1;
|
|
47
|
+
for (let guard = 0; guard < 4; guard++) {
|
|
48
|
+
const r = parts.findIndex((p) => /ratchet\.sh$/.test(p));
|
|
49
|
+
if (r !== -1 && parts.length > r + 2) { parts = parts.slice(r + 2); continue; }
|
|
50
|
+
const n = parts.findIndex((p) => /_native\.sh$/.test(p));
|
|
51
|
+
if (n !== -1 && parts.length > n + 2) { nativeAt = n; break; }
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
return { parts, nativeAt };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Куда подставлять каталог образца. Рецепт каталога всегда кончается `{dir}`, и при установке
|
|
58
|
+
// это превращается в «.». Если последнее слово не похоже на каталог проверки, подставлять
|
|
59
|
+
// некуда: команда написана руками, и угадывать значит объявить исправный гейт сломанным.
|
|
60
|
+
// Найдено код-ревью 2026-09-07 на примере `eslint . --max-warnings 0`.
|
|
61
|
+
function targetIsLast(parts) {
|
|
62
|
+
const last = parts[parts.length - 1];
|
|
63
|
+
return last === "." || last === "./";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Путь уходит в СТРОКУ КОМАНДЫ, а её исполняет `sh` — не Node. Для `sh` обратный слэш это
|
|
67
|
+
// экранирование, а не разделитель: `gates\\x\\red` превращается в `gatesxred`, каталога с таким
|
|
68
|
+
// именем нет, обход молчит, гейт выходит с нулём — и доказательство объявляет ИСПРАВНЫЙ гейт
|
|
69
|
+
// сломанным. Найдено вторым пользователем 2026-09-08 на Windows: `path.join` там даёт `\\`,
|
|
70
|
+
// и падали ровно те записи, что обходят дерево через `find`; на `grep -r` выживали, потому что
|
|
71
|
+
// Windows разбирает слэши сам. Худший из возможных отказов: комплект против «зелёного, потому
|
|
72
|
+
// что ничего не проверялось» сам выдал зелёное за красное и отобрал у проекта ступень.
|
|
73
|
+
// Нормализация стоит ЗДЕСЬ, а не у сборщика пути: это единственная дверь из мира путей Node
|
|
74
|
+
// в мир оболочки, и закрывать её надо один раз, кто бы путь ни собрал.
|
|
75
|
+
const forShell = (p) => String(p).replace(/\\/g, "/");
|
|
76
|
+
|
|
77
|
+
function commandFor(cmd, dirRaw) {
|
|
78
|
+
const dir = forShell(dirRaw);
|
|
79
|
+
const { parts, nativeAt } = unwrap(cmd);
|
|
80
|
+
const out = parts.slice();
|
|
81
|
+
out[out.length - 1] = dir;
|
|
82
|
+
// Фильтр образцов узнаёт, что ему дали именно образец, по своему первому аргументу.
|
|
83
|
+
if (nativeAt !== -1) out[nativeAt + 1] = dir;
|
|
84
|
+
return out.join(" ");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Каким рецептом написаны образцы. Читается из `gate.yml`, который `aqk add` кладёт в проект
|
|
88
|
+
// рядом с проверкой; если файла нет — молчим, значит запись не из каталога.
|
|
89
|
+
async function samplesForRecipe(samplesDir, name) {
|
|
90
|
+
const yml = join(CWD, samplesDir, name, "gate.yml");
|
|
91
|
+
if (!(await exists(yml))) return null;
|
|
92
|
+
try {
|
|
93
|
+
const rec = parseManifest(await readFile(yml, "utf8"));
|
|
94
|
+
const lang = typeof rec?.samples_for === "string" ? rec.samples_for.trim() : "";
|
|
95
|
+
if (!lang) return null;
|
|
96
|
+
const recipe = String(rec?.recipes?.[lang] || "").trim();
|
|
97
|
+
if (!recipe) return null;
|
|
98
|
+
return { lang, prog: recipe.split(/\s+/)[0] };
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function run(cmd, timeoutMs) {
|
|
105
|
+
const r = spawnSync(cmd, { shell: true, encoding: "utf8", cwd: CWD, timeout: timeoutMs });
|
|
106
|
+
const out = `${r.stdout || ""}${r.stderr || ""}`.trim();
|
|
107
|
+
// Убитый по таймауту процесс возвращает null — это не «ноль», а «не знаем».
|
|
108
|
+
const code = r.status === null ? 124 : r.status;
|
|
109
|
+
return { code, out };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Возвращает { proven, broken, unprovable, results } — числами и списком, чтобы вызывающий
|
|
113
|
+
// сам решал, что печатать и чем краснеть.
|
|
114
|
+
async function proveGates(man, { timeoutMs = 300000 } = {}) {
|
|
115
|
+
const gates = man?.gates && typeof man.gates === "object" && !Array.isArray(man.gates) ? man.gates : {};
|
|
116
|
+
const samplesDir = typeof man?.samples === "string" ? man.samples.trim() : "";
|
|
117
|
+
const results = [];
|
|
118
|
+
|
|
119
|
+
for (const [name, rawCmd] of Object.entries(gates)) {
|
|
120
|
+
const cmd = String(rawCmd || "").trim();
|
|
121
|
+
if (!cmd) {
|
|
122
|
+
results.push({ name, state: "broken", why: "empty" });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const s = await samplesFor(samplesDir, name);
|
|
126
|
+
if (!s) {
|
|
127
|
+
results.push({ name, state: "unprovable", why: "no-samples" });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Образцы бывают написаны под КОНКРЕТНЫЙ рецепт: запись без переносимой проверки называет
|
|
132
|
+
// его полем `samples_for`. Если в проекте стоит рецепт под другой язык, гонять по этим
|
|
133
|
+
// образцам нечего — они на чужом языке. Найдено первым же прогоном на своём репозитории:
|
|
134
|
+
// `dead-code` стоит у нас рецептом под JS (`knip`), а образцы у него питоновские, и
|
|
135
|
+
// «доказательство» объявляло исправный гейт сломанным.
|
|
136
|
+
// Сравниваем с командой БЕЗ обёрток: установленная запись выглядит как
|
|
137
|
+
// `bash gates/_native.sh . ruff check …`, и проверка по сырой строке объявляла бы
|
|
138
|
+
// «стоит другой рецепт» у каждой обёрнутой записи. Найдено код-ревью 2026-09-07:
|
|
139
|
+
// проект, поставивший `no-print-in-prod` с `ruff`, терял AQK-2 целиком.
|
|
140
|
+
const { parts: bare, nativeAt } = unwrap(cmd);
|
|
141
|
+
const effective = nativeAt === -1 ? bare : bare.slice(nativeAt + 2);
|
|
142
|
+
const forRecipe = await samplesForRecipe(samplesDir, name);
|
|
143
|
+
if (forRecipe && effective[0] !== forRecipe.prog) {
|
|
144
|
+
results.push({ name, state: "unprovable", why: "other-recipe", forRecipe });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (!targetIsLast(bare)) {
|
|
148
|
+
results.push({ name, state: "unprovable", why: "no-target" });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const red = run(commandFor(cmd, s.red), timeoutMs);
|
|
153
|
+
const green = run(commandFor(cmd, s.green), timeoutMs);
|
|
154
|
+
if (red.code === 0) {
|
|
155
|
+
results.push({ name, state: "broken", why: "red-passed", red, green });
|
|
156
|
+
} else if (green.code !== 0) {
|
|
157
|
+
results.push({ name, state: "broken", why: "green-failed", red, green });
|
|
158
|
+
} else {
|
|
159
|
+
results.push({ name, state: "proven", red, green });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const proven = results.filter((r) => r.state === "proven").length;
|
|
164
|
+
const broken = results.filter((r) => r.state === "broken").length;
|
|
165
|
+
const unprovable = results.filter((r) => r.state === "unprovable").length;
|
|
166
|
+
// Доказательство состоялось, если ни один доказуемый гейт не сломан И хоть один доказан.
|
|
167
|
+
// Второе условие обязательно: проект, у которого все гейты недоказуемы, ничего не доказал —
|
|
168
|
+
// именно так выглядит подделка с тремя `true`.
|
|
169
|
+
return { proven, broken, unprovable, ok: broken === 0 && proven > 0, results };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export { proveGates, commandFor };
|
package/tool/lib/repo.mjs
CHANGED
|
@@ -38,6 +38,15 @@ const MARKS = [
|
|
|
38
38
|
["has_docker", ["Dockerfile", "compose.yml", "compose.yaml", "docker-compose.yml", "docker-compose.yaml"]],
|
|
39
39
|
["has_deps", ["package.json", "pyproject.toml", "requirements.txt", "go.mod", "Cargo.toml", "Gemfile", "pom.xml", "composer.json"]],
|
|
40
40
|
["has_env", [".env", ".env.example", ".env.sample"]],
|
|
41
|
+
// Обвес самого агента: настройки, хуки, права. Отдельный признак нужен, потому что записи
|
|
42
|
+
// про него не касаются проектов, где агента не настраивали вовсе, — а таких большинство.
|
|
43
|
+
// Файл `.claude/settings.json` есть и у того, кто настроил один только список разрешений;
|
|
44
|
+
// записи этой группы сами промолчат, если проверять в нём нечего.
|
|
45
|
+
["has_agent_config", [".claude/settings.json", ".claude/settings.local.json", ".claude/hooks.json", ".claude/hooks/hooks.json"]],
|
|
46
|
+
// Агента здесь вообще используют: есть свод, который он читает при запуске. Признак шире,
|
|
47
|
+
// чем `has_agent_config`: настройки заводят не все, а свод — почти каждый, кто работает с
|
|
48
|
+
// агентом. Записи про личные файлы касаются именно вторых.
|
|
49
|
+
["has_agent_entry", ["CLAUDE.md", "AGENTS.md", ".claude", ".cursor/rules", ".github/copilot-instructions.md"]],
|
|
41
50
|
];
|
|
42
51
|
|
|
43
52
|
async function detectFacts(man) {
|
|
@@ -209,18 +218,38 @@ function whichSync(prog, env = process.env) {
|
|
|
209
218
|
// Выбор рецепта под стек проекта. Одна логика на два места: и `doctor`, и `add` показывают
|
|
210
219
|
// команду, но подставляют в неё разные пути — один в каталог пакета, другой в каталог проекта.
|
|
211
220
|
// Пока это были две копии, правка доезжала до одной из них — нашёл собственный гейт дублей.
|
|
212
|
-
|
|
221
|
+
// `missing` — необязательный массив: сюда складываются имена программ, которых не хватило.
|
|
222
|
+
// Без него отказ установки был тупиком: «нет команды ни под python, ни общей» — диагноз без
|
|
223
|
+
// действия. Правило «находка без действия закрывает окно, а не дефект» мы требуем от записей
|
|
224
|
+
// каталога; к собственной программе оно относится так же. Найдено первым прогоном в чужом
|
|
225
|
+
// репозитории (httpx) 2026-09-07.
|
|
226
|
+
function pickRecipe(rec, facts, missing) {
|
|
213
227
|
const recipes = rec.recipes && typeof rec.recipes === "object" ? rec.recipes : {};
|
|
214
228
|
|
|
215
229
|
// Родной рецепт лучше переносимого — но только если его есть чем выполнить. Поставить
|
|
216
230
|
// команду с неустановленной программой значит завести гейт, который встаёт с «not found»:
|
|
217
231
|
// отсутствие сигнала неотличимо от успеха.
|
|
218
232
|
const runnable = (c0) => Boolean(whichSync(String(c0).trim().split(/\s+/)[0]));
|
|
233
|
+
|
|
234
|
+
// `requires` проверяется ПЕРВЫМ. Раньше он стоял после перебора родных рецептов, и запись с
|
|
235
|
+
// рабочим родным рецептом и неудовлетворённым требованием ставилась как ни в чём не бывало —
|
|
236
|
+
// ровно тот отказ, ради которого поле и заведено. Приёмка и мутационная проверка применяют
|
|
237
|
+
// его безусловно; три реализации расходились. Найдено код-ревью 2026-09-07.
|
|
238
|
+
const needs = String(rec?.requires || "").trim().split(/[\s,]+/).filter(Boolean);
|
|
239
|
+
const lacking = needs.filter((p) => !whichSync(p));
|
|
240
|
+
if (lacking.length) {
|
|
241
|
+
for (const p of lacking) if (Array.isArray(missing) && !missing.includes(p)) missing.push(p);
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
219
245
|
for (const lang of facts.langs) {
|
|
220
246
|
if (!recipes[lang]) continue;
|
|
247
|
+
const prog = String(recipes[lang]).trim().split(/\s+/)[0];
|
|
221
248
|
if (runnable(recipes[lang])) return recipes[lang];
|
|
222
|
-
|
|
249
|
+
if (Array.isArray(missing) && !missing.includes(prog)) missing.push(prog);
|
|
250
|
+
console.log(c.dim(` ${c.yellow("!")} ${L.recipe.skipped(lang, prog)}`));
|
|
223
251
|
}
|
|
252
|
+
|
|
224
253
|
return recipes.any || null;
|
|
225
254
|
}
|
|
226
255
|
|
package/tool/lib/scope.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// tool/lib/scope.mjs —
|
|
1
|
+
// tool/lib/scope.mjs — разбор вывода гейта: сужение до дифа и отделение совета от находок.
|
|
2
2
|
//
|
|
3
3
|
// ЗАЧЕМ. Первый прогон в живом проекте показывает долг, накопленный годами: на репозитории в
|
|
4
4
|
// 36 тысяч файлов это тысячи находок. Человек видит стену красного, понимает, что разобрать её
|
|
@@ -33,6 +33,15 @@ const ANSI = new RegExp(String.fromCharCode(27) + "\\[[0-9;]*[a-zA-Z]", "g");
|
|
|
33
33
|
// принималась бы за находку и отбрасывалась.
|
|
34
34
|
const CANDIDATE = /[\w.@+-]+(?:\/[\w.@+-]+)*\.[A-Za-z][A-Za-z0-9]{0,9}/g;
|
|
35
35
|
|
|
36
|
+
// Все пути, названные в тексте. Тот же разбор, что при сужении: цвет снимается, вид пути
|
|
37
|
+
// нормализуется. Вынесено отдельно, потому что покрытие спрашивает у вывода обратное:
|
|
38
|
+
// не «попадает ли находка в диф», а «назвал ли гейт этот файл».
|
|
39
|
+
function pathsIn(text) {
|
|
40
|
+
const out = new Set();
|
|
41
|
+
for (const m of String(text).replace(ANSI, "").matchAll(CANDIDATE)) out.add(normPath(m[0]));
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
function inScope(candidate, files) {
|
|
37
46
|
const c = normPath(candidate);
|
|
38
47
|
if (files.has(c)) return true;
|
|
@@ -75,6 +84,41 @@ function scopeOutput(lines, files) {
|
|
|
75
84
|
return { kept, findings, scopable };
|
|
76
85
|
}
|
|
77
86
|
|
|
87
|
+
// СОВЕТ ПО ПОЧИНКЕ — ЭТО НЕ ХВОСТ ВЫВОДА, А ЕГО ГЛАВНАЯ ЧАСТЬ.
|
|
88
|
+
//
|
|
89
|
+
// Все записи каталога печатают последней строкой «почини: …». `doctor --run` показывал три
|
|
90
|
+
// первые строки и обрезал остальное — то есть ровно эту строку человек не видел никогда.
|
|
91
|
+
// Находка без действия закрывает окно, а не дефект. Отделяем совет от находок, чтобы находки
|
|
92
|
+
// можно было обрезать, а совет — нет.
|
|
93
|
+
//
|
|
94
|
+
// Опознаётся по метке в начале строки на любом из двух языков; продолжение совета — все строки
|
|
95
|
+
// после метки. Без метки совет не выдумывается: вывод, где не сказано, что делать, — это признак
|
|
96
|
+
// записи, которой нечего ответить, и приёмка каталога такую запись отклоняет.
|
|
97
|
+
const ADVICE_RE = /^\s*(почини|fix)\s*:/i;
|
|
98
|
+
|
|
99
|
+
// Находка опознаётся по «путь:строка:» — тем же признаком, что и при сужении по дифу.
|
|
100
|
+
// Нужен здесь, чтобы отличить продолжение совета от находки, стоящей сразу за ним.
|
|
101
|
+
const FINDING_RE = /:\d+:/;
|
|
102
|
+
|
|
103
|
+
function splitAdvice(lines) {
|
|
104
|
+
const findings = [];
|
|
105
|
+
const advice = [];
|
|
106
|
+
let inAdvice = false;
|
|
107
|
+
|
|
108
|
+
for (const raw of Array.isArray(lines) ? lines : []) {
|
|
109
|
+
const plain = String(raw).replace(ANSI, "");
|
|
110
|
+
if (ADVICE_RE.test(plain)) { inAdvice = true; advice.push(raw); continue; }
|
|
111
|
+
// Продолжение совета — строка с отступом, в которой нет находки. Всё остальное закрывает
|
|
112
|
+
// совет: иначе первая же метка утаскивала бы в жёлтое весь оставшийся вывод, а гейты
|
|
113
|
+
// печатают совет и посреди находок тоже. На живом выводе это давало сорок строк без
|
|
114
|
+
// обрезки — ровно ту стену, против которой написан весь модуль.
|
|
115
|
+
if (inAdvice && /^\s/.test(plain) && !FINDING_RE.test(plain)) { advice.push(raw); continue; }
|
|
116
|
+
inAdvice = false;
|
|
117
|
+
findings.push(raw);
|
|
118
|
+
}
|
|
119
|
+
return { findings, advice };
|
|
120
|
+
}
|
|
121
|
+
|
|
78
122
|
// Файлы, изменённые относительно ссылки. Новые файлы, ещё не добавленные в индекс, тоже входят:
|
|
79
123
|
// их код так же нов, как и остальной диф, а из `git diff` они не видны.
|
|
80
124
|
function changedFiles(ref, cwd) {
|
|
@@ -93,4 +137,4 @@ function changedFiles(ref, cwd) {
|
|
|
93
137
|
);
|
|
94
138
|
}
|
|
95
139
|
|
|
96
|
-
export { scopeOutput, changedFiles };
|
|
140
|
+
export { scopeOutput, splitAdvice, changedFiles, pathsIn, normPath };
|
package/tool/lib/templates.mjs
CHANGED
|
@@ -28,6 +28,7 @@ const MANIFEST_YML = [
|
|
|
28
28
|
"",
|
|
29
29
|
d.rules,
|
|
30
30
|
"rules: .aqk/rules",
|
|
31
|
+
"docs: .aqk/docs",
|
|
31
32
|
"gates:",
|
|
32
33
|
...d.gates,
|
|
33
34
|
"",
|
|
@@ -38,6 +39,8 @@ const MANIFEST_YML = [
|
|
|
38
39
|
d.lessons,
|
|
39
40
|
'lessons: ""',
|
|
40
41
|
"",
|
|
42
|
+
...d.advisory,
|
|
43
|
+
"",
|
|
41
44
|
].join("\n");
|
|
42
45
|
|
|
43
46
|
export { AGENTS_MD, CLAUDE_MD, MANIFEST_YML, GATE_YML_TEMPLATE, CHECK_SH_TEMPLATE, README_TEMPLATE };
|
package/tool/program.mjs
CHANGED
|
@@ -16,13 +16,16 @@
|
|
|
16
16
|
|
|
17
17
|
import { realpathSync } from "node:fs";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
|
-
import { c, SELF } from "./lib/core.mjs";
|
|
19
|
+
import { c, SELF, commandRows } from "./lib/core.mjs";
|
|
20
20
|
import { L } from "./i18n/index.mjs";
|
|
21
21
|
import { cmdInit, cmdNote, cmdBlob, cmdStart } from "./commands/project.mjs";
|
|
22
22
|
import { cmdDoctor } from "./commands/doctor.mjs";
|
|
23
23
|
import { cmdAdd, cmdNew, cmdRatchet, cmdFind, cmdWhy } from "./commands/gates.mjs";
|
|
24
24
|
import { cmdReport } from "./commands/report.mjs";
|
|
25
|
+
import { cmdLearn } from "./commands/learn.mjs";
|
|
25
26
|
import { cmdBadge } from "./commands/badge.mjs";
|
|
27
|
+
import { cmdProve } from "./commands/prove.mjs";
|
|
28
|
+
import { cmdContext } from "./commands/context.mjs";
|
|
26
29
|
|
|
27
30
|
// Разбор аргументов выполняется только при запуске файла как программы. При импорте —
|
|
28
31
|
// а так его читают модульные проверки tool/selfcheck/units.mjs — CLI запускаться не должен.
|
|
@@ -66,42 +69,40 @@ if (IS_MAIN) {
|
|
|
66
69
|
case "blob":
|
|
67
70
|
await cmdBlob();
|
|
68
71
|
break;
|
|
72
|
+
// Читает локальную переписку — поэтому только в терминал и всегда с кодом 0. Подробности
|
|
73
|
+
// и замер, на котором стоит отбор, — в шапке tool/commands/learn.mjs.
|
|
74
|
+
case "learn":
|
|
75
|
+
await cmdLearn();
|
|
76
|
+
break;
|
|
77
|
+
|
|
69
78
|
case "report":
|
|
70
79
|
await cmdReport();
|
|
71
80
|
break;
|
|
81
|
+
case "prove":
|
|
82
|
+
await cmdProve();
|
|
83
|
+
break;
|
|
84
|
+
// Печатает состояние репозитория для КОНТЕКСТА агента, а не для человека. Зовётся хуком
|
|
85
|
+
// SessionStart, поэтому ничего не запускает и всегда выходит с нулём: хук, роняющий запуск
|
|
86
|
+
// агента из-за неготового проекта, отключат в тот же день, и не станет ни хука, ни блока.
|
|
87
|
+
case "context":
|
|
88
|
+
await cmdContext(rest);
|
|
89
|
+
break;
|
|
72
90
|
case "badge":
|
|
73
91
|
await cmdBadge(rest);
|
|
74
92
|
break;
|
|
75
93
|
default: {
|
|
76
94
|
// Ширина колонки считается, а не подбирается пробелами: строки в двух языках разной
|
|
77
95
|
// длины, и вручную выровненная справка на втором языке разъезжается.
|
|
78
|
-
const
|
|
79
|
-
const rows = [
|
|
80
|
-
[`${SELF} init`, h.init],
|
|
81
|
-
[`${SELF} init --force`, h.initForce],
|
|
82
|
-
[`${SELF} start`, h.start],
|
|
83
|
-
[`${SELF} doctor`, h.doctor],
|
|
84
|
-
[`${SELF} doctor --run`, h.doctorRun],
|
|
85
|
-
[`${SELF} doctor --run --since main`, h.doctorSince],
|
|
86
|
-
[`${SELF} add ${h.name}`, h.add],
|
|
87
|
-
[`${SELF} find "…"`, h.find],
|
|
88
|
-
[`${SELF} why "…"`, h.why],
|
|
89
|
-
[`${SELF} ratchet ${h.name}`, h.ratchet],
|
|
90
|
-
[`${SELF} new ${h.name}`, h.new],
|
|
91
|
-
[`${SELF} note "…"`, h.note],
|
|
92
|
-
[`${SELF} blob`, h.blob],
|
|
93
|
-
[`${SELF} report`, h.report],
|
|
94
|
-
[`${SELF} badge`, h.badge],
|
|
95
|
-
];
|
|
96
|
+
const rows = commandRows(L).map((r) => [`${SELF} ${r.name}${r.args ? " " + r.args : ""}`, r.text]);
|
|
96
97
|
const width = Math.max(...rows.map(([cmdText]) => cmdText.length));
|
|
97
98
|
const lines = rows.map(([cmdText, text]) => ` ${c.bold(cmdText.padEnd(width))} ${text}`);
|
|
98
99
|
console.log(`
|
|
99
|
-
${c.bold("aqk")} — ${
|
|
100
|
+
${c.bold("aqk")} — ${L.help.tagline}
|
|
100
101
|
|
|
101
102
|
${lines.join("\n")}
|
|
102
103
|
|
|
103
|
-
${c.dim(
|
|
104
|
-
${c.dim(
|
|
104
|
+
${c.dim(L.help.noInstall)}
|
|
105
|
+
${c.dim(L.help.language)}
|
|
105
106
|
`);
|
|
106
107
|
process.exit(cmd ? 1 : 0);
|
|
107
108
|
}
|
package/tool/selfcheck/gates.sh
CHANGED
|
@@ -11,6 +11,15 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
|
11
11
|
CAT="$ROOT/kit/gates"
|
|
12
12
|
PASS=0; FAIL=0; WARN=0; UNVERIFIED=0
|
|
13
13
|
|
|
14
|
+
# СТРОГИЙ РЕЖИМ. «Не проверена здесь» — законное состояние на чужой машине, где нужного
|
|
15
|
+
# инструмента нет: требовать ruff от каждого, кто склонировал репозиторий, значит закрыть вклад.
|
|
16
|
+
# Но на НАШЕЙ машине, где инструменты ставятся отдельным шагом, это состояние означает, что шаг
|
|
17
|
+
# не сработал, — и жёлтая тильда неотличима от успеха ровно так же, как `pytest || true`.
|
|
18
|
+
# Проверено 2026-09-07: шаг `pipx install vulture` стоял в конвейере с самого его появления,
|
|
19
|
+
# ставил vulture исправно и клал его в каталог, которого нет в PATH задания. Запись dead-code не
|
|
20
|
+
# проверялась конвейером ни разу, и об этом никто не узнал.
|
|
21
|
+
STRICT="${AQK_GATES_STRICT:-0}"
|
|
22
|
+
|
|
14
23
|
ok() { printf ' \033[32m✔\033[0m %s\n' "$1"; PASS=$((PASS+1)); }
|
|
15
24
|
bad() { printf ' \033[31m✘\033[0m %s\n' "$1"; FAIL=$((FAIL+1)); }
|
|
16
25
|
warn() { printf ' \033[33m!\033[0m %s\n' "$1"; WARN=$((WARN+1)); }
|
|
@@ -30,6 +39,22 @@ printf '\n\033[1mtool/selfcheck/gates.sh\033[0m\n\n'
|
|
|
30
39
|
LIFE="$(AQK_LANG=ru node "$ROOT/tool/selfcheck/lifecycle.mjs" 2>/dev/null)"
|
|
31
40
|
life_field() { printf '%s\n' "$LIFE" | awk -F'|' -v s="$1" -v n="$2" '$1==s{print $n}'; }
|
|
32
41
|
|
|
42
|
+
# Запускает ОДИНОКУЮ копию check.sh — без соседнего _skip.sh — на зелёном образце записи.
|
|
43
|
+
# Успех означает провал приёмки: проверка, потерявшая свою библиотеку, обязана сказать об этом,
|
|
44
|
+
# а не выдать вердикт «чисто». Зелёный образец взят намеренно: на нём одинокая копия и раньше
|
|
45
|
+
# «проходила», так что разницу даёт именно потеря библиотеки, а не наличие нарушения.
|
|
46
|
+
lonely_copy_fails() {
|
|
47
|
+
_g="$1"
|
|
48
|
+
[ -f "$_g/check.sh" ] || return 0
|
|
49
|
+
_t="$(mktemp -d)" || return 0
|
|
50
|
+
mkdir -p "$_t/lone"
|
|
51
|
+
cp "$_g/check.sh" "$_t/lone/check.sh"
|
|
52
|
+
bash "$_t/lone/check.sh" "$_g/green" >/dev/null 2>&1
|
|
53
|
+
_c=$?
|
|
54
|
+
rm -rf "$_t"
|
|
55
|
+
[ "$_c" -ne 0 ]
|
|
56
|
+
}
|
|
57
|
+
|
|
33
58
|
for GATE in "$CAT"/*/; do
|
|
34
59
|
SLUG="$(basename "$GATE")"
|
|
35
60
|
YML="$GATE/gate.yml"
|
|
@@ -98,7 +123,22 @@ for GATE in "$CAT"/*/; do
|
|
|
98
123
|
# чью программу видно в системе: запись, которой нужен готовый инструмент, законна (правило
|
|
99
124
|
# «сперва готовое»), но проверить её можно только там, где инструмент стоит.
|
|
100
125
|
RECIPE="$(sed -n 's/^[[:space:]]*any:[[:space:]]*\(.*\)$/\1/p' "$YML" | head -1)"
|
|
126
|
+
PORTABLE=1
|
|
127
|
+
|
|
128
|
+
# Программа, названная полем `requires`. Нужна там, где переносимый рецепт — обёртка вокруг
|
|
129
|
+
# готового инструмента: первое слово команды тогда `bash`, и по нему не видно, чего не хватает.
|
|
130
|
+
# Без этой поправки отсутствие `checkwash` читалось как «гейт ругается на исправный код».
|
|
131
|
+
NEEDS="$(sed -n 's/^requires:[[:space:]]*\(.*\)$/\1/p' "$YML" | head -1 | tr ',' ' ')"
|
|
132
|
+
LACK=""
|
|
133
|
+
for PROG in $NEEDS; do
|
|
134
|
+
command -v "$PROG" >/dev/null 2>&1 || LACK="${LACK:+$LACK, }$PROG"
|
|
135
|
+
done
|
|
136
|
+
if [ -n "$LACK" ]; then
|
|
137
|
+
skip "$SLUG: НЕ ПРОВЕРЕНА здесь — нужен «$LACK»"
|
|
138
|
+
continue
|
|
139
|
+
fi
|
|
101
140
|
if [ -z "$RECIPE" ]; then
|
|
141
|
+
PORTABLE=0
|
|
102
142
|
# Каким рецептом написаны образцы — говорит сама запись. Угадывать нельзя: в системе может
|
|
103
143
|
# стоять npx, и питоновские образцы поедут проверяться фронтовым инструментом. Так и вышло.
|
|
104
144
|
FOR="$(sed -n 's/^samples_for:[[:space:]]*\(.*\)$/\1/p' "$YML" | head -1)"
|
|
@@ -138,8 +178,28 @@ for GATE in "$CAT"/*/; do
|
|
|
138
178
|
bad "$SLUG: арбитр промолчал на КРАСНОМ образце — гейт не ловит брак"
|
|
139
179
|
elif [ -z "$RED_OUT" ]; then
|
|
140
180
|
bad "$SLUG: арбитр покраснел на красном молча — по коду возврата не видно, ту ли поломку он нашёл"
|
|
181
|
+
# Находка без действия закрывает окно, а не дефект. Строка «почини: …» была у всех записей и
|
|
182
|
+
# раньше — но обрезалась в выводе прогона, то есть существовала для приёмки и не существовала
|
|
183
|
+
# для человека. Теперь она обязательна и здесь: правило, за которым не следит машина, — это
|
|
184
|
+
# пожелание, и через месяц запись без совета проедет незамеченной.
|
|
185
|
+
#
|
|
186
|
+
# ТОЛЬКО ДЛЯ ПЕРЕНОСИМОГО РЕЦЕПТА. Запись, целиком делегирующая готовому инструменту
|
|
187
|
+
# (`dead-code` — vulture, knip, staticcheck), печатает вывод ЭТОГО инструмента. Требовать от
|
|
188
|
+
# чужого вывода нашу строку — значит требовать невозможного, а невыполнимое требование норма
|
|
189
|
+
# либо обходит молча, либо ломает всем.
|
|
190
|
+
elif [ "$PORTABLE" -eq 1 ] && ! printf '%s\n' "$RED_OUT" | grep -qiE '^[[:space:]]*(почини|fix)[[:space:]]*:'; then
|
|
191
|
+
bad "$SLUG: на красном образце не сказано, ЧТО делать — нужна строка «почини: …»"
|
|
141
192
|
elif [ "$GRN_CODE" -ne 0 ]; then
|
|
142
193
|
bad "$SLUG: арбитр покраснел на ЗЕЛЁНОМ образце — гейт ругается на исправный код"
|
|
194
|
+
# Проверка без своей библиотеки обязана краснеть, а не молчать. Найдено 2026-09-08: строка
|
|
195
|
+
# `. _skip.sh 2>/dev/null || SKIP_NAMES=…` стояла в десяти записях и страховкой не была.
|
|
196
|
+
# Под bash — а гейты запускаются именно им — она подставляла только ПЕРЕМЕННУЮ; функции
|
|
197
|
+
# обхода оставались неопределёнными, конвейер печатал пустоту, и гейт выходил с НУЛЁМ на
|
|
198
|
+
# файле с настоящим нарушением. Худший из возможных отказов: проверка объявляет «чисто»,
|
|
199
|
+
# не посмотрев никуда. Ставим прибор, чтобы это не вернулось молча.
|
|
200
|
+
elif [ "$PORTABLE" -eq 1 ] && grep -q '_skip.sh' "$GATE/check.sh" 2>/dev/null &&
|
|
201
|
+
! lonely_copy_fails "$GATE"; then
|
|
202
|
+
bad "$SLUG: копия проверки без _skip.sh выходит с нулём — молчит вместо того, чтобы краснеть"
|
|
143
203
|
else
|
|
144
204
|
ok "$SLUG"
|
|
145
205
|
fi
|
|
@@ -154,4 +214,10 @@ fi
|
|
|
154
214
|
[ "$WARN" -gt 0 ] && printf ' \033[33m(условных: %s)\033[0m' "$WARN"
|
|
155
215
|
[ "$UNVERIFIED" -gt 0 ] && printf ' \033[33m(не проверено здесь: %s)\033[0m' "$UNVERIFIED"
|
|
156
216
|
printf '\n\n'
|
|
217
|
+
|
|
218
|
+
if [ "$STRICT" = "1" ] && [ "$UNVERIFIED" -gt 0 ]; then
|
|
219
|
+
printf ' \033[31mстрогий режим: %s записей не проверено, а здесь это обязано быть ошибкой\033[0m\n' "$UNVERIFIED"
|
|
220
|
+
printf ' почини: поставь названные инструменты и убедись, что они видны в PATH этого шага.\n\n'
|
|
221
|
+
exit $((FAIL + UNVERIFIED))
|
|
222
|
+
fi
|
|
157
223
|
exit "$FAIL"
|
|
@@ -19,11 +19,14 @@
|
|
|
19
19
|
set -uo pipefail
|
|
20
20
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
21
21
|
CAT="$ROOT/kit/gates"
|
|
22
|
-
PASS=0; FAIL=0; SKIP=0
|
|
22
|
+
PASS=0; FAIL=0; SKIP=0; UNVERIFIED=0
|
|
23
23
|
|
|
24
24
|
ok() { printf ' \033[32m✔\033[0m %s\n' "$1"; PASS=$((PASS+1)); }
|
|
25
25
|
bad() { printf ' \033[31m✘\033[0m %s\n' "$1"; FAIL=$((FAIL+1)); }
|
|
26
26
|
skip() { printf ' \033[2m·\033[0m %s\n' "$1"; SKIP=$((SKIP+1)); }
|
|
27
|
+
# «Нечем проверить здесь» — не то же, что «переносимого рецепта нет». Первое зависит от машины
|
|
28
|
+
# и лечится установкой; второе — свойство записи. Счётчики разные, чтобы итог не врал о причине.
|
|
29
|
+
unverified() { printf ' \033[33m~\033[0m %s\n' "$1"; UNVERIFIED=$((UNVERIFIED+1)); }
|
|
27
30
|
|
|
28
31
|
printf '\n\033[1mtool/selfcheck/mutation.sh\033[0m\n\n'
|
|
29
32
|
[ -d "$CAT" ] || { echo " каталога гейтов нет"; exit 1; }
|
|
@@ -54,6 +57,22 @@ for GATE in "$CAT"/*/; do
|
|
|
54
57
|
# машине без неё «не проверено» было бы неотличимо от «проверено»: ровно та тишина, против
|
|
55
58
|
# которой всё это написано. Записи без `any` называются вслух, а не пропускаются молча.
|
|
56
59
|
RECIPE="$(sed -n 's/^[[:space:]]*any:[[:space:]]*\(.*\)$/\1/p' "$YML" | head -1)"
|
|
60
|
+
# Запись может назвать нужную ей программу полем `requires`: по первому слову команды этого
|
|
61
|
+
# не видно, обёртка вокруг готового инструмента начинается с `bash`. Без этой поправки
|
|
62
|
+
# `test-not-adjusted` на машине без `checkwash` выглядел как «зелёный образец покраснел», хотя
|
|
63
|
+
# покраснел он от отсутствия инструмента, а не от мутации.
|
|
64
|
+
NEEDS="$(sed -n 's/^requires:[[:space:]]*\(.*\)$/\1/p' "$YML" | head -1 | tr ',' ' ')"
|
|
65
|
+
LACK=""
|
|
66
|
+
for PROG in $NEEDS; do
|
|
67
|
+
command -v "$PROG" >/dev/null 2>&1 || LACK="${LACK:+$LACK, }$PROG"
|
|
68
|
+
done
|
|
69
|
+
# Два разных состояния и два разных счётчика: «переносимого рецепта нет вовсе» и «рецепт есть,
|
|
70
|
+
# но нужного инструмента нет здесь». Один счётчик на оба означал бы, что итог врёт про причину
|
|
71
|
+
# — ровно то, что мы не прощаем гейтам. Найдено код-ревью 2026-09-07.
|
|
72
|
+
if [ -n "$LACK" ]; then
|
|
73
|
+
unverified "$SLUG: НЕ ПРОВЕРЕНА здесь — нужен «$LACK»"
|
|
74
|
+
continue
|
|
75
|
+
fi
|
|
57
76
|
if [ -z "$RECIPE" ]; then
|
|
58
77
|
skip "$SLUG: нет переносимого рецепта — мутации проверять нечем"
|
|
59
78
|
continue
|
|
@@ -91,5 +110,6 @@ else
|
|
|
91
110
|
printf ' \033[31mвердикт поплыл: %s\033[0m' "$FAIL"
|
|
92
111
|
fi
|
|
93
112
|
[ "$SKIP" -gt 0 ] && printf ' \033[2m(без переносимого рецепта: %s)\033[0m' "$SKIP"
|
|
113
|
+
[ "$UNVERIFIED" -gt 0 ] && printf ' \033[33m(не проверено здесь: %s)\033[0m' "$UNVERIFIED"
|
|
94
114
|
printf '\n\n'
|
|
95
115
|
exit "$FAIL"
|