agent-quality-kit 0.3.0 → 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.
- package/README.md +55 -0
- package/README.ru.md +55 -0
- package/kit/gates/_native.sh +35 -0
- package/kit/gates/_skip.sh +4 -1
- package/package.json +1 -1
- package/tool/commands/badge.mjs +79 -0
- package/tool/commands/gates.mjs +18 -3
- package/tool/i18n/en.mjs +13 -0
- package/tool/i18n/ru.mjs +13 -0
- package/tool/program.mjs +5 -0
- package/tool/selfcheck/smoke.sh +72 -0
- package/tool/selfcheck/units.mjs +21 -0
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/agent-quality-kit)
|
|
6
6
|
[](https://github.com/arsen-ask-lx/Agent_Quality_Kit/actions/workflows/ci.yml)
|
|
7
7
|
[](LICENSE)
|
|
8
|
+
[](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
|
package/README.ru.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/agent-quality-kit)
|
|
6
6
|
[](https://github.com/arsen-ask-lx/Agent_Quality_Kit/actions/workflows/ci.yml)
|
|
7
7
|
[](LICENSE)
|
|
8
|
+
[](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
|
|
@@ -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
|
package/kit/gates/_skip.sh
CHANGED
|
@@ -47,7 +47,10 @@ own_samples_filter() {
|
|
|
47
47
|
case "$DIR0" in
|
|
48
48
|
# Цель проверки — сам образец: тогда прятать его нельзя, иначе гейт «пройдёт» на красном.
|
|
49
49
|
*/red|*/red/|*/green|*/green/) SAMPLES="cat" ;;
|
|
50
|
-
|
|
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
|
+
"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 `[](${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 };
|
package/tool/commands/gates.mjs
CHANGED
|
@@ -34,15 +34,30 @@ async function installGate(slug, man, facts) {
|
|
|
34
34
|
|
|
35
35
|
// Общий список исключений едет вместе с проверкой: без него она читает окружение и
|
|
36
36
|
// зависимости, и человек получает тысячу чужих нарушений вместо сотни своих.
|
|
37
|
-
const
|
|
38
|
-
|
|
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
|
|
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: ${n} gates, all green. 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}`,
|
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}`);
|
package/tool/selfcheck/smoke.sh
CHANGED
|
@@ -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[](https://x)\n' > "$BDIR/README.md"
|
|
689
|
+
( cd "$BDIR" && node "$CLI" badge --check >/dev/null 2>&1 ); CHK_LIE=$?
|
|
690
|
+
printf '# проект\n\n[](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
|
package/tool/selfcheck/units.mjs
CHANGED
|
@@ -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
|
+
});
|