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.
- package/README.md +72 -0
- package/README.ru.md +71 -0
- package/kit/gates/_native.sh +35 -0
- package/kit/gates/_skip.sh +34 -3
- package/kit/gates/commit-explains-itself/gate.yml +1 -0
- package/kit/gates/complexity-limit/gate.yml +1 -0
- package/kit/gates/dead-code/gate.yml +1 -0
- package/kit/gates/deps-are-pinned/gate.yml +1 -0
- package/kit/gates/duplicate-code/gate.yml +1 -0
- package/kit/gates/entry-links-exist/gate.yml +1 -0
- package/kit/gates/file-size-limit/gate.yml +1 -0
- package/kit/gates/gate-has-samples/gate.yml +1 -0
- package/kit/gates/gates-are-runnable/gate.yml +1 -0
- package/kit/gates/gates-run-in-ci/gate.yml +1 -0
- package/kit/gates/lesson-has-outcome/gate.yml +1 -0
- package/kit/gates/no-print-in-prod/check.sh +3 -1
- package/kit/gates/no-print-in-prod/gate.yml +1 -0
- package/kit/gates/secrets-not-in-code/gate.yml +1 -0
- package/kit/gates/swallowed-error/gate.yml +1 -0
- package/kit/gates/todo-without-task/gate.yml +1 -0
- package/package.json +1 -1
- package/tool/commands/badge.mjs +79 -0
- package/tool/commands/doctor.mjs +38 -41
- package/tool/commands/gates.mjs +121 -112
- package/tool/commands/project.mjs +84 -85
- package/tool/commands/report.mjs +194 -0
- package/tool/i18n/en.mjs +436 -0
- package/tool/i18n/index.mjs +33 -0
- package/tool/i18n/ru.mjs +437 -0
- package/tool/i18n/templates-en.mjs +164 -0
- package/tool/i18n/templates-ru.mjs +170 -0
- package/tool/lib/core.mjs +5 -1
- package/tool/lib/manifest.mjs +12 -31
- package/tool/lib/repo.mjs +45 -21
- package/tool/lib/templates.mjs +38 -182
- package/tool/program.mjs +36 -9
- package/tool/selfcheck/gates.sh +7 -1
- package/tool/selfcheck/smoke.sh +169 -0
- package/tool/selfcheck/units.mjs +101 -5
package/tool/program.mjs
CHANGED
|
@@ -17,9 +17,12 @@
|
|
|
17
17
|
import { realpathSync } from "node:fs";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
19
|
import { c, SELF } from "./lib/core.mjs";
|
|
20
|
+
import { L } from "./i18n/index.mjs";
|
|
20
21
|
import { cmdInit, cmdNote, cmdBlob, cmdStart } from "./commands/project.mjs";
|
|
21
22
|
import { cmdDoctor } from "./commands/doctor.mjs";
|
|
22
23
|
import { cmdAdd, cmdNew, cmdRatchet, cmdFind, cmdWhy } from "./commands/gates.mjs";
|
|
24
|
+
import { cmdReport } from "./commands/report.mjs";
|
|
25
|
+
import { cmdBadge } from "./commands/badge.mjs";
|
|
23
26
|
|
|
24
27
|
// Разбор аргументов выполняется только при запуске файла как программы. При импорте —
|
|
25
28
|
// а так его читают модульные проверки tool/selfcheck/units.mjs — CLI запускаться не должен.
|
|
@@ -63,19 +66,43 @@ if (IS_MAIN) {
|
|
|
63
66
|
case "blob":
|
|
64
67
|
await cmdBlob();
|
|
65
68
|
break;
|
|
66
|
-
|
|
69
|
+
case "report":
|
|
70
|
+
await cmdReport();
|
|
71
|
+
break;
|
|
72
|
+
case "badge":
|
|
73
|
+
await cmdBadge(rest);
|
|
74
|
+
break;
|
|
75
|
+
default: {
|
|
76
|
+
// Ширина колонки считается, а не подбирается пробелами: строки в двух языках разной
|
|
77
|
+
// длины, и вручную выровненная справка на втором языке разъезжается.
|
|
78
|
+
const h = L.help;
|
|
79
|
+
const rows = [
|
|
80
|
+
[`${SELF} init`, h.init],
|
|
81
|
+
[`${SELF} init --force`, h.initForce],
|
|
82
|
+
[`${SELF} start`, h.start],
|
|
83
|
+
[`${SELF} doctor`, h.doctor],
|
|
84
|
+
[`${SELF} doctor --run`, h.doctorRun],
|
|
85
|
+
[`${SELF} add ${h.name}`, h.add],
|
|
86
|
+
[`${SELF} find "…"`, h.find],
|
|
87
|
+
[`${SELF} why "…"`, h.why],
|
|
88
|
+
[`${SELF} ratchet ${h.name}`, h.ratchet],
|
|
89
|
+
[`${SELF} new ${h.name}`, h.new],
|
|
90
|
+
[`${SELF} note "…"`, h.note],
|
|
91
|
+
[`${SELF} blob`, h.blob],
|
|
92
|
+
[`${SELF} report`, h.report],
|
|
93
|
+
[`${SELF} badge`, h.badge],
|
|
94
|
+
];
|
|
95
|
+
const width = Math.max(...rows.map(([cmdText]) => cmdText.length));
|
|
96
|
+
const lines = rows.map(([cmdText, text]) => ` ${c.bold(cmdText.padEnd(width))} ${text}`);
|
|
67
97
|
console.log(`
|
|
68
|
-
${c.bold("aqk")} —
|
|
98
|
+
${c.bold("aqk")} — ${h.tagline}
|
|
69
99
|
|
|
70
|
-
|
|
71
|
-
${c.bold(`${SELF} init --force`)} перезаписать уже существующие файлы
|
|
72
|
-
${c.bold(`${SELF} start`)} кода ещё нет: сторожа дня 0 и порядок работы
|
|
73
|
-
${c.bold(`${SELF} doctor`)} проверить, что разложено и чего не хватает\n ${c.bold(`${SELF} doctor --run`)} ещё и запустить объявленные гейты\n ${c.bold(`${SELF} add`)} <имя> поставить гейт из каталога в проект\n ${c.bold(`${SELF} find`)} "…" есть ли уже такой гейт — сверка по намерению\n ${c.bold(`${SELF} why`)} "…" поймал ошибку — почему её не поймал сторож\n ${c.bold(`${SELF} ratchet`)} <имя> храповик: старые нарушения — долг, новые не пускать\n ${c.bold(`${SELF} new`)} <имя> заготовка своего гейта для каталога
|
|
74
|
-
${c.bold(`${SELF} note`)} "…" записать урок в общий журнал шишек
|
|
75
|
-
${c.bold(`${SELF} blob`)} собрать методички в один файл GOD_AI.md
|
|
100
|
+
${lines.join("\n")}
|
|
76
101
|
|
|
77
|
-
${c.dim(
|
|
102
|
+
${c.dim(h.noInstall)}
|
|
103
|
+
${c.dim(h.language)}
|
|
78
104
|
`);
|
|
79
105
|
process.exit(cmd ? 1 : 0);
|
|
106
|
+
}
|
|
80
107
|
}
|
|
81
108
|
}
|
package/tool/selfcheck/gates.sh
CHANGED
|
@@ -33,9 +33,15 @@ for GATE in "$CAT"/*/; do
|
|
|
33
33
|
PROOF="$(field "$YML" proof)"
|
|
34
34
|
[ -n "$INTENT" ] || bad "$SLUG: пустое поле intent — по нему идёт дедупликация"
|
|
35
35
|
|
|
36
|
+
# Английское намерение обязательно для НАШЕГО каталога: программа печатает его тем, кто
|
|
37
|
+
# пришёл с англоязычной площадки, и запись без перевода показала бы им кириллицу. Для
|
|
38
|
+
# чужой записи, принесённой со стороны, поле остаётся необязательным — программа тогда
|
|
39
|
+
# покажет то, что есть. Требовать перевод от вкладчика значит закрыть вклад половине.
|
|
40
|
+
[ -n "$(field "$YML" intent_en)" ] || bad "$SLUG: нет intent_en — намерение на английском"
|
|
41
|
+
|
|
36
42
|
# Заготовка от `aqk new` не должна проехать как запись: незаполненный гейт — мёртвое
|
|
37
43
|
# правило, а мёртвое правило учит игнорировать и живые.
|
|
38
|
-
if grep -
|
|
44
|
+
if grep -qE 'ЗАПОЛНИ|FILL IN|FILL_IN' "$YML" "$GATE/README.md" "$GATE/check.sh" 2>/dev/null; then
|
|
39
45
|
bad "$SLUG: заготовка не заполнена — остались метки ЗАПОЛНИ"
|
|
40
46
|
continue
|
|
41
47
|
fi
|
package/tool/selfcheck/smoke.sh
CHANGED
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
|
|
14
14
|
set -uo pipefail
|
|
15
15
|
|
|
16
|
+
# Язык вывода закреплён: проверки ниже сверяют русский текст, а без этой строки они зависели бы
|
|
17
|
+
# от локали машины — на англоязычном раннере зелёное стало бы красным без единой правки в коде.
|
|
18
|
+
export AQK_LANG=ru
|
|
19
|
+
|
|
16
20
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
17
21
|
CLI="$ROOT/tool/program.mjs"
|
|
18
22
|
PASS=0
|
|
@@ -527,6 +531,171 @@ else
|
|
|
527
531
|
fi
|
|
528
532
|
rm -rf "$SHDIR" "$CEDIR"
|
|
529
533
|
|
|
534
|
+
# --- 32. вывод действительно на двух языках -----------------------------------
|
|
535
|
+
# Сверка ключей каталогов (units.mjs) доказывает, что строки не разошлись, но не доказывает,
|
|
536
|
+
# что выбор языка вообще доехал до вывода. Это проверяется только запуском.
|
|
537
|
+
EN_OUT=$(AQK_LANG=en node "$CLI" 2>&1)
|
|
538
|
+
RU_OUT=$(AQK_LANG=ru node "$CLI" 2>&1)
|
|
539
|
+
if printf '%s' "$EN_OUT" | grep -q "install a gate from the catalogue" &&
|
|
540
|
+
! printf '%s' "$EN_OUT" | grep -q '[а-яА-ЯёЁ]' &&
|
|
541
|
+
printf '%s' "$RU_OUT" | grep -q "поставить гейт из каталога"; then
|
|
542
|
+
ok "справка печатается на двух языках, в английской нет кириллицы"
|
|
543
|
+
else
|
|
544
|
+
bad "выбор языка не доехал до вывода" "$(printf '%s' "$EN_OUT" | head -4)"
|
|
545
|
+
fi
|
|
546
|
+
|
|
547
|
+
# --- 33. ссылка на репозиторий ведёт в репозиторий -----------------------------
|
|
548
|
+
# Ссылку собирали из имени пакета. Когда имя стало коротким («agent-quality-kit» вместо
|
|
549
|
+
# «github:владелец/репозиторий»), просьба про звезду поехала на github.com/agent-quality-kit —
|
|
550
|
+
# несуществующую страницу. Единственное место, где мы просим человека о чём-то, вело в никуда.
|
|
551
|
+
FBDIR="$(mktemp -d)"; FBPROJ="$(mktemp -d)"
|
|
552
|
+
FB_OUT=$( cd "$FBPROJ" && git init -q . && HOME="$FBDIR" node "$CLI" init 2>&1 )
|
|
553
|
+
if printf '%s' "$FB_OUT" | grep -qE 'https://github\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+'; then
|
|
554
|
+
ok "просьба про звезду ведёт на репозиторий, а не на github.com/<имя пакета>"
|
|
555
|
+
else
|
|
556
|
+
bad "ссылка на репозиторий собрана неверно" "$(printf '%s' "$FB_OUT" | grep -i github | head -2)"
|
|
557
|
+
fi
|
|
558
|
+
rm -rf "$FBDIR" "$FBPROJ"
|
|
559
|
+
|
|
560
|
+
# --- 34. обязательная форма отчёта -------------------------------------------
|
|
561
|
+
# ЗАЧЕМ. Первый чужой прогон дал отчёт «12 гейтов зелёные» — при том что все 12 стояли на
|
|
562
|
+
# слабейшем рецепте, а половина методичек не была прочитана. Пересказ по памяти выбирает
|
|
563
|
+
# удобное; отчёт обязан собираться прогоном.
|
|
564
|
+
REPDIR="$(mktemp -d)"
|
|
565
|
+
(
|
|
566
|
+
cd "$REPDIR" && git init -q . && mkdir -p src &&
|
|
567
|
+
printf 'def f():\n print("debug")\n' > src/a.py &&
|
|
568
|
+
node "$CLI" start >/dev/null 2>&1
|
|
569
|
+
)
|
|
570
|
+
REP_OUT=$( cd "$REPDIR" && node "$CLI" report 2>&1 ); REP_CODE=$?
|
|
571
|
+
if [ "$REP_CODE" -ne 0 ] && printf '%s' "$REP_OUT" | grep -q '❌ no-print-in-prod'; then
|
|
572
|
+
ok "report краснеет кодом возврата и называет упавший гейт"
|
|
573
|
+
else
|
|
574
|
+
bad "report не отличает красное от зелёного" "код $REP_CODE"
|
|
575
|
+
fi
|
|
576
|
+
if [ -f "$REPDIR/.aqk/report.md" ] && grep -q '^## ' "$REPDIR/.aqk/report.md"; then
|
|
577
|
+
ok "report сохраняет .aqk/report.md"
|
|
578
|
+
else
|
|
579
|
+
bad "report не сохранил файл отчёта" "$REPDIR/.aqk/report.md"
|
|
580
|
+
fi
|
|
581
|
+
# Путь к методичке ИЩЕТСЯ: baseline лежит в подпапке ai/, и жёстко вписанный путь уже соврал.
|
|
582
|
+
if printf '%s' "$REP_OUT" | grep -q '📖 .aqk/docs/ai/project-baseline.md'; then
|
|
583
|
+
ok "report находит методичку в подпапке, а не пишет путь наизусть"
|
|
584
|
+
else
|
|
585
|
+
bad "report не нашёл project-baseline.md" "$(printf '%s' "$REP_OUT" | grep -i baseline | head -1)"
|
|
586
|
+
fi
|
|
587
|
+
rm -rf "$REPDIR"
|
|
588
|
+
|
|
589
|
+
# --- 35. note пишет в журнал ЭТОГО проекта, а не в чужой ---------------------
|
|
590
|
+
# Команда требовала клон нашего репозитория и писала урок туда, игнорируя lessons: из
|
|
591
|
+
# манифеста проекта. Найдено первым чужим прогоном: человек завёл журнал руками.
|
|
592
|
+
NOTEDIR="$(mktemp -d)"; NOTEHOME="$(mktemp -d)"
|
|
593
|
+
(
|
|
594
|
+
cd "$NOTEDIR" && git init -q . && git config user.email t@t && git config user.name t &&
|
|
595
|
+
node "$CLI" init >/dev/null 2>&1 &&
|
|
596
|
+
mkdir -p incidents &&
|
|
597
|
+
sed -i 's|^lessons: ""|lessons: incidents|' .aqk.yml &&
|
|
598
|
+
printf '**Вывод.** 🔧 завели проверку\n' | HOME="$NOTEHOME" AQK_HOME="" node "$CLI" note "шишка" >/dev/null 2>&1
|
|
599
|
+
)
|
|
600
|
+
if [ -f "$NOTEDIR/incidents/README.md" ] && grep -q 'шишка' "$NOTEDIR/incidents/README.md"; then
|
|
601
|
+
ok "note пишет в journal этого проекта — lessons: из манифеста"
|
|
602
|
+
else
|
|
603
|
+
bad "note проигнорировал lessons: и ушёл искать чужой клон" "$NOTEDIR/incidents/README.md"
|
|
604
|
+
fi
|
|
605
|
+
rm -rf "$NOTEDIR" "$NOTEHOME"
|
|
606
|
+
|
|
607
|
+
# --- 36. .aqkignore прячет принесённый извне код ------------------------------
|
|
608
|
+
# ЗАЧЕМ. В чужом проекте референс, принесённый из другого репозитория, попадал в находки
|
|
609
|
+
# всех сканирующих гейтов. Единственным лечением была правка КОПИИ _skip.sh в проекте —
|
|
610
|
+
# то есть настройка правкой чужого файла, которую затрёт следующий `aqk add`.
|
|
611
|
+
IGNDIR="$(mktemp -d)"
|
|
612
|
+
mkdir -p "$IGNDIR/third-party/inner" "$IGNDIR/src"
|
|
613
|
+
printf 'def f():\n print("свой")\n' > "$IGNDIR/src/mine.py"
|
|
614
|
+
printf 'def f():\n print("чужой")\n' > "$IGNDIR/third-party/inner/theirs.py"
|
|
615
|
+
OUT_BEFORE="$(bash "$ROOT/kit/gates/no-print-in-prod/check.sh" "$IGNDIR" 2>&1)"
|
|
616
|
+
printf '# принесено из другого репозитория\nthird-party/\n' > "$IGNDIR/.aqkignore"
|
|
617
|
+
OUT_AFTER="$(bash "$ROOT/kit/gates/no-print-in-prod/check.sh" "$IGNDIR" 2>&1)"
|
|
618
|
+
if printf '%s' "$OUT_BEFORE" | grep -q 'theirs.py' &&
|
|
619
|
+
! printf '%s' "$OUT_AFTER" | grep -q 'theirs.py' &&
|
|
620
|
+
printf '%s' "$OUT_AFTER" | grep -q 'mine.py'; then
|
|
621
|
+
ok ".aqkignore прячет чужой код и не трогает свой"
|
|
622
|
+
else
|
|
623
|
+
bad ".aqkignore не работает" "до: $(printf '%s' "$OUT_BEFORE" | head -2) | после: $(printf '%s' "$OUT_AFTER" | head -2)"
|
|
624
|
+
fi
|
|
625
|
+
rm -rf "$IGNDIR"
|
|
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
|
+
|
|
530
699
|
# --- итог -------------------------------------------------------------------
|
|
531
700
|
printf '\n'
|
|
532
701
|
if [ "$FAIL" -eq 0 ]; then
|
package/tool/selfcheck/units.mjs
CHANGED
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
import test from "node:test";
|
|
13
13
|
import assert from "node:assert/strict";
|
|
14
14
|
import { parseManifest, manifestWithGate } from "../lib/manifest.mjs";
|
|
15
|
-
import { triggerVerdict, recipeFor, stems, overlap, EXT_LANG } from "../lib/repo.mjs";
|
|
15
|
+
import { triggerVerdict, recipeFor, stems, overlap, EXT_LANG, whichSync } from "../lib/repo.mjs";
|
|
16
|
+
import { CATALOGS, pickLang, L } from "../i18n/index.mjs";
|
|
17
|
+
import { badgeMarkdown, BADGE_RE, placesToCheck } from "../commands/badge.mjs";
|
|
18
|
+
import { dirname } from "node:path";
|
|
16
19
|
|
|
17
20
|
const facts = (over = {}) => ({ langs: new Set(), files: 0, ...over });
|
|
18
21
|
|
|
@@ -52,7 +55,8 @@ test("условия складываются по И: одно ложное с
|
|
|
52
55
|
|
|
53
56
|
test("причина, по которой запись скрыта, называется словами", () => {
|
|
54
57
|
const v = triggerVerdict({ trigger: { langs: "python, typescript" } }, facts({ langs: new Set(["go"]) }));
|
|
55
|
-
|
|
58
|
+
// Сверяем с каталогом, а не с буквами: текст переводится, а выбор причины — нет.
|
|
59
|
+
assert.equal(v.why, L.trigger.noLangs("python, typescript"));
|
|
56
60
|
});
|
|
57
61
|
|
|
58
62
|
test("always: false значит «никогда не применимо», а не «условие пропущено»", () => {
|
|
@@ -64,7 +68,7 @@ test("неизвестное условие скрывает запись, а н
|
|
|
64
68
|
// Молча пропустить незнакомое условие значит показать запись всем подряд.
|
|
65
69
|
const v = triggerVerdict({ trigger: { has_kubernetes: "true" } }, facts());
|
|
66
70
|
assert.equal(v.applies, false);
|
|
67
|
-
assert.
|
|
71
|
+
assert.equal(v.why, L.trigger.unknown("has_kubernetes"));
|
|
68
72
|
});
|
|
69
73
|
|
|
70
74
|
// --- выбор рецепта ------------------------------------------------------------
|
|
@@ -74,7 +78,7 @@ test("без родного языка берётся переносимый р
|
|
|
74
78
|
});
|
|
75
79
|
|
|
76
80
|
test("рецепта нет — так и сказано, а не пустая строка", () => {
|
|
77
|
-
assert.equal(recipeFor({ slug: "x", recipes: {} }, facts()),
|
|
81
|
+
assert.equal(recipeFor({ slug: "x", recipes: {} }, facts()), L.recipe.none);
|
|
78
82
|
});
|
|
79
83
|
|
|
80
84
|
// --- дедупликация по намерению ------------------------------------------------
|
|
@@ -101,5 +105,97 @@ test("гейт дописывается в блок gates и не дублиру
|
|
|
101
105
|
test("без блока gates программа объясняет, чего не хватает", () => {
|
|
102
106
|
const r = manifestWithGate("aqk: 1\n", "x", "bash y.sh");
|
|
103
107
|
assert.equal(r.text, null);
|
|
104
|
-
assert.
|
|
108
|
+
assert.equal(r.why, L.manifest.noGatesBlock);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// --- каталоги строк не расходятся ---------------------------------------------
|
|
112
|
+
// ЗАЧЕМ. «Поддерживаем два языка» — утверждение, которое обязана держать машина, а не память
|
|
113
|
+
// того, кто правил вывод в последний раз. Забытый ключ в одном каталоге даёт `undefined` в
|
|
114
|
+
// выводе — не отказ, а тихую порчу текста ровно у того, кто пришёл на втором языке.
|
|
115
|
+
// В массивы заходим тоже: ступени уровней и многострочные пояснения лежат массивами, и без
|
|
116
|
+
// этого проверка сравнивала бы их как один непрозрачный «object» — то есть не сравнивала.
|
|
117
|
+
// Заодно сверяется длина: пояснение из трёх строк на одном языке и из двух на другом — тоже
|
|
118
|
+
// расхождение.
|
|
119
|
+
function keyPaths(obj, prefix = "") {
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
122
|
+
const path = prefix ? `${prefix}.${k}` : k;
|
|
123
|
+
if (v && typeof v === "object") out.push(...keyPaths(v, path));
|
|
124
|
+
else out.push(`${path}:${typeof v}`);
|
|
125
|
+
}
|
|
126
|
+
return out.sort();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
test("оба каталога строк несут одни и те же ключи одного типа", () => {
|
|
130
|
+
const a = keyPaths(CATALOGS.ru);
|
|
131
|
+
const b = keyPaths(CATALOGS.en);
|
|
132
|
+
const onlyRu = a.filter((k) => !b.includes(k));
|
|
133
|
+
const onlyEn = b.filter((k) => !a.includes(k));
|
|
134
|
+
assert.deepEqual(onlyRu, [], `есть только в ru: ${onlyRu.join(", ")}`);
|
|
135
|
+
assert.deepEqual(onlyEn, [], `есть только в en: ${onlyEn.join(", ")}`);
|
|
136
|
+
assert.ok(a.length > 0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("ни одна строка вывода не осталась пустой", () => {
|
|
140
|
+
for (const [lang, cat] of Object.entries(CATALOGS)) {
|
|
141
|
+
for (const path of keyPaths(cat)) {
|
|
142
|
+
const [key, kind] = path.split(":");
|
|
143
|
+
if (kind !== "string") continue;
|
|
144
|
+
const value = key.split(".").reduce((o, k) => o[k], cat);
|
|
145
|
+
assert.ok(value.trim().length > 0, `пустая строка ${lang}.${key}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("язык берётся из AQK_LANG, потом из локали, иначе английский", () => {
|
|
151
|
+
assert.equal(pickLang({ AQK_LANG: "ru" }), "ru");
|
|
152
|
+
assert.equal(pickLang({ AQK_LANG: "en_US.UTF-8", LANG: "ru_RU.UTF-8" }), "en");
|
|
153
|
+
assert.equal(pickLang({ LANG: "ru_RU.UTF-8" }), "ru");
|
|
154
|
+
assert.equal(pickLang({ LC_ALL: "ru_RU.UTF-8", LANG: "en_US.UTF-8" }), "ru");
|
|
155
|
+
assert.equal(pickLang({ LANG: "de_DE.UTF-8" }), "en");
|
|
156
|
+
assert.equal(pickLang({}), "en");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// --- поиск программы в PATH ---------------------------------------------------
|
|
160
|
+
// ЗАЧЕМ. Раньше наличие программы проверялось через `command -v` в оболочке. На Windows
|
|
161
|
+
// оболочка — cmd.exe, где такой команды нет, и ответ был «не установлено» ДЛЯ ЛЮБОЙ
|
|
162
|
+
// программы: родной рецепт становился недостижим, гейт молча вставал на слабейший
|
|
163
|
+
// переносимый вариант, а прогон показывал зелёное. Нашлось на чужом прогоне, не у нас.
|
|
164
|
+
test("программа в PATH находится, несуществующая — нет", () => {
|
|
165
|
+
assert.ok(whichSync("node"), "node обязан находиться: им же запущена эта проверка");
|
|
166
|
+
assert.equal(whichSync("нет-такой-программы-12345"), null);
|
|
167
|
+
assert.equal(whichSync(""), null);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("поиск не зависит от оболочки — работает с пустым окружением", () => {
|
|
171
|
+
// Тот самый случай: оболочки нет или она другая. Ответ обязан быть «не нашли»,
|
|
172
|
+
// а не исключение и не ложное «нашли».
|
|
173
|
+
assert.equal(whichSync("node", { PATH: "" }), null);
|
|
174
|
+
const dir = dirname(process.execPath);
|
|
175
|
+
assert.ok(whichSync(process.platform === "win32" ? "node" : "node", { PATH: dir }));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("команда путём, а не именем, ищется на диске, а не в PATH", () => {
|
|
179
|
+
assert.ok(whichSync(process.execPath));
|
|
180
|
+
assert.equal(whichSync("./нет-такого-файла.sh"), null);
|
|
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 всё равно проверяется");
|
|
105
201
|
});
|