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
@@ -0,0 +1,170 @@
1
+ // tool/i18n/templates-ru.mjs — русские тексты, которые программа кладёт в чужой проект.
2
+ //
3
+ // ЗАЧЕМ ОТДЕЛЬНО ОТ ru.mjs. Шаблоны — это целые файлы, а не строки вывода: вместе они пробили
4
+ // бы собственный предел размера файла в 500 строк. Ключи всё равно сверяются с английскими:
5
+ // каталог подмешивает их к себе полем templates, и модульная проверка заходит внутрь.
6
+
7
+
8
+ // ОДИН источник правил, НЕСКОЛЬКО входов. Каждый инструмент читает свой файл, но оба ведут в
9
+ // .aqk/. Два расходящихся свода правил — худшее, что можно сделать: через месяц они врут
10
+ // по-разному, и никто не знает, какой настоящий.
11
+
12
+ const AGENTS_MD = `# AGENTS.md
13
+
14
+ > Точка входа для агента. Держи файл коротким: раздутый свод правил вытесняет саму задачу из
15
+ > контекста, и тогда игнорируются все правила разом. Всё длинное — по ссылкам ниже.
16
+
17
+ ## Железные правила
18
+
19
+ - **План до кода.** Нетривиальная задача начинается с плана, который человек одобрил словами.
20
+ - **Красный тест до кода.** Сначала проверка, которая падает, потом реализация.
21
+ - **Максимум 3 попытки.** Не решил за три — стоп и человеку, а не четвёртый заход.
22
+ - **Секреты только в окружении.** Никогда в коде, логах и коммитах.
23
+ - **Только файлы из задачи.** Заодно ничего не чиним.
24
+ - **Готово = доказано.** Назови арбитра: тест, живой прогон, сверка с источником. «Выглядит
25
+ рабочим» — не готово.
26
+ - **Ошибку не глотать.** Либо обработана и залогирована, либо проброшена.
27
+ - **Развилка — вопрос человеку.** Отступление от принятого решения не оформляется комментарием
28
+ в коде.
29
+ - **Отчёт о работе с комплектом — командой, а не словами.** Закончил — выполни \`aqk report\`.
30
+ Он собирается прогоном: пересказ по памяти всегда выбирает удобное и молчит о том, что гейт
31
+ стоит на слабейшем рецепте.
32
+
33
+ ## Оснастка AQK — это твои команды, а не человека
34
+
35
+ В репозитории стоит комплект AQK. Его смысл: **обещание проекта превращается в команду с кодом
36
+ возврата**, и дальше его держит машина, а не чья-то внимательность. «Не оставляем отладочную
37
+ печать» — текст, который можно проигнорировать; команда, возвращающая не ноль, — нельзя.
38
+
39
+ Запускать их — твоя работа. Человек смотрит на список дыр и решает, какие закрывать.
40
+
41
+ | Команда | Что делает | Когда звать |
42
+ |---|---|---|
43
+ | \`aqk doctor\` | смотрит репозиторий и печатает три списка: что уже держит машина, чего не хватает, что этому проекту не нужно и почему | начало работы над качеством; «что тут вообще есть» |
44
+ | \`aqk doctor --run\` | **запускает** объявленные проверки и показывает, кто нашёл брак | перед сдачей; после правок; всегда, когда нужен факт, а не обещание |
45
+ | \`aqk add <имя>\` | ставит проверку из каталога: копирует её и образцы в проект, объявляет в манифесте | человек согласился закрыть дыру из списка \`doctor\` |
46
+ | \`aqk ratchet <имя>\` | старые нарушения записывает долгом, новые перестаёт пускать | проверка краснеет на старом коде, и чинить его сейчас никто не будет |
47
+ | \`aqk find "…"\` | ищет по смыслу, есть ли уже такая проверка | прежде чем изобретать свою |
48
+ | \`aqk note "…"\` | пишет урок в общий журнал | процесс или прибор подвели: проверка соврала, правило обошли |
49
+ | \`aqk report\` | собирает прогоном отчёт: что стоит и каким рецептом, чего нет, что комплект велел прочитать | **обязательно** в конце работы с комплектом — вместо пересказа по памяти |
50
+
51
+ Если команды \`aqk\` нет в системе — комплект ставили разово, без установки. Тогда вместо
52
+ \`aqk\` пиши \`npx agent-quality-kit\`. Любая команда сама печатает тот
53
+ вызов, который сработает у тебя.
54
+
55
+ **Три вещи, которые надо понимать, а не запоминать:**
56
+
57
+ 1. **«Объявлен» и «работает» — разные утверждения.** \`doctor\` без \`--run\` честно говорит, что
58
+ проверки не запускал. Не выдавай объявленное за работающее.
59
+ 2. **Проверка без двух образцов ничего не доказывает.** Красный — код, на котором она обязана
60
+ сработать; зелёный — правильный, на котором обязана молчать. Зелёный важнее: без него однажды
61
+ она покраснеет на верном коде, и её выключат вместе с остальными.
62
+ 3. **Правило вводится храповиком, а не большой чисткой.** Чистка откладывается навсегда, потому
63
+ что она большая. Храповик даёт действующее правило со дня установки.
64
+
65
+ **Повторился дефект того же класса — это не повод быть внимательнее, а повод завести проверку.**
66
+ Дисциплина не масштабируется, механика — да.
67
+
68
+ ## Чужой код в репозитории
69
+
70
+ Код, который лежит здесь, но написан не здесь (референс, вендоринг, генерация),
71
+ исключается файлом \`.aqkignore\` в корне — по шаблону на строку. Правка копии \`_skip.sh\`
72
+ не настройка: её затрёт следующий \`aqk add\`.
73
+
74
+ ## Где что лежит
75
+
76
+ - \`.aqk/rules/\` — стандарты: общие, тесты, безопасность
77
+ - \`.aqk/docs/\` — методички: минимум проекта, харнес, процесс, исследования
78
+ - \`.aqk/docs/project-baseline.md\` — **начни отсюда**, если проект новый
79
+
80
+ ## Команды
81
+
82
+ <!-- Заполни под свой проект. Команда, которую нельзя скопировать и выполнить, — не команда. -->
83
+
84
+ - сборка: \`\`
85
+ - тесты: \`\`
86
+ - линтер: \`\`
87
+ - всё разом перед пушем: \`\`
88
+
89
+ ## Чего в этом проекте нет
90
+
91
+ <!-- Пиши сюда честно. Ненаписанное «нет» агент додумает как «есть». -->
92
+ `;
93
+
94
+ const CLAUDE_MD = `# CLAUDE.md
95
+
96
+ Правила этого проекта живут в \`AGENTS.md\` — читай его.
97
+
98
+ Один свод правил, несколько точек входа: \`AGENTS.md\` для агентов, понимающих его,
99
+ \`CLAUDE.md\` — для Claude Code. Держать два расходящихся свода нельзя: через месяц они врут
100
+ по-разному, и непонятно, какой настоящий.
101
+
102
+ @AGENTS.md
103
+ `;
104
+
105
+ // Манифест — единственный машиночитаемый файл стандарта. Всё остальное человекочитаемо.
106
+ // Пустые значения оставлены НАМЕРЕННО: заполненная заглушка врала бы про уровень.
107
+
108
+ const GATE_YML_TEMPLATE = (slug) => `# Запись каталога AQK. Норма и все поля — kit/gates/README.md
109
+ # Пока строки ниже не заполнены, проверка отклонит эту запись — так и задумано.
110
+
111
+ # Одной фразой: какой класс брака ловит. По этому полю идёт сверка «есть ли уже такое».
112
+ intent: ЗАПОЛНИ — какой класс брака ловит
113
+
114
+ # Когда запись показывается человеку. Условие обязано быть запросом к репозиторию,
115
+ # который умеет вычислить программа: always | langs: python, go | has_gates: true
116
+ trigger:
117
+ always: true
118
+
119
+ # Команда-арбитр под каждый стек. {gate} — папка записи, {dir} — что проверяем.
120
+ # any — переносимая команда без сторонних программ.
121
+ recipes:
122
+ any: bash {gate}/check.sh {dir}
123
+
124
+ # Реальный отказ, который эта проверка поймала. «Хорошая практика» не принимается:
125
+ # ссылайся на запись журнала — incidents/README.md
126
+ proof: ЗАПОЛНИ — какой отказ поймала и чего он стоил
127
+ `;
128
+
129
+ const CHECK_SH_TEMPLATE = `#!/usr/bin/env sh
130
+ # Проверка. Возвращает 0 — чисто, не 0 — брак. В тексте отказа должно быть НАПИСАНО,
131
+ # что сделать: он попадает прямо в контекст агента, и с инструкцией он чинит сам.
132
+ DIR="\${1:-.}"
133
+ . "$(dirname "$0")/../_skip.sh" 2>/dev/null || SKIP_NAMES=".git .aqk node_modules .venv"
134
+
135
+ # own_samples_filter прячет ТОЛЬКО gates/<имя>/red|green/ — не любую папку с таким именем
136
+ # в проекте. --exclude-dir=red по голому имени однажды спрятал бы настоящую пользовательскую
137
+ # папку red/ (нашли на secrets-not-in-code — kit/docs/ai/... журнал, 2026-09-04).
138
+ HITS=$(grep -rnE $(skip_grep "$DIR") 'ЗАПОЛНИ_ШАБЛОН_ПОИСКА' "$DIR" 2>/dev/null | own_samples_filter "$DIR")
139
+ if [ -n "$HITS" ]; then
140
+ echo "$HITS"
141
+ echo " почини: ЗАПОЛНИ — что именно сделать"
142
+ exit 1
143
+ fi
144
+ exit 0
145
+ `;
146
+
147
+ const README_TEMPLATE = (slug) => `# ЗАПОЛНИ — заголовок одной строкой
148
+
149
+ **Намерение.** Что и почему не должно попадать в код.
150
+
151
+ **Какой отказ это поймало.** Что сломалось, в каком проекте, чего это стоило. Без этого
152
+ запись не принимается: «это хорошая практика» набирает сотни пунктов, среди которых нельзя
153
+ выбрать.
154
+
155
+ **Почему машина, а не внимательность.** В какой момент человек это пропускает.
156
+
157
+ **Готовый аналог.** Есть ли такая проверка в ruff, eslint, semgrep? Если есть — рецепт под этот
158
+ язык обязан брать её, а своя проверка остаётся запасной. Если нет — написать, что именно
159
+ проверено: «не искал» и «нет» — разные утверждения.
160
+
161
+ **Чего НЕ ловит.** Честно назвать границу. Гейт, о пределах которого умалчивают, опаснее
162
+ отсутствующего: на него понадеются.
163
+
164
+ **Образцы.** \`red/\` — что здесь нарушено. \`green/\` — то же самое, но правильно.
165
+ `;
166
+
167
+ export const templates = {
168
+ AGENTS_MD, CLAUDE_MD,
169
+ GATE_YML_TEMPLATE, CHECK_SH_TEMPLATE, README_TEMPLATE,
170
+ };
package/tool/lib/core.mjs CHANGED
@@ -36,6 +36,10 @@ const exists = async (p) => access(p, constants.F_OK).then(() => true, () => fal
36
36
  // Имя в реестре, а не адрес репозитория: короче, скачивается 230 КБ вместо клона всего
37
37
  // репозитория и не заставляет человека ждать три минуты в тишине на первой же команде.
38
38
  const REPO = "agent-quality-kit";
39
+ // Адрес репозитория отдельно от имени пакета. Когда имя стало коротким, ссылка «поставь
40
+ // звезду» собиралась из него и вела на github.com/agent-quality-kit — несуществующую
41
+ // страницу. Два разных адреса, собранные из одной строки, однажды разъезжаются.
42
+ const REPO_URL = "https://github.com/arsen-ask-lx/Agent_Quality_Kit";
39
43
 
40
44
  function selfCmd() {
41
45
  const p = process.argv[1] || "";
@@ -96,5 +100,5 @@ export {
96
100
  copyDir, writeIfAbsent,
97
101
  PKG_ROOT, CWD, DOCS_SRC, RULES_SRC, TARGET_DIR,
98
102
  MANIFEST, GATES_SRC, PROJECT_GATES, RATCHET_DIR, RATCHET_LIB,
99
- SELF, REPO, c, exists, die, FEEDBACK_MARK,
103
+ SELF, REPO_URL, c, exists, die, FEEDBACK_MARK,
100
104
  };
@@ -3,6 +3,7 @@
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { join } from "node:path";
5
5
  import { CWD, MANIFEST, PROJECT_GATES, exists } from "./core.mjs";
6
+ import { L } from "../i18n/index.mjs";
6
7
 
7
8
  // СТАНДАРТ. Уровень — не самооценка и не галочка в README, а вычисляемое утверждение:
8
9
  // каждая ступень проверяется файлами на диске. Утверждение, которое нельзя проверить
@@ -75,36 +76,16 @@ async function assessLevel(man) {
75
76
  const gates = man?.gates && typeof man.gates === "object" && !Array.isArray(man.gates) ? man.gates : {};
76
77
  const filledGates = Object.entries(gates).filter(([, cmd]) => String(cmd || "").trim());
77
78
 
78
- const steps = [
79
- {
80
- level: 0,
81
- title: "манифест и точка входа",
82
- ok: Boolean(man?.aqk) && entriesExist,
83
- need: "создай .aqk.yml и укажи в entry файл, который агент читает первым (AGENTS.md)",
84
- gives: "любой инструмент понимает, что читать в этом репозитории",
85
- },
86
- {
87
- level: 1,
88
- title: "правила и работающие гейты",
89
- ok: (await has(man?.rules)) && filledGates.length > 0,
90
- need: "укажи rules (каталог стандартов) и заполни хотя бы один гейт в gates реальной командой",
91
- gives: "проверки объявлены командами, а не описаны словами",
92
- },
93
- {
94
- level: 2,
95
- title: "гейты доказаны, долг под храповиком",
96
- ok: (await has(man?.samples)) && (await has(man?.ratchets)),
97
- need: "заведи samples (красные и зелёные образцы гейтов) и ratchets (реестры долга)",
98
- gives: "гейт доказал, что ловит брак и молчит на исправном коде",
99
- },
100
- {
101
- level: 3,
102
- title: "уроки возвращаются в работу",
103
- ok: isUrl(man?.lessons) || (await has(man?.lessons)),
104
- need: "укажи lessons — путь или адрес журнала, где каждый инцидент даёт вывод",
105
- gives: "проект учится: одна и та же шишка не набивается дважды",
106
- },
79
+ // Условие ступени — здесь, её описание — в каталоге строк: текст переводится, условие нет.
80
+ // Разложить их по разным файлам стоило того, чтобы перевод не мог случайно поменять смысл
81
+ // проверки; порядок ступеней связывает их по индексу и сверяется модульной проверкой.
82
+ const conditions = [
83
+ Boolean(man?.aqk) && entriesExist,
84
+ (await has(man?.rules)) && filledGates.length > 0,
85
+ (await has(man?.samples)) && (await has(man?.ratchets)),
86
+ isUrl(man?.lessons) || (await has(man?.lessons)),
107
87
  ];
88
+ const steps = conditions.map((ok, level) => ({ level, ok, ...L.levels[level] }));
108
89
 
109
90
  let reached = -1;
110
91
  for (const s of steps) {
@@ -120,8 +101,8 @@ function manifestWithGate(text, slug, cmd) {
120
101
  const entry = ` ${slug}: "${cmd}"`;
121
102
 
122
103
  const gi = lines.findIndex((l) => /^gates:\s*$/.test(l));
123
- if (gi === -1) return { text: null, why: .aqk.yml нет блока gates:" };
124
- if (lines.some((l) => new RegExp(`^\\s+${slug}:`).test(l))) return { text: null, why: "уже объявлен" };
104
+ if (gi === -1) return { text: null, why: L.manifest.noGatesBlock };
105
+ if (lines.some((l) => new RegExp(`^\\s+${slug}:`).test(l))) return { text: null, why: L.manifest.alreadyDeclared };
125
106
 
126
107
  let last = gi;
127
108
  for (let i = gi + 1; i < lines.length; i++) {
package/tool/lib/repo.mjs CHANGED
@@ -2,10 +2,12 @@
2
2
  // триггера, выбор рецепта, сверка по намерению.
3
3
 
4
4
  import { readdir, readFile } from "node:fs/promises";
5
+ import { existsSync, statSync } from "node:fs";
5
6
  import { join, resolve } from "node:path";
6
7
  import { spawnSync } from "node:child_process";
7
8
  import { CWD, GATES_SRC, c, exists } from "./core.mjs";
8
9
  import { parseManifest } from "./manifest.mjs";
10
+ import { L, LANG } from "../i18n/index.mjs";
9
11
 
10
12
  // Каталог лежит в комплекте, а не в проекте: записи общие для всех, проект лишь
11
13
  // решает, какие из них у него стоят. Показывать все подряд нельзя — это и есть
@@ -103,7 +105,11 @@ async function readCatalog() {
103
105
  const yml = join(GATES_SRC, name.name, "gate.yml");
104
106
  if (!(await exists(yml))) continue;
105
107
  const rec = parseManifest(await readFile(yml, "utf8"));
106
- out.push({ slug: name.name, ...rec });
108
+ // Намерение показывается на языке вывода. Английское поле необязательно: запись, принесённая
109
+ // без него, покажет русское намерение — это хуже перевода, но честнее пустой строки, и
110
+ // не закрывает вклад тому, кто пишет на одном языке.
111
+ const intent = (LANG === "en" ? rec.intent_en : rec.intent) || rec.intent || rec.intent_en || "";
112
+ out.push({ slug: name.name, ...rec, intent });
107
113
  }
108
114
  return out.sort((a, b) => a.slug.localeCompare(b.slug));
109
115
  }
@@ -121,30 +127,22 @@ const CONDITIONS = {
121
127
  const want = String(val).split(",").map((x) => x.trim()).filter(Boolean);
122
128
  return want.some((l) => f.langs.has(l))
123
129
  ? { ok: true }
124
- : { ok: false, why: `нет языков: ${want.join(", ")}` };
130
+ : { ok: false, why: L.trigger.noLangs(want.join(", ")) };
125
131
  },
126
132
 
127
133
  files_gt: (val, f) =>
128
- f.files > Number(val) ? { ok: true } : { ok: false, why: `меньше ${val} файлов — рано` },
134
+ f.files > Number(val) ? { ok: true } : { ok: false, why: L.trigger.tooFewFiles(val) },
129
135
 
130
136
  files_lt: (val, f) =>
131
- f.files < Number(val) ? { ok: true } : { ok: false, why: `больше ${val} файлов` },
137
+ f.files < Number(val) ? { ok: true } : { ok: false, why: L.trigger.tooManyFiles(val) },
132
138
  };
133
139
 
134
- const FLAG_WHY = {
135
- has_gates: ["в манифесте не объявлено ни одного гейта", "гейты уже объявлены"],
136
- has_ci: ["в репозитории нет конвейера", "конвейер уже есть"],
137
- has_db: ["не видно базы данных: ни миграций, ни sql", "база данных есть"],
138
- has_docker: ["нет Dockerfile или compose", "docker уже есть"],
139
- has_deps: ["не видно файла зависимостей", "зависимости объявлены"],
140
- has_tests: ["не видно тестов", "тесты есть"],
141
- has_env: ["нет файла окружения", "файл окружения есть"],
142
- };
140
+ const FLAG_WHY = L.trigger.flags;
143
141
 
144
142
  function triggerVerdict(rec, facts) {
145
143
  const t = rec.trigger && typeof rec.trigger === "object" && !Array.isArray(rec.trigger) ? rec.trigger : {};
146
144
  const keys = Object.keys(t);
147
- if (!keys.length) return { applies: false, why: "триггер не задан" };
145
+ if (!keys.length) return { applies: false, why: L.trigger.notSet };
148
146
 
149
147
  for (const key of keys) {
150
148
  const raw = String(t[key]).trim();
@@ -166,11 +164,39 @@ function triggerVerdict(rec, facts) {
166
164
  continue;
167
165
  }
168
166
 
169
- return { applies: false, why: `условие «${key программа не умеет считать` };
167
+ return { applies: false, why: L.trigger.unknown(key) };
170
168
  }
171
169
  return { applies: true };
172
170
  }
173
171
 
172
+ // Есть ли такая программа в PATH. Своим обходом, а не `command -v`: на Windows оболочка —
173
+ // cmd.exe, где такой команды нет вовсе, и проверка возвращала «не установлено» ДЛЯ ЛЮБОЙ
174
+ // программы. Следствие было тихим и потому худшим: родной рецепт (ruff, eslint, jscpd) там
175
+ // недостижим в принципе, гейт молча вставал на слабейший переносимый вариант, а `doctor --run`
176
+ // показывал зелёное. Нашлось только на чужом прогоне — журнал, 2026-09-04.
177
+ function whichSync(prog, env = process.env) {
178
+ if (!prog) return null;
179
+ // Путь, а не имя: команду вроде ./scripts/check.sh искать в PATH бессмысленно.
180
+ if (prog.includes("/") || prog.includes("\\")) return existsSync(prog) ? prog : null;
181
+
182
+ const sep = process.platform === "win32" ? ";" : ":";
183
+ const dirs = String(env.PATH || env.Path || "").split(sep).filter(Boolean);
184
+ // На Windows исполняемость задаёт расширение, а не флаг доступа: ruff — это ruff.exe.
185
+ const exts = process.platform === "win32"
186
+ ? String(env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)
187
+ : [""];
188
+
189
+ for (const dir of dirs) {
190
+ for (const ext of exts) {
191
+ const full = join(dir.replace(/^"|"$/g, ""), prog + ext);
192
+ try {
193
+ if (statSync(full).isFile()) return full;
194
+ } catch { /* нет такого файла — идём дальше, это не ошибка */ }
195
+ }
196
+ }
197
+ return null;
198
+ }
199
+
174
200
  // Арбитр под стек этого проекта: сначала родной рецепт, иначе — переносимый `any`.
175
201
  // Подсказка, которую нельзя скопировать и выполнить, бесполезна.
176
202
  // Выбор рецепта под стек проекта. Одна логика на два места: и `doctor`, и `add` показывают
@@ -182,21 +208,18 @@ function pickRecipe(rec, facts) {
182
208
  // Родной рецепт лучше переносимого — но только если его есть чем выполнить. Поставить
183
209
  // команду с неустановленной программой значит завести гейт, который встаёт с «not found»:
184
210
  // отсутствие сигнала неотличимо от успеха.
185
- const runnable = (c0) => {
186
- const prog = String(c0).trim().split(/\s+/)[0];
187
- return spawnSync(`command -v ${prog}`, { shell: true, stdio: "ignore" }).status === 0;
188
- };
211
+ const runnable = (c0) => Boolean(whichSync(String(c0).trim().split(/\s+/)[0]));
189
212
  for (const lang of facts.langs) {
190
213
  if (!recipes[lang]) continue;
191
214
  if (runnable(recipes[lang])) return recipes[lang];
192
- console.log(c.dim(` ${c.yellow("!")} рецепт под ${lang} пропущен: «${String(recipes[lang]).split(/\s+/)[0]}» не установлен`));
215
+ console.log(c.dim(` ${c.yellow("!")} ${L.recipe.skipped(lang, String(recipes[lang]).split(/\s+/)[0])}`));
193
216
  }
194
217
  return recipes.any || null;
195
218
  }
196
219
 
197
220
  function recipeFor(rec, facts) {
198
221
  const cmd = pickRecipe(rec, facts);
199
- if (!cmd) return "рецепт не описан";
222
+ if (!cmd) return L.recipe.none;
200
223
  return String(cmd)
201
224
  .replace(/\{gate\}/g, join(GATES_SRC, rec.slug))
202
225
  .replace(/\{dir\}/g, ".");
@@ -265,6 +288,7 @@ async function matchCatalog(query) {
265
288
  // Наружу — то, что действительно импортируют другие файлы и модульные проверки. Экспорт,
266
289
  // который никто не берёт, читается как часть договора и мешает менять внутренности.
267
290
  export {
291
+ whichSync,
268
292
  EXT_LANG, detectFacts, readCatalog, triggerVerdict, pickRecipe, recipeFor,
269
293
  stems, overlap, matchCatalog,
270
294
  };
@@ -1,187 +1,43 @@
1
1
  // tool/lib/templates.mjs — тексты, которые программа кладёт в чужой проект.
2
2
  //
3
3
  // ЗАЧЕМ ОТДЕЛЬНО. Это не код, а содержимое: правят его чаще и по другим причинам, чем логику.
4
- // Лёжа вперемешку с логикой, они мешали читать и то, и другое.
5
-
6
- import { SELF } from "./core.mjs";
7
-
8
- // ОДИН источник правил, НЕСКОЛЬКО входов. Каждый инструмент читает свой файл, но оба ведут в
9
- // .aqk/. Два расходящихся свода правил — худшее, что можно сделать: через месяц они врут
10
- // по-разному, и никто не знает, какой настоящий.
11
-
12
- const AGENTS_MD = `# AGENTS.md
13
-
14
- > Точка входа для агента. Держи файл коротким: раздутый свод правил вытесняет саму задачу из
15
- > контекста, и тогда игнорируются все правила разом. Всё длинное по ссылкам ниже.
16
-
17
- ## Железные правила
18
-
19
- - **План до кода.** Нетривиальная задача начинается с плана, который человек одобрил словами.
20
- - **Красный тест до кода.** Сначала проверка, которая падает, потом реализация.
21
- - **Максимум 3 попытки.** Не решил за три — стоп и человеку, а не четвёртый заход.
22
- - **Секреты только в окружении.** Никогда в коде, логах и коммитах.
23
- - **Только файлы из задачи.** Заодно ничего не чиним.
24
- - **Готово = доказано.** Назови арбитра: тест, живой прогон, сверка с источником. «Выглядит
25
- рабочим» — не готово.
26
- - **Ошибку не глотать.** Либо обработана и залогирована, либо проброшена.
27
- - **Развилка — вопрос человеку.** Отступление от принятого решения не оформляется комментарием
28
- в коде.
29
-
30
- ## Оснастка AQK — это твои команды, а не человека
31
-
32
- В репозитории стоит комплект AQK. Его смысл: **обещание проекта превращается в команду с кодом
33
- возврата**, и дальше его держит машина, а не чья-то внимательность. «Не оставляем отладочную
34
- печать» — текст, который можно проигнорировать; команда, возвращающая не ноль, — нельзя.
35
-
36
- Запускать их — твоя работа. Человек смотрит на список дыр и решает, какие закрывать.
37
-
38
- | Команда | Что делает | Когда звать |
39
- |---|---|---|
40
- | \`aqk doctor\` | смотрит репозиторий и печатает три списка: что уже держит машина, чего не хватает, что этому проекту не нужно и почему | начало работы над качеством; «что тут вообще есть» |
41
- | \`aqk doctor --run\` | **запускает** объявленные проверки и показывает, кто нашёл брак | перед сдачей; после правок; всегда, когда нужен факт, а не обещание |
42
- | \`aqk add <имя>\` | ставит проверку из каталога: копирует её и образцы в проект, объявляет в манифесте | человек согласился закрыть дыру из списка \`doctor\` |
43
- | \`aqk ratchet <имя>\` | старые нарушения записывает долгом, новые перестаёт пускать | проверка краснеет на старом коде, и чинить его сейчас никто не будет |
44
- | \`aqk find "…"\` | ищет по смыслу, есть ли уже такая проверка | прежде чем изобретать свою |
45
- | \`aqk note "…"\` | пишет урок в общий журнал | процесс или прибор подвели: проверка соврала, правило обошли |
46
-
47
- Если команды \`aqk\` нет в системе — комплект ставили разово, без установки. Тогда вместо
48
- \`aqk\` пиши \`npx agent-quality-kit\`. Любая команда сама печатает тот
49
- вызов, который сработает у тебя.
50
-
51
- **Три вещи, которые надо понимать, а не запоминать:**
52
-
53
- 1. **«Объявлен» и «работает» — разные утверждения.** \`doctor\` без \`--run\` честно говорит, что
54
- проверки не запускал. Не выдавай объявленное за работающее.
55
- 2. **Проверка без двух образцов ничего не доказывает.** Красный — код, на котором она обязана
56
- сработать; зелёный — правильный, на котором обязана молчать. Зелёный важнее: без него однажды
57
- она покраснеет на верном коде, и её выключат вместе с остальными.
58
- 3. **Правило вводится храповиком, а не большой чисткой.** Чистка откладывается навсегда, потому
59
- что она большая. Храповик даёт действующее правило со дня установки.
60
-
61
- **Повторился дефект того же класса — это не повод быть внимательнее, а повод завести проверку.**
62
- Дисциплина не масштабируется, механика — да.
63
-
64
- ## Где что лежит
65
-
66
- - \`.aqk/rules/\` — стандарты: общие, тесты, безопасность
67
- - \`.aqk/docs/\` — методички: минимум проекта, харнес, процесс, исследования
68
- - \`.aqk/docs/project-baseline.md\` — **начни отсюда**, если проект новый
69
-
70
- ## Команды
71
-
72
- <!-- Заполни под свой проект. Команда, которую нельзя скопировать и выполнить, — не команда. -->
73
-
74
- - сборка: \`\`
75
- - тесты: \`\`
76
- - линтер: \`\`
77
- - всё разом перед пушем: \`\`
78
-
79
- ## Чего в этом проекте нет
80
-
81
- <!-- Пиши сюда честно. Ненаписанное «нет» агент додумает как «есть». -->
82
- `;
83
-
84
- const CLAUDE_MD = `# CLAUDE.md
85
-
86
- Правила этого проекта живут в \`AGENTS.md\` — читай его.
87
-
88
- Один свод правил, несколько точек входа: \`AGENTS.md\` для агентов, понимающих его,
89
- \`CLAUDE.md\` — для Claude Code. Держать два расходящихся свода нельзя: через месяц они врут
90
- по-разному, и непонятно, какой настоящий.
91
-
92
- @AGENTS.md
93
- `;
94
-
95
- // Манифест — единственный машиночитаемый файл стандарта. Всё остальное человекочитаемо.
96
- // Пустые значения оставлены НАМЕРЕННО: заполненная заглушка врала бы про уровень.
97
- const MANIFEST_YML = `# .aqk.yml — манифест Agent Quality Kit
98
- # Что это: машиночитаемое описание того, как в этом репозитории живут агенты.
99
- # Уровень соответствия считает \`aqk doctor\`. Пустое поле = ступень не пройдена,
100
- # и это честно: заполнять заглушками бессмысленно, проверяются файлы, а не слова.
101
-
102
- aqk: 1
103
-
104
- # AQK-0 — что агент читает первым.
105
- entry:
106
- - AGENTS.md
107
-
108
- # AQK-1 — где стандарты и какие проверки обязательны.
109
- rules: .aqk/rules
110
- gates:
111
- # Имя: команда, возвращающая 0 или не 0. Пустое объявление защиты не даёт и бракуется
112
- # проверкой «объявленный гейт запускается» — поэтому здесь примеры, а не заготовки.
113
- # lint: "ruff check ."
114
- # test: "pytest -q"
115
- # Поставить готовую запись из каталога вместе с образцами: aqk add <имя>
116
-
117
- # AQK-2 — чем доказано, что гейты работают, и где реестры долга.
118
- # samples: каталог с красными и зелёными образцами (гейт обязан краснеть на первом
119
- # и молчать на втором). ratchets: списки известных нарушений, которые могут только
120
- # укорачиваться.
121
- samples: ""
122
- ratchets: ""
123
-
124
- # AQK-3 — где копятся уроки. Путь или адрес.
125
- lessons: ""
126
- `;
127
-
128
- const GATE_YML_TEMPLATE = (slug) => `# Запись каталога AQK. Норма и все поля — kit/gates/README.md
129
- # Пока строки ниже не заполнены, проверка отклонит эту запись — так и задумано.
130
-
131
- # Одной фразой: какой класс брака ловит. По этому полю идёт сверка «есть ли уже такое».
132
- intent: ЗАПОЛНИ — какой класс брака ловит
133
-
134
- # Когда запись показывается человеку. Условие обязано быть запросом к репозиторию,
135
- # который умеет вычислить программа: always | langs: python, go | has_gates: true
136
- trigger:
137
- always: true
138
-
139
- # Команда-арбитр под каждый стек. {gate} — папка записи, {dir} — что проверяем.
140
- # any — переносимая команда без сторонних программ.
141
- recipes:
142
- any: bash {gate}/check.sh {dir}
143
-
144
- # Реальный отказ, который эта проверка поймала. «Хорошая практика» не принимается:
145
- # ссылайся на запись журнала — incidents/README.md
146
- proof: ЗАПОЛНИ — какой отказ поймала и чего он стоил
147
- `;
148
-
149
- const CHECK_SH_TEMPLATE = `#!/usr/bin/env sh
150
- # Проверка. Возвращает 0 — чисто, не 0 — брак. В тексте отказа должно быть НАПИСАНО,
151
- # что сделать: он попадает прямо в контекст агента, и с инструкцией он чинит сам.
152
- DIR="\${1:-.}"
153
- . "$(dirname "$0")/../_skip.sh" 2>/dev/null || SKIP_NAMES=".git .aqk node_modules .venv"
154
-
155
- # own_samples_filter прячет ТОЛЬКО gates/<имя>/red|green/ — не любую папку с таким именем
156
- # в проекте. --exclude-dir=red по голому имени однажды спрятал бы настоящую пользовательскую
157
- # папку red/ (нашли на secrets-not-in-code — kit/docs/ai/... журнал, 2026-09-04).
158
- HITS=$(grep -rnE $(skip_grep "$DIR") 'ЗАПОЛНИ_ШАБЛОН_ПОИСКА' "$DIR" 2>/dev/null | own_samples_filter "$DIR")
159
- if [ -n "$HITS" ]; then
160
- echo "$HITS"
161
- echo " почини: ЗАПОЛНИ — что именно сделать"
162
- exit 1
163
- fi
164
- exit 0
165
- `;
166
-
167
- const README_TEMPLATE = (slug) => `# ЗАПОЛНИ — заголовок одной строкой
168
-
169
- **Намерение.** Что и почему не должно попадать в код.
170
-
171
- **Какой отказ это поймало.** Что сломалось, в каком проекте, чего это стоило. Без этого
172
- запись не принимается: «это хорошая практика» набирает сотни пунктов, среди которых нельзя
173
- выбрать.
174
-
175
- **Почему машина, а не внимательность.** В какой момент человек это пропускает.
176
-
177
- **Готовый аналог.** Есть ли такая проверка в ruff, eslint, semgrep? Если есть — рецепт под этот
178
- язык обязан брать её, а своя проверка остаётся запасной. Если нет — написать, что именно
179
- проверено: «не искал» и «нет» — разные утверждения.
180
-
181
- **Чего НЕ ловит.** Честно назвать границу. Гейт, о пределах которого умалчивают, опаснее
182
- отсутствующего: на него понадеются.
183
-
184
- **Образцы.** \`red/\` — что здесь нарушено. \`green/\` — то же самое, но правильно.
185
- `;
4
+ // Сами тексты живут в tool/i18n/templates-{ru,en}.mjs здесь только выбор языка. Человек,
5
+ // пришедший с англоязычной страницы пакета, получал английский интерфейс и русский AGENTS.md
6
+ // у себя в репозитории: файл, который он и его агент читают первым.
7
+
8
+ import { L } from "../i18n/index.mjs";
9
+
10
+ const {
11
+ AGENTS_MD, CLAUDE_MD,
12
+ GATE_YML_TEMPLATE, CHECK_SH_TEMPLATE, README_TEMPLATE,
13
+ } = L.templates;
14
+
15
+ // Схема манифеста ОДНА на все языки, переводятся только комментарии. Пока схема лежала в
16
+ // двух языковых файлах, гейт дублей поймал её сам: добавь кто-то ключ в один шаблон и забудь
17
+ // про другой — англоязычный пользователь получил бы другой манифест. Ключи машиночитаемы,
18
+ // расходиться им нельзя; комментарии человекочитаемы, им положено.
19
+ const d = L.manifestDoc;
20
+ const MANIFEST_YML = [
21
+ ...d.head,
22
+ "",
23
+ "aqk: 1",
24
+ "",
25
+ d.entry,
26
+ "entry:",
27
+ " - AGENTS.md",
28
+ "",
29
+ d.rules,
30
+ "rules: .aqk/rules",
31
+ "gates:",
32
+ ...d.gates,
33
+ "",
34
+ ...d.samples,
35
+ 'samples: ""',
36
+ 'ratchets: ""',
37
+ "",
38
+ d.lessons,
39
+ 'lessons: ""',
40
+ "",
41
+ ].join("\n");
186
42
 
187
43
  export { AGENTS_MD, CLAUDE_MD, MANIFEST_YML, GATE_YML_TEMPLATE, CHECK_SH_TEMPLATE, README_TEMPLATE };