agent-quality-kit 0.2.5 → 0.4.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 (39) hide show
  1. package/README.md +72 -0
  2. package/README.ru.md +71 -0
  3. package/kit/gates/_native.sh +35 -0
  4. package/kit/gates/_skip.sh +34 -3
  5. package/kit/gates/commit-explains-itself/gate.yml +1 -0
  6. package/kit/gates/complexity-limit/gate.yml +1 -0
  7. package/kit/gates/dead-code/gate.yml +1 -0
  8. package/kit/gates/deps-are-pinned/gate.yml +1 -0
  9. package/kit/gates/duplicate-code/gate.yml +1 -0
  10. package/kit/gates/entry-links-exist/gate.yml +1 -0
  11. package/kit/gates/file-size-limit/gate.yml +1 -0
  12. package/kit/gates/gate-has-samples/gate.yml +1 -0
  13. package/kit/gates/gates-are-runnable/gate.yml +1 -0
  14. package/kit/gates/gates-run-in-ci/gate.yml +1 -0
  15. package/kit/gates/lesson-has-outcome/gate.yml +1 -0
  16. package/kit/gates/no-print-in-prod/check.sh +3 -1
  17. package/kit/gates/no-print-in-prod/gate.yml +1 -0
  18. package/kit/gates/secrets-not-in-code/gate.yml +1 -0
  19. package/kit/gates/swallowed-error/gate.yml +1 -0
  20. package/kit/gates/todo-without-task/gate.yml +1 -0
  21. package/package.json +1 -1
  22. package/tool/commands/badge.mjs +79 -0
  23. package/tool/commands/doctor.mjs +38 -41
  24. package/tool/commands/gates.mjs +121 -112
  25. package/tool/commands/project.mjs +84 -85
  26. package/tool/commands/report.mjs +194 -0
  27. package/tool/i18n/en.mjs +436 -0
  28. package/tool/i18n/index.mjs +33 -0
  29. package/tool/i18n/ru.mjs +437 -0
  30. package/tool/i18n/templates-en.mjs +164 -0
  31. package/tool/i18n/templates-ru.mjs +170 -0
  32. package/tool/lib/core.mjs +5 -1
  33. package/tool/lib/manifest.mjs +12 -31
  34. package/tool/lib/repo.mjs +45 -21
  35. package/tool/lib/templates.mjs +38 -182
  36. package/tool/program.mjs +36 -9
  37. package/tool/selfcheck/gates.sh +7 -1
  38. package/tool/selfcheck/smoke.sh +169 -0
  39. package/tool/selfcheck/units.mjs +101 -5
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,18 @@ 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
+
73
128
  ## Installing a gate
74
129
 
75
130
  ```bash
@@ -104,6 +159,23 @@ let it grow. The rule applies from the day it is installed — the old code stay
104
159
  via `npx` the package is temporary, and tomorrow the command in your manifest would point at
105
160
  nothing.
106
161
 
162
+ ## Third-party code inside the repository
163
+
164
+ A reference copy, vendored code, generated clients — code that lives here but was not written
165
+ here. The scanning checks will skip it if you add `.aqkignore` in the root: one pattern per line,
166
+ `#` starts a comment, and `*` does not cross `/`.
167
+
168
+ ```
169
+ # brought in from another repository
170
+ third-party/
171
+ vendor/
172
+ *.generated.js
173
+ ```
174
+
175
+ `aqk report` prints the contents of this file as its own section. Hiding things silently is the
176
+ same class as a silent gate: a line here means there is no protection along that path, and will
177
+ not be.
178
+
107
179
  ## The catalogue of promises
108
180
 
109
181
  `doctor` inspects the repository — languages, existing gates — and shows **only what applies**:
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,18 @@ 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
+
72
127
  ## Поставить гейт
73
128
 
74
129
  ```bash
@@ -101,6 +156,22 @@ aqk why "файл вырос до девяти тысяч строк"
101
156
  `add` **копирует проверку в репозиторий**, а не ссылается на пакет: при установке через `npx`
102
157
  пакет временный, и завтра команда в манифесте указывала бы в никуда.
103
158
 
159
+ ## Чужой код в репозитории
160
+
161
+ Референс, вендоринг, сгенерированные клиенты — код лежит здесь, но написан не здесь. Сканирующие
162
+ проверки по нему не пойдут, если завести `.aqkignore` в корне: по шаблону на строку, `#` —
163
+ комментарий, звёздочка не переходит через `/`.
164
+
165
+ ```
166
+ # принесено из другого репозитория
167
+ third-party/
168
+ vendor/
169
+ *.generated.js
170
+ ```
171
+
172
+ `aqk report` печатает содержимое этого файла отдельным разделом. Скрытое молча — тот же класс,
173
+ что молчащий гейт: строка здесь означает, что защиты по этому пути нет и не будет.
174
+
104
175
  ## Каталог обещаний
