agent-quality-kit 0.4.1 → 0.5.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 +101 -12
- package/README.ru.md +101 -11
- package/kit/gates/README.md +15 -0
- package/kit/gates/_native.sh +18 -2
- package/kit/gates/_skip.sh +18 -0
- package/kit/gates/color-from-token/README.md +52 -0
- package/kit/gates/color-from-token/check.sh +69 -0
- package/kit/gates/color-from-token/gate.yml +15 -0
- package/kit/gates/color-from-token/green/Button.tsx +4 -0
- package/kit/gates/color-from-token/green/Panel.vue +4 -0
- package/kit/gates/color-from-token/green/card.css +5 -0
- package/kit/gates/color-from-token/green/notes.md +2 -0
- package/kit/gates/color-from-token/green/tokens.css +7 -0
- package/kit/gates/color-from-token/red/Button.tsx +4 -0
- package/kit/gates/color-from-token/red/Panel.vue +4 -0
- package/kit/gates/color-from-token/red/card.css +5 -0
- package/kit/gates/commit-explains-itself/check.sh +25 -2
- package/kit/gates/duplicate-code/check.sh +13 -4
- package/kit/gates/duplicate-code/gate.yml +11 -2
- package/kit/gates/gate-has-samples/check.sh +9 -3
- package/kit/gates/gates-are-runnable/check.sh +7 -1
- package/kit/gates/gates-run-in-ci/check.sh +7 -1
- package/kit/gates/lesson-has-outcome/check.sh +6 -1
- package/kit/ratchet/ratchet.sh +9 -2
- package/llms.txt +57 -0
- package/package.json +8 -3
- package/tool/commands/doctor.mjs +62 -7
- package/tool/commands/gates.mjs +9 -2
- package/tool/commands/project.mjs +7 -4
- package/tool/commands/report.mjs +2 -2
- package/tool/i18n/en.mjs +38 -0
- package/tool/i18n/ru.mjs +43 -0
- package/tool/lib/baseline.mjs +87 -0
- package/tool/lib/core.mjs +10 -2
- package/tool/lib/manifest.mjs +33 -2
- package/tool/lib/repo.mjs +7 -0
- package/tool/selfcheck/gates.sh +9 -2
- package/tool/selfcheck/mutation.sh +95 -0
- package/tool/selfcheck/smoke.sh +216 -8
- package/tool/selfcheck/syntax.sh +9 -1
- package/tool/selfcheck/units.mjs +76 -1
|
@@ -19,7 +19,8 @@ find "$DIR" $(skip_find "$DIR") $TESTS -type f \
|
|
|
19
19
|
! -name 'test_*' ! -name '*_test.*' ! -name '*.test.*' ! -name '*.spec.*' \
|
|
20
20
|
-print 2>/dev/null | only_code | own_samples_filter "$DIR" \
|
|
21
21
|
| while IFS= read -r F; do is_generated "$F" || printf '%s\n' "$F"; done \
|
|
22
|
-
|
|
|
22
|
+
| LC_ALL=C sort \
|
|
23
|
+
| xargs -r env LC_ALL=C awk -v WIN="$WIN" '
|
|
23
24
|
FNR == 1 { n = 0; delete buf }
|
|
24
25
|
{
|
|
25
26
|
line = $0
|
|
@@ -37,11 +38,19 @@ find "$DIR" $(skip_find "$DIR") $TESTS -type f \
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
' 2>/dev/null | sort -u \
|
|
40
|
-
| awk -F' и |: ' '
|
|
41
|
+
| LC_ALL=C awk -F' и |: ' '
|
|
41
42
|
# Один повторённый кусок даёт столько сообщений, на сколько окон он делится: восемь
|
|
42
43
|
# строк — восемь почти одинаковых строк отчёта. Схлопываем в одну на пару файлов.
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
#
|
|
45
|
+
# Пара упорядочивается лексикографически, а НЕ в порядке обхода. Порядок, в котором
|
|
46
|
+
# find отдаёт файлы, свой на каждой системе (здесь — по хешу имени), и реестр, снятый
|
|
47
|
+
# на одной машине, краснел в конвейере целиком: те же дубли читались как новые, а
|
|
48
|
+
# храповик объявлял их исправленными и вычёркивал. Одной сортировки списка файлов мало:
|
|
49
|
+
# когда кусок лежит в трёх файлах, «первым» становится тот, кого раньше отдал обход.
|
|
50
|
+
{ split($1, a, ":"); split($2, b, ":")
|
|
51
|
+
if (a[1] <= b[1]) { pair = a[1] " и " b[1]; loc = $1 " и " $2 }
|
|
52
|
+
else { pair = b[1] " и " a[1]; loc = $2 " и " $1 }
|
|
53
|
+
if (!(pair in seen)) { seen[pair] = loc }
|
|
45
54
|
cnt[pair]++ }
|
|
46
55
|
END { for (p in seen) print seen[p] ": одинаковый кусок" (cnt[p] > 1 ? " (окон: " cnt[p] ")" : "") }
|
|
47
56
|
' | sort > /tmp/.dup.$$
|
|
@@ -7,7 +7,16 @@ trigger:
|
|
|
7
7
|
recipes:
|
|
8
8
|
any: bash {gate}/check.sh {dir}
|
|
9
9
|
# jscpd умеет и Python, и TypeScript одним прогоном и считает похожесть, а не совпадение.
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
#
|
|
11
|
+
# --format обязателен: без него jscpd считает клонами разметку, JSON и текст. Замер на живом
|
|
12
|
+
# проекте (Django + React, 2750 файлов кода): 1846 клонов, из них 435 — markdown, json и
|
|
13
|
+
# text, то есть 23.6% находок не про код вовсе. Переносимая проверка смотрит только в
|
|
14
|
+
# расширения кода (CODE_EXT в _skip.sh), и родная обязана мерить то же самое: иначе один
|
|
15
|
+
# гейт означает разное на разных машинах.
|
|
16
|
+
#
|
|
17
|
+
# «c#» в списке нет намеренно: разбор манифеста режет строку по «#», и рецепт обрывался бы
|
|
18
|
+
# на «c». Обёртку _native.sh не пишем — её добавляет сама программа при установке гейта.
|
|
19
|
+
javascript: npx --yes jscpd@5 --min-lines 8 --threshold 1 --format "javascript,jsx,typescript,tsx,vue,python,go,ruby,java,php,rust,kotlin,swift,scala" {dir}
|
|
20
|
+
typescript: npx --yes jscpd@5 --min-lines 8 --threshold 1 --format "javascript,jsx,typescript,tsx,vue,python,go,ruby,java,php,rust,kotlin,swift,scala" {dir}
|
|
12
21
|
|
|
13
22
|
proof: incidents/README.md — «2026-08-25 разбор 1069 коммитов»: число моделей захардкожено в четырёх местах четырьмя разными значениями
|
|
@@ -15,9 +15,15 @@
|
|
|
15
15
|
DIR="${1:-.}"
|
|
16
16
|
MAN="$DIR/.aqk.yml"
|
|
17
17
|
[ -f "$MAN" ] || { echo "нет .aqk.yml — проверять нечего"; exit 0; }
|
|
18
|
+
# Перевод строк из windows-чекаута снимается ДО разбора. Жадный захват в sed («[^"#]*») и
|
|
19
|
+
# якорь конца строки («s/"$//») проглатывают \r: путь получался с кавычкой на хвосте, а
|
|
20
|
+
# красный образец переставал краснеть. Найдено мутационной проверкой
|
|
21
|
+
# (tool/selfcheck/mutation.sh), а не на Windows-машине — её у нас до сих пор нет.
|
|
22
|
+
MANTEXT="$(tr -d '\r' < "$MAN")"
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
|
|
25
|
+
SAMPLES=$(printf '%s\n' "$MANTEXT" | sed -n 's/^samples:[[:space:]]*"\{0,1\}\([^"#]*\)"\{0,1\}[[:space:]]*$/\1/p' | head -1)
|
|
26
|
+
KEYS=$(printf '%s\n' "$MANTEXT" | awk '/^gates:/{g=1;next} /^[A-Za-z]/{g=0} g && /^[[:space:]]+[A-Za-z0-9_-]+:/{sub(/:.*/,"");gsub(/[[:space:]]/,"");print}')
|
|
21
27
|
|
|
22
28
|
[ -z "$KEYS" ] && { echo "гейтов не объявлено — проверять нечего"; exit 0; }
|
|
23
29
|
|
|
@@ -32,7 +38,7 @@ fi
|
|
|
32
38
|
BAD=0
|
|
33
39
|
for K in $KEYS; do
|
|
34
40
|
# Команда записи целиком — по ней видно, сканер это или собственный прогон проекта.
|
|
35
|
-
CMD=$(awk -v k="$K" '/^gates:/{g=1;next} /^[A-Za-z]/{g=0} g && $0 ~ "^[[:space:]]+" k ":" {sub(/^[[:space:]]*[A-Za-z0-9_-]+:[[:space:]]*/,"");gsub(/^"|"$/,"");print;exit}'
|
|
41
|
+
CMD=$(printf '%s\n' "$MANTEXT" | awk -v k="$K" '/^gates:/{g=1;next} /^[A-Za-z]/{g=0} g && $0 ~ "^[[:space:]]+" k ":" {sub(/^[[:space:]]*[A-Za-z0-9_-]+:[[:space:]]*/,"");gsub(/^"|"$/,"");print;exit}')
|
|
36
42
|
case "$CMD" in
|
|
37
43
|
*"$SAMPLES/"*) ;; # сканер из каталога — образцы обязательны
|
|
38
44
|
*) continue ;; # свой прогон — арбитр внутри него
|
|
@@ -5,9 +5,15 @@
|
|
|
5
5
|
DIR="${1:-.}"
|
|
6
6
|
MAN="$DIR/.aqk.yml"
|
|
7
7
|
[ -f "$MAN" ] || { echo "нет .aqk.yml — проверять нечего"; exit 0; }
|
|
8
|
+
# Перевод строк из windows-чекаута снимается ДО разбора. Жадный захват в sed («[^"#]*») и
|
|
9
|
+
# якорь конца строки («s/"$//») проглатывают \r: путь получался с кавычкой на хвосте, а
|
|
10
|
+
# красный образец переставал краснеть. Найдено мутационной проверкой
|
|
11
|
+
# (tool/selfcheck/mutation.sh), а не на Windows-машине — её у нас до сих пор нет.
|
|
12
|
+
MANTEXT="$(tr -d '\r' < "$MAN")"
|
|
13
|
+
|
|
8
14
|
|
|
9
15
|
# строки вида ` имя: "команда"` внутри блока gates:
|
|
10
|
-
LINES=$(awk '/^gates:/{g=1;next} /^[A-Za-z]/{g=0} g && /^[[:space:]]+[A-Za-z0-9_-]+:/{print}'
|
|
16
|
+
LINES=$(printf '%s\n' "$MANTEXT" | awk '/^gates:/{g=1;next} /^[A-Za-z]/{g=0} g && /^[[:space:]]+[A-Za-z0-9_-]+:/{print}')
|
|
11
17
|
[ -z "$LINES" ] && { echo "гейтов не объявлено"; exit 0; }
|
|
12
18
|
|
|
13
19
|
BAD=0
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
DIR="${1:-.}"
|
|
5
5
|
MAN="$DIR/.aqk.yml"
|
|
6
6
|
[ -f "$MAN" ] || { echo "нет .aqk.yml — проверять нечего"; exit 0; }
|
|
7
|
+
# Перевод строк из windows-чекаута снимается ДО разбора. Жадный захват в sed («[^"#]*») и
|
|
8
|
+
# якорь конца строки («s/"$//») проглатывают \r: путь получался с кавычкой на хвосте, а
|
|
9
|
+
# красный образец переставал краснеть. Найдено мутационной проверкой
|
|
10
|
+
# (tool/selfcheck/mutation.sh), а не на Windows-машине — её у нас до сих пор нет.
|
|
11
|
+
MANTEXT="$(tr -d '\r' < "$MAN")"
|
|
12
|
+
|
|
7
13
|
|
|
8
14
|
CI=$(find "$DIR/.github/workflows" "$DIR/.gitlab-ci.yml" "$DIR/.circleci" "$DIR/Jenkinsfile" \
|
|
9
15
|
-type f 2>/dev/null)
|
|
@@ -18,7 +24,7 @@ if grep -qE 'doctor[[:space:]]+--run|--run[[:space:]]+.*doctor' $CI 2>/dev/null;
|
|
|
18
24
|
exit 0
|
|
19
25
|
fi
|
|
20
26
|
|
|
21
|
-
NAMES=$(awk '/^gates:/{g=1;next} /^[A-Za-z]/{g=0} g && /^[[:space:]]+[A-Za-z0-9_-]+:/{print}'
|
|
27
|
+
NAMES=$(printf '%s\n' "$MANTEXT" | awk '/^gates:/{g=1;next} /^[A-Za-z]/{g=0} g && /^[[:space:]]+[A-Za-z0-9_-]+:/{print}')
|
|
22
28
|
[ -z "$NAMES" ] && { echo "гейтов не объявлено"; exit 0; }
|
|
23
29
|
|
|
24
30
|
BAD=0
|
|
@@ -9,7 +9,12 @@ DIR="${1:-.}"
|
|
|
9
9
|
MAN="$DIR/.aqk.yml"
|
|
10
10
|
|
|
11
11
|
# Где журнал — говорит манифест. Своего мнения у проверки быть не должно: путь у каждого свой.
|
|
12
|
-
|
|
12
|
+
# Перевод строк из windows-чекаута снимается ДО разбора. Жадный захват в sed («[^"#]*») и
|
|
13
|
+
# якорь конца строки («s/"$//») проглатывают \r: путь получался с кавычкой на хвосте, а
|
|
14
|
+
# красный образец переставал краснеть. Найдено мутационной проверкой
|
|
15
|
+
# (tool/selfcheck/mutation.sh), а не на Windows-машине — её у нас до сих пор нет.
|
|
16
|
+
MANTEXT="$(tr -d '\r' < "$MAN" 2>/dev/null)"
|
|
17
|
+
LESSONS=$(printf '%s\n' "$MANTEXT" | sed -n 's/^lessons:[[:space:]]*"\{0,1\}\([^"#]*\)"\{0,1\}[[:space:]]*$/\1/p' 2>/dev/null | head -1)
|
|
13
18
|
case "$LESSONS" in
|
|
14
19
|
"" ) echo "в .aqk.yml не указано поле lessons — журнала нет"; exit 0 ;;
|
|
15
20
|
http*://* ) echo "журнал вынесен наружу ($LESSONS) — здесь не проверить"; exit 0 ;;
|
package/kit/ratchet/ratchet.sh
CHANGED
|
@@ -17,7 +17,12 @@ REG="${1:-}"; shift || true
|
|
|
17
17
|
|
|
18
18
|
# Ключ нарушения обязан переживать правку соседних строк, иначе сдвиг на строку читается
|
|
19
19
|
# как новое нарушение. Поэтому номер строки из ключа убирается.
|
|
20
|
-
|
|
20
|
+
#
|
|
21
|
+
# Два шаблона, а не один: у гейта duplicate-code строка несёт ДВА места — «a:171 и b:188: …»,
|
|
22
|
+
# и за номером первого идёт не двоеточие, а пробел. Шаблон с двоеточием убирал номер только
|
|
23
|
+
# у второго файла пары, и сдвиг кода в первом читался как новое нарушение — ровно то, от чего
|
|
24
|
+
# храповик защищает.
|
|
25
|
+
keys() { grep -E '^[^[:space:]].*:' | sed -E 's/:[0-9]+:/:/g; s/:[0-9]+( |$)/:\1/g' | LC_ALL=C sort -u; }
|
|
21
26
|
|
|
22
27
|
OUT="$("$@" 2>&1)"
|
|
23
28
|
NOW="$(printf '%s\n' "$OUT" | keys)"
|
|
@@ -26,7 +31,9 @@ if [ ! -f "$REG" ]; then
|
|
|
26
31
|
echo "нет реестра $REG — сначала: aqk ratchet <гейт>"
|
|
27
32
|
exit 2
|
|
28
33
|
fi
|
|
29
|
-
|
|
34
|
+
# Та же сортировка, что в keys(): comm сравнивает построчно и требует, чтобы оба входа были
|
|
35
|
+
# упорядочены одинаково. Разная локаль у двух сторон — это тихо разъехавшееся сравнение.
|
|
36
|
+
WAS="$(grep -vE '^\s*(#|$)' "$REG" | LC_ALL=C sort -u)"
|
|
30
37
|
|
|
31
38
|
NEW="$(comm -23 <(printf '%s\n' "$NOW") <(printf '%s\n' "$WAS"))"
|
|
32
39
|
GONE="$(comm -13 <(printf '%s\n' "$NOW") <(printf '%s\n' "$WAS"))"
|
package/llms.txt
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# AQK — Agent Quality Kit
|
|
2
|
+
|
|
3
|
+
> A standard and a CLI that check whether a repository is ready to have its code written by AI
|
|
4
|
+
> coding agents. Every rule the project promises to follow becomes a command with an exit code,
|
|
5
|
+
> so a machine holds the promise instead of somebody's attention. Reports a level from AQK-0 to
|
|
6
|
+
> AQK-3, computed by a run — never by a questionnaire and never by a model's opinion.
|
|
7
|
+
|
|
8
|
+
Vendor-neutral: works with any coding agent (Claude Code, Codex, Cursor, Gemini CLI, GitHub
|
|
9
|
+
Copilot, Windsurf, Aider, OpenCode) and with no AI at all. It calls no vendor API and needs no
|
|
10
|
+
key. Stack-neutral: portable checks are plain `sh`; where the project already has a native tool
|
|
11
|
+
(ruff, eslint, knip, jscpd) the check uses it instead and says so when it falls back.
|
|
12
|
+
|
|
13
|
+
Zero runtime dependencies. Node 18+ and an `sh` shell. MIT.
|
|
14
|
+
|
|
15
|
+
## Use it
|
|
16
|
+
|
|
17
|
+
- Check an existing repository: `npx agent-quality-kit doctor` (read-only: writes nothing, sends
|
|
18
|
+
nothing)
|
|
19
|
+
- Start a new one: `npx agent-quality-kit start`
|
|
20
|
+
- Install one guard from the catalogue: `npx agent-quality-kit add <name>`
|
|
21
|
+
- Check the minimum a project needs before agents can be handed the work:
|
|
22
|
+
`npx agent-quality-kit doctor --baseline` (14 of 50 points confirmed by a run, ecosystem-neutral;
|
|
23
|
+
the other 36 are named as a number, not hidden)
|
|
24
|
+
- Fail a pipeline below a level or on a failed gate: `npx agent-quality-kit doctor --run --min 1`
|
|
25
|
+
- Exit codes: 0 pass, 1 below the level or a gate failed
|
|
26
|
+
- As a pre-commit hook: `repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit` with
|
|
27
|
+
`id: aqk` (blocking), `aqk-doctor` (read-only) or `aqk-baseline`. pre-commit installs the
|
|
28
|
+
package itself; there are no dependencies to pull in.
|
|
29
|
+
- As a GitHub Action: `uses: arsen-ask-lx/Agent_Quality_Kit@v0.4.2` with `min: 1`
|
|
30
|
+
(https://github.com/marketplace/actions/agent-quality-kit-aqk)
|
|
31
|
+
|
|
32
|
+
## What makes it different
|
|
33
|
+
|
|
34
|
+
- The level is computed by running what the repository declares — not from a self-assessment
|
|
35
|
+
questionnaire, not from the presence of a config file, not from a language model's judgement.
|
|
36
|
+
- Every guard must ship a red and a green sample, and a machine checks that the guard goes red on
|
|
37
|
+
the red one and stays quiet on the green one. A check that cannot fail is indistinguishable
|
|
38
|
+
from a check that works; this is the only place that requirement is enforced.
|
|
39
|
+
- An entry enters the catalogue only when it names a real failure it caught, recorded in the
|
|
40
|
+
bruise journal.
|
|
41
|
+
|
|
42
|
+
## Files it reads and writes
|
|
43
|
+
|
|
44
|
+
- `AGENTS.md` — what the agent reads first (the entry point; `CLAUDE.md` and others work too)
|
|
45
|
+
- `.aqk.yml` — the manifest: entry, rules, gates as commands, samples, ratchets, lessons
|
|
46
|
+
- `.aqkignore` — paths the scanning checks must not read (brought-in code, vendored, generated)
|
|
47
|
+
|
|
48
|
+
## Documentation
|
|
49
|
+
|
|
50
|
+
- [README](README.md): what it is, how to install, the catalogue, the badge, CI
|
|
51
|
+
- [SPEC.md](SPEC.md): the standard itself — levels, manifest, how an entry is accepted
|
|
52
|
+
- [Project baseline](kit/docs/ai/project-baseline.md): what a project needs before agents can be
|
|
53
|
+
handed the work, in plain words, independent of language and tooling
|
|
54
|
+
- [The gate catalogue](kit/gates/README.md): what a gate is and the bar an entry must clear
|
|
55
|
+
- [The bruise journal](incidents/README.md): every defect found, and what was made of it
|
|
56
|
+
- [CONTRIBUTING.md](CONTRIBUTING.md): how to bring a gate
|
|
57
|
+
- [SECURITY.md](SECURITY.md): reporting a vulnerability, and the trust model of the manifest
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-quality-kit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Turns the rules an agent is supposed to follow into commands with exit codes, and reports which of them actually run. Zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"aqk": "tool/program.mjs"
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"kit",
|
|
12
12
|
"README.md",
|
|
13
13
|
"README.ru.md",
|
|
14
|
+
"llms.txt",
|
|
14
15
|
"LICENSE"
|
|
15
16
|
],
|
|
16
17
|
"engines": {
|
|
@@ -34,7 +35,11 @@
|
|
|
34
35
|
"codex",
|
|
35
36
|
"cursor",
|
|
36
37
|
"harness",
|
|
37
|
-
"repository-standard"
|
|
38
|
+
"repository-standard",
|
|
39
|
+
"agents-md",
|
|
40
|
+
"agents",
|
|
41
|
+
"github-action",
|
|
42
|
+
"npx"
|
|
38
43
|
],
|
|
39
44
|
"knip": {
|
|
40
45
|
"entry": [
|
package/tool/commands/doctor.mjs
CHANGED
|
@@ -4,10 +4,46 @@ import { readFile, mkdir, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import { CWD, PKG_ROOT, TARGET_DIR, SELF, c, exists } from "../lib/core.mjs";
|
|
7
|
-
import { readManifest, assessLevel } from "../lib/manifest.mjs";
|
|
7
|
+
import { readManifest, assessLevel, unknownKeys, KNOWN_KEYS } from "../lib/manifest.mjs";
|
|
8
8
|
import { detectFacts, readCatalog, triggerVerdict, recipeFor } from "../lib/repo.mjs";
|
|
9
|
+
import { assessBaseline, DEP_FILES, BASELINE_TOTAL } from "../lib/baseline.mjs";
|
|
9
10
|
import { L } from "../i18n/index.mjs";
|
|
10
11
|
|
|
12
|
+
// Обязательный минимум проекта — прогоном, а не по памяти. До сих пор это было единственное
|
|
13
|
+
// место, где комплект просил верить на слово, что человек прочитал методичку и сверился.
|
|
14
|
+
async function reportBaseline(man, facts) {
|
|
15
|
+
const { readdir, readFile } = await import("node:fs/promises");
|
|
16
|
+
let files = [];
|
|
17
|
+
try {
|
|
18
|
+
files = (await readdir(CWD, { withFileTypes: true })).map((d) => d.name);
|
|
19
|
+
} catch { /* пустой список честнее выдуманного: ни один пункт не подтвердится */ }
|
|
20
|
+
|
|
21
|
+
// Файлы зависимостей читаются целиком и склеиваются: трекер ошибок объявляют по-разному в
|
|
22
|
+
// каждой экосистеме, а искать его надо одинаково.
|
|
23
|
+
let depsText = "";
|
|
24
|
+
for (const f of DEP_FILES) {
|
|
25
|
+
if (!files.some((n) => n.toLowerCase() === f)) continue;
|
|
26
|
+
try { depsText += (await readFile(join(CWD, f), "utf8")).toLowerCase() + "\n"; } catch { /* нечитаемый файл — просто не признак */ }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const rows = assessBaseline({ files, gateKeys: facts.gateKeys, facts, manifest: man || {}, depsText });
|
|
30
|
+
const okCount = rows.filter((r) => r.ok).length;
|
|
31
|
+
|
|
32
|
+
console.log(c.bold(`\n ${L.baseline.heading}\n`));
|
|
33
|
+
console.log(c.dim(` ${L.baseline.intro(rows.length, BASELINE_TOTAL)}`));
|
|
34
|
+
console.log(c.dim(` ${L.baseline.caveat}\n`));
|
|
35
|
+
for (const r of rows) {
|
|
36
|
+
const mark = r.ok ? c.green("✔") : c.yellow("✘");
|
|
37
|
+
const title = L.baseline.titles[r.key] || r.key;
|
|
38
|
+
console.log(` ${mark} ${String(r.n).padStart(2)}. ${title}`);
|
|
39
|
+
console.log(c.dim(` ${r.ok ? L.baseline.by(r.by) : L.baseline.none}`));
|
|
40
|
+
}
|
|
41
|
+
console.log(
|
|
42
|
+
"\n " + (okCount === rows.length ? c.green(`${okCount}/${rows.length}`) : c.yellow(`${okCount}/${rows.length}`)) +
|
|
43
|
+
c.dim(` · ${L.baseline.eyes(BASELINE_TOTAL - rows.length, "kit/docs/ai/project-baseline.md")}\n`)
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
11
47
|
async function reportCatalog(man, facts) {
|
|
12
48
|
const catalog = await readCatalog();
|
|
13
49
|
if (!catalog.length) return;
|
|
@@ -158,6 +194,15 @@ async function cmdDoctor() {
|
|
|
158
194
|
}
|
|
159
195
|
|
|
160
196
|
const man = await readManifest();
|
|
197
|
+
|
|
198
|
+
// Опечатка в имени поля означала «поля нет»: вердикт выдавался неверный, а причина молчала.
|
|
199
|
+
// Называем поле и говорим, какие бывают — иначе человек ищет ошибку в проекте, а она в файле.
|
|
200
|
+
const unknown = unknownKeys(man);
|
|
201
|
+
if (unknown.length) {
|
|
202
|
+
console.log(c.yellow(`\n ${L.doctor.manifestUnknown(unknown)}`));
|
|
203
|
+
console.log(c.dim(` ${L.doctor.manifestKnown(KNOWN_KEYS)}\n`));
|
|
204
|
+
}
|
|
205
|
+
|
|
161
206
|
const { reached, steps } = await assessLevel(man);
|
|
162
207
|
|
|
163
208
|
console.log(c.bold(`\n ${L.doctor.levelHeading}\n`));
|
|
@@ -190,15 +235,21 @@ async function cmdDoctor() {
|
|
|
190
235
|
}
|
|
191
236
|
|
|
192
237
|
const facts = await detectFacts(man);
|
|
238
|
+
if (process.argv.includes("--baseline")) {
|
|
239
|
+
await reportBaseline(man, facts);
|
|
240
|
+
process.exit(0);
|
|
241
|
+
}
|
|
193
242
|
await reportCatalog(man, facts);
|
|
194
243
|
|
|
195
244
|
// «Объявлен» ≠ «работает». Без --run говорим это вслух, а не молчим.
|
|
196
245
|
const wantRun = process.argv.includes("--run");
|
|
197
246
|
const gates = declaredGates(man);
|
|
198
247
|
let gateFailed = 0;
|
|
248
|
+
let failedNames = [];
|
|
199
249
|
if (wantRun) {
|
|
200
250
|
const run = runGates(man);
|
|
201
251
|
gateFailed = run.failed;
|
|
252
|
+
failedNames = run.results.filter((r) => !r.ok).map((r) => r.name);
|
|
202
253
|
await writeRunReport({ version, reached, results: run.results });
|
|
203
254
|
} else if (gates.length) {
|
|
204
255
|
console.log(
|
|
@@ -211,12 +262,16 @@ async function cmdDoctor() {
|
|
|
211
262
|
const minIdx = process.argv.indexOf("--min");
|
|
212
263
|
const min = minIdx > -1 ? Number(process.argv[minIdx + 1]) : null;
|
|
213
264
|
if (min !== null) {
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
265
|
+
const levelOk = reached >= min;
|
|
266
|
+
const pass = levelOk && gateFailed === 0;
|
|
267
|
+
// Две разные развилки, и сообщение обязано их различать. «Порог не пройден: сейчас AQK-1»
|
|
268
|
+
// при пороге AQK-1 противоречит само себе и отправляет чинить манифест, когда падал гейт.
|
|
269
|
+
const now = reached < 0 ? L.doctor.levelNone : reached;
|
|
270
|
+
let line;
|
|
271
|
+
if (pass) line = c.green(` ${L.doctor.thresholdPass(min)}\n`);
|
|
272
|
+
else if (!levelOk) line = c.red(` ${L.doctor.thresholdFail(min, now)}\n`);
|
|
273
|
+
else line = c.red(` ${L.doctor.thresholdGateFail(min, now, failedNames)}\n`);
|
|
274
|
+
console.log(line);
|
|
220
275
|
process.exit(pass ? 0 : 1);
|
|
221
276
|
}
|
|
222
277
|
process.exit(missing || reached < 0 || gateFailed ? 1 : 0);
|
package/tool/commands/gates.mjs
CHANGED
|
@@ -44,7 +44,13 @@ async function installGate(slug, man, facts) {
|
|
|
44
44
|
let cmd = picked
|
|
45
45
|
.replace(/\{gate\}/g, `${PROJECT_GATES}/${slug}`)
|
|
46
46
|
.replace(/\{dir\}/g, ".");
|
|
47
|
-
|
|
47
|
+
// Отказ, а не смерть. `add` ставит одну запись — там смерть уместна и остаётся в вызывающем.
|
|
48
|
+
// `start` ставит пачку, и падение на одной записи оставляло проект с тремя сторожами вместо
|
|
49
|
+
// двенадцати, без единого слова про остальные девять. Найдено прогоном на Windows: там нет
|
|
50
|
+
// ни `ruff`, ни `vulture`, и установка обрывалась на записи `dead-code`, которой нужен
|
|
51
|
+
// настоящий инструмент. Отсутствие сигнала неотличимо от успеха — здесь оно было внутри
|
|
52
|
+
// самой установки.
|
|
53
|
+
if (!cmd) return { rec, cmd: null, copied, declared: false, why: null, noRecipe: true };
|
|
48
54
|
|
|
49
55
|
// Родной инструмент не знает про наши образцы и выдаёт их как находки — в любом проекте,
|
|
50
56
|
// куда поставили гейты. Заворачиваем его в общий фильтр. Переносимая проверка фильтрует
|
|
@@ -81,7 +87,8 @@ async function cmdAdd(args) {
|
|
|
81
87
|
console.log(c.dim(` ${L.add.installAnyway}\n`));
|
|
82
88
|
}
|
|
83
89
|
|
|
84
|
-
const { cmd, copied, declared, why } = await installGate(slug, man, facts);
|
|
90
|
+
const { cmd, copied, declared, why, noRecipe } = await installGate(slug, man, facts);
|
|
91
|
+
if (noRecipe) die(L.add.noRecipe(slug, [...facts.langs].join("/") || L.add.thisStack));
|
|
85
92
|
|
|
86
93
|
console.log(c.bold(`\naqk add ${slug}\n`));
|
|
87
94
|
console.log(` ${c.green("✔")} ${PROJECT_GATES}/${slug}/ ${c.dim(L.add.copied(copied.length))}`);
|
|
@@ -6,8 +6,7 @@ import { spawnSync } from "node:child_process";
|
|
|
6
6
|
import { join, dirname, relative } from "node:path";
|
|
7
7
|
import {
|
|
8
8
|
CWD, PKG_ROOT, DOCS_SRC, RULES_SRC, TARGET_DIR, MANIFEST, SELF, REPO_URL, c, exists, die,
|
|
9
|
-
copyDir, writeIfAbsent, FEEDBACK_MARK,
|
|
10
|
-
} from "../lib/core.mjs";
|
|
9
|
+
copyDir, writeIfAbsent, FEEDBACK_MARK, docPath } from "../lib/core.mjs";
|
|
11
10
|
import { AGENTS_MD, CLAUDE_MD, MANIFEST_YML } from "../lib/templates.mjs";
|
|
12
11
|
import { readManifest } from "../lib/manifest.mjs";
|
|
13
12
|
import { detectFacts, readCatalog, triggerVerdict } from "../lib/repo.mjs";
|
|
@@ -199,7 +198,7 @@ async function cmdBlob() {
|
|
|
199
198
|
await walk(dir);
|
|
200
199
|
|
|
201
200
|
for (const full of found) {
|
|
202
|
-
out += `\n\n${"=".repeat(78)}\n<!-- ${L.blob.source(
|
|
201
|
+
out += `\n\n${"=".repeat(78)}\n<!-- ${L.blob.source(docPath(PKG_ROOT, full))} -->\n${"=".repeat(78)}\n\n`;
|
|
203
202
|
// Ссылки на соседние файлы в склейке ведут в никуда: соседей рядом больше нет,
|
|
204
203
|
// все они внутри этого же текста. Оставляем подпись, снимаем разметку.
|
|
205
204
|
const body = (await readFile(full, "utf8")).replace(
|
|
@@ -268,7 +267,11 @@ async function cmdStart(args) {
|
|
|
268
267
|
if (declared.has(rec.slug)) continue;
|
|
269
268
|
const v = triggerVerdict(rec, facts0);
|
|
270
269
|
if (!v.applies) { skipped.push([rec.slug, v.why]); continue; }
|
|
271
|
-
const { cmd } = await installGate(rec.slug, man, facts0);
|
|
270
|
+
const { cmd, noRecipe } = await installGate(rec.slug, man, facts0);
|
|
271
|
+
// Записи, которой нужен инструмент, а его на машине нет, здесь не место — но и вся
|
|
272
|
+
// установка из-за неё останавливаться не должна. Причина называется вслух и попадает
|
|
273
|
+
// в тот же список пропущенного, что и записи, не подошедшие по триггеру.
|
|
274
|
+
if (noRecipe) { skipped.push([rec.slug, L.start.noRecipeHere]); declared.add(rec.slug); continue; }
|
|
272
275
|
put.push([rec.slug, cmd, rec.intent || ""]);
|
|
273
276
|
declared.add(rec.slug);
|
|
274
277
|
added++;
|
package/tool/commands/report.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { mkdir, writeFile, readdir, readFile } from "node:fs/promises";
|
|
17
17
|
import { join, relative } from "node:path";
|
|
18
|
-
import { CWD, TARGET_DIR, SELF, c, exists } from "../lib/core.mjs";
|
|
18
|
+
import { CWD, TARGET_DIR, SELF, c, exists, docPath } from "../lib/core.mjs";
|
|
19
19
|
import { readManifest, assessLevel } from "../lib/manifest.mjs";
|
|
20
20
|
import { detectFacts, readCatalog, triggerVerdict, whichSync } from "../lib/repo.mjs";
|
|
21
21
|
import { runGates, declaredGates } from "./doctor.mjs";
|
|
@@ -59,7 +59,7 @@ async function findDoc(name) {
|
|
|
59
59
|
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
60
60
|
const full = join(dir, e.name);
|
|
61
61
|
if (e.isDirectory()) { const hit = await walk(full); if (hit) return hit; }
|
|
62
|
-
else if (e.name === name) return
|
|
62
|
+
else if (e.name === name) return docPath(CWD, full);
|
|
63
63
|
}
|
|
64
64
|
return null;
|
|
65
65
|
};
|
package/tool/i18n/en.mjs
CHANGED
|
@@ -73,8 +73,44 @@ export const en = {
|
|
|
73
73
|
declaredNotRun: (n) => `${n} gates declared, but never run.`,
|
|
74
74
|
declaredNotRunWhy: (cmd) => ` "declared" and "works" are different claims: ${cmd}`,
|
|
75
75
|
|
|
76
|
+
manifestUnknown: (keys) =>
|
|
77
|
+
`The manifest has fields the standard does not know: ${keys.join(", ")}. Looks like a typo — ` +
|
|
78
|
+
`such a field is silently read as absent, and the verdict comes out wrong.`,
|
|
79
|
+
manifestKnown: (keys) => `Manifest fields: ${keys.join(", ")}`,
|
|
76
80
|
thresholdPass: (min) => `Threshold AQK-${min} passed.`,
|
|
77
81
|
thresholdFail: (min, now) => `Threshold AQK-${min} NOT passed: currently AQK-${now}.`,
|
|
82
|
+
thresholdGateFail: (min, now, names) =>
|
|
83
|
+
`Threshold AQK-${min} passed (currently AQK-${now}), but a gate failed: ${names.join(", ")}.`,
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
baseline: {
|
|
87
|
+
heading: "The minimum a project needs",
|
|
88
|
+
intro: (checked, total) =>
|
|
89
|
+
`a machine confirms ${checked} of ${total} points; the rest are for your eyes, in the guide`,
|
|
90
|
+
eyes: (n, path) => `${n} points a machine cannot check — they live in ${path}`,
|
|
91
|
+
caveat: "presence is what gets checked, not whether it works: \"a linter is configured\" and \"a linter catches things\" are different claims",
|
|
92
|
+
by: (b) =>
|
|
93
|
+
"proven by: " +
|
|
94
|
+
({ gate: `gate ${b.value}`, fact: `repository scan: ${b.value}`,
|
|
95
|
+
manifest: `field ${b.value} in the manifest`, dep: `dependency ${b.value}`,
|
|
96
|
+
file: b.value }[b.kind] || b.value),
|
|
97
|
+
none: "no conventional marker — check by eye, it may be done another way",
|
|
98
|
+
titles: {
|
|
99
|
+
oneCommand: "one command brings the whole project up",
|
|
100
|
+
lockfile: "exact versions pinned in a lockfile",
|
|
101
|
+
sameEnv: "the environment is the same for everyone and in CI",
|
|
102
|
+
formatter: "formatting is uniform and applied automatically",
|
|
103
|
+
linter: "a linter is configured",
|
|
104
|
+
types: "type checking exists",
|
|
105
|
+
secretScan: "secret scanning",
|
|
106
|
+
fileSize: "a file size limit",
|
|
107
|
+
ownInvariants: "the project's own invariants",
|
|
108
|
+
tests: "arbiters of correctness: tests exist",
|
|
109
|
+
pipeline: "a pipeline exists",
|
|
110
|
+
errorTracker: "errors are collected separately from logs",
|
|
111
|
+
machineReadable: "the project is machine-readable",
|
|
112
|
+
rulesInRepo: "rules live in the repository and are versioned",
|
|
113
|
+
},
|
|
78
114
|
},
|
|
79
115
|
|
|
80
116
|
trigger: {
|
|
@@ -91,6 +127,7 @@ export const en = {
|
|
|
91
127
|
has_deps: ["no dependency file in sight", "dependencies are declared"],
|
|
92
128
|
has_tests: ["no tests in sight", "tests exist"],
|
|
93
129
|
has_env: ["no environment file", "an environment file exists"],
|
|
130
|
+
has_ui: ["no stylesheets or UI components in sight", "a UI exists: stylesheets or components"],
|
|
94
131
|
},
|
|
95
132
|
},
|
|
96
133
|
|
|
@@ -299,6 +336,7 @@ export const en = {
|
|
|
299
336
|
},
|
|
300
337
|
|
|
301
338
|
start: {
|
|
339
|
+
noRecipeHere: "needs a tool that is not on this machine",
|
|
302
340
|
initFailed: (cmd) => `Could not lay out the kit. Start with ${cmd}`,
|
|
303
341
|
tooManyFiles: (n) => `This repository already has ${n} code files — that is a different scenario.`,
|
|
304
342
|
useDoctor: (cmd) => `${cmd} will inspect what is here and split the entries into three lists:`,
|
package/tool/i18n/ru.mjs
CHANGED
|
@@ -74,8 +74,49 @@ export const ru = {
|
|
|
74
74
|
declaredNotRun: (n) => `${n} гейтов объявлено, но не запускалось.`,
|
|
75
75
|
declaredNotRunWhy: (cmd) => ` «Объявлен» и «работает» — разные утверждения: ${cmd}`,
|
|
76
76
|
|
|
77
|
+
manifestUnknown: (keys) =>
|
|
78
|
+
`В манифесте поля, которых стандарт не знает: ${keys.join(", ")}. Похоже на опечатку — ` +
|
|
79
|
+
`такое поле молча читается как отсутствующее, и вердикт выходит неверным.`,
|
|
80
|
+
manifestKnown: (keys) => `Поля манифеста: ${keys.join(", ")}`,
|
|
77
81
|
thresholdPass: (min) => `Порог AQK-${min} пройден.`,
|
|
78
82
|
thresholdFail: (min, now) => `Порог AQK-${min} НЕ пройден: сейчас AQK-${now}.`,
|
|
83
|
+
// Ступень взята, но прогон красный — это другое утверждение, и виновника называем сразу:
|
|
84
|
+
// иначе его ищут глазами выше по логу конвейера.
|
|
85
|
+
thresholdGateFail: (min, now, names) =>
|
|
86
|
+
`Порог AQK-${min} пройден (сейчас AQK-${now}), но упал гейт: ${names.join(", ")}.`,
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
baseline: {
|
|
90
|
+
heading: "Обязательный минимум проекта",
|
|
91
|
+
intro: (checked, total) =>
|
|
92
|
+
`машина подтверждает ${checked} пунктов из ${total}; остальные — глазами, по методичке`,
|
|
93
|
+
eyes: (n, path) => `${n} пунктов машина проверить не может — они в ${path}`,
|
|
94
|
+
caveat: "проверяется НАЛИЧИЕ признака, а не то, что он работает: «линтер настроен» и «линтер ловит» — разные утверждения",
|
|
95
|
+
by: (b) =>
|
|
96
|
+
"подтверждено: " +
|
|
97
|
+
({ gate: `гейт ${b.value}`, fact: `осмотр репозитория: ${b.value}`,
|
|
98
|
+
manifest: `поле ${b.value} в манифесте`, dep: `зависимость ${b.value}`,
|
|
99
|
+
file: b.value }[b.kind] || b.value),
|
|
100
|
+
// «Не найдено» и «нет» — разные утверждения. Признак ищется по общепринятым именам; проект
|
|
101
|
+
// может делать то же самое своим способом, и тогда пункт остаётся человеку, а не считается
|
|
102
|
+
// проваленным. Иначе прибор врёт на первом же нестандартном проекте — на нас самих и врал.
|
|
103
|
+
none: "общепринятого признака нет — проверь глазами, возможно сделано иначе",
|
|
104
|
+
titles: {
|
|
105
|
+
oneCommand: "одна команда поднимает проект целиком",
|
|
106
|
+
lockfile: "точные версии записаны в файл-замок",
|
|
107
|
+
sameEnv: "среда одинакова у всех и в конвейере",
|
|
108
|
+
formatter: "форматирование единое и применяется автоматически",
|
|
109
|
+
linter: "линтер настроен",
|
|
110
|
+
types: "проверка типов есть",
|
|
111
|
+
secretScan: "поиск секретов",
|
|
112
|
+
fileSize: "ограничение размера файла",
|
|
113
|
+
ownInvariants: "свои инварианты проекта",
|
|
114
|
+
tests: "арбитры правильности: тесты есть",
|
|
115
|
+
pipeline: "конвейер есть",
|
|
116
|
+
errorTracker: "ошибки собираются отдельно от логов",
|
|
117
|
+
machineReadable: "проект читается машиной",
|
|
118
|
+
rulesInRepo: "правила живут в репозитории и версионируются",
|
|
119
|
+
},
|
|
79
120
|
},
|
|
80
121
|
|
|
81
122
|
trigger: {
|
|
@@ -92,6 +133,7 @@ export const ru = {
|
|
|
92
133
|
has_deps: ["не видно файла зависимостей", "зависимости объявлены"],
|
|
93
134
|
has_tests: ["не видно тестов", "тесты есть"],
|
|
94
135
|
has_env: ["нет файла окружения", "файл окружения есть"],
|
|
136
|
+
has_ui: ["не видно стилей и компонентов интерфейса", "интерфейс есть: стили или компоненты"],
|
|
95
137
|
},
|
|
96
138
|
},
|
|
97
139
|
|
|
@@ -300,6 +342,7 @@ export const ru = {
|
|
|
300
342
|
},
|
|
301
343
|
|
|
302
344
|
start: {
|
|
345
|
+
noRecipeHere: "нужен инструмент, которого нет на этой машине",
|
|
303
346
|
initFailed: (cmd) => `Не получилось разложить комплект. Начни с ${cmd}`,
|
|
304
347
|
tooManyFiles: (n) => `В репозитории уже ${n} файлов кода — это другой сценарий.`,
|
|
305
348
|
useDoctor: (cmd) => `${cmd} осмотрит, что есть, и разделит записи на три списка:`,
|