agent-quality-kit 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -10
- package/README.ru.md +68 -11
- package/kit/docs/ai/index.md +1 -0
- package/kit/docs/api-e2e.md +214 -0
- package/kit/docs/ready-made-rules.md +85 -0
- package/kit/gates/README.md +22 -0
- package/kit/gates/api-contract-has-arbiter/README.md +63 -0
- package/kit/gates/api-contract-has-arbiter/check.sh +117 -0
- package/kit/gates/api-contract-has-arbiter/gate.yml +15 -0
- package/kit/gates/api-contract-has-arbiter/green/.github/workflows/ci.yml +12 -0
- package/kit/gates/api-contract-has-arbiter/green/openapi.yaml +18 -0
- package/kit/gates/api-contract-has-arbiter/red/.github/workflows/ci.yml +11 -0
- package/kit/gates/api-contract-has-arbiter/red/openapi.yaml +18 -0
- package/kit/gates/ci-actually-fails/check.sh +9 -1
- package/kit/gates/commit-explains-itself/check.sh +15 -0
- package/kit/gates/complexity-limit/red/deep.go +17 -0
- package/kit/gates/complexity-limit/red/deep.rs +17 -0
- package/kit/gates/gate-not-weakened/red/suppress.go +5 -0
- package/kit/gates/gate-not-weakened/red/suppress.rs +3 -0
- package/kit/gates/protection-not-removed/README.md +67 -0
- package/kit/gates/protection-not-removed/check.sh +101 -0
- package/kit/gates/protection-not-removed/gate.yml +10 -0
- package/kit/gates/protection-not-removed/green/.aqk.yml +7 -0
- package/kit/gates/protection-not-removed/green/gates-declared.txt +4 -0
- package/kit/gates/protection-not-removed/red/.aqk.yml +7 -0
- package/kit/gates/protection-not-removed/red/gates-declared.txt +4 -0
- package/kit/gates/secrets-not-in-code/red/leak.go +9 -0
- package/kit/gates/secrets-not-in-code/red/leak.rs +5 -0
- package/kit/gates/todo-without-task/red/later.go +6 -0
- package/kit/gates/todo-without-task/red/later.rs +4 -0
- package/llms.txt +10 -4
- package/package.json +2 -1
- package/tool/commands/context.mjs +36 -1
- package/tool/commands/doctor.mjs +56 -2
- package/tool/commands/gates.mjs +2 -0
- package/tool/commands/probe.mjs +228 -0
- package/tool/commands/vitals.mjs +11 -3
- package/tool/i18n/en-docs.mjs +10 -0
- package/tool/i18n/en-gates.mjs +309 -0
- package/tool/i18n/en.mjs +10 -281
- package/tool/i18n/ru-docs.mjs +10 -0
- package/tool/i18n/ru-gates.mjs +311 -0
- package/tool/i18n/ru.mjs +10 -280
- package/tool/lib/cadence.mjs +57 -0
- package/tool/lib/core.mjs +1 -0
- package/tool/lib/evidence.mjs +15 -2
- package/tool/lib/execution.mjs +67 -0
- package/tool/lib/history.mjs +82 -0
- package/tool/lib/manifest.mjs +1 -1
- package/tool/lib/protection.mjs +52 -0
- package/tool/lib/prove.mjs +40 -13
- package/tool/lib/repo.mjs +12 -2
- package/tool/program.mjs +7 -0
- package/tool/selfcheck/smoke/_fixture.mjs +89 -0
- package/tool/selfcheck/smoke/api-contract.test.mjs +79 -0
- package/tool/selfcheck/smoke/commit-report.test.mjs +47 -0
- package/tool/selfcheck/smoke/protection.test.mjs +101 -0
- package/tool/selfcheck/smoke/release-tools.test.mjs +55 -0
- package/tool/selfcheck/smoke/verdict.test.mjs +40 -0
- package/tool/selfcheck/smoke.sh +194 -7
- package/tool/selfcheck/units-cadence.mjs +69 -0
- package/tool/selfcheck/units-context.mjs +24 -0
- package/tool/selfcheck/units-evidence.mjs +34 -0
- package/tool/selfcheck/units-execution.mjs +88 -0
- package/tool/selfcheck/units-level.mjs +32 -1
- package/tool/selfcheck/units-probe.mjs +100 -0
- package/tool/selfcheck/units-repo.mjs +31 -1
- package/tool/selfcheck/units-vitals.mjs +19 -0
package/tool/selfcheck/smoke.sh
CHANGED
|
@@ -23,7 +23,15 @@ PASS=0
|
|
|
23
23
|
FAIL=0
|
|
24
24
|
|
|
25
25
|
ok() { printf ' \033[32m✔\033[0m %s\n' "$1"; PASS=$((PASS + 1)); }
|
|
26
|
-
|
|
26
|
+
# Упавшие называются ПОИМЁННО И С ПРИЧИНОЙ в итоге, а не только по ходу. Прогон запускают гейтом, а вывод
|
|
27
|
+
# упавшего гейта обрезается — и причина, напечатанная в середине двухсот строк, до глаз не
|
|
28
|
+
# доезжает. Час поисков в конвейере 2026-09-09 стоил ровно этого: список красных был, а увидеть
|
|
29
|
+
# его было нельзя. Имени мало: следующий круг конвейера показал имя и не показал причину —
|
|
30
|
+
# она печатается сразу под находкой, то есть в середине, которую обрезка и съедает.
|
|
31
|
+
FAILED_NAMES=""
|
|
32
|
+
bad() { printf ' \033[31m✘\033[0m %s\n' "$1"; printf ' %s\n' "${2:-}"; FAIL=$((FAIL + 1));
|
|
33
|
+
FAILED_NAMES="${FAILED_NAMES:+$FAILED_NAMES
|
|
34
|
+
}$1 — ${2:-без пояснения}"; }
|
|
27
35
|
|
|
28
36
|
# Node на Windows видит мир глазами Windows, а Git Bash — глазами POSIX: путь вида
|
|
29
37
|
# /tmp/tmp.XXXX, отданный в `node -e`, там не существует, и проверка падает не на том, что
|
|
@@ -35,6 +43,35 @@ node_in() { D="$1"; shift; ( cd "$D" && node "$@" ); }
|
|
|
35
43
|
WORK="$(mktemp -d)"
|
|
36
44
|
trap 'rm -rf "$WORK"' EXIT
|
|
37
45
|
|
|
46
|
+
# ГЕРМЕТИЧНОСТЬ. Три строки, каждая закрывает свой класс отказов, найденный сплошным чтением
|
|
47
|
+
# и замерами 2026-09-09. Прогон обязан давать один и тот же ответ на любой машине и в любой
|
|
48
|
+
# день — иначе «116 зелёных» означает не «работает», а «сегодня совпало».
|
|
49
|
+
#
|
|
50
|
+
# 1. TMPDIR внутрь $WORK. В файле 65 вызовов `mktemp -d` и ОДИН trap — на $WORK. Прерванный
|
|
51
|
+
# прогон оставлял 64 каталога, а два из них несут по копии всего дерева (`cp -r tool kit`,
|
|
52
|
+
# ≈4000 файлов). Одна строка вместо правки шестидесяти пяти: mktemp читает TMPDIR, и вся
|
|
53
|
+
# временная работа попадает под уже существующую уборку.
|
|
54
|
+
# 2. HOME в песочницу. Комплект пишет `~/.config/aqk/feedback-shown` — просьбу про звезду
|
|
55
|
+
# показывают один раз НА МАШИНУ. Из 65 фикстур свой HOME задавали 8. Значит на машине
|
|
56
|
+
# разработчика (отметка лежит с сентября) `init` молчит, а на свежем раннере — говорит:
|
|
57
|
+
# один и тот же прогон получает РАЗНЫЙ ввод. Плюс прогон гадил в настоящий домашний каталог.
|
|
58
|
+
# 3. AQK_UPDATE=0. `doctor --brief` ходит в реестр npm по сети с таймаутом 3 с — и только вне
|
|
59
|
+
# конвейера (`updateWanted` выключается при CI). То есть локально тесты сетевые, а в
|
|
60
|
+
# конвейере нет. Сеть в тестах — отдельный класс флейков во всех разборах; здесь она ещё и
|
|
61
|
+
# делает две среды разными по построению.
|
|
62
|
+
export TMPDIR="$WORK/tmp"; mkdir -p "$TMPDIR"
|
|
63
|
+
# PYTHONUSERBASE — ДО подмены HOME и из-за неё. Инструменты, поставленные `pip --user`
|
|
64
|
+
# (`vulture`, `checkwash`), это python-скрипты, которые ищут свои модули в
|
|
65
|
+
# `$HOME/.local/lib/python*/site-packages`. С песочницей вместо HOME импорт падает, а гейт,
|
|
66
|
+
# чей арбитр упал, печатает «арбитр покраснел на ЗЕЛЁНОМ образце» — то есть проверка врёт о
|
|
67
|
+
# записи каталога, а не о своей поломке. Найдено 2026-09-09 в тот же час, когда инструменты
|
|
68
|
+
# были поставлены: до установки этот путь кода не исполнялся вовсе.
|
|
69
|
+
# Переменная — штатный способ указать python, где искать пользовательские пакеты; бинарям
|
|
70
|
+
# (`ruff`, `zizmor`, `slopcheck`) она не нужна, и им песочница не мешала.
|
|
71
|
+
export PYTHONUSERBASE="${PYTHONUSERBASE:-$HOME/.local}"
|
|
72
|
+
export HOME="$WORK/home"; export USERPROFILE="$HOME"; mkdir -p "$HOME"
|
|
73
|
+
export AQK_UPDATE=0
|
|
74
|
+
|
|
38
75
|
printf '\n\033[1mtool/selfcheck/smoke.sh\033[0m\n\n'
|
|
39
76
|
|
|
40
77
|
# --- 1. синтаксис самой программы ------------------------------------------
|
|
@@ -345,10 +382,24 @@ if [ -f "$BLOBDIR/GOD_AI.md" ]; then
|
|
|
345
382
|
bad "blob собрал не все методички" "в kit/docs $EXPECT_MD, в склейке $GOT_MD"
|
|
346
383
|
fi
|
|
347
384
|
# Ссылки на соседние файлы внутри склейки ведут в никуда: соседей рядом больше нет.
|
|
348
|
-
|
|
385
|
+
#
|
|
386
|
+
# ДВА ПРОХОДА, А НЕ ОПЕРЕЖАЮЩАЯ ПРОВЕРКА. Здесь стояло `grep -qE '...(?!https?:)...'`, и это
|
|
387
|
+
# была ЛОЖЬ: `(?!` — синтаксис PCRE, в POSIX ERE его нет. GNU grep печатает предупреждение и
|
|
388
|
+
# не находит ничего, ugrep падает с ошибкой разбора — в обоих случаях `if` уходит в `else`, и
|
|
389
|
+
# проверка печатала зелёное НА ЛЮБЫХ ДАННЫХ. Ровно тот грех, ради поимки которого написан весь
|
|
390
|
+
# комплект, внутри прибора, который его ищет. Найдено сплошным чтением 2026-09-09.
|
|
391
|
+
#
|
|
392
|
+
# Поэтому же ниже проверяется САМА ПРОВЕРКА: подделываем склейку с относительной ссылкой и
|
|
393
|
+
# требуем, чтобы её нашли. Без этого следующая такая опечатка снова проедет зелёной.
|
|
394
|
+
rel_links() { grep -oE '\]\([^)]+\.md[^)]*\)' "$1" 2>/dev/null | grep -vE '^\]\(https?:' | grep -q .; }
|
|
395
|
+
printf '%s' "$(cat "$BLOBDIR/GOD_AI.md")" > "$BLOBDIR/doctored.md"
|
|
396
|
+
printf '\nсм. [соседний файл](other.md)\n' >> "$BLOBDIR/doctored.md"
|
|
397
|
+
if rel_links "$BLOBDIR/GOD_AI.md"; then
|
|
349
398
|
bad "в склейке остались ссылки на соседние файлы" "внутри одного файла они ведут в никуда"
|
|
399
|
+
elif ! rel_links "$BLOBDIR/doctored.md"; then
|
|
400
|
+
bad "проверка ссылок не может покраснеть" "подделанная склейка с относительной ссылкой прошла"
|
|
350
401
|
else
|
|
351
|
-
ok "ссылки на соседние файлы
|
|
402
|
+
ok "ссылки на соседние файлы сняты, и проверка это умеет заметить"
|
|
352
403
|
fi
|
|
353
404
|
else
|
|
354
405
|
bad "blob не создал GOD_AI.md" "$BLOBDIR"
|
|
@@ -579,7 +630,7 @@ REPDIR="$(mktemp -d)"
|
|
|
579
630
|
(
|
|
580
631
|
cd "$REPDIR" && git init -q . && mkdir -p src &&
|
|
581
632
|
printf 'a = 1 # noqa\n' > src/a.py &&
|
|
582
|
-
node "$CLI" start > /
|
|
633
|
+
node "$CLI" start > "$WORK/start.log" 2>&1
|
|
583
634
|
)
|
|
584
635
|
REP_OUT=$( cd "$REPDIR" && node "$CLI" report 2>&1 ); REP_CODE=$?
|
|
585
636
|
if [ "$REP_CODE" -ne 0 ] && printf '%s' "$REP_OUT" | grep -q '❌ gate-not-weakened'; then
|
|
@@ -591,7 +642,7 @@ else
|
|
|
591
642
|
G_LS=$( cd "$REPDIR" && ls gates 2>&1 | tr '\n' ' ' )
|
|
592
643
|
G_DECL=$( cd "$REPDIR" && sed -n '/^gates:/,$p' .aqk.yml 2>/dev/null | grep -cE '^[[:space:]]+[A-Za-z0-9_-]+:' )
|
|
593
644
|
G_OUT=$( cd "$REPDIR" && bash gates/gate-not-weakened/check.sh . 2>&1 | head -2 ); G_CODE=$?
|
|
594
|
-
bad "report не отличает красное от зелёного" "код отчёта $REP_CODE; гейт напрямую: код $G_CODE, вывод «$(printf '%s' "$G_OUT" | tr '\n' ' ')»; в gates/: «$G_LS»; объявлено гейтов: $G_DECL; хвост start: «$(tail -4 /
|
|
645
|
+
bad "report не отличает красное от зелёного" "код отчёта $REP_CODE; гейт напрямую: код $G_CODE, вывод «$(printf '%s' "$G_OUT" | tr '\n' ' ')»; в gates/: «$G_LS»; объявлено гейтов: $G_DECL; хвост start: «$(tail -4 "$WORK/start.log" 2>/dev/null | tr '\n' ' ')»"
|
|
595
646
|
fi
|
|
596
647
|
if [ -f "$REPDIR/.aqk/report.md" ] && grep -q '^## ' "$REPDIR/.aqk/report.md"; then
|
|
597
648
|
ok "report сохраняет .aqk/report.md"
|
|
@@ -681,7 +732,12 @@ BDIR="$(mktemp -d)"
|
|
|
681
732
|
)
|
|
682
733
|
B_OUT=$( cd "$BDIR" && node "$CLI" badge 2>&1 )
|
|
683
734
|
B_LVL=$(printf '%s' "$B_OUT" | sed -n 's|.*img.shields.io/badge/AQK-\([0-9]\)-.*|\1|p' | head -1)
|
|
684
|
-
|
|
735
|
+
# ЗНАЧОК СВЕРЯЕТСЯ С ДОКАЗАННЫМ УРОВНЕМ. `badge` гоняет доказательство сам, а `doctor` без
|
|
736
|
+
# `--run` его не гоняет и честно говорит «не доказано» — это РАЗНЫЕ вопросы, и сравнивать их
|
|
737
|
+
# нельзя. Пока доказательство ни на что не влияло в этой фикстуре, оба ответа совпадали
|
|
738
|
+
# случайно; как только у проекта появился храповик, они разошлись, и проверка обвинила
|
|
739
|
+
# исправную программу. Найдено 2026-09-10 правкой, не трогавшей ни значок, ни уровень.
|
|
740
|
+
D_LVL=$( cd "$BDIR" && node "$CLI" doctor --run 2>&1 | sed -n 's/.*Уровень: AQK-\([0-9]\).*/\1/p' | head -1 )
|
|
685
741
|
if [ -n "$B_LVL" ] && [ "$B_LVL" = "$D_LVL" ]; then
|
|
686
742
|
ok "badge выдаёт значок с уровнем doctor (AQK-$B_LVL)"
|
|
687
743
|
else
|
|
@@ -1731,7 +1787,7 @@ rm -rf "$PRVP"
|
|
|
1731
1787
|
# и шесть `units-*.mjs`), и заметить это можно было только сверкой руками. Тот же класс, что
|
|
1732
1788
|
# «гейт объявлен и не существует»: расхождение молчит.
|
|
1733
1789
|
MISSING=""
|
|
1734
|
-
for F in "$ROOT"/tool/lib/*.mjs "$ROOT"/tool/commands/*.mjs "$ROOT"/tool/selfcheck
|
|
1790
|
+
for F in "$ROOT"/tool/lib/*.mjs "$ROOT"/tool/commands/*.mjs "$ROOT"/tool/selfcheck/* "$ROOT"/tool/selfcheck/*/*; do
|
|
1735
1791
|
B=$(basename "$F")
|
|
1736
1792
|
grep -qF "$B" "$ROOT/AGENTS.md" || MISSING="$MISSING $B"
|
|
1737
1793
|
done
|
|
@@ -1786,6 +1842,132 @@ else
|
|
|
1786
1842
|
fi
|
|
1787
1843
|
chmod 700 "$HOMEH" 2>/dev/null; rm -rf "$HOMEP" "$HOMEH"
|
|
1788
1844
|
|
|
1845
|
+
# --- 109. probe: находит слепоту и не выдаёт её за покрытие ---------------------
|
|
1846
|
+
# ЗАЧЕМ КОМАНДА. `doctor` говорит «держит машина N» — N из чего? `prove` доказывает, что гейт
|
|
1847
|
+
# ловит брак НА СВОЁМ образце. Ни один не отвечает: что в ЭТОМ репозитории не прикрыто ничем.
|
|
1848
|
+
# Замер 2026-09-09 на самом комплекте: настоящий файл, подсаженные проглоченная ошибка и печать,
|
|
1849
|
+
# 21 объявленный гейт — покраснело НОЛЬ. Узнать об этом было нечем.
|
|
1850
|
+
# Проверяются ОБА конца: класс, который объявленный гейт ловит, обязан быть зелёным, а тот,
|
|
1851
|
+
# которого не ловит никто, — красным. Проверка одного конца пропустила бы команду, которая
|
|
1852
|
+
# всегда говорит «не прикрыто», и команду, которая всегда говорит «прикрыто».
|
|
1853
|
+
PRBP="$(mktemp -d)"
|
|
1854
|
+
(
|
|
1855
|
+
cd "$PRBP" && git init -q . && git config user.email a@b && git config user.name a
|
|
1856
|
+
mkdir -p src && printf 'def send(to):\n return 1\n' > src/mailer.py
|
|
1857
|
+
git add -A && git commit -qm "feat: почта"
|
|
1858
|
+
printf '# правка\n' >> src/mailer.py && git add -A && git commit -qm "fix: письма не уходили"
|
|
1859
|
+
node "$CLI" init && node "$CLI" add todo-without-task
|
|
1860
|
+
# Коммитим ВСЁ, что положили init и add: иначе дерево грязное ещё до пробы, и проверка
|
|
1861
|
+
# «probe ничего не пишет» меряла бы мою же фикстуру, а не команду.
|
|
1862
|
+
git add -A && git commit -qm "Сделано: комплект. Не уверен: ничего"
|
|
1863
|
+
) >/dev/null 2>&1
|
|
1864
|
+
PRB=$( cd "$PRBP" && AQK_LANG=ru node "$CLI" probe --top 1 2>&1 ); PRB_C=$?
|
|
1865
|
+
# Слепое: secrets-not-in-code не объявлен, образец под .py у него есть — обязан быть красным.
|
|
1866
|
+
# Прикрытое: todo-without-task объявлен — обязан быть зелёным и назвать, кто поймал.
|
|
1867
|
+
if [ "$PRB_C" -eq 0 ] &&
|
|
1868
|
+
printf '%s' "$PRB" | grep -q "НЕ ЛОВИТ НИКТО" &&
|
|
1869
|
+
printf '%s' "$PRB" | grep -q "ловит: todo-without-task" &&
|
|
1870
|
+
printf '%s' "$PRB" | grep -q "починок в истории: 1"; then
|
|
1871
|
+
ok "probe: слепые классы названы, прикрытый назван поимённо, прогон не уронен"
|
|
1872
|
+
else
|
|
1873
|
+
bad "probe не различает слепое и прикрытое" "код $PRB_C, слепых $(printf '%s' "$PRB" | grep -c 'НЕ ЛОВИТ'), пойманных $(printf '%s' "$PRB" | grep -c 'ловит:')"
|
|
1874
|
+
fi
|
|
1875
|
+
# КОД человека проба не трогает: образец живёт во временном каталоге. Если бы он попал в
|
|
1876
|
+
# репозиторий, команда осмотра стала бы командой правки — и её выключили бы в тот же день.
|
|
1877
|
+
# Своя отметка каденции — исключение, и единственное: она лежит в `.aqk/`, там же, где отчёт
|
|
1878
|
+
# прогона, и без неё каденция невозможна (следующий прогон не узнает, что проба была).
|
|
1879
|
+
# Проверяется именно это разделение: за пределами `.aqk/` — ни следа.
|
|
1880
|
+
DIRTY=$( cd "$PRBP" && git status --porcelain | grep -cv ' \.aqk/' )
|
|
1881
|
+
if [ "$DIRTY" -eq 0 ]; then
|
|
1882
|
+
ok "probe не трогает код: пишет только свою отметку в .aqk/"
|
|
1883
|
+
else
|
|
1884
|
+
bad "probe оставил следы вне .aqk/" "$(cd "$PRBP" && git status --porcelain | grep -v ' \.aqk/' | head -3 | tr '\n' ' ')"
|
|
1885
|
+
fi
|
|
1886
|
+
rm -rf "$PRBP"
|
|
1887
|
+
|
|
1888
|
+
# --- 111. проба случается САМА, один раз, и попадает агенту в контекст -----------
|
|
1889
|
+
# ГЛАВНОЕ В ЭТОЙ ПРОВЕРКЕ — не механика, а замысел. Владелец сформулировал так: «команду, о
|
|
1890
|
+
# которой надо вспомнить, агент не вспомнит, а человек о ней не узнает». Это тот же класс, что
|
|
1891
|
+
# файл, который можно не прочитать, — и весь комплект написан против него. Значит `probe`,
|
|
1892
|
+
# оставленная ручной, отвечает на важнейший вопрос и не задаётся никем.
|
|
1893
|
+
# Проверяются три вещи, и каждая — отдельный способ провалиться:
|
|
1894
|
+
# 1. прогон делает пробу САМ, когда её не делали;
|
|
1895
|
+
# 2. второй прогон подряд её НЕ повторяет — иначе каденция превращается в шум;
|
|
1896
|
+
# 3. агент узнаёт результат из блока состояния, не зная про команду.
|
|
1897
|
+
CADP="$(mktemp -d)"
|
|
1898
|
+
(
|
|
1899
|
+
cd "$CADP" && git init -q . && git config user.email a@b && git config user.name a
|
|
1900
|
+
mkdir -p src && printf 'def s():\n return 1\n' > src/a.py
|
|
1901
|
+
git add -A && git commit -qm "feat: старт"
|
|
1902
|
+
printf '# п\n' >> src/a.py && git add -A && git commit -qm "fix: не работало"
|
|
1903
|
+
node "$CLI" init && node "$CLI" add todo-without-task
|
|
1904
|
+
) >/dev/null 2>&1
|
|
1905
|
+
CTX0=$( cd "$CADP" && AQK_LANG=ru node "$CLI" context 2>&1 )
|
|
1906
|
+
RUN1=$( cd "$CADP" && AQK_LANG=ru node "$CLI" doctor --run 2>&1 )
|
|
1907
|
+
RUN2=$( cd "$CADP" && AQK_LANG=ru node "$CLI" doctor --run 2>&1 )
|
|
1908
|
+
CTX1=$( cd "$CADP" && AQK_LANG=ru node "$CLI" context 2>&1 )
|
|
1909
|
+
if printf '%s' "$CTX0" | grep -q "НЕИЗВЕСТНО" &&
|
|
1910
|
+
printf '%s' "$RUN1" | grep -q "ещё не делали" &&
|
|
1911
|
+
! printf '%s' "$RUN2" | grep -q "ещё не делали" &&
|
|
1912
|
+
printf '%s' "$CTX1" | grep -q "Не прикрыто ничем"; then
|
|
1913
|
+
ok "проба запускается прогоном сама, не повторяется и попадает агенту в контекст"
|
|
1914
|
+
else
|
|
1915
|
+
bad "каденция пробы не работает" \
|
|
1916
|
+
"до: $(printf '%s' "$CTX0" | grep -c 'НЕИЗВЕСТНО'), первый: $(printf '%s' "$RUN1" | grep -c 'ещё не делали'), второй: $(printf '%s' "$RUN2" | grep -c 'ещё не делали'), после: $(printf '%s' "$CTX1" | grep -c 'Не прикрыто')"
|
|
1917
|
+
fi
|
|
1918
|
+
# Выключатель обязан быть у всего, что случается само: иначе первый же, кому это помешало,
|
|
1919
|
+
# выключит весь прогон, а не одну пробу.
|
|
1920
|
+
CADP2="$(mktemp -d)"
|
|
1921
|
+
( cd "$CADP2" && git init -q . && git config user.email a@b && git config user.name a
|
|
1922
|
+
mkdir -p src && printf 'def s():\n return 1\n' > src/a.py
|
|
1923
|
+
git add -A && git commit -qm "feat: старт"
|
|
1924
|
+
node "$CLI" init && node "$CLI" add todo-without-task ) >/dev/null 2>&1
|
|
1925
|
+
OFF=$( cd "$CADP2" && AQK_PROBE=0 AQK_LANG=ru node "$CLI" doctor --run 2>&1 )
|
|
1926
|
+
if ! printf '%s' "$OFF" | grep -q "ещё не делали"; then
|
|
1927
|
+
ok "AQK_PROBE=0 выключает пробу, не трогая прогон"
|
|
1928
|
+
else
|
|
1929
|
+
bad "проба игнорирует выключатель" "AQK_PROBE=0 не подействовал"
|
|
1930
|
+
fi
|
|
1931
|
+
rm -rf "$CADP" "$CADP2"
|
|
1932
|
+
|
|
1933
|
+
# --- переехавшие проверки: встроенный раннер --------------------------------
|
|
1934
|
+
# ПОЧЕМУ ЗДЕСЬ МОСТ, А НЕ ВТОРАЯ КОМАНДА. Проверки переезжают на `node --test` по одной, и всё
|
|
1935
|
+
# это время у прогона обязана оставаться ОДНА точка входа и ОДИН счётчик: два числа в двух
|
|
1936
|
+
# местах через месяц разойдутся, и никто не заметит, что половина не запускается.
|
|
1937
|
+
#
|
|
1938
|
+
# ЗАЧЕМ ПЕРЕЕЗД. У встроенного раннера есть то, чего нет у этого файла и не появится: свой
|
|
1939
|
+
# каталог и уборка на каждую проверку, таймаут на каждую, точечный перезапуск одной по имени
|
|
1940
|
+
# (`--test-name-pattern`). Диагностика отказа падает с сорока секунд до одной. Проверено на
|
|
1941
|
+
# Node 20.20.2 — той версии, что стоит в конвейере.
|
|
1942
|
+
NODE_SMOKE="$ROOT/tool/selfcheck/smoke"
|
|
1943
|
+
if [ -d "$NODE_SMOKE" ]; then
|
|
1944
|
+
NS_OUT=$(cd "$ROOT" && node --test --test-reporter=tap "$NODE_SMOKE"/*.test.mjs 2>&1)
|
|
1945
|
+
# Разбираем только верхний уровень TAP: вложенные строки идут с отступом.
|
|
1946
|
+
while IFS= read -r NS_LINE; do
|
|
1947
|
+
case "$NS_LINE" in
|
|
1948
|
+
"ok "*) ok "${NS_LINE#*- }" ;;
|
|
1949
|
+
"not ok "*)
|
|
1950
|
+
# ОБЪЯСНЕНИЕ ОБЯЗАНО ДОЕХАТЬ. Первая редакция моста печатала только «не прошло» и совет
|
|
1951
|
+
# запустить вручную — то есть отправляла гадать ровно там, где ответ уже был получен.
|
|
1952
|
+
# Стоило это трёх кругов конвейера: проверка падала только на раннере, а сообщение
|
|
1953
|
+
# утверждения оставалось в выводе, который мост выбрасывал. TAP кладёт его в блок после
|
|
1954
|
+
# строки `not ok`, полем `error:`.
|
|
1955
|
+
# TAP кладёт многострочное сообщение блочным скаляром: строка `error: |-`, а сам текст
|
|
1956
|
+
# идёт следующими строками с отступом. Первая редакция брала только строку `error:` и
|
|
1957
|
+
# печатала «|-» — то есть снова ничего.
|
|
1958
|
+
NS_WHY=$(printf '%s\n' "$NS_OUT" | awk '
|
|
1959
|
+
/^[[:space:]]+error:/ { inerr = 1; sub(/^[[:space:]]*error:[[:space:]]*/, ""); if ($0 != "|-" && $0 != "") print; next }
|
|
1960
|
+
inerr && /^[[:space:]]+(code|stack|failureType|type|duration_ms):/ { inerr = 0 }
|
|
1961
|
+
inerr && /^[[:space:]]*\.\.\.[[:space:]]*$/ { inerr = 0 }
|
|
1962
|
+
inerr { sub(/^[[:space:]]+/, ""); print }
|
|
1963
|
+
' | head -6 | tr '\n' ' ')
|
|
1964
|
+
bad "${NS_LINE#*- }" "${NS_WHY:-подробности: node --test $NODE_SMOKE/*.test.mjs}" ;;
|
|
1965
|
+
esac
|
|
1966
|
+
done <<EOF
|
|
1967
|
+
$(printf '%s\n' "$NS_OUT" | grep -E '^(ok|not ok) ')
|
|
1968
|
+
EOF
|
|
1969
|
+
fi
|
|
1970
|
+
|
|
1789
1971
|
# --- итог -------------------------------------------------------------------
|
|
1790
1972
|
printf '\n'
|
|
1791
1973
|
if [ "$FAIL" -eq 0 ]; then
|
|
@@ -1795,4 +1977,9 @@ else
|
|
|
1795
1977
|
fi
|
|
1796
1978
|
|
|
1797
1979
|
printf ' \033[2mне покрыто: СОДЕРЖАНИЕ документов (сверяется опись и структура, не текст),\n установка с GitHub через npx\033[0m\n\n'
|
|
1980
|
+
|
|
1981
|
+
# ИМЕНА УПАВШИХ — САМОЙ ПОСЛЕДНЕЙ СТРОКОЙ, и это не косметика. Прогон запускают гейтом, а вывод
|
|
1982
|
+
# упавшего гейта обрезается: видно голову и хвост. Значит единственное место, где список
|
|
1983
|
+
# гарантированно доедет до глаз, — конец. Проверено прогоном с нарочно сломанной проверкой.
|
|
1984
|
+
[ "$FAIL" -eq 0 ] || printf ' \033[31mупало: %s\033[0m\n\n' "$FAILED_NAMES"
|
|
1798
1985
|
exit "$FAIL"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Проверки каденции пробы. Написаны ДО кода.
|
|
2
|
+
//
|
|
3
|
+
// ЗАЧЕМ ЭТО ВООБЩЕ. Владелец сформулировал точнее, чем было в замысле: «команду, о которой надо
|
|
4
|
+
// вспомнить, агент не вспомнит, а человек о ней не узнает». Это тот же класс, что файл, который
|
|
5
|
+
// можно не прочитать, — и решать его надо так же: не напоминанием, а тем, что оно случается
|
|
6
|
+
// само. Единица — коммиты, а не сутки: репозиторий, в котором месяц не работали, перепроверять
|
|
7
|
+
// незачем, а сто коммитов за день перепроверить надо.
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { probeDue, probeState, probeEvery, PROBE_EVERY } from "../lib/cadence.mjs";
|
|
11
|
+
|
|
12
|
+
test("порог по умолчанию — сто коммитов, и он назван числом, а не спрятан", () => {
|
|
13
|
+
assert.equal(PROBE_EVERY, 100);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("проба нужна, когда с прошлой прошло не меньше порога", () => {
|
|
17
|
+
assert.equal(probeDue({ at: 100 }, 199, 100), false);
|
|
18
|
+
assert.equal(probeDue({ at: 100 }, 200, 100), true);
|
|
19
|
+
assert.equal(probeDue({ at: 100 }, 350, 100), true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Пробы не было НИКОГДА — это не «свежая». Молчание здесь означало бы «всё прикрыто»,
|
|
23
|
+
// а прикрыто ли — неизвестно.
|
|
24
|
+
test("если пробы не было вовсе — она нужна", () => {
|
|
25
|
+
assert.equal(probeDue(null, 1, 100), true);
|
|
26
|
+
assert.equal(probeDue({}, 500, 100), true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// История короче порога: проба всё равно нужна один раз, иначе новый репозиторий узнает,
|
|
30
|
+
// чего он не видит, только на сто первом коммите.
|
|
31
|
+
test("в молодом репозитории проба нужна сразу, а не после сотого коммита", () => {
|
|
32
|
+
assert.equal(probeDue(null, 3, 100), true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("счётчик коммитов неизвестен — состояние неизвестно, а не «свежо»", () => {
|
|
36
|
+
assert.equal(probeDue({ at: 10 }, null, 100), false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Состояние для человека и для агента: три исхода, и они не сливаются.
|
|
40
|
+
test("состояние пробы: не делалась, устарела, свежая", () => {
|
|
41
|
+
assert.deepEqual(probeState(null, 40, 100), { state: "never", behind: null });
|
|
42
|
+
assert.deepEqual(probeState({ at: 10 }, 40, 100), { state: "fresh", behind: 30 });
|
|
43
|
+
assert.deepEqual(probeState({ at: 10 }, 210, 100), { state: "stale", behind: 200 });
|
|
44
|
+
assert.deepEqual(probeState({ at: 10 }, null, 100), { state: "unknown", behind: null });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Порог — свойство ПРОЕКТА, а не наше: сто коммитов на репозитории с десятком коммитов в час
|
|
48
|
+
// это трижды в день, а на редком проекте столько не наберётся никогда.
|
|
49
|
+
test("порог берётся из манифеста, умолчание остаётся при пустом поле", () => {
|
|
50
|
+
assert.equal(probeEvery({ probe: "250" }), 250);
|
|
51
|
+
assert.equal(probeEvery({ probe: 250 }), 250);
|
|
52
|
+
assert.equal(probeEvery({}), PROBE_EVERY);
|
|
53
|
+
assert.equal(probeEvery(null), PROBE_EVERY);
|
|
54
|
+
assert.equal(probeEvery({ probe: "" }), PROBE_EVERY);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Ноль — это «не делать», а не «делать всегда»: выключатель в манифесте нужен тому, кто не
|
|
58
|
+
// может передать переменную окружения, — например конвейеру чужой площадки.
|
|
59
|
+
test("ноль выключает пробу", () => {
|
|
60
|
+
assert.equal(probeEvery({ probe: 0 }), 0);
|
|
61
|
+
assert.equal(probeDue(null, 5, 0), true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Неразобранное значение обязано быть НАЗВАНО, а не подменено умолчанием: иначе в манифесте
|
|
65
|
+
// написано одно, а происходит другое — ровно та тихая неправда, против которой весь комплект.
|
|
66
|
+
test("непонятое значение порога — null, а не тихое умолчание", () => {
|
|
67
|
+
assert.equal(probeEvery({ probe: "часто" }), null);
|
|
68
|
+
assert.equal(probeEvery({ probe: -5 }), null);
|
|
69
|
+
});
|
|
@@ -186,3 +186,27 @@ test("без --full свод не вливается — только ссылк
|
|
|
186
186
|
assert.doesNotMatch(t, /Правило одно/);
|
|
187
187
|
assert.match(t, /AGENTS\.md/);
|
|
188
188
|
});
|
|
189
|
+
|
|
190
|
+
// --- СОСТОЯНИЯ ПРОБЫ: пять, а веток было три ---------------------------------------------
|
|
191
|
+
// ЗАЧЕМ. Блок читает машина: пустое место и «всё прикрыто» она примет за разрешение. Состояний
|
|
192
|
+
// у пробы пять — `never`, `off`, `unknown`, `stale`, `fresh`, — а печатались три ветки: «не
|
|
193
|
+
// делалась», «есть непрокрытое», иначе «всё прикрыто». В последнюю падали ДВА состояния, где мы
|
|
194
|
+
// как раз ничего не знаем:
|
|
195
|
+
// off — пробу выключили в манифесте (`probe: 0`);
|
|
196
|
+
// unknown — счётчик коммитов не взять либо в манифесте непонятое значение.
|
|
197
|
+
// Обе печатались как «в местах пробы всё прикрыто» — то есть выключенная и несостоявшаяся
|
|
198
|
+
// проверка выдавались за проверку, сказавшую «чисто».
|
|
199
|
+
test("выключенная и несостоявшаяся проба не выдаются за «всё прикрыто»", () => {
|
|
200
|
+
// Якорь берётся из САМОГО текста, а не из представления о нём: первая редакция искала слово
|
|
201
|
+
// «прикрыт», которого в строке про чистую пробу нет, — и краснела на исправном выводе.
|
|
202
|
+
const clean = text({ probe: { state: "fresh", behind: 3, blind: 0 } });
|
|
203
|
+
assert.match(clean, /каждый применимый класс/, "строка про чистую пробу пропала");
|
|
204
|
+
|
|
205
|
+
const off = text({ probe: { state: "off", behind: null, blind: 0 } });
|
|
206
|
+
assert.doesNotMatch(off, /всё прикрыто|прикрыто ничем нет/,
|
|
207
|
+
`выключенная проба выдана за покрытие: ${off}`);
|
|
208
|
+
assert.match(off, /выключен|отключен/i, `не сказано, что проба выключена: ${off}`);
|
|
209
|
+
|
|
210
|
+
const unknown = text({ probe: { state: "unknown", behind: null, blind: 0 } });
|
|
211
|
+
assert.match(unknown, /НЕИЗВЕСТНО|неизвестн/i, `несостоявшаяся проба не названа: ${unknown}`);
|
|
212
|
+
});
|
|
@@ -81,3 +81,37 @@ test("покрытие: файл вне всех целей остаётся н
|
|
|
81
81
|
assert.deepEqual(cov.silent.get("tool/a.js"), ["size"]);
|
|
82
82
|
assert.deepEqual(cov.uncovered, [".github/workflows/ci.yml"]);
|
|
83
83
|
});
|
|
84
|
+
|
|
85
|
+
// --- НАЗВАН ПРОВЕРКОЙ — ЭТО ТОЛЬКО ВЫВОД ------------------------------------------------
|
|
86
|
+
// ЗАЧЕМ. Пути брались из КОМАНДЫ и вывода разом: `pathsIn(cmd + "\n" + out)`. Гейт, не
|
|
87
|
+
// напечатавший ни слова, делал «названным» любой файл, упомянутый в строке запуска. Опыт
|
|
88
|
+
// 2026-09-09: молчащая проверка `sh checks/quiet.sh .` дала
|
|
89
|
+
// названо проверкой: [ 'checks/quiet.sh' ]
|
|
90
|
+
// Так «доказывалась» у нас каждая правка `kit/gates/*/check.sh` и `tool/selfcheck/*.sh` —
|
|
91
|
+
// то есть самих проверок, где правда важнее всего.
|
|
92
|
+
//
|
|
93
|
+
// Но и выбросить команду нельзя: она отвечает на ДРУГОЙ вопрос — куда гейт был направлен.
|
|
94
|
+
// Файл, названный в команде и не упомянутый в выводе, — это «обойдён молча», а не «никто не
|
|
95
|
+
// смотрел»: разница между «просмотрен и чист» и «не просмотрен» и есть предмет этого модуля.
|
|
96
|
+
test("молчащий гейт не «называет» файл из своей команды, но и не теряет его", async () => {
|
|
97
|
+
const { coverage } = await import("../lib/evidence.mjs");
|
|
98
|
+
const results = [{ name: "тихий", cmd: "sh checks/quiet.sh .", out: "" }];
|
|
99
|
+
const r = coverage(["checks/quiet.sh"], results, () => false);
|
|
100
|
+
assert.deepEqual([...r.covered.keys()], [], "путь из команды выдан за находку проверки");
|
|
101
|
+
assert.deepEqual([...r.silent.keys()], ["checks/quiet.sh"], "файл, куда гейт направлен, потерян");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("гейт, направленный на конкретный файл, накрывает именно его", async () => {
|
|
105
|
+
const { coverage } = await import("../lib/evidence.mjs");
|
|
106
|
+
const results = [{ name: "линт", cmd: "eslint src/a.js", out: "" }];
|
|
107
|
+
const r = coverage(["src/a.js", "src/b.js"], results, () => false);
|
|
108
|
+
assert.deepEqual([...r.silent.keys()], ["src/a.js"]);
|
|
109
|
+
assert.deepEqual(r.uncovered, ["src/b.js"]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("напечатанный путь по-прежнему означает «названо»", async () => {
|
|
113
|
+
const { coverage } = await import("../lib/evidence.mjs");
|
|
114
|
+
const results = [{ name: "линт", cmd: "eslint .", out: "src/a.js:3: находка" }];
|
|
115
|
+
const r = coverage(["src/a.js"], results, () => false);
|
|
116
|
+
assert.deepEqual([...r.covered.keys()], ["src/a.js"]);
|
|
117
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// tool/selfcheck/units-execution.mjs — исход ЗАПУСКА процесса, отдельно от смысла находки.
|
|
2
|
+
// Написаны ДО реализации.
|
|
3
|
+
//
|
|
4
|
+
// ЗАЧЕМ. `prove.mjs` считал находкой ЛЮБОЙ ненулевой код на красном образце. Опыт 2026-09-09:
|
|
5
|
+
// проверка, которая ВИСНЕТ на красном образце и молчит на зелёном, получала вердикт
|
|
6
|
+
// `proven: 1, ok: true` — то есть зависший гейт объявлялся ловящим брак. Это `pytest || true`
|
|
7
|
+
// в функции, которая считает наш главный уровень.
|
|
8
|
+
//
|
|
9
|
+
// ПОЧЕМУ НЕЛЬЗЯ ОДНИМ ПРАВИЛОМ «1 — находка, 2+ — сбой». Замер по настоящим инструментам:
|
|
10
|
+
// ruff чисто 0 · находка 1 (и синтаксическая ошибка тоже 1) · неверный флаг 2
|
|
11
|
+
// · НЕСУЩЕСТВУЮЩИЙ ПУТЬ → 0 и «All checks passed!»
|
|
12
|
+
// eslint чисто 0 · находка 1 · настройка или внутренняя ошибка 2
|
|
13
|
+
// vulture чисто 0 · НАХОДКА 3 · плохой ввод 1 · ошибка CLI 2
|
|
14
|
+
// pylint чисто 0 · находка битовой маской (2 ошибка, 4 предупреждение, 8, 16)
|
|
15
|
+
// · ОШИБКА ВЫЗОВА 32
|
|
16
|
+
// Общее правило переврало бы vulture и pylint в обе стороны. Значит знание о кодах живёт
|
|
17
|
+
// рядом с инструментом — нормализующим адаптером, а протокол остаётся простым.
|
|
18
|
+
import test from "node:test";
|
|
19
|
+
import assert from "node:assert/strict";
|
|
20
|
+
import { classify, findingCodes } from "../lib/execution.mjs";
|
|
21
|
+
|
|
22
|
+
// Вход — то, что отдаёт spawnSync: { status, signal, error }.
|
|
23
|
+
const R = (over = {}) => ({ status: 0, signal: null, error: undefined, ...over });
|
|
24
|
+
|
|
25
|
+
test("ноль — чисто, объявленный код находки — находка", () => {
|
|
26
|
+
assert.equal(classify(R({ status: 0 })).state, "clean");
|
|
27
|
+
assert.equal(classify(R({ status: 1 })).state, "finding");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Убитый по таймауту процесс отдаёт status null. Прежний код превращал его в 124 и считал
|
|
31
|
+
// находкой — а сам же комментарий рядом называл это «не знаем».
|
|
32
|
+
test("таймаут — не находка, а сбой инструмента", () => {
|
|
33
|
+
const r = classify(R({ status: null, signal: "SIGTERM" }));
|
|
34
|
+
assert.equal(r.state, "infra_error");
|
|
35
|
+
assert.equal(r.reason, "timeout");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("убийство сигналом — сбой инструмента", () => {
|
|
39
|
+
const r = classify(R({ status: null, signal: "SIGKILL" }));
|
|
40
|
+
assert.equal(r.state, "infra_error");
|
|
41
|
+
assert.match(r.reason, /timeout|signal/);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Процесс не запустился вовсе: нет программы, нет прав, нет каталога.
|
|
45
|
+
// Node при истечении срока ставит И signal, И error с кодом ETIMEDOUT. Если смотреть на
|
|
46
|
+
// error первым и не различать его код, таймаут называется «ошибкой запуска»: состояние верное,
|
|
47
|
+
// причина ложная. Поймано первым же контрпримером после правки — вердикт стал верным, а
|
|
48
|
+
// объяснение врало.
|
|
49
|
+
test("таймаут не выдаётся за ошибку запуска: у Node при сроке стоит и signal, и error", () => {
|
|
50
|
+
const timedOut = { status: null, signal: "SIGTERM",
|
|
51
|
+
error: Object.assign(new Error("spawnSync /bin/sh ETIMEDOUT"), { code: "ETIMEDOUT" }) };
|
|
52
|
+
assert.equal(classify(timedOut).reason, "timeout");
|
|
53
|
+
const notFound = { status: null, signal: null,
|
|
54
|
+
error: Object.assign(new Error("spawnSync ENOENT"), { code: "ENOENT" }) };
|
|
55
|
+
assert.equal(classify(notFound).reason, "spawn_error");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("ошибка запуска — сбой инструмента, а не находка", () => {
|
|
59
|
+
const r = classify(R({ status: null, signal: null, error: new Error("spawn ENOENT") }));
|
|
60
|
+
assert.equal(r.state, "infra_error");
|
|
61
|
+
assert.equal(r.reason, "spawn_error");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("неожиданный код — сбой инструмента, а не находка", () => {
|
|
65
|
+
const r = classify(R({ status: 2 }));
|
|
66
|
+
assert.equal(r.state, "infra_error");
|
|
67
|
+
assert.equal(r.reason, "unexpected_exit");
|
|
68
|
+
assert.equal(r.code, 2);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Знание о кодах живёт рядом с инструментом. Числа — из замера, а не из памяти.
|
|
72
|
+
test("адаптеры знают свои коды: vulture 3 — находка, pylint 32 — ошибка вызова", () => {
|
|
73
|
+
assert.equal(classify(R({ status: 3 }), findingCodes("vulture")).state, "finding");
|
|
74
|
+
assert.equal(classify(R({ status: 1 }), findingCodes("vulture")).state, "infra_error");
|
|
75
|
+
|
|
76
|
+
assert.equal(classify(R({ status: 2 }), findingCodes("pylint")).state, "finding");
|
|
77
|
+
assert.equal(classify(R({ status: 24 }), findingCodes("pylint")).state, "finding");
|
|
78
|
+
assert.equal(classify(R({ status: 32 }), findingCodes("pylint")).state, "infra_error");
|
|
79
|
+
|
|
80
|
+
// Незнакомая программа — умолчание: 0 чисто, 1 находка, остальное сбой. Это ЧЕСТНЕЕ, чем
|
|
81
|
+
// догадка: неизвестный код становится «не знаем», а не «поймал».
|
|
82
|
+
assert.equal(classify(R({ status: 1 }), findingCodes("неизвестный-инструмент")).state, "finding");
|
|
83
|
+
assert.equal(classify(R({ status: 3 }), findingCodes("неизвестный-инструмент")).state, "infra_error");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("наши собственные обёртки: 2 — «нет инструмента», а не находка", () => {
|
|
87
|
+
assert.equal(classify(R({ status: 2 }), findingCodes("bash")).state, "infra_error");
|
|
88
|
+
});
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// node --test tool/selfcheck/units-level.mjs
|
|
10
10
|
import test from "node:test";
|
|
11
11
|
import assert from "node:assert/strict";
|
|
12
|
-
import { commandFor } from "../lib/prove.mjs";
|
|
12
|
+
import { commandFor, verdict } from "../lib/prove.mjs";
|
|
13
13
|
import { assessLevel, layoutChecks, unknownKeys, KNOWN_KEYS, parseManifest, coversOf, coversUnproven, unparsedLines } from "../lib/manifest.mjs";
|
|
14
14
|
import { pickLang, langFromText } from "../i18n/index.mjs";
|
|
15
15
|
|
|
@@ -266,3 +266,34 @@ test("правильный манифест не порождает жалоб",
|
|
|
266
266
|
test("комментарии и пустые строки не считаются потерянными", () => {
|
|
267
267
|
assert.deepEqual(unparsedLines("# заметка\n\naqk: 1\n # ещё\n"), []);
|
|
268
268
|
});
|
|
269
|
+
|
|
270
|
+
// --- ВЕРДИКТ ДОКАЗАТЕЛЬСТВА: один доказанный гейт не покрывает недоказанный -------------
|
|
271
|
+
// ЗАЧЕМ. Прежнее правило было `broken === 0 && proven > 0`. Оно позволяло ОДНОМУ доказанному
|
|
272
|
+
// гейту компенсировать сколько угодно недоказанных: проект с пятью объявленными проверками,
|
|
273
|
+
// из которых четыре не смогли отработать, получал AQK-2 за счёт пятой. Ступень называется
|
|
274
|
+
// «гейты доказаны», а доказан был один.
|
|
275
|
+
//
|
|
276
|
+
// Различие тонкое и обязательное: «нечем доказывать» бывает ЗАКОННЫМ (нет образцов, нет
|
|
277
|
+
// программы на этой машине, стоит рецепт под другой язык) — такое ступень не отнимает.
|
|
278
|
+
// А «запускали и не смогло отработать» — это сбой, и он ступень отнимает: иначе таймаут
|
|
279
|
+
// арбитра снова становится способом получить зелёное.
|
|
280
|
+
test("сбой арбитра отнимает ступень, законная недоказуемость — нет", () => {
|
|
281
|
+
assert.equal(verdict([{ state: "proven" }]).ok, true);
|
|
282
|
+
assert.equal(verdict([]).ok, false, "доказывать нечего — не доказано");
|
|
283
|
+
assert.equal(verdict([{ state: "unprovable", why: "no-samples" }]).ok, false,
|
|
284
|
+
"ни один гейт не доказан");
|
|
285
|
+
|
|
286
|
+
// Законная недоказуемость рядом с доказанным гейтом ступень не отнимает.
|
|
287
|
+
assert.equal(verdict([
|
|
288
|
+
{ state: "proven" },
|
|
289
|
+
{ state: "unprovable", why: "needs-program" },
|
|
290
|
+
]).ok, true);
|
|
291
|
+
|
|
292
|
+
// А вот сбой арбитра — отнимает, сколько бы соседей ни было доказано.
|
|
293
|
+
assert.equal(verdict([
|
|
294
|
+
{ state: "proven" },
|
|
295
|
+
{ state: "unprovable", why: "infra", reason: "timeout" },
|
|
296
|
+
]).ok, false, "таймаут арбитра компенсирован соседним гейтом");
|
|
297
|
+
|
|
298
|
+
assert.equal(verdict([{ state: "proven" }, { state: "broken" }]).ok, false);
|
|
299
|
+
});
|