agent-quality-kit 0.5.0 → 0.7.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 (135) hide show
  1. package/README.md +53 -2
  2. package/README.ru.md +35 -1
  3. package/kit/docs/ai/agent-harness-playbook.md +1 -1
  4. package/kit/docs/ready-made-rules.md +65 -0
  5. package/kit/gates/README.md +60 -0
  6. package/kit/gates/ci-actually-fails/README.md +54 -0
  7. package/kit/gates/ci-actually-fails/check.sh +116 -0
  8. package/kit/gates/ci-actually-fails/gate.yml +14 -0
  9. package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +30 -0
  10. package/kit/gates/ci-actually-fails/red/.github/workflows/ci.yml +12 -0
  11. package/kit/gates/ci-actually-fails/red/.github/workflows/soft.yml +15 -0
  12. package/kit/gates/color-from-token/check.sh +13 -1
  13. package/kit/gates/commit-explains-itself/README.md +13 -3
  14. package/kit/gates/commit-explains-itself/check.sh +8 -4
  15. package/kit/gates/complexity-limit/README.md +5 -0
  16. package/kit/gates/complexity-limit/check.sh +21 -2
  17. package/kit/gates/complexity-limit/green/test_fixtures.py +14 -0
  18. package/kit/gates/deps-are-pinned/README.md +14 -1
  19. package/kit/gates/deps-are-pinned/check.sh +6 -1
  20. package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/pyproject.toml +12 -0
  21. package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/requirements.txt +3 -0
  22. package/kit/gates/deps-are-pinned/red/pyproject-loose/pyproject.toml +12 -0
  23. package/kit/gates/deps-are-pinned/red/pyproject-loose/requirements.txt +3 -0
  24. package/kit/gates/duplicate-code/README.md +11 -2
  25. package/kit/gates/duplicate-code/check.sh +31 -4
  26. package/kit/gates/duplicate-code/gate.yml +8 -0
  27. package/kit/gates/duplicate-code/green/imports_a.go +20 -0
  28. package/kit/gates/duplicate-code/green/imports_b.go +19 -0
  29. package/kit/gates/entry-links-exist/README.md +5 -0
  30. package/kit/gates/entry-links-exist/check.sh +6 -0
  31. package/kit/gates/entry-links-exist/green/AGENTS.md +3 -0
  32. package/kit/gates/file-size-limit/README.md +9 -2
  33. package/kit/gates/file-size-limit/check.sh +13 -1
  34. package/kit/gates/gate-not-weakened/README.md +54 -0
  35. package/kit/gates/gate-not-weakened/check.sh +84 -0
  36. package/kit/gates/gate-not-weakened/gate.yml +15 -0
  37. package/kit/gates/gate-not-weakened/green/checkout.ts +8 -0
  38. package/kit/gates/gate-not-weakened/green/payments.py +6 -0
  39. package/kit/gates/gate-not-weakened/green/release.sh +2 -0
  40. package/kit/gates/gate-not-weakened/red/checkout.ts +9 -0
  41. package/kit/gates/gate-not-weakened/red/payments.py +6 -0
  42. package/kit/gates/gate-not-weakened/red/release.sh +2 -0
  43. package/kit/gates/hook-actually-fires/README.md +74 -0
  44. package/kit/gates/hook-actually-fires/check.sh +183 -0
  45. package/kit/gates/hook-actually-fires/gate.yml +15 -0
  46. package/kit/gates/hook-actually-fires/green/.claude/hooks/hooks.json +3 -0
  47. package/kit/gates/hook-actually-fires/green/.claude/settings.json +74 -0
  48. package/kit/gates/hook-actually-fires/green/.claude/settings.local.json +74 -0
  49. package/kit/gates/hook-actually-fires/red/.claude/hooks/hooks.json +4 -0
  50. package/kit/gates/hook-actually-fires/red/.claude/settings.json +53 -0
  51. package/kit/gates/no-phantom-package/README.md +84 -0
  52. package/kit/gates/no-phantom-package/check.sh +161 -0
  53. package/kit/gates/no-phantom-package/gate.yml +20 -0
  54. package/kit/gates/no-phantom-package/green/AGENTS.md +15 -0
  55. package/kit/gates/no-phantom-package/red/AGENTS.md +15 -0
  56. package/kit/gates/no-print-in-prod/README.md +33 -39
  57. package/kit/gates/no-print-in-prod/gate.yml +14 -6
  58. package/kit/gates/personal-config-not-shared/README.md +66 -0
  59. package/kit/gates/personal-config-not-shared/check.sh +103 -0
  60. package/kit/gates/personal-config-not-shared/gate.yml +16 -0
  61. package/kit/gates/personal-config-not-shared/green/.aqk-tracked +9 -0
  62. package/kit/gates/personal-config-not-shared/red/.aqk-tracked +6 -0
  63. package/kit/gates/promise-has-gate/README.md +50 -0
  64. package/kit/gates/promise-has-gate/check.sh +88 -0
  65. package/kit/gates/promise-has-gate/gate.yml +14 -0
  66. package/kit/gates/promise-has-gate/green/.aqk.yml +6 -0
  67. package/kit/gates/promise-has-gate/green/AGENTS.md +7 -0
  68. package/kit/gates/promise-has-gate/red/.aqk.yml +6 -0
  69. package/kit/gates/promise-has-gate/red/AGENTS.md +7 -0
  70. package/kit/gates/secrets-not-in-code/check.sh +13 -1
  71. package/kit/gates/swallowed-error/README.md +36 -18
  72. package/kit/gates/swallowed-error/gate.yml +13 -3
  73. package/kit/gates/test-has-assertion/README.md +47 -0
  74. package/kit/gates/test-has-assertion/check.sh +206 -0
  75. package/kit/gates/test-has-assertion/gate.yml +15 -0
  76. package/kit/gates/test-has-assertion/green/checkout.test.ts +9 -0
  77. package/kit/gates/test-has-assertion/green/test_billing.py +17 -0
  78. package/kit/gates/test-has-assertion/red/checkout.test.ts +8 -0
  79. package/kit/gates/test-has-assertion/red/test_billing.py +14 -0
  80. package/kit/gates/test-not-adjusted/README.md +79 -0
  81. package/kit/gates/test-not-adjusted/check.sh +136 -0
  82. package/kit/gates/test-not-adjusted/gate.yml +19 -0
  83. package/kit/gates/test-not-adjusted/green/after/calc.py +6 -0
  84. package/kit/gates/test-not-adjusted/green/after/tests/test_calc.py +9 -0
  85. package/kit/gates/test-not-adjusted/green/before/calc.py +2 -0
  86. package/kit/gates/test-not-adjusted/green/before/tests/test_calc.py +5 -0
  87. package/kit/gates/test-not-adjusted/red/after/calc.py +2 -0
  88. package/kit/gates/test-not-adjusted/red/after/tests/test_calc.py +5 -0
  89. package/kit/gates/test-not-adjusted/red/before/calc.py +2 -0
  90. package/kit/gates/test-not-adjusted/red/before/tests/test_calc.py +7 -0
  91. package/kit/gates/todo-without-task/README.md +6 -0
  92. package/kit/gates/todo-without-task/check.sh +13 -1
  93. package/kit/ratchet/ratchet.sh +70 -2
  94. package/kit/rules/general.md +23 -0
  95. package/kit/rules-en/general.md +82 -0
  96. package/kit/rules-en/security.md +33 -0
  97. package/kit/rules-en/testing.md +48 -0
  98. package/llms.txt +2 -1
  99. package/package.json +4 -2
  100. package/tool/commands/badge.mjs +7 -1
  101. package/tool/commands/doctor.mjs +90 -10
  102. package/tool/commands/gates.mjs +19 -5
  103. package/tool/commands/project.mjs +15 -2
  104. package/tool/commands/prove.mjs +67 -0
  105. package/tool/commands/report.mjs +4 -1
  106. package/tool/i18n/en-docs.mjs +70 -0
  107. package/tool/i18n/en.mjs +66 -54
  108. package/tool/i18n/ru-docs.mjs +70 -0
  109. package/tool/i18n/ru.mjs +66 -54
  110. package/tool/i18n/templates-en.mjs +9 -9
  111. package/tool/i18n/templates-ru.mjs +9 -9
  112. package/tool/lib/core.mjs +7 -1
  113. package/tool/lib/manifest.mjs +72 -5
  114. package/tool/lib/prove.mjs +160 -0
  115. package/tool/lib/repo.mjs +31 -2
  116. package/tool/lib/scope.mjs +131 -0
  117. package/tool/lib/templates.mjs +2 -0
  118. package/tool/program.mjs +6 -0
  119. package/tool/selfcheck/gates.sh +86 -3
  120. package/tool/selfcheck/lifecycle.mjs +29 -0
  121. package/tool/selfcheck/mutation.sh +21 -1
  122. package/tool/selfcheck/smoke.sh +329 -36
  123. package/tool/selfcheck/units-level.mjs +60 -0
  124. package/tool/selfcheck/units.mjs +196 -1
  125. package/kit/gates/no-print-in-prod/check.sh +0 -38
  126. package/kit/gates/no-print-in-prod/green/docs.ts +0 -15
  127. package/kit/gates/no-print-in-prod/green/main.go +0 -8
  128. package/kit/gates/no-print-in-prod/green/main.rs +0 -4
  129. package/kit/gates/no-print-in-prod/red/main.go +0 -8
  130. package/kit/gates/no-print-in-prod/red/main.rs +0 -4
  131. package/kit/gates/swallowed-error/check.sh +0 -54
  132. package/kit/gates/swallowed-error/green/run.js +0 -8
  133. package/kit/gates/swallowed-error/red/run.js +0 -3
  134. /package/kit/gates/commit-explains-itself/green/{COMMIT_MSG → .aqk-commit-msg} +0 -0
  135. /package/kit/gates/commit-explains-itself/red/{COMMIT_MSG → .aqk-commit-msg} +0 -0
