agent-quality-kit 0.8.0 → 0.10.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 +154 -12
- package/README.ru.md +185 -27
- package/kit/docs/ai/index.md +1 -0
- package/kit/docs/ai/project-baseline.md +14 -0
- package/kit/docs/api-e2e.md +214 -0
- package/kit/docs/ready-made-rules.md +188 -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/color-from-token/check.sh +5 -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/lesson-has-outcome/check.sh +5 -1
- package/kit/gates/mcp-server-resolves/README.md +62 -0
- package/kit/gates/mcp-server-resolves/check.sh +110 -0
- package/kit/gates/mcp-server-resolves/gate.yml +18 -0
- package/kit/gates/mcp-server-resolves/green/.mcp.json +20 -0
- package/kit/gates/mcp-server-resolves/red/.mcp.json +16 -0
- package/kit/gates/protection-not-removed/README.md +67 -0
- package/kit/gates/protection-not-removed/check.sh +92 -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 +25 -4
- package/package.json +3 -6
- package/tool/commands/context.mjs +37 -6
- package/tool/commands/doctor.mjs +139 -16
- package/tool/commands/probe.mjs +228 -0
- package/tool/commands/project.mjs +18 -2
- package/tool/commands/prove.mjs +1 -0
- package/tool/commands/vitals.mjs +167 -0
- package/tool/i18n/en-docs.mjs +48 -0
- package/tool/i18n/en-gates.mjs +309 -0
- package/tool/i18n/en.mjs +26 -279
- package/tool/i18n/index.mjs +36 -3
- package/tool/i18n/ru-docs.mjs +48 -0
- package/tool/i18n/ru-gates.mjs +311 -0
- package/tool/i18n/ru.mjs +26 -278
- package/tool/lib/banner.mjs +59 -0
- package/tool/lib/brief.mjs +192 -0
- package/tool/lib/cadence.mjs +57 -0
- package/tool/lib/core.mjs +3 -0
- package/tool/lib/history.mjs +82 -0
- package/tool/lib/manifest.mjs +146 -15
- package/tool/lib/prove.mjs +11 -1
- package/tool/lib/repo.mjs +43 -3
- package/tool/program.mjs +33 -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/verdict.test.mjs +40 -0
- package/tool/selfcheck/smoke.sh +528 -6
- package/tool/selfcheck/units-banner.mjs +65 -0
- package/tool/selfcheck/units-brief.mjs +97 -0
- package/tool/selfcheck/units-cadence.mjs +69 -0
- package/tool/selfcheck/units-context.mjs +3 -1
- package/tool/selfcheck/units-level.mjs +147 -1
- package/tool/selfcheck/units-probe.mjs +100 -0
- package/tool/selfcheck/units-repo.mjs +164 -0
- package/tool/selfcheck/units-vitals.mjs +81 -0
- package/tool/selfcheck/units.mjs +3 -75
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,26 @@ 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
|
+
export HOME="$WORK/home"; export USERPROFILE="$HOME"; mkdir -p "$HOME"
|
|
64
|
+
export AQK_UPDATE=0
|
|
65
|
+
|
|
38
66
|
printf '\n\033[1mtool/selfcheck/smoke.sh\033[0m\n\n'
|
|
39
67
|
|
|
40
68
|
# --- 1. синтаксис самой программы ------------------------------------------
|
|
@@ -345,10 +373,24 @@ if [ -f "$BLOBDIR/GOD_AI.md" ]; then
|
|
|
345
373
|
bad "blob собрал не все методички" "в kit/docs $EXPECT_MD, в склейке $GOT_MD"
|
|
346
374
|
fi
|
|
347
375
|
# Ссылки на соседние файлы внутри склейки ведут в никуда: соседей рядом больше нет.
|
|
348
|
-
|
|
376
|
+
#
|
|
377
|
+
# ДВА ПРОХОДА, А НЕ ОПЕРЕЖАЮЩАЯ ПРОВЕРКА. Здесь стояло `grep -qE '...(?!https?:)...'`, и это
|
|
378
|
+
# была ЛОЖЬ: `(?!` — синтаксис PCRE, в POSIX ERE его нет. GNU grep печатает предупреждение и
|
|
379
|
+
# не находит ничего, ugrep падает с ошибкой разбора — в обоих случаях `if` уходит в `else`, и
|
|
380
|
+
# проверка печатала зелёное НА ЛЮБЫХ ДАННЫХ. Ровно тот грех, ради поимки которого написан весь
|
|
381
|
+
# комплект, внутри прибора, который его ищет. Найдено сплошным чтением 2026-09-09.
|
|
382
|
+
#
|
|
383
|
+
# Поэтому же ниже проверяется САМА ПРОВЕРКА: подделываем склейку с относительной ссылкой и
|
|
384
|
+
# требуем, чтобы её нашли. Без этого следующая такая опечатка снова проедет зелёной.
|
|
385
|
+
rel_links() { grep -oE '\]\([^)]+\.md[^)]*\)' "$1" 2>/dev/null | grep -vE '^\]\(https?:' | grep -q .; }
|
|
386
|
+
printf '%s' "$(cat "$BLOBDIR/GOD_AI.md")" > "$BLOBDIR/doctored.md"
|
|
387
|
+
printf '\nсм. [соседний файл](other.md)\n' >> "$BLOBDIR/doctored.md"
|
|
388
|
+
if rel_links "$BLOBDIR/GOD_AI.md"; then
|
|
349
389
|
bad "в склейке остались ссылки на соседние файлы" "внутри одного файла они ведут в никуда"
|
|
390
|
+
elif ! rel_links "$BLOBDIR/doctored.md"; then
|
|
391
|
+
bad "проверка ссылок не может покраснеть" "подделанная склейка с относительной ссылкой прошла"
|
|
350
392
|
else
|
|
351
|
-
ok "ссылки на соседние файлы
|
|
393
|
+
ok "ссылки на соседние файлы сняты, и проверка это умеет заметить"
|
|
352
394
|
fi
|
|
353
395
|
else
|
|
354
396
|
bad "blob не создал GOD_AI.md" "$BLOBDIR"
|
|
@@ -579,7 +621,7 @@ REPDIR="$(mktemp -d)"
|
|
|
579
621
|
(
|
|
580
622
|
cd "$REPDIR" && git init -q . && mkdir -p src &&
|
|
581
623
|
printf 'a = 1 # noqa\n' > src/a.py &&
|
|
582
|
-
node "$CLI" start > /
|
|
624
|
+
node "$CLI" start > "$WORK/start.log" 2>&1
|
|
583
625
|
)
|
|
584
626
|
REP_OUT=$( cd "$REPDIR" && node "$CLI" report 2>&1 ); REP_CODE=$?
|
|
585
627
|
if [ "$REP_CODE" -ne 0 ] && printf '%s' "$REP_OUT" | grep -q '❌ gate-not-weakened'; then
|
|
@@ -591,7 +633,7 @@ else
|
|
|
591
633
|
G_LS=$( cd "$REPDIR" && ls gates 2>&1 | tr '\n' ' ' )
|
|
592
634
|
G_DECL=$( cd "$REPDIR" && sed -n '/^gates:/,$p' .aqk.yml 2>/dev/null | grep -cE '^[[:space:]]+[A-Za-z0-9_-]+:' )
|
|
593
635
|
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 /
|
|
636
|
+
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
637
|
fi
|
|
596
638
|
if [ -f "$REPDIR/.aqk/report.md" ] && grep -q '^## ' "$REPDIR/.aqk/report.md"; then
|
|
597
639
|
ok "report сохраняет .aqk/report.md"
|
|
@@ -1437,6 +1479,481 @@ else
|
|
|
1437
1479
|
fi
|
|
1438
1480
|
rm -rf "$GENP"
|
|
1439
1481
|
|
|
1482
|
+
# --- 95. covers: запись закрыта другим арбитром, но только объявленным ---------
|
|
1483
|
+
# Просьба первого чужого пользователя, названная им первой: у него complexity-limit,
|
|
1484
|
+
# no-print-in-prod и swallowed-error держит biome, а doctor каждый прогон печатал «применимо,
|
|
1485
|
+
# но не поставлено: 5» — неправду. Неправда в СОБСТВЕННОМ выводе дороже всех прочих: весь
|
|
1486
|
+
# стандарт стоит на том, что вывод не врёт. Но поле не признание на слово: гейт, который
|
|
1487
|
+
# «закрывает», обязан быть объявлен в gates:, иначе covers становится способом объявить защиту,
|
|
1488
|
+
# которой нет. Проверяются оба конца сразу.
|
|
1489
|
+
COVP="$(mktemp -d)"
|
|
1490
|
+
( cd "$COVP" && git init -q . && mkdir -p src rules &&
|
|
1491
|
+
printf 'const x=1;\n' > src/a.ts && printf '# вход\n' > AGENTS.md &&
|
|
1492
|
+
printf 'правило\n' > rules/r.md && printf '.x\n' > .gitignore &&
|
|
1493
|
+
printf 'aqk: 1\nentry: [AGENTS.md]\nrules: rules\ngates:\n lint: "true"\ncovers:\n lint: [no-print-in-prod, swallowed-error]\n biome: [duplicate-code]\n' > .aqk.yml ) >/dev/null 2>&1
|
|
1494
|
+
COV=$( cd "$COVP" && AQK_LANG=ru node "$CLI" doctor 2>&1 )
|
|
1495
|
+
if printf '%s' "$COV" | grep -q "закрыто другим арбитром 2" &&
|
|
1496
|
+
printf '%s' "$COV" | grep -q "covers называет гейт, которого нет" &&
|
|
1497
|
+
printf '%s' "$COV" | grep -q "biome" &&
|
|
1498
|
+
! printf '%s' "$COV" | grep -q "неизвестное поле"; then
|
|
1499
|
+
ok "covers снимает запись с долга — и только когда закрывающий гейт объявлен"
|
|
1500
|
+
else
|
|
1501
|
+
bad "covers посчитан неверно" "$(printf '%s' "$COV" | grep -E 'Итого|covers' | head -3)"
|
|
1502
|
+
fi
|
|
1503
|
+
rm -rf "$COVP"
|
|
1504
|
+
|
|
1505
|
+
# --- 96. язык вывода — настройка проекта, а не машины -------------------------
|
|
1506
|
+
# Просьба первого чужого пользователя: «язык берётся из LC_ALL/LANG, а на Windows их просто нет:
|
|
1507
|
+
# русский проект получает английский вывод». Проверяются оба конца порядка сразу: манифест
|
|
1508
|
+
# сильнее локали, но переменная окружения сильнее манифеста — иначе у человека, набравшего
|
|
1509
|
+
# AQK_LANG=en руками, не остаётся способа получить английский.
|
|
1510
|
+
LNGP="$(mktemp -d)"
|
|
1511
|
+
( cd "$LNGP" && git init -q . && printf '# вход\n' > AGENTS.md &&
|
|
1512
|
+
printf 'aqk: 1\nlang: ru\nentry: [AGENTS.md]\n' > .aqk.yml ) >/dev/null 2>&1
|
|
1513
|
+
BY_MAN=$( cd "$LNGP" && env -u AQK_LANG LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 node "$CLI" context 2>&1 | head -1 )
|
|
1514
|
+
BY_ENV=$( cd "$LNGP" && AQK_LANG=en node "$CLI" context 2>&1 | head -1 )
|
|
1515
|
+
if printf '%s' "$BY_MAN" | grep -q "состояние этого репозитория" &&
|
|
1516
|
+
printf '%s' "$BY_ENV" | grep -q "the state of this repository"; then
|
|
1517
|
+
ok "язык берётся из манифеста поверх локали, а переменная окружения — поверх манифеста"
|
|
1518
|
+
else
|
|
1519
|
+
bad "порядок выбора языка нарушен" "по манифесту: $BY_MAN | по окружению: $BY_ENV"
|
|
1520
|
+
fi
|
|
1521
|
+
rm -rf "$LNGP"
|
|
1522
|
+
|
|
1523
|
+
# --- 97. совет про браузер у агента — только тому, кому он нужен --------------
|
|
1524
|
+
# Решение владельца 2026-09-08: инструмент, дающий агенту браузер, надо рекомендовать. Но совет,
|
|
1525
|
+
# показанный не тому, стоит доверия всем остальным советам — та же норма, что у записей каталога.
|
|
1526
|
+
# Проверяются все три конца: интерфейс есть и сервера нет → совет; интерфейса нет → тишина;
|
|
1527
|
+
# сервер уже стоит → тишина.
|
|
1528
|
+
BRWP="$(mktemp -d)"
|
|
1529
|
+
( cd "$BRWP" && git init -q . && mkdir -p src && printf 'body{color:#fff}\n' > src/a.css &&
|
|
1530
|
+
printf 'export const A=1\n' > src/a.tsx && printf '# вход\n' > AGENTS.md ) >/dev/null 2>&1
|
|
1531
|
+
WITH_UI=$( cd "$BRWP" && AQK_LANG=ru node "$CLI" doctor 2>&1 )
|
|
1532
|
+
( cd "$BRWP" && printf '{"mcpServers":{"b":{"command":"npx","args":["-y","chrome-devtools-mcp@1.9.0"]}}}\n' > .mcp.json )
|
|
1533
|
+
WITH_SRV=$( cd "$BRWP" && AQK_LANG=ru node "$CLI" doctor 2>&1 )
|
|
1534
|
+
NOUIP="$(mktemp -d)"
|
|
1535
|
+
( cd "$NOUIP" && git init -q . && printf 'print(1)\n' > a.py && printf '# вход\n' > AGENTS.md ) >/dev/null 2>&1
|
|
1536
|
+
NO_UI=$( cd "$NOUIP" && AQK_LANG=ru node "$CLI" doctor 2>&1 )
|
|
1537
|
+
if printf '%s' "$WITH_UI" | grep -q "нет браузера" &&
|
|
1538
|
+
! printf '%s' "$WITH_SRV" | grep -q "нет браузера" &&
|
|
1539
|
+
! printf '%s' "$NO_UI" | grep -q "нет браузера"; then
|
|
1540
|
+
ok "совет про браузер даётся проекту с интерфейсом и молчит, когда сервер уже есть"
|
|
1541
|
+
else
|
|
1542
|
+
bad "совет про браузер показан не тому" "с ui: $(printf '%s' "$WITH_UI" | grep -c 'нет браузера'), с сервером: $(printf '%s' "$WITH_SRV" | grep -c 'нет браузера'), без ui: $(printf '%s' "$NO_UI" | grep -c 'нет браузера')"
|
|
1543
|
+
fi
|
|
1544
|
+
rm -rf "$BRWP" "$NOUIP"
|
|
1545
|
+
|
|
1546
|
+
# --- 98. заявка covers сверяется, а не принимается на слово -------------------
|
|
1547
|
+
# Поле covers завели утром того же дня, и тогда же честно записали: оно снимает запись с долга
|
|
1548
|
+
# ПО СЛОВУ человека. К вечеру это перестало быть теорией: запуск на настоящем ruff.toml из живого
|
|
1549
|
+
# проекта показал девятнадцать групп правил в extend-select и НЕ пойманный print() — группы T20
|
|
1550
|
+
# среди них нет. Заявка «no-print-in-prod держит наш lint» была бы ложной, а запись ушла бы из
|
|
1551
|
+
# долга. Поле, снимающее неправду из вывода, само стало бы способом её произвести.
|
|
1552
|
+
CVUP="$(mktemp -d)"
|
|
1553
|
+
( cd "$CVUP" && git init -q . && printf 'x=1\n' > a.py && printf '# вход\n' > AGENTS.md &&
|
|
1554
|
+
printf 'aqk: 1\nentry: [AGENTS.md]\ngates:\n lint: "ruff check ."\ncovers:\n lint: [no-print-in-prod]\n' > .aqk.yml ) >/dev/null 2>&1
|
|
1555
|
+
( cd "$CVUP" && printf 'extend-select = ["I","B","UP","SIM"]\n' > ruff.toml )
|
|
1556
|
+
WITHOUT=$( cd "$CVUP" && AQK_LANG=ru node "$CLI" doctor 2>&1 )
|
|
1557
|
+
( cd "$CVUP" && printf 'extend-select = ["I","B","UP","SIM","T20"]\n' > ruff.toml )
|
|
1558
|
+
WITHT20=$( cd "$CVUP" && AQK_LANG=ru node "$CLI" doctor 2>&1 )
|
|
1559
|
+
if printf '%s' "$WITHOUT" | grep -q "заявка не подтверждена" &&
|
|
1560
|
+
printf '%s' "$WITHOUT" | grep -q "T20" &&
|
|
1561
|
+
! printf '%s' "$WITHT20" | grep -q "заявка не подтверждена"; then
|
|
1562
|
+
ok "covers сверяется с кодами правил: без T20 говорит вслух, с T20 молчит"
|
|
1563
|
+
else
|
|
1564
|
+
bad "сверка заявки covers не работает" "без T20: $(printf '%s' "$WITHOUT" | grep -c 'не подтверждена'), с T20: $(printf '%s' "$WITHT20" | grep -c 'не подтверждена')"
|
|
1565
|
+
fi
|
|
1566
|
+
rm -rf "$CVUP"
|
|
1567
|
+
|
|
1568
|
+
# --- 99. краткий режим: присутствие видно, совет не надоедает -----------------
|
|
1569
|
+
# Хук pre-commit молчит на успехе — это его умолчание. Комплект, который всё держит, для
|
|
1570
|
+
# человека неотличим от невставленного: «скачал и че дальше» — дословная жалоба владельца.
|
|
1571
|
+
# Это ровно тот порок, против которого написан комплект, только у нас самих.
|
|
1572
|
+
# Проверяются три вещи сразу: строка есть всегда; совет приходит один раз, а не на каждый
|
|
1573
|
+
# прогон; переменная его выключает.
|
|
1574
|
+
BRFP="$(mktemp -d)"
|
|
1575
|
+
( cd "$BRFP" && git init -q . && printf 'x=1\n' > a.py && printf '# вход\n' > AGENTS.md &&
|
|
1576
|
+
printf '.x\n' > .gitignore && mkdir -p rules .aqk/docs && printf 'п\n' > rules/r.md &&
|
|
1577
|
+
printf 'aqk: 1\nentry: [AGENTS.md]\nrules: rules\ngates:\n ok: "true"\n' > .aqk.yml ) >/dev/null 2>&1
|
|
1578
|
+
B1=$( cd "$BRFP" && AQK_LANG=ru node "$CLI" doctor --run --min 1 --brief 2>&1 ); B1C=$?
|
|
1579
|
+
B2=$( cd "$BRFP" && AQK_LANG=ru node "$CLI" doctor --run --min 1 --brief 2>&1 )
|
|
1580
|
+
rm -f "$BRFP/.aqk/advice-shown"
|
|
1581
|
+
B3=$( cd "$BRFP" && AQK_ADVICE=0 AQK_LANG=ru node "$CLI" doctor --run --min 1 --brief 2>&1 )
|
|
1582
|
+
if [ "$B1C" -eq 0 ] &&
|
|
1583
|
+
printf '%s' "$B1" | grep -qE "^(❖ )?AQK держит" &&
|
|
1584
|
+
printf '%s' "$B1" | grep -q "поставить:" &&
|
|
1585
|
+
printf '%s' "$B2" | grep -qE "^(❖ )?AQK держит" &&
|
|
1586
|
+
! printf '%s' "$B2" | grep -q "поставить:" &&
|
|
1587
|
+
! printf '%s' "$B3" | grep -q "поставить:" &&
|
|
1588
|
+
[ "$(printf '%s\n' "$B2" | wc -l)" -le 2 ]; then
|
|
1589
|
+
ok "краткий режим: строка есть всегда, совет один раз в сутки и выключается"
|
|
1590
|
+
else
|
|
1591
|
+
bad "краткий режим ведёт себя не так" "первый: $(printf '%s' "$B1" | head -1) · второй строк: $(printf '%s\n' "$B2" | wc -l)"
|
|
1592
|
+
fi
|
|
1593
|
+
rm -rf "$BRFP"
|
|
1594
|
+
|
|
1595
|
+
# --- 100. в awk-программах гейтов нет повторителей {n} ------------------------
|
|
1596
|
+
# Часть сборок mawk не понимает повторители в регулярках. Условие с {4} не
|
|
1597
|
+
# совпадает НИКОГДА, и гейт молча выходит с нулём на собственном красном образце: ровно тот
|
|
1598
|
+
# отказ, против которого написан комплект. Поймано сборкой docker-образа 2026-09-08 — на хосте
|
|
1599
|
+
# gawk, и ни один прогон не краснел. Проверка статическая и потому дешёвая; настоящий прогон
|
|
1600
|
+
# под старым awk делает задание `image` в конвейере — там образ, в котором дефект и нашёлся.
|
|
1601
|
+
# ВАЖНО, ЧТО ИМЕННО СБОРКА: mawk 1.3.4 20240123 (ubuntu-latest, эта машина) повторители
|
|
1602
|
+
# ПОНИМАЕТ, mawk 1.3.4 20200120 (node:22-slim) — нет. Замерено 2026-09-09. Поэтому «прогнать
|
|
1603
|
+
# под mawk» ничего не значит без указания, под каким,
|
|
1604
|
+
# а прочитать исходники — можем.
|
|
1605
|
+
AWKBAD=""
|
|
1606
|
+
for G in "$ROOT"/kit/gates/*/check.sh "$ROOT"/kit/gates/_skip.sh; do
|
|
1607
|
+
[ -f "$G" ] || continue
|
|
1608
|
+
# Только строки внутри awk-программ; комментарии оболочки (#) не считаем.
|
|
1609
|
+
# Только регулярки AWK: `t ~ /…/` и образец-действие `/…/ {`. Шаблоны для grep, лежащие в
|
|
1610
|
+
# переменных оболочки, сюда не попадают намеренно — GNU grep и busybox grep повторители
|
|
1611
|
+
# понимают оба, и краснеть на них значит краснеть на нормальном укладе.
|
|
1612
|
+
HIT=$(sed 's/^[[:space:]]*#.*$//' "$G" | grep -nE '(~[[:space:]]*/|^[[:space:]]*/)' \
|
|
1613
|
+
| grep -E '\{[0-9]+(,[0-9]*)?\}' | head -2)
|
|
1614
|
+
[ -n "$HIT" ] && AWKBAD="${AWKBAD:+$AWKBAD; }$(basename "$(dirname "$G")"): $(printf '%s' "$HIT" | head -1 | cut -c1-60)"
|
|
1615
|
+
done
|
|
1616
|
+
if [ -z "$AWKBAD" ]; then
|
|
1617
|
+
ok "в проверках каталога нет повторителей {n} — они не работают под mawk"
|
|
1618
|
+
else
|
|
1619
|
+
bad "повторитель {n} в проверке: под mawk она молчит" "$AWKBAD"
|
|
1620
|
+
fi
|
|
1621
|
+
|
|
1622
|
+
# --- 101. проверка версии: раз в сутки, не в конвейере, молча при отказе ------
|
|
1623
|
+
# До этой строки комплект не делал ни одного исходящего запроса. Раз делает — обязан делать
|
|
1624
|
+
# ровно то, что о нём написано: спрашивать реестр не чаще раза в сутки и НЕ спрашивать в
|
|
1625
|
+
# конвейере. Само наличие новой версии здесь не проверяется — она зависит от реестра, а
|
|
1626
|
+
# проверка, зависящая от чужого сервера, краснеет по чужой воле. Сравнение версий проверено
|
|
1627
|
+
# модульно; здесь — то, что вокруг него.
|
|
1628
|
+
UPDP="$(mktemp -d)"
|
|
1629
|
+
( cd "$UPDP" && git init -q . && printf 'x=1\n' > a.py && printf '# вход\n' > AGENTS.md &&
|
|
1630
|
+
printf '.x\n' > .gitignore && mkdir -p rules .aqk/docs && printf 'п\n' > rules/r.md &&
|
|
1631
|
+
printf 'aqk: 1\nentry: [AGENTS.md]\nrules: rules\ngates:\n ok: "true"\n' > .aqk.yml ) >/dev/null 2>&1
|
|
1632
|
+
( cd "$UPDP" && env -u CI -u GITHUB_ACTIONS AQK_LANG=ru node "$CLI" doctor --run --min 1 --brief ) >/dev/null 2>&1
|
|
1633
|
+
ASKED=$([ -f "$UPDP/.aqk/update-checked" ] && echo да || echo нет)
|
|
1634
|
+
rm -f "$UPDP/.aqk/update-checked"
|
|
1635
|
+
( cd "$UPDP" && CI=true AQK_LANG=ru node "$CLI" doctor --run --min 1 --brief ) >/dev/null 2>&1
|
|
1636
|
+
IN_CI=$([ -f "$UPDP/.aqk/update-checked" ] && echo да || echo нет)
|
|
1637
|
+
rm -f "$UPDP/.aqk/update-checked"
|
|
1638
|
+
( cd "$UPDP" && env -u CI AQK_UPDATE=0 AQK_LANG=ru node "$CLI" doctor --run --min 1 --brief ) >/dev/null 2>&1
|
|
1639
|
+
OFF=$([ -f "$UPDP/.aqk/update-checked" ] && echo да || echo нет)
|
|
1640
|
+
# «нет сети» — тоже допустимый исход первого случая: молчание при отказе и есть требование.
|
|
1641
|
+
if [ "$IN_CI" = "нет" ] && [ "$OFF" = "нет" ]; then
|
|
1642
|
+
ok "версия спрашивается вне конвейера ($ASKED), не спрашивается в конвейере и при AQK_UPDATE=0"
|
|
1643
|
+
else
|
|
1644
|
+
bad "проверка версии спрашивает там, где не должна" "вне CI: $ASKED, в CI: $IN_CI, выключено: $OFF"
|
|
1645
|
+
fi
|
|
1646
|
+
rm -rf "$UPDP"
|
|
1647
|
+
|
|
1648
|
+
# --- 102. строка манифеста, которую разбор не понял, называется вслух ---------
|
|
1649
|
+
# Найдено случайно 2026-09-09: подсадили падающий гейт с именем «плохой», чтобы посмотреть на
|
|
1650
|
+
# вывод, — прогон вышел с НУЛЁМ. Гейт не упал: его не существовало. Разбор берёт имена только
|
|
1651
|
+
# латиницей, а строку, не подошедшую под это, выбрасывал без единого слова. Человек видит
|
|
1652
|
+
# проверку в файле, а её нет — наш класс в чистом виде, и хуже опечатки в имени поля: ту мы
|
|
1653
|
+
# называем с 2026-09-06, а эту не называли вовсе.
|
|
1654
|
+
UNPP="$(mktemp -d)"
|
|
1655
|
+
( cd "$UNPP" && git init -q . && printf '# вход\n' > AGENTS.md &&
|
|
1656
|
+
printf 'aqk: 1\nentry: [AGENTS.md]\nlang: ru\ngates:\n ok: "true"\n плохой: "false"\n' > .aqk.yml ) >/dev/null 2>&1
|
|
1657
|
+
UNP=$( cd "$UNPP" && node "$CLI" doctor 2>&1 )
|
|
1658
|
+
( cd "$UNPP" && printf 'aqk: 1\nentry: [AGENTS.md]\nlang: ru\ngates:\n ok: "true"\n bad: "false"\n' > .aqk.yml )
|
|
1659
|
+
OKM=$( cd "$UNPP" && node "$CLI" doctor 2>&1 )
|
|
1660
|
+
if printf '%s' "$UNP" | grep -q "НЕ ДЕЙСТВУЕТ" &&
|
|
1661
|
+
printf '%s' "$UNP" | grep -q "строка 6" &&
|
|
1662
|
+
! printf '%s' "$OKM" | grep -q "НЕ ДЕЙСТВУЕТ"; then
|
|
1663
|
+
ok "непонятая строка манифеста называется с номером, понятая — молчит"
|
|
1664
|
+
else
|
|
1665
|
+
bad "потерянная строка манифеста не названа" "$(printf '%s' "$UNP" | grep -c 'НЕ ДЕЙСТВУЕТ') на кириллице, $(printf '%s' "$OKM" | grep -c 'НЕ ДЕЙСТВУЕТ') на латинице"
|
|
1666
|
+
fi
|
|
1667
|
+
rm -rf "$UNPP"
|
|
1668
|
+
|
|
1669
|
+
# --- 103. vitals: отказ роняет, выбор и незнание — нет ------------------------
|
|
1670
|
+
# `doctor` смотрит на репозиторий, `prove` — на гейты, `context` — на состояние. На саму
|
|
1671
|
+
# обвязку не смотрел никто: стоят ли инструменты объявленных гейтов, прописан ли хук в
|
|
1672
|
+
# .git/hooks НА САМОМ ДЕЛЕ. Раньше это выяснялось красным гейтом посреди коммита.
|
|
1673
|
+
# Первая версия ставила крест хуку, которого нет, — и на нашем же репозитории вышло два креста
|
|
1674
|
+
# за сознательное решение (pre-commit локально не ставим, гоняем в CI). Команда, которая кричит
|
|
1675
|
+
# «сломано» про выбор, перестаёт читаться вместе с настоящими отказами. Проверяются оба конца.
|
|
1676
|
+
VITP="$(mktemp -d)"
|
|
1677
|
+
( cd "$VITP" && git init -q . && printf '# вход\n' > AGENTS.md &&
|
|
1678
|
+
printf 'aqk: 1\nentry: [AGENTS.md]\nlang: ru\ngates:\n ok: "true"\n' > .aqk.yml ) >/dev/null 2>&1
|
|
1679
|
+
VOK=$( cd "$VITP" && AQK_LANG=ru node "$CLI" vitals 2>&1 ); VOK_C=$?
|
|
1680
|
+
( cd "$VITP" && printf 'aqk: 1\nentry: [AGENTS.md]\nlang: ru\ngates:\n lint: "инструментакоторогонет ."\n' > .aqk.yml )
|
|
1681
|
+
VBAD=$( cd "$VITP" && AQK_LANG=ru node "$CLI" vitals 2>&1 ); VBAD_C=$?
|
|
1682
|
+
if [ "$VOK_C" -eq 0 ] && [ "$VBAD_C" -eq 1 ] &&
|
|
1683
|
+
printf '%s' "$VBAD" | grep -q "НЕ НАЙДЕНЫ" &&
|
|
1684
|
+
printf '%s' "$VOK" | grep -q "хук pre-commit"; then
|
|
1685
|
+
ok "vitals: пропавший инструмент роняет прогон, отсутствие хука — нет"
|
|
1686
|
+
else
|
|
1687
|
+
bad "vitals путает отказ с выбором" "исправный код $VOK_C, сломанный код $VBAD_C"
|
|
1688
|
+
fi
|
|
1689
|
+
rm -rf "$VITP"
|
|
1690
|
+
|
|
1691
|
+
# --- 106. совещательный гейт назван всегда и не роняет прогон никогда ---------
|
|
1692
|
+
# Два отказа, найденные 2026-09-09 сверкой документации с кодом. ПЕРВЫЙ: зелёный совещательный
|
|
1693
|
+
# печатался обычной галочкой — гейт, который уронить сборку НЕ МОЖЕТ, по выводу неотличим от
|
|
1694
|
+
# того, который может, и список `advisory:` был виден только в день, когда он покраснел. Ровно
|
|
1695
|
+
# та тишина, против которой написан стандарт, только про сам прибор. ВТОРОЙ: при `--since` гейт,
|
|
1696
|
+
# чей вывод не содержит путей, шёл через ветку «сузить нечем» с безусловным failed++ — то есть
|
|
1697
|
+
# совещательный ронял прогон. Проверено сравнением с кодом до починки: было 1, стало 0.
|
|
1698
|
+
# Команда гейта — ФАЙЛ, а не строка с `;`. На windows `spawnSync(shell: true)` запускает
|
|
1699
|
+
# cmd.exe, а не sh: там `echo x; exit 1` печатается целиком и выходит с НУЛЁМ. Первая версия
|
|
1700
|
+
# этой проверки так и провалилась в конвейере на windows — фикстура, а не код. Заодно урок про
|
|
1701
|
+
# сам тест: утверждение «зелёный помечен» искало подстроку «совещательный», которая есть и в
|
|
1702
|
+
# пометке КРАСНОГО совещательного, — то есть проходило бы и при полном отсутствии починки.
|
|
1703
|
+
# Теперь ищется текст, который бывает только на зелёной строке.
|
|
1704
|
+
ADVP="$(mktemp -d)"
|
|
1705
|
+
(
|
|
1706
|
+
cd "$ADVP" && git init -q . && git config user.email a@b && git config user.name a
|
|
1707
|
+
printf '# вход\n' > AGENTS.md && mkdir -p r && printf 'x\n' > r/a.md
|
|
1708
|
+
printf '#!/bin/sh\necho "вердикт без путей"\nexit 1\n' > noscope.sh
|
|
1709
|
+
printf '#!/bin/sh\nexit 0\n' > quiet.sh
|
|
1710
|
+
printf 'aqk: 1\nentry: [AGENTS.md]\nrules: r\ngates:\n adv: "bash quiet.sh"\nadvisory:\n - adv\n' > .aqk.yml
|
|
1711
|
+
git add -A && git commit -qm "Сделано: основа. Не уверен: ничего"
|
|
1712
|
+
) >/dev/null 2>&1
|
|
1713
|
+
ADVBASE=$( cd "$ADVP" && git rev-parse --abbrev-ref HEAD )
|
|
1714
|
+
( cd "$ADVP" && git checkout -qb feat && printf 'y\n' > r/b.md && git add -A &&
|
|
1715
|
+
git commit -qm "Сделано: файл. Не уверен: ничего" ) >/dev/null 2>&1
|
|
1716
|
+
ADVGREEN=$( cd "$ADVP" && AQK_LANG=ru node "$CLI" doctor --run --min 1 2>&1 )
|
|
1717
|
+
( cd "$ADVP" && printf 'aqk: 1\nentry: [AGENTS.md]\nrules: r\ngates:\n adv: "bash noscope.sh"\nadvisory:\n - adv\n' > .aqk.yml )
|
|
1718
|
+
( cd "$ADVP" && node "$CLI" doctor --run --min 1 --since "$ADVBASE" >/dev/null 2>&1 ); ADV_C=$?
|
|
1719
|
+
( cd "$ADVP" && printf 'aqk: 1\nentry: [AGENTS.md]\nrules: r\ngates:\n adv: "bash noscope.sh"\n' > .aqk.yml )
|
|
1720
|
+
( cd "$ADVP" && node "$CLI" doctor --run --min 1 --since "$ADVBASE" >/dev/null 2>&1 ); BLOCK_C=$?
|
|
1721
|
+
if printf '%s' "$ADVGREEN" | grep -q "уронить прогон не может" && [ "$ADV_C" -eq 0 ] && [ "$BLOCK_C" -eq 1 ]; then
|
|
1722
|
+
ok "совещательный назван и на зелёном, и не роняет прогон даже когда сузить нечем"
|
|
1723
|
+
else
|
|
1724
|
+
bad "совещательный гейт неотличим или роняет прогон" "зелёный помечен: $(printf '%s' "$ADVGREEN" | grep -c 'уронить прогон не может'), код совещательного $ADV_C, код блокирующего $BLOCK_C"
|
|
1725
|
+
fi
|
|
1726
|
+
rm -rf "$ADVP"
|
|
1727
|
+
|
|
1728
|
+
# --- 107. prove пропускает запись без её программы, а не обвиняет её ----------
|
|
1729
|
+
# ТО ЖЕ, ЧТО НАШЁЛ ВТОРОЙ ПОЛЬЗОВАТЕЛЬ, ТОЛЬКО ДРУГОЙ ДВЕРЬЮ. Переносимый рецепт бывает обёрткой
|
|
1730
|
+
# вокруг готового инструмента: первое слово команды тогда `bash`, и по нему не видно, чего не
|
|
1731
|
+
# хватает. Без программы обёртка краснеет на ОБОИХ образцах, и prove объявлял исправный гейт
|
|
1732
|
+
# сломанным — «краснеет на исправном коде». От prove зависят ступень AQK-2 и значок, то есть
|
|
1733
|
+
# отсутствие чужой программы отбирало у проекта уровень. Обвинение вместо диагноза.
|
|
1734
|
+
# Приёмка (gates.sh) и мутационная проверка поле `requires` читают давно; prove — не читал.
|
|
1735
|
+
# Проверено 2026-09-09 на своём же репозитории: не стояли slopcheck и zizmor, prove дал два
|
|
1736
|
+
# обвинения и код 1. Проверяются ОБА конца: без поля — обвинение и код 1, с полем — пропуск и 0.
|
|
1737
|
+
PRVP="$(mktemp -d)"
|
|
1738
|
+
(
|
|
1739
|
+
cd "$PRVP" && git init -q . && printf '# вход\n' > AGENTS.md && mkdir -p r s/wrap/red s/wrap/green
|
|
1740
|
+
printf 'x\n' > s/wrap/red/a.txt && printf 'x\n' > s/wrap/green/a.txt
|
|
1741
|
+
# Обёртка вокруг программы, которой на машине нет: краснеет на обоих образцах.
|
|
1742
|
+
printf '#!/bin/sh\nпрограммыкоторойнет "$1" || exit 2\n' > wrap.sh
|
|
1743
|
+
# Настоящая проверка: prove требует, чтобы хоть один гейт был ДОКАЗАН, иначе проект,
|
|
1744
|
+
# у которого всё недоказуемо, получал бы уровень ни за что. Команда обязана кончаться
|
|
1745
|
+
# каталогом — иначе подставлять образец некуда, и гейт сам станет недоказуемым.
|
|
1746
|
+
printf '#!/bin/sh\ngrep -rq НЕЛЬЗЯ "$1" && exit 1\nexit 0\n' > real.sh
|
|
1747
|
+
printf 'aqk: 1\nentry: [AGENTS.md]\nrules: r\nsamples: s\ngates:\n wrap: "sh wrap.sh ."\n real: "sh real.sh ."\n' > .aqk.yml
|
|
1748
|
+
) >/dev/null 2>&1
|
|
1749
|
+
# Второй гейт обязан быть доказуемым: prove требует хотя бы одного доказанного.
|
|
1750
|
+
( cd "$PRVP" && mkdir -p s/real/red s/real/green &&
|
|
1751
|
+
printf 'НЕЛЬЗЯ\n' > s/real/red/a.txt && printf 'можно\n' > s/real/green/a.txt )
|
|
1752
|
+
PRV_NOREQ=$( cd "$PRVP" && AQK_LANG=ru node "$CLI" prove 2>&1 ); PRV_NOREQ_C=$?
|
|
1753
|
+
( cd "$PRVP" && printf 'intent: обёртка\nrequires: программыкоторойнет\nrecipes:\n any: "sh wrap.sh {dir}"\n' > s/wrap/gate.yml )
|
|
1754
|
+
PRV_REQ=$( cd "$PRVP" && AQK_LANG=ru node "$CLI" prove 2>&1 ); PRV_REQ_C=$?
|
|
1755
|
+
# Тот же вопрос задаёт `vitals`, и до 2026-09-09 он отвечал на него иначе: печатал «все
|
|
1756
|
+
# инструменты на месте», потому что смотрел только на ПЕРВОЕ СЛОВО команды — `sh`, который есть
|
|
1757
|
+
# всегда. Две команды об одном репозитории говорили разное; читатель верил той, что зеленее.
|
|
1758
|
+
PRV_VIT=$( cd "$PRVP" && AQK_LANG=ru node "$CLI" vitals 2>&1 )
|
|
1759
|
+
if [ "$PRV_NOREQ_C" -ne 0 ] && [ "$PRV_REQ_C" -eq 0 ] &&
|
|
1760
|
+
printf '%s' "$PRV_REQ" | grep -q "НЕ ПРОВЕРЕНА здесь" &&
|
|
1761
|
+
printf '%s' "$PRV_VIT" | grep -q "программыкоторойнет"; then
|
|
1762
|
+
ok "prove и vitals одинаково видят программу из requires: пропуск, а не обвинение"
|
|
1763
|
+
else
|
|
1764
|
+
bad "prove путает «нечем проверить» со «сломан», либо vitals её не видит" \
|
|
1765
|
+
"без поля код $PRV_NOREQ_C, с полем код $PRV_REQ_C, vitals назвал: $(printf '%s' "$PRV_VIT" | grep -c 'программыкоторойнет')"
|
|
1766
|
+
fi
|
|
1767
|
+
rm -rf "$PRVP"
|
|
1768
|
+
|
|
1769
|
+
# --- 104. опись в AGENTS.md покрывает все исходники ---------------------------
|
|
1770
|
+
# AGENTS.md — первое, что читает агент, и раздел «Где что лежит» для него карта. Карта,
|
|
1771
|
+
# отставшая от дерева, хуже её отсутствия: агент уверен, что видел всё. Проверено 2026-09-09 —
|
|
1772
|
+
# в описи не было восьми файлов из четырнадцати добавленных за неделю (`brief.mjs`, `banner.mjs`
|
|
1773
|
+
# и шесть `units-*.mjs`), и заметить это можно было только сверкой руками. Тот же класс, что
|
|
1774
|
+
# «гейт объявлен и не существует»: расхождение молчит.
|
|
1775
|
+
MISSING=""
|
|
1776
|
+
for F in "$ROOT"/tool/lib/*.mjs "$ROOT"/tool/commands/*.mjs "$ROOT"/tool/selfcheck/* "$ROOT"/tool/selfcheck/*/*; do
|
|
1777
|
+
B=$(basename "$F")
|
|
1778
|
+
grep -qF "$B" "$ROOT/AGENTS.md" || MISSING="$MISSING $B"
|
|
1779
|
+
done
|
|
1780
|
+
if [ -z "$MISSING" ]; then
|
|
1781
|
+
ok "опись «Где что лежит» называет каждый исходник"
|
|
1782
|
+
else
|
|
1783
|
+
bad "AGENTS.md не называет файлы" "$MISSING"
|
|
1784
|
+
fi
|
|
1785
|
+
|
|
1786
|
+
# --- 105. два README не расходятся структурно --------------------------------
|
|
1787
|
+
# AGENTS.md требует, чтобы README и его перевод не расходились, и до сегодня требование не
|
|
1788
|
+
# работало: на 2026-09-09 в английском было 23 раздела, в русском 22 — «Первый прогон на
|
|
1789
|
+
# настоящем проекте» отсутствовал целиком, а «Как ввести правило» стояло в другом разделе.
|
|
1790
|
+
# Читатель одного языка получал инструкцию, которой у читателя другого не было. Сверяется
|
|
1791
|
+
# ПОСЛЕДОВАТЕЛЬНОСТЬ уровней, а не тексты: заголовки на разных языках сравнивать нельзя.
|
|
1792
|
+
# Строки внутри ``` не считаются: в примере .aqkignore есть строка, начинающаяся с #.
|
|
1793
|
+
levels() {
|
|
1794
|
+
awk '/^```/ { inf = !inf; next } !inf && /^#/ { sub(/[^#].*/, ""); print }' "$1"
|
|
1795
|
+
}
|
|
1796
|
+
if [ "$(levels "$ROOT/README.md")" = "$(levels "$ROOT/README.ru.md")" ]; then
|
|
1797
|
+
ok "README.md и README.ru.md несут одни и те же разделы ($(levels "$ROOT/README.md" | wc -l | tr -d ' '))"
|
|
1798
|
+
else
|
|
1799
|
+
bad "README и перевод разошлись структурно" \
|
|
1800
|
+
"англ. $(levels "$ROOT/README.md" | wc -l | tr -d ' '), рус. $(levels "$ROOT/README.ru.md" | wc -l | tr -d ' ')"
|
|
1801
|
+
fi
|
|
1802
|
+
|
|
1803
|
+
# --- 108. init доходит до конца, когда дом недоступен для записи ---------------
|
|
1804
|
+
# НАШЁЛ КОНВЕЙЕР, А НЕ ПРОГОН. Одноразовая пометка «просьбу про звезду уже показывали» пишется
|
|
1805
|
+
# в $HOME/.config/aqk. В контейнере, запущенном `--user 1001:127`, у этого uid нет записи в
|
|
1806
|
+
# /etc/passwd, homedir() даёт «/», и mkdir падает с EACCES на «/.config» — роняя ВЕСЬ init.
|
|
1807
|
+
# То есть любой, кто набрал команду из нашей же документации по docker, получал падение вместо
|
|
1808
|
+
# установки. Локально не воспроизводилось по случайности: uid разработчика 1000 совпадает с
|
|
1809
|
+
# пользователем `node` в образе, у которого дом есть; на раннере uid 1001 — не совпадает.
|
|
1810
|
+
# Удобство не имеет права ронять работу команды. Молчать о нём тоже нельзя — это swallowed-error.
|
|
1811
|
+
HOMEP="$(mktemp -d)"; HOMEH="$(mktemp -d)"
|
|
1812
|
+
chmod 500 "$HOMEH" 2>/dev/null
|
|
1813
|
+
# На windows chmod прав не отнимает: там эта проверка ничего не проверяет и говорит об этом,
|
|
1814
|
+
# а не зеленеет молча. Тот же приём, что «НЕ ПРОВЕРЕНА здесь» в приёмке каталога.
|
|
1815
|
+
if ( : > "$HOMEH/проба" ) 2>/dev/null; then
|
|
1816
|
+
rm -f "$HOMEH/проба"
|
|
1817
|
+
ok "init при недоступном доме — НЕ ПРОВЕРЕНА здесь: chmod прав не отнимает"
|
|
1818
|
+
else
|
|
1819
|
+
( cd "$HOMEP" && git init -q . ) >/dev/null 2>&1
|
|
1820
|
+
HOMEOUT=$( cd "$HOMEP" && HOME="$HOMEH" AQK_LANG=ru node "$CLI" init 2>&1 ); HOME_C=$?
|
|
1821
|
+
if [ "$HOME_C" -eq 0 ] && [ -f "$HOMEP/.aqk.yml" ] &&
|
|
1822
|
+
printf '%s' "$HOMEOUT" | grep -q "запомнить не удалось"; then
|
|
1823
|
+
ok "init доходит до конца при недоступном доме и называет, чего не смог"
|
|
1824
|
+
else
|
|
1825
|
+
bad "init падает или молчит, когда дом недоступен для записи" \
|
|
1826
|
+
"код $HOME_C, манифест $([ -f "$HOMEP/.aqk.yml" ] && echo есть || echo нет)"
|
|
1827
|
+
fi
|
|
1828
|
+
fi
|
|
1829
|
+
chmod 700 "$HOMEH" 2>/dev/null; rm -rf "$HOMEP" "$HOMEH"
|
|
1830
|
+
|
|
1831
|
+
# --- 109. probe: находит слепоту и не выдаёт её за покрытие ---------------------
|
|
1832
|
+
# ЗАЧЕМ КОМАНДА. `doctor` говорит «держит машина N» — N из чего? `prove` доказывает, что гейт
|
|
1833
|
+
# ловит брак НА СВОЁМ образце. Ни один не отвечает: что в ЭТОМ репозитории не прикрыто ничем.
|
|
1834
|
+
# Замер 2026-09-09 на самом комплекте: настоящий файл, подсаженные проглоченная ошибка и печать,
|
|
1835
|
+
# 21 объявленный гейт — покраснело НОЛЬ. Узнать об этом было нечем.
|
|
1836
|
+
# Проверяются ОБА конца: класс, который объявленный гейт ловит, обязан быть зелёным, а тот,
|
|
1837
|
+
# которого не ловит никто, — красным. Проверка одного конца пропустила бы команду, которая
|
|
1838
|
+
# всегда говорит «не прикрыто», и команду, которая всегда говорит «прикрыто».
|
|
1839
|
+
PRBP="$(mktemp -d)"
|
|
1840
|
+
(
|
|
1841
|
+
cd "$PRBP" && git init -q . && git config user.email a@b && git config user.name a
|
|
1842
|
+
mkdir -p src && printf 'def send(to):\n return 1\n' > src/mailer.py
|
|
1843
|
+
git add -A && git commit -qm "feat: почта"
|
|
1844
|
+
printf '# правка\n' >> src/mailer.py && git add -A && git commit -qm "fix: письма не уходили"
|
|
1845
|
+
node "$CLI" init && node "$CLI" add todo-without-task
|
|
1846
|
+
# Коммитим ВСЁ, что положили init и add: иначе дерево грязное ещё до пробы, и проверка
|
|
1847
|
+
# «probe ничего не пишет» меряла бы мою же фикстуру, а не команду.
|
|
1848
|
+
git add -A && git commit -qm "Сделано: комплект. Не уверен: ничего"
|
|
1849
|
+
) >/dev/null 2>&1
|
|
1850
|
+
PRB=$( cd "$PRBP" && AQK_LANG=ru node "$CLI" probe --top 1 2>&1 ); PRB_C=$?
|
|
1851
|
+
# Слепое: secrets-not-in-code не объявлен, образец под .py у него есть — обязан быть красным.
|
|
1852
|
+
# Прикрытое: todo-without-task объявлен — обязан быть зелёным и назвать, кто поймал.
|
|
1853
|
+
if [ "$PRB_C" -eq 0 ] &&
|
|
1854
|
+
printf '%s' "$PRB" | grep -q "НЕ ЛОВИТ НИКТО" &&
|
|
1855
|
+
printf '%s' "$PRB" | grep -q "ловит: todo-without-task" &&
|
|
1856
|
+
printf '%s' "$PRB" | grep -q "починок в истории: 1"; then
|
|
1857
|
+
ok "probe: слепые классы названы, прикрытый назван поимённо, прогон не уронен"
|
|
1858
|
+
else
|
|
1859
|
+
bad "probe не различает слепое и прикрытое" "код $PRB_C, слепых $(printf '%s' "$PRB" | grep -c 'НЕ ЛОВИТ'), пойманных $(printf '%s' "$PRB" | grep -c 'ловит:')"
|
|
1860
|
+
fi
|
|
1861
|
+
# КОД человека проба не трогает: образец живёт во временном каталоге. Если бы он попал в
|
|
1862
|
+
# репозиторий, команда осмотра стала бы командой правки — и её выключили бы в тот же день.
|
|
1863
|
+
# Своя отметка каденции — исключение, и единственное: она лежит в `.aqk/`, там же, где отчёт
|
|
1864
|
+
# прогона, и без неё каденция невозможна (следующий прогон не узнает, что проба была).
|
|
1865
|
+
# Проверяется именно это разделение: за пределами `.aqk/` — ни следа.
|
|
1866
|
+
DIRTY=$( cd "$PRBP" && git status --porcelain | grep -cv ' \.aqk/' )
|
|
1867
|
+
if [ "$DIRTY" -eq 0 ]; then
|
|
1868
|
+
ok "probe не трогает код: пишет только свою отметку в .aqk/"
|
|
1869
|
+
else
|
|
1870
|
+
bad "probe оставил следы вне .aqk/" "$(cd "$PRBP" && git status --porcelain | grep -v ' \.aqk/' | head -3 | tr '\n' ' ')"
|
|
1871
|
+
fi
|
|
1872
|
+
rm -rf "$PRBP"
|
|
1873
|
+
|
|
1874
|
+
# --- 111. проба случается САМА, один раз, и попадает агенту в контекст -----------
|
|
1875
|
+
# ГЛАВНОЕ В ЭТОЙ ПРОВЕРКЕ — не механика, а замысел. Владелец сформулировал так: «команду, о
|
|
1876
|
+
# которой надо вспомнить, агент не вспомнит, а человек о ней не узнает». Это тот же класс, что
|
|
1877
|
+
# файл, который можно не прочитать, — и весь комплект написан против него. Значит `probe`,
|
|
1878
|
+
# оставленная ручной, отвечает на важнейший вопрос и не задаётся никем.
|
|
1879
|
+
# Проверяются три вещи, и каждая — отдельный способ провалиться:
|
|
1880
|
+
# 1. прогон делает пробу САМ, когда её не делали;
|
|
1881
|
+
# 2. второй прогон подряд её НЕ повторяет — иначе каденция превращается в шум;
|
|
1882
|
+
# 3. агент узнаёт результат из блока состояния, не зная про команду.
|
|
1883
|
+
CADP="$(mktemp -d)"
|
|
1884
|
+
(
|
|
1885
|
+
cd "$CADP" && git init -q . && git config user.email a@b && git config user.name a
|
|
1886
|
+
mkdir -p src && printf 'def s():\n return 1\n' > src/a.py
|
|
1887
|
+
git add -A && git commit -qm "feat: старт"
|
|
1888
|
+
printf '# п\n' >> src/a.py && git add -A && git commit -qm "fix: не работало"
|
|
1889
|
+
node "$CLI" init && node "$CLI" add todo-without-task
|
|
1890
|
+
) >/dev/null 2>&1
|
|
1891
|
+
CTX0=$( cd "$CADP" && AQK_LANG=ru node "$CLI" context 2>&1 )
|
|
1892
|
+
RUN1=$( cd "$CADP" && AQK_LANG=ru node "$CLI" doctor --run 2>&1 )
|
|
1893
|
+
RUN2=$( cd "$CADP" && AQK_LANG=ru node "$CLI" doctor --run 2>&1 )
|
|
1894
|
+
CTX1=$( cd "$CADP" && AQK_LANG=ru node "$CLI" context 2>&1 )
|
|
1895
|
+
if printf '%s' "$CTX0" | grep -q "НЕИЗВЕСТНО" &&
|
|
1896
|
+
printf '%s' "$RUN1" | grep -q "ещё не делали" &&
|
|
1897
|
+
! printf '%s' "$RUN2" | grep -q "ещё не делали" &&
|
|
1898
|
+
printf '%s' "$CTX1" | grep -q "Не прикрыто ничем"; then
|
|
1899
|
+
ok "проба запускается прогоном сама, не повторяется и попадает агенту в контекст"
|
|
1900
|
+
else
|
|
1901
|
+
bad "каденция пробы не работает" \
|
|
1902
|
+
"до: $(printf '%s' "$CTX0" | grep -c 'НЕИЗВЕСТНО'), первый: $(printf '%s' "$RUN1" | grep -c 'ещё не делали'), второй: $(printf '%s' "$RUN2" | grep -c 'ещё не делали'), после: $(printf '%s' "$CTX1" | grep -c 'Не прикрыто')"
|
|
1903
|
+
fi
|
|
1904
|
+
# Выключатель обязан быть у всего, что случается само: иначе первый же, кому это помешало,
|
|
1905
|
+
# выключит весь прогон, а не одну пробу.
|
|
1906
|
+
CADP2="$(mktemp -d)"
|
|
1907
|
+
( cd "$CADP2" && git init -q . && git config user.email a@b && git config user.name a
|
|
1908
|
+
mkdir -p src && printf 'def s():\n return 1\n' > src/a.py
|
|
1909
|
+
git add -A && git commit -qm "feat: старт"
|
|
1910
|
+
node "$CLI" init && node "$CLI" add todo-without-task ) >/dev/null 2>&1
|
|
1911
|
+
OFF=$( cd "$CADP2" && AQK_PROBE=0 AQK_LANG=ru node "$CLI" doctor --run 2>&1 )
|
|
1912
|
+
if ! printf '%s' "$OFF" | grep -q "ещё не делали"; then
|
|
1913
|
+
ok "AQK_PROBE=0 выключает пробу, не трогая прогон"
|
|
1914
|
+
else
|
|
1915
|
+
bad "проба игнорирует выключатель" "AQK_PROBE=0 не подействовал"
|
|
1916
|
+
fi
|
|
1917
|
+
rm -rf "$CADP" "$CADP2"
|
|
1918
|
+
|
|
1919
|
+
# --- переехавшие проверки: встроенный раннер --------------------------------
|
|
1920
|
+
# ПОЧЕМУ ЗДЕСЬ МОСТ, А НЕ ВТОРАЯ КОМАНДА. Проверки переезжают на `node --test` по одной, и всё
|
|
1921
|
+
# это время у прогона обязана оставаться ОДНА точка входа и ОДИН счётчик: два числа в двух
|
|
1922
|
+
# местах через месяц разойдутся, и никто не заметит, что половина не запускается.
|
|
1923
|
+
#
|
|
1924
|
+
# ЗАЧЕМ ПЕРЕЕЗД. У встроенного раннера есть то, чего нет у этого файла и не появится: свой
|
|
1925
|
+
# каталог и уборка на каждую проверку, таймаут на каждую, точечный перезапуск одной по имени
|
|
1926
|
+
# (`--test-name-pattern`). Диагностика отказа падает с сорока секунд до одной. Проверено на
|
|
1927
|
+
# Node 20.20.2 — той версии, что стоит в конвейере.
|
|
1928
|
+
NODE_SMOKE="$ROOT/tool/selfcheck/smoke"
|
|
1929
|
+
if [ -d "$NODE_SMOKE" ]; then
|
|
1930
|
+
NS_OUT=$(cd "$ROOT" && node --test --test-reporter=tap "$NODE_SMOKE"/*.test.mjs 2>&1)
|
|
1931
|
+
# Разбираем только верхний уровень TAP: вложенные строки идут с отступом.
|
|
1932
|
+
while IFS= read -r NS_LINE; do
|
|
1933
|
+
case "$NS_LINE" in
|
|
1934
|
+
"ok "*) ok "${NS_LINE#*- }" ;;
|
|
1935
|
+
"not ok "*)
|
|
1936
|
+
# ОБЪЯСНЕНИЕ ОБЯЗАНО ДОЕХАТЬ. Первая редакция моста печатала только «не прошло» и совет
|
|
1937
|
+
# запустить вручную — то есть отправляла гадать ровно там, где ответ уже был получен.
|
|
1938
|
+
# Стоило это трёх кругов конвейера: проверка падала только на раннере, а сообщение
|
|
1939
|
+
# утверждения оставалось в выводе, который мост выбрасывал. TAP кладёт его в блок после
|
|
1940
|
+
# строки `not ok`, полем `error:`.
|
|
1941
|
+
# TAP кладёт многострочное сообщение блочным скаляром: строка `error: |-`, а сам текст
|
|
1942
|
+
# идёт следующими строками с отступом. Первая редакция брала только строку `error:` и
|
|
1943
|
+
# печатала «|-» — то есть снова ничего.
|
|
1944
|
+
NS_WHY=$(printf '%s\n' "$NS_OUT" | awk '
|
|
1945
|
+
/^[[:space:]]+error:/ { inerr = 1; sub(/^[[:space:]]*error:[[:space:]]*/, ""); if ($0 != "|-" && $0 != "") print; next }
|
|
1946
|
+
inerr && /^[[:space:]]+(code|stack|failureType|type|duration_ms):/ { inerr = 0 }
|
|
1947
|
+
inerr && /^[[:space:]]*\.\.\.[[:space:]]*$/ { inerr = 0 }
|
|
1948
|
+
inerr { sub(/^[[:space:]]+/, ""); print }
|
|
1949
|
+
' | head -6 | tr '\n' ' ')
|
|
1950
|
+
bad "${NS_LINE#*- }" "${NS_WHY:-подробности: node --test $NODE_SMOKE/*.test.mjs}" ;;
|
|
1951
|
+
esac
|
|
1952
|
+
done <<EOF
|
|
1953
|
+
$(printf '%s\n' "$NS_OUT" | grep -E '^(ok|not ok) ')
|
|
1954
|
+
EOF
|
|
1955
|
+
fi
|
|
1956
|
+
|
|
1440
1957
|
# --- итог -------------------------------------------------------------------
|
|
1441
1958
|
printf '\n'
|
|
1442
1959
|
if [ "$FAIL" -eq 0 ]; then
|
|
@@ -1445,5 +1962,10 @@ else
|
|
|
1445
1962
|
printf ' \033[31mпровалено: %s из %s\033[0m\n\n' "$FAIL" "$((PASS + FAIL))"
|
|
1446
1963
|
fi
|
|
1447
1964
|
|
|
1448
|
-
printf ' \033[2mне покрыто:
|
|
1965
|
+
printf ' \033[2mне покрыто: СОДЕРЖАНИЕ документов (сверяется опись и структура, не текст),\n установка с GitHub через npx\033[0m\n\n'
|
|
1966
|
+
|
|
1967
|
+
# ИМЕНА УПАВШИХ — САМОЙ ПОСЛЕДНЕЙ СТРОКОЙ, и это не косметика. Прогон запускают гейтом, а вывод
|
|
1968
|
+
# упавшего гейта обрезается: видно голову и хвост. Значит единственное место, где список
|
|
1969
|
+
# гарантированно доедет до глаз, — конец. Проверено прогоном с нарочно сломанной проверкой.
|
|
1970
|
+
[ "$FAIL" -eq 0 ] || printf ' \033[31mупало: %s\033[0m\n\n' "$FAILED_NAMES"
|
|
1449
1971
|
exit "$FAIL"
|