105
176
 
106
177
  `doctor` смотрит на репозиторий — языки, наличие гейтов — и показывает **только применимое**:
@@ -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
@@ -21,11 +21,42 @@ migrations"
21
21
  # (red-team тесты, что угодно) становился невидим для secrets-not-in-code во всех проектах,
22
22
  # куда ставили гейт. Фильтр ниже смотрит на путь целиком: только `gates/<имя>/red|green/`,
23
23
  # а не голое имя каталога.
24
+ # Список исключений САМОГО ПРОЕКТА: файл .aqkignore в корне, по шаблону на строку, «#» —
25
+ # комментарий. Нужен для кода, который лежит в репозитории, но написан не здесь: референс,
26
+ # вендоринг, сгенерированные клиенты. Без него единственным способом настройки была правка
27
+ # КОПИИ этого файла в проекте — то есть настройка правкой чужого файла, которую затирает
28
+ # следующий `aqk add`. Найдено первым чужим прогоном.
29
+ #
30
+ # Шаблон — фрагмент пути: `vendor/`, `apps/legacy`, `*.generated.js`. Звёздочка не переходит
31
+ # через «/», как в .gitignore, — иначе «src/*» прятало бы весь проект.
32
+ aqkignore_re() {
33
+ F="${1:-.}/.aqkignore"
34
+ [ -f "$F" ] || return 1
35
+ sed -e 's/#.*$//' -e 's|/*[[:space:]]*$||' -e 's/^[[:space:]]*//' "$F" |
36
+ grep -v '^$' |
37
+ sed -e 's/[][(){}.^$+?|\\]/\\&/g' -e 's/\*/[^\/]*/g' |
38
+ tr '\n' '|' | sed 's/|$//'
39
+ }
40
+
41
+ # Один фильтр на два дела, и это осознанно: его зовут все семь сканирующих проверок, а их
42
+ # копии уже лежат в чужих проектах. Добавить сюда — значит, что обновление комплекта включает
43
+ # .aqkignore и у тех, кто ставил гейты раньше, без перекопирования их check.sh.
24
44
  own_samples_filter() {
25
- case "${1:-}" in
26
- */red|*/red/|*/green|*/green/) cat ;;
27
- *) grep -vE '/gates/[^/]+/(red|green)(/|$)' ;;
45
+ DIR0="${1:-}"
46
+ RE="$(aqkignore_re "$DIR0")"
47
+ case "$DIR0" in
48
+ # Цель проверки — сам образец: тогда прятать его нельзя, иначе гейт «пройдёт» на красном.
49
+ */red|*/red/|*/green|*/green/) SAMPLES="cat" ;;
50
+ # «(^|/)» обязательно: переносимые проверки печатают «./gates/…», а родной инструмент —
51
+ # «gates/…» без точки. Пока шаблон требовал ведущий «/», образцы прятались только от
52
+ # первых, и родной рецепт выдавал их как находки.
53
+ *) SAMPLES="grep -vE (^|/)gates/[^/]+/(red|green)(/|\$)" ;;
28
54
  esac
55
+ if [ -n "$RE" ]; then
56
+ $SAMPLES | grep -vE "(^|/)($RE)(/|:|\$)"
57
+ else
58
+ $SAMPLES
59
+ fi
29
60
  }
30
61
 
31
62
  # Расширения, где `print` и маркеры долга — конструкции языка, а не текст. Проверять по ним
@@ -1,4 +1,5 @@
1
1
  intent: коммит несёт мини-отчёт — что сделано и в чём агент не уверен
2
+ intent_en: the commit carries a mini-report — what was done and what the agent is unsure about
2
3
 
3
4
  trigger:
4
5
  always: true
@@ -1,4 +1,5 @@
1
1
  intent: функция не вырастает до сложности, в которой её нельзя удержать в голове
2
+ intent_en: a function does not grow past the complexity you can hold in your head
2
3
 
3
4
  trigger:
4
5
  always: true
@@ -1,4 +1,5 @@
1
1
  intent: код, который никто не вызывает, не остаётся в проекте
2
+ intent_en: code nobody calls does not stay in the project
2
3
 
3
4
  # Только там, где есть готовый инструмент: переносимой проверки здесь быть не может, а
4
5
  # показывать запись тем, кому её нечем исполнить, — значит показывать работу, которую
@@ -1,4 +1,5 @@
1
1
  intent: версии зависимостей закреплены — сборка воспроизводима