package/llms.txt CHANGED
@@ -22,11 +22,12 @@ Zero runtime dependencies. Node 18+ and an `sh` shell. MIT.
22
22
  `npx agent-quality-kit doctor --baseline` (14 of 50 points confirmed by a run, ecosystem-neutral;
23
23
  the other 36 are named as a number, not hidden)
24
24
  - Fail a pipeline below a level or on a failed gate: `npx agent-quality-kit doctor --run --min 1`
25
+ - Show only what a diff introduced, so a legacy repo is usable from day one: `doctor --run --since main`
25
26
  - Exit codes: 0 pass, 1 below the level or a gate failed
26
27
  - As a pre-commit hook: `repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit` with
27
28
  `id: aqk` (blocking), `aqk-doctor` (read-only) or `aqk-baseline`. pre-commit installs the
28
29
  package itself; there are no dependencies to pull in.
29
- - As a GitHub Action: `uses: arsen-ask-lx/Agent_Quality_Kit@v0.4.2` with `min: 1`
30
+ - As a GitHub Action: `uses: arsen-ask-lx/Agent_Quality_Kit@v0.7.0` with `min: 1`
30
31
  (https://github.com/marketplace/actions/agent-quality-kit-aqk)
31
32
 
32
33
  ## What makes it different
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-quality-kit",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Turns the rules an agent is supposed to follow into commands with exit codes, and reports which of them actually run. Zero dependencies.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -43,7 +43,9 @@
43
43
  ],
