agent-quality-kit 0.3.0 → 0.4.1

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 CHANGED
@@ -5,6 +5,7 @@
5
5
  [![npm](https://img.shields.io/npm/v/agent-quality-kit)](https://www.npmjs.com/package/agent-quality-kit)
6
6
  [![checks](https://github.com/arsen-ask-lx/Agent_Quality_Kit/actions/workflows/ci.yml/badge.svg)](https://github.com/arsen-ask-lx/Agent_Quality_Kit/actions/workflows/ci.yml)
7
7
  [![MIT licence](https://img.shields.io/npm/l/agent-quality-kit)](LICENSE)
8
+ [![AQK-3](https://img.shields.io/badge/AQK-3-2ea44f)](https://github.com/arsen-ask-lx/Agent_Quality_Kit)
8
9
 
9
10
  **A standard for whether a repository is ready to have its code written by agents.** Every
10
11
  promise the project makes turns into a command with an exit code — held by a machine, not by
@@ -18,10 +19,40 @@ npx agent-quality-kit start # no code yet: day-zero guards, right away
18
19
  npx agent-quality-kit doctor # code already exists: your level and what to install
19
20
  ```
20
21
 
22
+ `doctor` only reads: it writes no file and sends nothing anywhere. It is safe to point at
23
+ a repository you have not decided anything about yet.
24
+
21
25
  Nothing to install — `npx` fetches the package itself (230 KB). The bleeding edge straight from
22
26
  the repository is `npx github:arsen-ask-lx/Agent_Quality_Kit doctor`, but the first run that way
23
27
  stays silent for two or three minutes: it clones the whole repository.
24
28
 
29
+ ## What this looks like
30
+
31
+ Someone else's project, three files, nothing configured:
32
+
33
+ ```console
34
+ $ npx agent-quality-kit start # installs the guards and declares them in the manifest
35
+ $ npx agent-quality-kit doctor --run # runs them
36
+
37
+ ✘ secrets-not-in-code exit 1
38
+ ./src/api/mailer.py:1:API_KEY = "sk_live_51Hxx…"
39
+ fix: take the value out of the file, put it in an environment variable
40
+ and revoke the old key. it cannot be scrubbed from history any more.
41
+ ✘ swallowed-error exit 1
42
+ ./src/api/mailer.py:7: caught and dropped — except Exception:
43
+ fix: either handle it and log it, or re-raise.
44
+ ✘ no-print-in-prod exit 1
45
+ ./src/web/app.js:3: console.log("debug", x);
46
+ ./src/api/mailer.py:8: print("sent", to)
47
+ ✘ todo-without-task exit 1
48
+ ./src/web/app.js:1:// TODO: rewrite this
49
+ ✔ file-size-limit · entry-links-exist · complexity-limit
50
+ ```
51
+
52
+ The failure text is written for an agent: it says **what exactly to do**. The exit code is for
53
+ your pipeline. Not one finding inside the kit's own samples: the native tool runs through the
54
+ same filter as the portable check.
55
+
25
56
  **Requirements.** Node 18+ and an `sh` shell — present on macOS, Linux and WSL; Git Bash works on
26
57
  Windows. The portable checks are written in `sh` on purpose: it exists everywhere code is built.
27
58
 
@@ -35,6 +66,18 @@ without them; if the project already has `ruff`, `eslint` or `vulture`, the entr
35
66
  native rule instead — it is more precise. One entry, `dead-code`, does not work at all without a
36
67
  real tool and honestly hides itself: you cannot build a call graph with a text search.
37
68
 
69
+ ## What this is not
70
+
71
+ | Looks like | The difference |
72
+ |---|---|
73
+ | **a linter** (`ruff`, `eslint`) | AQK does not replace them, it **uses** them: if the tool is on the system, the entry takes its rule — it is more precise. A linter answers "this code is clean"; AQK answers "in this repository, this particular promise is held by a machine, and here is the proof" |
74
+ | **`pre-commit` and hooks** | they run checks. AQK answers a different question: which checks exist here at all, whether they work, and what this project has already been burned by — machine-readably, for an agent, a pipeline and a newcomer |
75
+ | **a checklist or an awesome list** | an entry is accepted only if it names a **real failure** it caught, and its arbiter goes red on the red sample and stays quiet on the green one. A machine checks that, not a reviewer |
76
+ | **a repository scorecard** (compliance badges) | they measure maturity and hand out a grade. The AQK level measures how **machine-readable** your practice is, and says outright that it is not a verdict on the project: a hundred working checks with no manifest is AQK-0 |
77
+
78
+ In one sentence: **a promise the project makes turns into a command with an exit code, and from
79
+ then on a machine holds it, not somebody's attention.**
80
+
38
81
  ## How it works
39
82
 
40
83
  The whole standard is one `.aqk.yml` file in the repository root:
@@ -70,6 +113,33 @@ would mean trust in the author rather than a fact.
70
113
  aqk doctor --run --min 1 # in CI: fails below AQK-1 OR if any gate failed
71
114
  ```
72
115
 
116
+ ## The badge
117
+
118
+ ```bash
119
+ aqk badge # runs the declared gates, prints the markdown — only if every one is green
120
+ aqk badge --check # in CI: exit 1 the day the badge in your README stops matching the run
121
+ ```
122
+
123
+ A badge nobody re-computes is a claim, not a fact — which is the very thing this project
124
+ replaces. So `aqk badge` prints nothing over a red gate, and `aqk badge --check` fails your
125
+ pipeline on the day the README and the repository part ways. The badge at the top of this file
126
+ is checked that way on every push.
127
+
128
+ ## In your pipeline
129
+
130
+ ```yaml
131
+ - uses: arsen-ask-lx/Agent_Quality_Kit@v0.4.1
132
+ with:
133
+ min: 1 # the build fails below AQK-1, or if any declared gate failed
134
+ ```
135
+
136
+ The action is a thin wrapper around one command and holds no logic of its own — without it,
137
+ the same thing in a single line:
138
+
139
+ ```yaml
140
+ - run: npx agent-quality-kit doctor --run --min 1
141
+ ```
142
+
73
143
  ## Installing a gate
74
144
 
75
145
  ```bash
package/README.ru.md CHANGED
@@ -5,6 +5,7 @@
5
5
  [![npm](https://img.shields.io/npm/v/agent-quality-kit)](https://www.npmjs.com/package/agent-quality-kit)
6
6
  [![проверки](https://github.com/arsen-ask-lx/Agent_Quality_Kit/actions/workflows/ci.yml/badge.svg)](https://github.com/arsen-ask-lx/Agent_Quality_Kit/actions/workflows/ci.yml)
7
7
  [![лицензия MIT](https://img.shields.io/npm/l/agent-quality-kit)](LICENSE)
8
+ [![AQK-3](https://img.shields.io/badge/AQK-3-2ea44f)](https://github.com/arsen-ask-lx/Agent_Quality_Kit)
8
9
 
9
10
  **Стандарт готовности репозитория к тому, что код в нём пишет агент.** Обещание проекта
10
11
  становится командой с кодом возврата — и его держит машина, а не чья-то добрая воля.
@@ -17,10 +18,40 @@ npx agent-quality-kit start # кода ещё нет: сторожа дня
17
18
  npx agent-quality-kit doctor # код уже есть: уровень и что поставить
18
19
  ```
19
20
 
21
+ `doctor` только читает: ни одного файла не пишет и никуда ничего не отправляет. Его можно
22
+ направить на репозиторий, о котором ещё ничего не решено.
23
+
20
24
  Ставить ничего не нужно, `npx` скачает пакет сам (230 КБ). Свежая версия прямо из репозитория —
21
25
  `npx github:arsen-ask-lx/Agent_Quality_Kit doctor`, но первый запуск такого вида молчит две-три
22
26
  минуты: он клонирует репозиторий целиком.
23
27
 
28
+ ## Что это выглядит так
29
+
30
+ Чужой проект, три файла, ничего не настроено:
31
+
32
+ ```console
33
+ $ npx agent-quality-kit start # ставит сторожей и объявляет их в манифесте
34
+ $ npx agent-quality-kit doctor --run # запускает их
35
+
36
+ ✘ secrets-not-in-code код 1
37
+ ./src/api/mailer.py:1:API_KEY = "sk_live_51Hxx…"
38
+ почини: убери значение из файла, положи его в переменную окружения
39
+ и отзови старый ключ. из истории секрет уже не вычистить.
40
+ ✘ swallowed-error код 1
41
+ ./src/api/mailer.py:7: перехват без обработки — except Exception:
42
+ почини: либо обработай и запиши в лог, либо пробрось дальше.
43
+ ✘ no-print-in-prod код 1
44
+ ./src/web/app.js:3: console.log("debug", x);
45
+ ./src/api/mailer.py:8: print("sent", to)
46
+ ✘ todo-without-task код 1
47
+ ./src/web/app.js:1:// TODO: переписать
48
+ ✔ file-size-limit · entry-links-exist · complexity-limit
49
+ ```
50
+
51
+ Текст отказа написан для агента: в нём сказано, **что именно сделать**. Код возврата — для
52
+ конвейера. Ни одной находки в самих образцах комплекта: родной инструмент запускается через
53
+ тот же фильтр, что и переносимая проверка.
54
+
24
55
  **Что нужно.** Node 18+ и оболочка `sh` — она есть в macOS, Linux и WSL; на Windows подойдёт
25
56
  Git Bash. Переносимые проверки написаны на `sh` намеренно: он есть везде, где собирают код.
26
57
 
@@ -34,6 +65,18 @@ Issue» — ничего не постится сама, только текст
34
65
  оно точнее. Одна запись, `dead-code`, без готового инструмента не работает вовсе и честно
35
66
  скрывается: граф вызовов поиском по тексту не построить.
36
67
 
68
+ ## Чем это не является
69
+
70
+ | Похоже на | В чём разница |
71
+ |---|---|
72
+ | **линтер** (`ruff`, `eslint`) | AQK их не заменяет, а **берёт**: если инструмент есть в системе, запись возьмёт его правило — оно точнее. Линтер отвечает «этот код чист», AQK — «в этом репозитории такое-то обещание держит машина, и вот доказательство» |
73
+ | **`pre-commit` и хуки** | они запускают проверки. AQK отвечает на другой вопрос: какие проверки тут вообще есть, работают ли они и на чём здесь уже обжигались — машиночитаемо, для агента, конвейера и нового человека |
74
+ | **чек-лист или awesome-список** | пункт принимается, только если назван **реальный отказ**, который он поймал, и его арбитр краснеет на красном образце и молчит на зелёном. Проверяет это машина, а не рецензент |
75
+ | **скоринг репозитория** (значки соответствия) | они мерят зрелость и дают оценку. Уровень AQK мерит **машиночитаемость** практики и прямо говорит, что это не оценка проекта: сотня работающих проверок без манифеста — это AQK-0 |
76
+
77
+ Одно предложение: **обещание проекта превращается в команду с кодом возврата, и дальше его
78
+ держит машина, а не чья-то внимательность.**
79
+
37
80
  ## Как устроено
38
81
 
39
82
  Весь стандарт — файл `.aqk.yml` в корне:
@@ -69,6 +112,33 @@ lessons: incidents # где копятся уроки
69
112
  aqk doctor --run --min 1 # в конвейере: ошибка, если ниже AQK-1 ИЛИ упал хоть один гейт
70
113
  ```
71
114
 
115
+ ## Значок
116
+
117
+ ```bash
118
+ aqk badge # прогоняет объявленные гейты и печатает строку — только если все зелёные
119
+ aqk badge --check # в конвейере: код 1 в тот день, когда значок в README разошёлся с прогоном
120
+ ```
121
+
122
+ Значок, который никто не пересчитывает, — это заявление, а не факт: ровно то, что этот проект
123
+ и заменяет. Поэтому при красном гейте `aqk badge` не печатает ничего, а `aqk badge --check`
124
+ роняет конвейер в тот день, когда README и репозиторий разошлись. Значок в начале этого файла
125
+ проверяется так на каждом пуше.
126
+
127
+ ## В твоём конвейере
128
+
129
+ ```yaml
130
+ - uses: arsen-ask-lx/Agent_Quality_Kit@v0.4.1
131
+ with:
132
+ min: 1 # сборка падает ниже AQK-1 или если упал любой объявленный гейт
133
+ ```
134
+
135
+ Действие — тонкая обёртка над одной командой и своей логики не имеет; без него то же самое
136
+ одной строкой:
137
+
138
+ ```yaml
139
+ - run: npx agent-quality-kit doctor --run --min 1
140
+ ```
141
+
72
142
  ## Поставить гейт
73
143
 
74
144
  ```bash
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env sh
2
+ # Запуск РОДНОГО инструмента (ruff, vulture, eslint, jscpd) через тот же фильтр образцов,
3
+ # которым пользуются переносимые проверки.
4
+ #
5
+ # ЗАЧЕМ. Переносимая проверка прячет `gates/<имя>/red|green` — искусственный код, положенный
6
+ # самим комплектом. Родной инструмент о них не знает и выдаёт их как находки: в любом проекте,
7
+ # куда поставили гейты, `ruff --select T20 .` покажет печать из нашего же красного образца.
8
+ # Гейт, который на девять десятых состоит из собственных образцов, читать не будут — его
9
+ # выключат целиком. Ровно так же выключают гейт, где 94% находок пришли из чужого кода.
10
+ #
11
+ # ПОЧЕМУ ФИЛЬТР ВЫВОДА, А НЕ ФЛАГ ИСКЛЮЧЕНИЯ У КАЖДОГО ИНСТРУМЕНТА. Флаг у всех свой
12
+ # (`--exclude`, `--ignore-pattern`, `--ignore`), синтаксис шаблона у всех разный, а главное —
13
+ # статичный флаг нельзя снять, когда проверяют САМ образец: тогда инструмент спрячет ровно то,
14
+ # что должен найти, и запись пройдёт приёмку зелёной на красном образце. Фильтр вывода знает,
15
+ # что за каталог ему дали, и снимает исключение сам — та же логика, что в own_samples_filter.
16
+ #
17
+ # bash gates/_native.sh <каталог> <команда инструмента…>
18
+
19
+ DIR="${1:-.}"
20
+ shift || true
21
+ [ "$#" -gt 0 ] || { echo "нечего запускать: не передана команда инструмента"; exit 2; }
22
+
23
+ . "$(dirname "$0")/_skip.sh" 2>/dev/null || { echo "не найден _skip.sh рядом с _native.sh"; exit 2; }
24
+
25
+ OUT="$("$@" 2>&1)"; CODE=$?
26
+ LEFT="$(printf '%s' "$OUT" | own_samples_filter "$DIR")"
27
+
28
+ # Отказ не выдумываем: если инструмент завершился успешно, результат успешен, что бы ни
29
+ # осталось в выводе. Красным делаем только то, что инструмент И счёл отказом, И что пережило
30
+ # фильтр — иначе спрятанный образец превратился бы в неустранимый красный.
31
+ if [ "$CODE" -ne 0 ] && [ -n "$LEFT" ]; then
32
+ printf '%s\n' "$LEFT"
33
+ exit "$CODE"
34
+ fi
35
+ exit 0
@@ -47,7 +47,10 @@ own_samples_filter() {
47
47
  case "$DIR0" in
48
48
  # Цель проверки — сам образец: тогда прятать его нельзя, иначе гейт «пройдёт» на красном.
49
49
  */red|*/red/|*/green|*/green/) SAMPLES="cat" ;;
50
- *) SAMPLES="grep -vE /gates/[^/]+/(red|green)(/|\$)" ;;
50
+ # «(^|/)» обязательно: переносимые проверки печатают «./gates/…», а родной инструмент —
51
+ # «gates/…» без точки. Пока шаблон требовал ведущий «/», образцы прятались только от
52
+ # первых, и родной рецепт выдавал их как находки.
53
+ *) SAMPLES="grep -vE (^|/)gates/[^/]+/(red|green)(/|\$)" ;;
51
54
  esac
52
55
  if [ -n "$RE" ]; then
53
56
  $SAMPLES | grep -vE "(^|/)($RE)(/|:|\$)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-quality-kit",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "AQK — Agent Quality Kit: переносимый комплект, приводящий проект в состояние, пригодное для работы агентов. Правила, механические упоры, накопленные уроки. Одна команда, любой инструмент.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,79 @@
1
+ // tool/commands/badge.mjs — значок уровня для чужого README и команда, которая держит его правдой.
2
+ //
3
+ // ЗАЧЕМ. Значок в README — обычно заявление автора: нарисовал один раз, дальше он живёт своей
4
+ // жизнью и через месяц врёт. Здесь он выдаётся только после прогона объявленных гейтов, а
5
+ // `--check` роняет конвейер, когда README разошёлся с фактом. Иначе мы раздавали бы ровно ту
6
+ // самую картинку-обещание, против которой весь стандарт.
7
+
8
+ import { readFile } from "node:fs/promises";
9
+ import { join } from "node:path";
10
+ import { CWD, SELF, REPO_URL, c, exists, die } from "../lib/core.mjs";
11
+ import { readManifest, assessLevel } from "../lib/manifest.mjs";
12
+ import { runGates, declaredGates } from "./doctor.mjs";
13
+ import { L } from "../i18n/index.mjs";
14
+
15
+ // Один разбор на запись и на чтение: значок, который мы печатаем, обязан читаться нами же.
16
+ const BADGE_RE = /img\.shields\.io\/badge\/AQK-(\d)-/;
17
+
18
+ function badgeMarkdown(level) {
19
+ const color = level >= 3 ? "2ea44f" : level >= 2 ? "blue" : "orange";
20
+ return `[![AQK-${level}](https://img.shields.io/badge/AQK-${level}-${color})](${REPO_URL})`;
21
+ }
22
+
23
+ // Где искать значок: точка входа для агента и README на виду у человека. Список короткий
24
+ // намеренно — обход всего дерева нашёл бы значок в чужой копии и посчитал бы его нашим.
25
+ function placesToCheck(man) {
26
+ const entry = Array.isArray(man?.entry) ? man.entry.map(String) : [];
27
+ return [...new Set([...entry, "README.md", "README.ru.md"])];
28
+ }
29
+
30
+ async function cmdBadge(args = []) {
31
+ const check = args.includes("--check");
32
+
33
+ const man = await readManifest();
34
+ if (!man) die(`\n ${L.badge.noManifest(`${SELF} init`)}\n`);
35
+
36
+ const { reached } = await assessLevel(man);
37
+ if (reached < 0) die(`\n ${L.badge.notReached(`${SELF} doctor`)}\n`);
38
+
39
+ // Прогон, а не манифест. Значок при красном гейте — это и есть недоказанное утверждение.
40
+ const gates = declaredGates(man);
41
+ if (gates.length) {
42
+ const run = runGates(man);
43
+ if (run.failed) {
44
+ const red = run.results.filter((r) => !r.ok).map((r) => r.name).join(", ");
45
+ die(`\n ${L.badge.redGates(run.failed, red)}\n`);
46
+ }
47
+ }
48
+
49
+ const markdown = badgeMarkdown(reached);
50
+
51
+ if (!check) {
52
+ console.log(`\n${markdown}\n`);
53
+ console.log(c.dim(` ${L.badge.hint(gates.length)}`));
54
+ console.log(c.dim(` ${L.badge.keepTrue(`${SELF} badge --check`)}\n`));
55
+ process.exit(0);
56
+ }
57
+
58
+ const places = placesToCheck(man);
59
+ const found = [];
60
+ for (const rel of places) {
61
+ const p = join(CWD, rel);
62
+ if (!(await exists(p))) continue;
63
+ const m = BADGE_RE.exec(await readFile(p, "utf8"));
64
+ if (m) found.push({ rel, level: Number(m[1]) });
65
+ }
66
+
67
+ if (!found.length) die(`\n ${L.badge.checkMissing(places.join(", "))}\n ${markdown}\n`);
68
+
69
+ const wrong = found.filter((f) => f.level !== reached);
70
+ if (wrong.length) {
71
+ const where = wrong.map((f) => `${f.rel} (AQK-${f.level})`).join(", ");
72
+ die(`\n ${L.badge.checkMismatch(where, reached)}\n ${markdown}\n`);
73
+ }
74
+
75
+ console.log(c.green(`\n ${L.badge.checkOk(reached, found.map((f) => f.rel).join(", "))}\n`));
76
+ process.exit(0);
77
+ }
78
+
79
+ export { cmdBadge, badgeMarkdown, BADGE_RE, placesToCheck };
@@ -34,15 +34,30 @@ async function installGate(slug, man, facts) {
34
34
 
35
35
  // Общий список исключений едет вместе с проверкой: без него она читает окружение и
36
36
  // зависимости, и человек получает тысячу чужих нарушений вместо сотни своих.
37
- const skipSrc = join(GATES_SRC, "_skip.sh");
38
- if (await exists(skipSrc)) await copyFile(skipSrc, join(CWD, PROJECT_GATES, "_skip.sh"));
37
+ for (const helper of ["_skip.sh", "_native.sh"]) {
38
+ const from = join(GATES_SRC, helper);
39
+ if (await exists(from)) await copyFile(from, join(CWD, PROJECT_GATES, helper));
40
+ }
39
41
 
40
42
  // Команда под стек проекта, с путями внутри репозитория, а не внутри пакета.
41
- const cmd = String(pickRecipe(rec, facts) || "")
43
+ const picked = String(pickRecipe(rec, facts) || "");
44
+ let cmd = picked
42
45
  .replace(/\{gate\}/g, `${PROJECT_GATES}/${slug}`)
43
46
  .replace(/\{dir\}/g, ".");
44
47
  if (!cmd) die(L.add.noRecipe(slug, [...facts.langs].join("/") || L.add.thisStack));
45
48
 
49
+ // Родной инструмент не знает про наши образцы и выдаёт их как находки — в любом проекте,
50
+ // куда поставили гейты. Заворачиваем его в общий фильтр. Переносимая проверка фильтрует
51
+ // себя сама, её заворачивать незачем.
52
+ //
53
+ // Обёртка ставится ЗДЕСЬ, а не в самом рецепте, ровно по одной причине: приёмка каталога
54
+ // (`gates.sh`) гоняет рецепт по красному образцу напрямую, без обёртки, — и продолжает
55
+ // видеть то, что должна. Статичный флаг исключения в рецепте спрятал бы образец от
56
+ // приёмки, и запись прошла бы зелёной на красном.
57
+ const recipes = rec.recipes && typeof rec.recipes === "object" ? rec.recipes : {};
58
+ const isPortable = picked === String(recipes.any || "");
59
+ if (!isPortable) cmd = `bash ${PROJECT_GATES}/_native.sh . ${cmd}`;
60
+
46
61
  const manPath = join(CWD, MANIFEST);
47
62
  const { text, why } = manifestWithGate(await readFile(manPath, "utf8"), slug, cmd);
48
63
  if (text) await writeFile(manPath, text, "utf8");
package/tool/i18n/en.mjs CHANGED
@@ -23,6 +23,7 @@ export const en = {
23
23
  note: "record a lesson in the shared bruise journal",
24
24
  blob: "assemble the guides into a single GOD_AI.md",
25
25
  report: "the mandatory report form: what is in place, what is not, what was not read",
26
+ badge: "a level badge for your README — and a check that it does not lie",
26
27
  noInstall: "Without installing: npx agent-quality-kit init",
27
28
  language: "Output language: AQK_LANG=ru (or en), otherwise your system locale",
28
29
  },
@@ -355,6 +356,18 @@ export const en = {
355
356
  lessons: "# AQK-3 — where lessons accumulate. A path or an address.",
356
357
  },
357
358
 
359
+ badge: {
360
+ noManifest: (cmd) => `No .aqk.yml — there is no level yet. Start with ${cmd}`,
361
+ notReached: (cmd) => `AQK-0 is not reached — there is nothing to put on a badge. What is missing: ${cmd}`,
362
+ redGates: (n, names) =>
363
+ `Red gates: ${n} (${names}). A badge issued over a red gate is the author's claim, not a machine's fact.`,
364
+ hint: (n) => `Proven by a run. Green gates: ${n}. Paste the line above into your README.`,
365
+ keepTrue: (cmd) => `To keep the badge from turning into a lie, put this in your pipeline: ${cmd}`,
366
+ checkMissing: (places) => `No AQK badge in any of: ${places}. This is the line to paste:`,
367
+ checkMismatch: (where, level) => `The badge lies: ${where}, while the run says AQK-${level}. Replace it with:`,
368
+ checkOk: (level, where) => `Badge matches the run: AQK-${level} — ${where}`,
369
+ },
370
+
358
371
  report2: {
359
372
  title: "AQK report",
360
373
  noManifest: (cmd) => `No .aqk.yml — nothing to report on. Start with ${cmd}`,
package/tool/i18n/ru.mjs CHANGED
@@ -24,6 +24,7 @@ export const ru = {
24
24
  note: "записать урок в общий журнал шишек",
25
25
  blob: "собрать методички в один файл GOD_AI.md",
26
26
  report: "обязательная форма отчёта: что стоит, что нет, что не прочитано",
27
+ badge: "значок уровня для README — и проверка, что он не врёт",
27
28
  noInstall: "Без установки: npx agent-quality-kit init",
28
29
  language: "Язык вывода: AQK_LANG=en (или ru), иначе по системной локали",
29
30
  },
@@ -356,6 +357,18 @@ export const ru = {
356
357
  lessons: "# AQK-3 — где копятся уроки. Путь или адрес.",
357
358
  },
358
359
 
360
+ badge: {
361
+ noManifest: (cmd) => `Нет .aqk.yml — уровня ещё нет. Начни с ${cmd}`,
362
+ notReached: (cmd) => `AQK-0 не достигнут — значок выдавать не за что. Чего не хватает: ${cmd}`,
363
+ redGates: (n, names) =>
364
+ `Красных гейтов: ${n} (${names}). Значок при красном гейте — заявление автора, а не факт машины.`,
365
+ hint: (n) => `Проверено прогоном. Зелёных гейтов: ${n}. Строку выше — в README.`,
366
+ keepTrue: (cmd) => `Чтобы значок не превратился во враньё, поставь в конвейер: ${cmd}`,
367
+ checkMissing: (places) => `Значка AQK нет ни в одном из файлов: ${places}. Вставить нужно этот:`,
368
+ checkMismatch: (where, level) => `Значок врёт: ${where}, а прогон говорит AQK-${level}. Заменить на:`,
369
+ checkOk: (level, where) => `Значок совпал с прогоном: AQK-${level} — ${where}`,
370
+ },
371
+
359
372
  report2: {
360
373
  title: "Отчёт AQK",
361
374
  noManifest: (cmd) => `Нет .aqk.yml — отчитываться не о чем. Начни с ${cmd}`,
@@ -42,6 +42,7 @@ Running them is your job. The human looks at the list of holes and decides which
42
42
  | \`aqk find "…"\` | searches by meaning for an existing check | before inventing your own |
43
43
  | \`aqk note "…"\` | writes a lesson into the shared journal | the process or an instrument let you down: a check lied, a rule was bypassed |
44
44
  | \`aqk report\` | assembles a report from an actual run: what is in place and with which recipe, what is missing, what the kit told you to read | **mandatory** at the end of working with the kit — instead of a summary from memory |
45
+ | \`aqk badge\` | prints a level badge for the README — but only after running the declared gates, and stays silent over a red one; \`--check\` fails the pipeline when the badge stops matching the run | the human asked to show the level, or you are setting up CI |
45
46
 
46
47
  If there is no \`aqk\` command on the system, the kit was used without installing. Then write
47
48
  \`npx agent-quality-kit\` instead of \`aqk\`. Every command prints the invocation that will
@@ -47,6 +47,7 @@ const AGENTS_MD = `# AGENTS.md
47
47
  | \`aqk find "…"\` | ищет по смыслу, есть ли уже такая проверка | прежде чем изобретать свою |
48
48
  | \`aqk note "…"\` | пишет урок в общий журнал | процесс или прибор подвели: проверка соврала, правило обошли |
49
49
  | \`aqk report\` | собирает прогоном отчёт: что стоит и каким рецептом, чего нет, что комплект велел прочитать | **обязательно** в конце работы с комплектом — вместо пересказа по памяти |
50
+ | \`aqk badge\` | печатает значок уровня для README — но только после прогона объявленных гейтов, а при красном молчит; \`--check\` роняет конвейер, когда значок разошёлся с прогоном | человек попросил показать уровень; настраиваешь конвейер |
50
51
 
51
52
  Если команды \`aqk\` нет в системе — комплект ставили разово, без установки. Тогда вместо
52
53
  \`aqk\` пиши \`npx agent-quality-kit\`. Любая команда сама печатает тот
package/tool/program.mjs CHANGED
@@ -22,6 +22,7 @@ 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 { cmdBadge } from "./commands/badge.mjs";
25
26
 
26
27
  // Разбор аргументов выполняется только при запуске файла как программы. При импорте —
27
28
  // а так его читают модульные проверки tool/selfcheck/units.mjs — CLI запускаться не должен.
@@ -68,6 +69,9 @@ if (IS_MAIN) {
68
69
  case "report":
69
70
  await cmdReport();
70
71
  break;
72
+ case "badge":
73
+ await cmdBadge(rest);
74
+ break;
71
75
  default: {
72
76
  // Ширина колонки считается, а не подбирается пробелами: строки в двух языках разной
73
77
  // длины, и вручную выровненная справка на втором языке разъезжается.
@@ -86,6 +90,7 @@ if (IS_MAIN) {
86
90
  [`${SELF} note "…"`, h.note],
87
91
  [`${SELF} blob`, h.blob],
88
92
  [`${SELF} report`, h.report],
93
+ [`${SELF} badge`, h.badge],
89
94
  ];
90
95
  const width = Math.max(...rows.map(([cmdText]) => cmdText.length));
91
96
  const lines = rows.map(([cmdText, text]) => ` ${c.bold(cmdText.padEnd(width))} ${text}`);
@@ -624,6 +624,78 @@ else
624
624
  fi
625
625
  rm -rf "$IGNDIR"
626
626
 
627
+ # --- 37. родной рецепт не ругается на образцы гейтов --------------------------
628
+ # ЗАЧЕМ. Переносимая проверка прячет gates/<имя>/red|green через own_samples_filter, а родной
629
+ # инструмент о них не знает и выдаёт их как находки — в ЛЮБОМ проекте, куда поставили гейты.
630
+ # Всплыло, только когда починка поиска программ в PATH сделала родные рецепты достижимыми:
631
+ # до этого они молча не запускались. Гейт, который на 90% состоит из своих же образцов,
632
+ # выключают целиком — см. журнал, 2026-09-04.
633
+ if command -v vulture >/dev/null 2>&1; then
634
+ NATDIR="$(mktemp -d)"
635
+ (
636
+ cd "$NATDIR" && git init -q . && git config user.email t@t && git config user.name t &&
637
+ mkdir -p src && printf 'def used():\n return 1\n\nprint(used())\n' > src/ok.py &&
638
+ node "$CLI" init >/dev/null 2>&1 && node "$CLI" add dead-code >/dev/null 2>&1
639
+ )
640
+ NAT_CMD=$(sed -n 's/^ dead-code: "\(.*\)"$/\1/p' "$NATDIR/.aqk.yml")
641
+ NAT_OUT=$( cd "$NATDIR" && eval "$NAT_CMD" 2>&1 )
642
+ if printf '%s' "$NAT_OUT" | grep -q 'gates/'; then
643
+ bad "родной рецепт выдаёт образцы гейтов как находки" "$(printf '%s' "$NAT_OUT" | head -2)"
644
+ else
645
+ ok "родной рецепт не ругается на образцы гейтов"
646
+ fi
647
+ rm -rf "$NATDIR"
648
+ else
649
+ ok "родной рецепт не проверен здесь — нет vulture"
650
+ fi
651
+
652
+ # --- 38. badge выдаёт значок с тем же уровнем, что и doctor -------------------
653
+ # ЗАЧЕМ. Значок в чужом README — единственное, что делает стандарт видимым за пределами
654
+ # нашего репозитория. Если он покажет уровень, отличный от того, что считает doctor, это
655
+ # ровно то враньё, против которого весь стандарт.
656
+ BDIR="$(mktemp -d)"
657
+ (
658
+ cd "$BDIR" && git init -q . && git config user.email t@t && git config user.name t &&
659
+ mkdir -p src && printf 'def f():\n return 1\n' > src/a.py &&
660
+ node "$CLI" init >/dev/null 2>&1 && node "$CLI" add file-size-limit >/dev/null 2>&1
661
+ )
662
+ B_OUT=$( cd "$BDIR" && node "$CLI" badge 2>&1 )
663
+ B_LVL=$(printf '%s' "$B_OUT" | sed -n 's|.*img.shields.io/badge/AQK-\([0-9]\)-.*|\1|p' | head -1)
664
+ D_LVL=$( cd "$BDIR" && node "$CLI" doctor 2>&1 | sed -n 's/.*Уровень: AQK-\([0-9]\).*/\1/p' | head -1 )
665
+ if [ -n "$B_LVL" ] && [ "$B_LVL" = "$D_LVL" ]; then
666
+ ok "badge выдаёт значок с уровнем doctor (AQK-$B_LVL)"
667
+ else
668
+ bad "badge и doctor разошлись в уровне" "badge=[$B_LVL] doctor=[$D_LVL]"
669
+ fi
670
+
671
+ # --- 39. badge молчит, когда гейт красный ------------------------------------
672
+ # ЗАЧЕМ. Значок, выданный при красном гейте, — это заявление автора, а не факт машины.
673
+ sed -i.bak 's|^ file-size-limit: .*|&\n broken: "sh -c '"'"'exit 1'"'"'"|' "$BDIR/.aqk.yml"
674
+ B_RED=$( cd "$BDIR" && node "$CLI" badge 2>&1 ); B_RED_CODE=$?
675
+ # Условие «нет значка» само по себе зелёное и у несуществующей команды — поэтому здесь
676
+ # требуется ещё и названный виновник: иначе проверка не умеет краснеть.
677
+ if [ "$B_RED_CODE" -ne 0 ] && ! printf '%s' "$B_RED" | grep -q 'img.shields.io' &&
678
+ printf '%s' "$B_RED" | grep -q 'broken'; then
679
+ ok "badge отказывает при красном гейте"
680
+ else
681
+ bad "badge выдал значок при красном гейте" "код=$B_RED_CODE $(printf '%s' "$B_RED" | head -2)"
682
+ fi
683
+ mv "$BDIR/.aqk.yml.bak" "$BDIR/.aqk.yml"
684
+
685
+ # --- 40. badge --check ловит устаревший значок в README ----------------------
686
+ # ЗАЧЕМ. Значок, который никто не пересчитывает, через месяц врёт. Смысл он приобретает
687
+ # только вместе с командой, которая роняет конвейер, когда README разошёлся с фактом.
688
+ printf '# проект\n\n[![AQK-3](https://img.shields.io/badge/AQK-3-2ea44f)](https://x)\n' > "$BDIR/README.md"
689
+ ( cd "$BDIR" && node "$CLI" badge --check >/dev/null 2>&1 ); CHK_LIE=$?
690
+ printf '# проект\n\n[![AQK-%s](https://img.shields.io/badge/AQK-%s-2ea44f)](https://x)\n' "$D_LVL" "$D_LVL" > "$BDIR/README.md"
691
+ ( cd "$BDIR" && node "$CLI" badge --check >/dev/null 2>&1 ); CHK_TRUE=$?
692
+ if [ "$CHK_LIE" -ne 0 ] && [ "$CHK_TRUE" -eq 0 ]; then
693
+ ok "badge --check ловит устаревший значок и пропускает верный"
694
+ else
695
+ bad "badge --check не различает верный и устаревший значок" "врущий=$CHK_LIE верный=$CHK_TRUE"
696
+ fi
697
+ rm -rf "$BDIR"
698
+
627
699
  # --- итог -------------------------------------------------------------------
628
700
  printf '\n'
629
701
  if [ "$FAIL" -eq 0 ]; then
@@ -14,6 +14,7 @@ import assert from "node:assert/strict";
14
14
  import { parseManifest, manifestWithGate } from "../lib/manifest.mjs";
15
15
  import { triggerVerdict, recipeFor, stems, overlap, EXT_LANG, whichSync } from "../lib/repo.mjs";
16
16
  import { CATALOGS, pickLang, L } from "../i18n/index.mjs";
17
+ import { badgeMarkdown, BADGE_RE, placesToCheck } from "../commands/badge.mjs";
17
18
  import { dirname } from "node:path";
18
19
 
19
20
  const facts = (over = {}) => ({ langs: new Set(), files: 0, ...over });
@@ -178,3 +179,23 @@ test("команда путём, а не именем, ищется на дис
178
179
  assert.ok(whichSync(process.execPath));
179
180
  assert.equal(whichSync("./нет-такого-файла.sh"), null);
180
181
  });
182
+
183
+ // --- значок уровня ------------------------------------------------------------
184
+ // ЗАЧЕМ. Значок печатает одна функция, а читает его обратно другое выражение — в том же
185
+ // файле, но независимо. Разойдись они, и `badge --check` перестал бы узнавать собственный
186
+ // значок: конвейер молча зеленел бы на любом README. Тишина, неотличимая от успеха.
187
+ test("значок читается тем же разбором, каким печатается", () => {
188
+ for (const level of [0, 1, 2, 3]) {
189
+ const found = BADGE_RE.exec(badgeMarkdown(level));
190
+ assert.ok(found, `значок AQK-${level} не разобрался`);
191
+ assert.equal(Number(found[1]), level);
192
+ }
193
+ });
194
+
195
+ test("значок ищется в точке входа и в README, без повторов", () => {
196
+ const places = placesToCheck({ entry: ["AGENTS.md", "README.md"] });
197
+ assert.ok(places.includes("AGENTS.md"));
198
+ assert.ok(places.includes("README.md"));
199
+ assert.equal(places.filter((p) => p === "README.md").length, 1);
200
+ assert.ok(placesToCheck({}).includes("README.md"), "без entry README всё равно проверяется");
201
+ });