2
+ intent_en: dependency versions are pinned — the build is reproducible
2
3
 
3
4
  trigger:
4
5
  has_deps: true
@@ -1,4 +1,5 @@
1
1
  intent: один и тот же код не размножается по проекту копиями
2
+ intent_en: the same code does not multiply into copies across the project
2
3
 
3
4
  trigger:
4
5
  files_gt: 20
@@ -2,6 +2,7 @@
2
2
  # Читается программой; всё, что нельзя выполнить, живёт в README.md рядом.
3
3
 
4
4
  intent: файлы, на которые ссылается точка входа, существуют на диске
5
+ intent_en: files the entry point links to actually exist on disk
5
6
 
6
7
  # Когда запись показывается человеку. Отсутствие триггера сделало бы её шумом
7
8
  # для тех, кого она не касается.
@@ -1,4 +1,5 @@
1
1
  intent: файл не вырастает до размера, в котором агент теряется
2
+ intent_en: a file does not grow to a size where the agent gets lost
2
3
 
3
4
  trigger:
4
5
  always: true
@@ -1,4 +1,5 @@
1
1
  intent: у каждого объявленного гейта есть красный и зелёный образец
2
+ intent_en: every declared gate has a red and a green sample
2
3
 
3
4
  trigger:
4
5
  has_gates: true
@@ -1,4 +1,5 @@
1
1
  intent: каждый гейт, объявленный в манифесте, существует и запускается
2
+ intent_en: every gate declared in the manifest exists and runs
2
3
 
3
4
  trigger:
4
5
  has_gates: true
@@ -1,4 +1,5 @@
1
1
  intent: каждый объявленный гейт запускается конвейером, а не только руками
2
+ intent_en: every declared gate runs in the pipeline, not only by hand
2
3
 
3
4
  # Условия складываются: запись касается только тех, у кого есть и гейты, и конвейер.
4
5
  # Нет конвейера — сначала он, а не эта проверка.
@@ -1,4 +1,5 @@
1
1
  intent: каждая записанная шишка кончается решением, а не рассказом
2
+ intent_en: every recorded bruise ends in a decision, not a story
2
3
 
3
4
  # Проверять нечего там, где журнала нет: запись сама скажет об этом и промолчит.
4
5
  trigger:
@@ -14,7 +14,9 @@ DIR="${1:-.}"
14
14
  #
15
15
  # На настоящем проекте без этого различия 285 находок из 330 пришли из scripts/ и оснастки.
16
16
  # Гейт, который на 86% состоит из ложных сработок, выключают целиком.
17
- TOOLING="--exclude-dir=scripts --exclude-dir=tools --exclude-dir=bin --exclude-dir=examples --exclude-dir=notebooks --exclude-dir=docs --exclude-dir=.claude --exclude-dir=gates --exclude-dir=gates-reference"
17
+ TOOLING="--exclude-dir=scripts --exclude-dir=tools --exclude-dir=bin --exclude-dir=examples --exclude-dir=notebooks --exclude-dir=docs --exclude-dir=.claude --exclude-dir=gates"
18
+ # `gates-reference` отсюда убран: это имя каталога ОДНОГО проекта, зашитое в общий
19
+ # инструмент. Такому место в .aqkignore самого проекта — он появился позже этой строки.
18
20
 
19
21
  # Проект называет СВОИ каталоги, где печать — интерфейс, а не отладка: у программы командной
20
22
  # строки это её исходники целиком. Объявляется в манифесте, рядом с командой, и потому видно
@@ -1,4 +1,5 @@
1
1
  intent: отладочная печать не доезжает до прод-кода
2
+ intent_en: debug printing does not reach production code
2
3
 
3
4
  # Запись касается только языков, где есть эта конструкция. В проекте на Go или
4
5
  # Rust она не показывается вовсе.
@@ -1,4 +1,5 @@
1
1
  intent: ключи, пароли и приватные ключи не попадают в код
2
+ intent_en: keys, passwords and private keys do not end up in the code
2
3
 
3
4
  trigger:
4
5
  always: true
@@ -1,4 +1,5 @@
1
1
  intent: ошибка не глушится молча — она обработана и записана либо проброшена
2
+ intent_en: an error is not silently swallowed — it is handled and logged, or re-raised
2
3
 
3
4
  trigger:
4
5
  always: true
@@ -1,4 +1,5 @@
1
1
  intent: маркеров «доделать потом» нет в готовом коде — вместо них заведённая задача
2
+ intent_en: "fix later" markers are absent from finished code — a filed task replaces them
2
3
 
3
4
  trigger:
4
5
  always: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-quality-kit",
3
- "version": "0.2.5",
3
+ "version": "0.4.0",
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 };