44
44
  "knip": {
45
45
  "entry": [
46
- "tool/selfcheck/units.mjs"
46
+ "tool/selfcheck/units.mjs",
47
+ "tool/selfcheck/units-level.mjs",
48
+ "tool/selfcheck/lifecycle.mjs"
47
49
  ],
48
50
  "project": [
49
51
  "tool/**/*.mjs"
@@ -9,6 +9,7 @@ import { readFile } from "node:fs/promises";
9
9
  import { join } from "node:path";
10
10
  import { CWD, SELF, REPO_URL, c, exists, die } from "../lib/core.mjs";
11
11
  import { readManifest, assessLevel } from "../lib/manifest.mjs";
12
+ import { proveGates } from "../lib/prove.mjs";
12
13
  import { runGates, declaredGates } from "./doctor.mjs";
13
14
  import { L } from "../i18n/index.mjs";
14
15
 
@@ -33,7 +34,12 @@ async function cmdBadge(args = []) {
33
34
  const man = await readManifest();
34
35
  if (!man) die(`\n ${L.badge.noManifest(`${SELF} init`)}\n`);
35
36
 
36
- const { reached } = await assessLevel(man);
37
+ // Значок самое громкое утверждение комплекта, и доказывать его обязательно. Без этого
38
+ // проект с гейтом «true» получал AQK-3 и зелёную картинку в README: проверено прогоном.
39
+ // Сказать вслух, что идёт: доказательство гоняет каждый гейт по двум образцам, и молчащая
40
+ // пауза читается как зависание. Найдено код-ревью 2026-09-07.
41
+ console.log(c.dim(` ${L.prove.running}`));
42
+ const { reached } = await assessLevel(man, await proveGates(man));
37
43
  if (reached < 0) die(`\n ${L.badge.notReached(`${SELF} doctor`)}\n`);
38
44
 
39
45
  // Прогон, а не манифест. Значок при красном гейте — это и есть недоказанное утверждение.
@@ -3,8 +3,10 @@
3
3
  import { readFile, mkdir, writeFile } from "node:fs/promises";
4
4
  import { join, resolve } from "node:path";
5
5
  import { spawnSync } from "node:child_process";
6
- import { CWD, PKG_ROOT, TARGET_DIR, SELF, c, exists } from "../lib/core.mjs";
7
- import { readManifest, assessLevel, unknownKeys, KNOWN_KEYS } from "../lib/manifest.mjs";
6
+ import { scopeOutput, splitAdvice, changedFiles } from "../lib/scope.mjs";
7
+ import { CWD, PKG_ROOT, TARGET_DIR, SELF, c, exists, die } from "../lib/core.mjs";
8
+ import { readManifest, assessLevel, unknownKeys, KNOWN_KEYS, advisorySet } from "../lib/manifest.mjs";
9
+ import { proveGates } from "../lib/prove.mjs";
8
10
  import { detectFacts, readCatalog, triggerVerdict, recipeFor } from "../lib/repo.mjs";
9
11
  import { assessBaseline, DEP_FILES, BASELINE_TOTAL } from "../lib/baseline.mjs";
10
12
  import { L } from "../i18n/index.mjs";
@@ -94,9 +96,27 @@ function declaredGates(man) {
94
96
  .filter(([, cmd]) => cmd);
95
97
  }
96
98
 
97
- function runGates(man) {
99
+ // Ссылка, относительно которой сужается вывод: `--since main`, `--since HEAD~5`.
100
+ // Без значения флаг бессмыслен — молча взять умолчание нельзя: «сужено не тем» неотличимо
101
+ // от «не сужено».
102
+ function sinceRef(argv = process.argv) {
103
+ const i = argv.indexOf("--since");
104
+ if (i === -1) return null;
105
+ const v = argv[i + 1];
106
+ return v && !v.startsWith("-") ? v : null;
107
+ }
108
+
109
+ function runGates(man, opts = {}) {
98
110
  const gates = declaredGates(man);
99
111
  if (!gates.length) return { failed: 0, ran: 0, results: [] };
112
+ const advisory = advisorySet(man);
113
+
114
+ // Сужение по дифу — договор с человеком, и он должен видеть, ЧТО именно сужено. Пустой диф
115
+ // называется вслух: иначе «все гейты зелёные» означало бы «сравнили не с тем» и читалось бы
116
+ // как успех. Это тот же класс, что и весь стандарт, только внутри нашего флага.
117
+ const scoped = opts.since ? changedFiles(opts.since, CWD) : null;
118
+ if (opts.since && scoped === null) die(L.doctor.sinceBadRef(opts.since));
119
+ if (scoped) console.log(c.dim(`\n ${L.doctor.sinceHeading(opts.since, scoped.size)}`));
100
120
 
101
121
  console.log(c.bold(`\n ${L.doctor.runHeading}\n`));
102
122
  let failed = 0;
@@ -116,17 +136,69 @@ function runGates(man) {
116
136
  const code = r.status;
117
137
  if (code === 0) {
118
138
  console.log(` ${c.green("✔")} ${name.padEnd(14)} ${c.dim(`${secs}s · ${cmd}`)}`);
139
+ // Зелёный гейт иногда всё-таки говорит человеку что-то важное: храповик, дошедший до цели,
140
+ // просит убрать обёртку. Вывод успешного гейта не показывался вовсе, и это сообщение
141
+ // уходило в никуда — тот же класс, что обрезанный совет у красного, только тише.
142
+ // Показываем ровно строки с меткой совета: остальной вывод успешной проверки — шум.
143
+ const okAdvice = splitAdvice(`${r.stdout || ""}${r.stderr || ""}`.trim().split("\n").filter(Boolean)).advice;
144
+ for (const line of okAdvice.slice(0, 6)) console.log(c.yellow(` ${line.trim().slice(0, 110)}`));
119
145
  results.push({ name, cmd, ok: true, secs });
120
146
  } else {
147
+ const raw = `${r.stdout || ""}${r.stderr || ""}`.trim().split("\n").filter(Boolean);
148
+ // Совет отделяется ДО сужения. Иначе он сам попадает под фильтр по путям: сообщение
149
+ // храповика про вышедший срок называет путь к реестру, реестра в дифе нет, и гейт,
150
+ // обязанный краснеть по сроку, печатался зелёным с пометкой «находки вне дифа».
151
+ // Ровно то, что стандарт запрещает: срок без последствия. Найдено ревью 2026-09-06.
152
+ const parted = splitAdvice(raw);
153
+ let out = parted.findings;
154
+ const alwaysAdvice = parted.advice;
155
+
156
+ // Сужение до дифа. Три исхода, и все три называются вслух.
157
+ if (scoped) {
158
+ const s = scopeOutput(out, scoped);
159
+ // Гейт, у которого находок нет вовсе, а есть только совет, сузить нечем: его вердикт
160
+ // не про файлы. Признать такой успешным — вернуть ту же тишину другим путём.
161
+ if (!s.scopable || out.length === 0) {
162
+ // Гейт печатает вердикт без путей — сузить нечем. Признать его успешным значило бы
163
+ // выдать провал за тишину; остаётся красным, и причина названа.
164
+ console.log(` ${c.red("✘")} ${name.padEnd(14)} ${c.red(L.doctor.exitCode(code))} ${c.dim(`· ${L.doctor.notScopable}`)}`);
165
+ failed++;
166
+ results.push({ name, cmd, ok: false, secs, code, note: L.doctor.notScopable });
167
+ continue;
168
+ }
169
+ if (s.findings === 0) {
170
+ // Долг есть, но не в том, что внёс диф. Зелёный — но с числом спрятанного: молчаливое
171
+ // «всё хорошо» здесь было бы неправдой.
172
+ console.log(` ${c.green("✔")} ${name.padEnd(14)} ${c.dim(`${secs}s · ${L.doctor.outsideDiff(out.length)}`)}`);
173
+ results.push({ name, cmd, ok: true, secs, scopedAway: out.length });
174
+ continue;
175
+ }
176
+ out = s.kept;
177
+ }
178
+
121
179
  failed++;
122
- const out = `${r.stdout || ""}${r.stderr || ""}`.trim().split("\n").filter(Boolean);
123
- console.log(` ${c.red("✘")} ${name.padEnd(14)} ${c.red(L.doctor.exitCode(code))} ${c.dim(`· ${secs}s · ${cmd}`)}`);
180
+ // Находки обрезаются, совет никогда. Все записи каталога печатают «почини: …» последней
181
+ // строкой, и при обрезке до трёх строк человек не видел именно её: находка без действия
182
+ // закрывает окно, а не дефект.
183
+ // Совещательный гейт показывает находки и не роняет прогон. Знак другой, чтобы «показано»
184
+ // и «провалено» не читались одинаково; в сводке ниже он назван поимённо.
185
+ const isAdvisory = advisory.has(name);
186
+ if (isAdvisory) failed--;
187
+ const mark = isAdvisory ? c.yellow("!") : c.red("✘");
188
+ const verdict = isAdvisory ? c.yellow(L.doctor.advisoryMark) : c.red(L.doctor.exitCode(code));
189
+ console.log(` ${mark} ${name.padEnd(14)} ${verdict} ${c.dim(`· ${secs}s · ${cmd}`)}`);
124
190
  for (const line of out.slice(0, 3)) console.log(c.dim(` ${line.slice(0, 100)}`));
125
191
  if (out.length > 3) console.log(c.dim(` ${L.doctor.moreLines(out.length - 3)}`));
126
- results.push({ name, cmd, ok: false, secs, code });
192
+ // Совет тоже не бесконечен: гейт, зовущий помощник шесть раз, печатает его шесть раз.
193
+ for (const line of alwaysAdvice.slice(0, 6)) console.log(c.yellow(` ${line.trim().slice(0, 110)}`));
194
+ results.push({ name, cmd, ok: false, secs, code, advisory: isAdvisory });
127
195
  }
128
196
  }
129
- return { failed, ran: gates.length, results };
197
+ // Совещательные, которые покраснели, называются вслух ВСЕГДА. Молчание о них — ровно та
198
+ // тишина, против которой построен стандарт: проверка выключена, а выглядит как её отсутствие.
199
+ const advisoryFailed = results.filter((x) => x.advisory).map((x) => x.name);
200
+ if (advisoryFailed.length) console.log(`\n ${c.yellow(L.doctor.advisorySummary(advisoryFailed))}`);
201
+ return { failed, ran: gates.length, results, advisoryFailed };
130
202
  }
131
203
 
132
204
  // Короткий отчёт «что из этого реально брали» — не для человека, а для агента в следующей
@@ -203,12 +275,20 @@ async function cmdDoctor() {
203
275
  console.log(c.dim(` ${L.doctor.manifestKnown(KNOWN_KEYS)}\n`));
204
276
  }
205
277
 
206
- const { reached, steps } = await assessLevel(man);
278
+ // Доказательство считается только при прогоне: узнать, ловит ли гейт брак, нельзя иначе как
279
+ // запустив его по образцу. Без прогона ступени со второй помечаются «не доказано» — это
280
+ // честнее, чем показывать их выполненными по наличию папок.
281
+ const proof = process.argv.includes("--run") ? await proveGates(man) : null;
282
+ const { reached, steps } = await assessLevel(man, proof);
207
283
 
208
284
  console.log(c.bold(`\n ${L.doctor.levelHeading}\n`));
209
285
  for (const s of steps) {
210
286
  const mark = s.ok ? c.green("✔") : reached + 1 === s.level ? c.yellow("→") : c.dim("·");
211
- console.log(` ${mark} AQK-${s.level} ${s.title}`);
287
+ const note = !s.ok && s.needsProof ? c.dim(` · ${L.doctor.levelUnproven(`${SELF} prove`)}`) : "";
288
+ console.log(` ${mark} AQK-${s.level} ${s.title}${note}`);
289
+ }
290
+ if (proof && proof.broken) {
291
+ console.log(c.red(`\n ${L.doctor.gatesDoNotCatch(proof.broken, `${SELF} prove`)}`));
212
292
  }
213
293
 
214
294
  const next = steps.find((s) => !s.ok);
@@ -247,7 +327,7 @@ async function cmdDoctor() {
247
327
  let gateFailed = 0;
248
328
  let failedNames = [];
249
329
  if (wantRun) {
250
- const run = runGates(man);
330
+ const run = runGates(man, { since: sinceRef() });
251
331
  gateFailed = run.failed;
252
332
  failedNames = run.results.filter((r) => !r.ok).map((r) => r.name);
253
333
  await writeRunReport({ version, reached, results: run.results });
@@ -8,7 +8,7 @@ import {
8
8
  CWD, PKG_ROOT, GATES_SRC, PROJECT_GATES, RATCHET_DIR, RATCHET_LIB, MANIFEST, SELF, c, exists, die,
9
9
  copyDir,
10
10
  } from "../lib/core.mjs";
11
- import { parseManifest, readManifest, manifestWithGate } from "../lib/manifest.mjs";
11
+ import { parseManifest, readManifest, manifestWithGate, entryLifecycle } from "../lib/manifest.mjs";
12
12
  import {
13
13
  detectFacts, readCatalog, pickRecipe, triggerVerdict, stems, overlap, matchCatalog,
14
14
  } from "../lib/repo.mjs";
@@ -28,6 +28,13 @@ async function installGate(slug, man, facts) {
28
28
  if (!(await exists(src))) die(L.add.noSuchGate(slug, `${SELF} doctor`));
29
29
 
30
30
  const rec = { slug, ...parseManifest(await readFile(join(src, "gate.yml"), "utf8")) };
31
+
32
+ // Выведенную запись не ставим. Молча пропустить нельзя — человек пришёл за конкретной
33
+ // проверкой и обязан узнать, кто её заменил; ответ «а что теперь» и есть цена вывода.
34
+ // Проверка ДО копирования: иначе в проекте остаётся папка гейта, которого не будет в манифесте.
35
+ const life = entryLifecycle(rec);
36
+ if (life.state === "deprecated") return { rec, cmd: null, copied: [], declared: false, why: null, retired: life.supersededBy || "" };
37
+
31
38
  const dst = join(CWD, PROJECT_GATES, slug);
32
39
  await mkdir(dst, { recursive: true });
33
40
  const copied = await copyDir(src, dst, { force: false });
@@ -40,7 +47,8 @@ async function installGate(slug, man, facts) {
40
47
  }
41
48
 
42
49
  // Команда под стек проекта, с путями внутри репозитория, а не внутри пакета.
43
- const picked = String(pickRecipe(rec, facts) || "");
50
+ const missing = [];
51
+ const picked = String(pickRecipe(rec, facts, missing) || "");
44
52
  let cmd = picked
45
53
  .replace(/\{gate\}/g, `${PROJECT_GATES}/${slug}`)
46
54
  .replace(/\{dir\}/g, ".");
@@ -50,7 +58,7 @@ async function installGate(slug, man, facts) {
50
58
  // ни `ruff`, ни `vulture`, и установка обрывалась на записи `dead-code`, которой нужен
51
59
  // настоящий инструмент. Отсутствие сигнала неотличимо от успеха — здесь оно было внутри
52
60
  // самой установки.
53
- if (!cmd) return { rec, cmd: null, copied, declared: false, why: null, noRecipe: true };
61
+ if (!cmd) return { rec, cmd: null, copied, declared: false, why: null, noRecipe: true, missing };
54
62
 
55
63
  // Родной инструмент не знает про наши образцы и выдаёт их как находки — в любом проекте,
56
64
  // куда поставили гейты. Заворачиваем его в общий фильтр. Переносимая проверка фильтрует
@@ -87,8 +95,14 @@ async function cmdAdd(args) {
87
95
  console.log(c.dim(` ${L.add.installAnyway}\n`));
88
96
  }
89
97
 
90
- const { cmd, copied, declared, why, noRecipe } = await installGate(slug, man, facts);
91
- if (noRecipe) die(L.add.noRecipe(slug, [...facts.langs].join("/") || L.add.thisStack));
98
+ const { cmd, copied, declared, why, noRecipe, retired, missing } = await installGate(slug, man, facts);
99
+ if (retired !== undefined) die(L.lifecycle.installDeprecated(slug, retired ? `${SELF} add ${retired}` : "—"));
100
+ // «Рецепта нет» и «рецепт есть, а инструмента нет» — разные причины и разные починки.
101
+ // Диагноз, противоречащий gate.yml, стоит доверия всему выводу. Найдено код-ревью 2026-09-07.
102
+ if (noRecipe) {
103
+ if (missing && missing.length) die(L.add.toolMissing(slug, missing));
104
+ die(L.add.noRecipe(slug, [...facts.langs].join("/") || L.add.thisStack, missing));
105
+ }
92
106
 
93
107
  console.log(c.bold(`\naqk add ${slug}\n`));
94
108
  console.log(` ${c.green("✔")} ${PROJECT_GATES}/${slug}/ ${c.dim(L.add.copied(copied.length))}`);
@@ -11,7 +11,7 @@ import { AGENTS_MD, CLAUDE_MD, MANIFEST_YML } from "../lib/templates.mjs";
11
11
  import { readManifest } from "../lib/manifest.mjs";
12
12
  import { detectFacts, readCatalog, triggerVerdict } from "../lib/repo.mjs";
13
13
  import { installGate } from "./gates.mjs";
14
- import { L } from "../i18n/index.mjs";
14
+ import { L, LANG } from "../i18n/index.mjs";
15
15
 
16
16
  async function cmdInit(args) {
17
17
  const force = args.includes("--force");
@@ -46,6 +46,13 @@ async function cmdInit(args) {
46
46
  for (const f of created.slice(0, 8)) console.log(` ${f}`);
47
47
  if (created.length > 8) console.log(c.dim(` ${L.init.andMore(created.length - 8)}`));
48
48
  }
49
+ // Методички остаются на русском — решение владельца, принятое 2026-09-07, а не недоделка.
50
+ // Сказать об этом обязательно: человек, открывший `.aqk/docs/` и увидевший чужой язык, иначе
51
+ // решит, что установка сломалась. Правила переведены, методички нет; молчать об этом значит
52
+ // выдать решение за оплошность.
53
+ if (LANG === "en" && created.some((f) => f.includes(`${TARGET_DIR}/docs/`) || f.includes(`${TARGET_DIR}\\docs\\`))) {
54
+ console.log(c.dim(`\n ${L.init.docsRu}`));
55
+ }
49
56
  if (skipped.length) {
50
57
  console.log(c.yellow(`\n ${L.init.kept(skipped.length)}`));
51
58
  for (const f of skipped) console.log(` ${f}`);
@@ -267,7 +274,13 @@ async function cmdStart(args) {
267
274
  if (declared.has(rec.slug)) continue;
268
275
  const v = triggerVerdict(rec, facts0);
269
276
  if (!v.applies) { skipped.push([rec.slug, v.why]); continue; }
270
- const { cmd, noRecipe } = await installGate(rec.slug, man, facts0);
277
+ const { cmd, noRecipe, retired } = await installGate(rec.slug, man, facts0);
278
+ // Выведенная запись в пачку не идёт, но и молчать о ней нельзя: она попадает в тот же
279
+ // список пропущенного с названным преемником.
280
+ if (retired !== undefined) {
281
+ skipped.push([rec.slug, L.lifecycle.installDeprecated(rec.slug, retired || "—")]);
282
+ declared.add(rec.slug); continue;
283
+ }
271
284
  // Записи, которой нужен инструмент, а его на машине нет, здесь не место — но и вся
272
285
  // установка из-за неё останавливаться не должна. Причина называется вслух и попадает
273
286
  // в тот же список пропущенного, что и записи, не подошедшие по триггеру.
@@ -0,0 +1,67 @@
1
+ // tool/commands/prove.mjs — `aqk prove`: доказать, что гейты проекта ловят брак.
2
+ //
3
+ // Отдельная команда, а не флаг: вопрос «работают ли мои проверки» задают сам по себе, и ответ
4
+ // на него нужен раньше, чем прогон по коду. Прогон говорит «сегодня чисто»; доказательство —
5
+ // «а если бы было грязно, я бы это увидел».
6
+ import { readManifest } from "../lib/manifest.mjs";
7
+ import { proveGates } from "../lib/prove.mjs";
8
+ import { c, SELF } from "../lib/core.mjs";
9
+ import { L } from "../i18n/index.mjs";
10
+
11
+ function line(r) {
12
+ const P = L.prove;
13
+ const pad = r.name.padEnd(22);
14
+ if (r.state === "proven") return ` ${c.green("✔")} ${pad} ${c.dim(P.okRed)}`;
15
+ if (r.state === "unprovable") {
16
+ const why =
17
+ r.why === "no-samples" ? P.noSamples
18
+ : r.why === "other-recipe" ? P.otherRecipe(r.forRecipe.lang)
19
+ : r.why === "no-target" ? P.noTarget
20
+ : P.empty;
21
+ return ` ${c.dim("~")} ${c.dim(pad)} ${c.dim(why)}`;
22
+ }
23
+ const why = r.why === "red-passed" ? P.redPassed : r.why === "green-failed" ? P.greenFailed : P.empty;
24
+ return ` ${c.red("✘")} ${pad} ${c.red(why)}`;
25
+ }
26
+
27
+ async function cmdProve() {
28
+ const man = await readManifest();
29
+ const P = L.prove;
30
+ console.log(c.bold(`\n${P.title}\n`));
31
+
32
+ // «Манифеста нет» и «гейтов не объявлено» — разные причины и разные починки. Остальные
33
+ // команды это различают; здесь не различалось. Найдено код-ревью 2026-09-07.
34
+ if (!man) {
35
+ console.log(` ${c.red(L.ratchet.noManifest(`${SELF} init`))}\n`);
36
+ process.exit(1);
37
+ }
38
+ const gates = man?.gates && typeof man.gates === "object" && !Array.isArray(man.gates) ? man.gates : {};
39
+ if (!Object.keys(gates).length) {
40
+ console.log(` ${P.noGates}\n`);
41
+ process.exit(1);
42
+ }
43
+ if (!String(man?.samples || "").trim()) {
44
+ console.log(` ${c.red(P.noSamplesDir)}\n`);
45
+ process.exit(1);
46
+ }
47
+
48
+ const res = await proveGates(man);
49
+ // Сначала сломанные: красное называется первым, иначе его не читают.
50
+ for (const r of res.results.filter((x) => x.state === "broken")) console.log(line(r));
51
+ for (const r of res.results.filter((x) => x.state === "proven")) console.log(line(r));
52
+ for (const r of res.results.filter((x) => x.state === "unprovable")) console.log(line(r));
53
+
54
+ const parts = [c.green(P.proven(res.proven))];
55
+ if (res.broken) parts.push(c.red(P.broken(res.broken)));
56
+ if (res.unprovable) parts.push(c.dim(P.unprovable(res.unprovable)));
57
+ console.log(`\n ${parts.join(" ")}`);
58
+
59
+ if (!res.ok && res.proven === 0 && res.broken === 0) {
60
+ console.log(`\n ${c.yellow(P.nothingProven)}`);
61
+ console.log(` ${c.dim(P.fix(`${SELF} add ${L.help.name}`))}`);
62
+ }
63
+ console.log("");
64
+ process.exit(res.ok ? 0 : 1);
65
+ }
66
+
67
+ export { cmdProve };
@@ -17,6 +17,7 @@ import { mkdir, writeFile, readdir, readFile } from "node:fs/promises";
17
17
  import { join, relative } from "node:path";
18
18
  import { CWD, TARGET_DIR, SELF, c, exists, docPath } from "../lib/core.mjs";
19
19
  import { readManifest, assessLevel } from "../lib/manifest.mjs";
20
+ import { proveGates } from "../lib/prove.mjs";
20
21
  import { detectFacts, readCatalog, triggerVerdict, whichSync } from "../lib/repo.mjs";
21
22
  import { runGates, declaredGates } from "./doctor.mjs";
22
23
  import { L } from "../i18n/index.mjs";
@@ -73,7 +74,9 @@ async function cmdReport() {
73
74
  process.exit(1);
74
75
  }
75
76
 
76
- const { reached } = await assessLevel(man);
77
+ // Значок самое громкое утверждение комплекта, и доказывать его обязательно. Без этого
78
+ // проект с гейтом «true» получал AQK-3 и зелёную картинку в README: проверено прогоном.
79
+ const { reached } = await assessLevel(man, await proveGates(man));
77
80
  const facts = await detectFacts(man);
78
81
  const catalog = await readCatalog();
79
82
  const bySlug = Object.fromEntries(catalog.map((r) => [r.slug, r]));
@@ -0,0 +1,70 @@
1
+ // tool/i18n/en-docs.mjs — text that ends up IN A FILE, not in the terminal.
2
+ //
3
+ // WHY SEPARATE. The string catalogue outgrew its own 500-line limit — caught by our own
4
+ // file-size-limit gate. The seam follows meaning rather than the midpoint: here is what the
5
+ // program WRITES (comments in .aqk.yml, level names, the report form), and in en.mjs what it
6
+ // SAYS. These texts live differently: the first is read months later in someone's repository,
7
+ // the second for one second in a terminal.
8
+
9
+ const enDocs = {
10
+ manifestDoc: {
11
+ head: [
12
+ "# .aqk.yml — the Agent Quality Kit manifest",
13
+ "# What this is: a machine-readable description of how agents live in this repository.",
14
+ "# `aqk doctor` computes the compliance level. An empty field = the level is not reached,",
15
+ "# and that is honest: filling it with placeholders is pointless, files are checked, not words.",
16
+ ],
17
+ entry: "# AQK-0 — what the agent reads first.",
18
+ rules: "# AQK-1 — where the standards are and which checks are mandatory.",
19
+ gates: [
20
+ " # name: a command returning 0 or non-zero. An empty declaration protects nothing and is",
21
+ ' # rejected by the "a declared gate runs" check — hence examples here, not placeholders.',
22
+ ' # lint: "ruff check ."',
23
+ ' # test: "pytest -q"',
24
+ " # To install a ready entry from the catalogue together with its samples: aqk add <name>",
25
+ ],
26
+ samples: [
27
+ "# AQK-2 — what proves the gates work, and where the debt registries are.",
28
+ "# samples: the directory with red and green samples (a gate must go red on the first and",
29
+ "# stay quiet on the second). ratchets: lists of known violations that may only get",
30
+ "# shorter.",
31
+ ],
32
+ lessons: "# AQK-3 — where lessons accumulate. A path or an address.",
33
+ advisory: [
34
+ "# Advisory gates: they show findings but do not fail the run. The third way to introduce",
35
+ "# a rule, next to the ratchet and the big clean-up. The list is named on every run:",
36
+ "# an advisory gate everyone forgot about is a switched-off check.",
37
+ "# advisory:",
38
+ "# - complexity-limit",
39
+ ],
40
+ },
41
+ levels: [
42
+ {
43
+ title: "a manifest and an entry point",
44
+ need: "create .aqk.yml and point entry at the file an agent reads first (AGENTS.md)",
45
+ gives: "any tool understands what to read in this repository",
46
+ },
47
+ {
48
+ title: "rules and working gates",
49
+ need: "set rules (the standards directory) and fill at least one gate in gates with a real command",
50
+ gives: "checks are declared as commands, not described in prose",
51
+ },
52
+ {
53
+ title: "gates are proven, debt is under a ratchet",
54
+ need: "set samples (red and green gate samples) and ratchets (debt registries)",
55
+ gives: "the gate has proven it catches defects and stays quiet on correct code",
56
+ },
57
+ {
58
+ title: "lessons come back into the work",
59
+ need: "set lessons — the path or address of a journal where every incident yields a conclusion",
60
+ gives: "the project learns: the same bruise is not collected twice",
61
+ },
62
+ ],
63
+ report: {
64
+ title: "aqk doctor --run",
65
+ version: "version",
66
+ level: "level",
67
+ summary: (ok, all) => `total: ${ok} of ${all} green`,
68
+ },
69
+ };
70
+ export { enDocs };