agent-quality-kit 0.4.2 → 0.6.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 +136 -12
- package/README.ru.md +119 -11
- package/kit/docs/ready-made-rules.md +40 -0
- package/kit/gates/README.md +35 -0
- package/kit/gates/_native.sh +18 -2
- package/kit/gates/_skip.sh +18 -0
- package/kit/gates/ci-actually-fails/README.md +42 -0
- package/kit/gates/ci-actually-fails/check.sh +93 -0
- package/kit/gates/ci-actually-fails/gate.yml +14 -0
- package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +14 -0
- package/kit/gates/ci-actually-fails/red/.github/workflows/ci.yml +12 -0
- package/kit/gates/color-from-token/README.md +52 -0
- package/kit/gates/color-from-token/check.sh +69 -0
- package/kit/gates/color-from-token/gate.yml +15 -0
- package/kit/gates/color-from-token/green/Button.tsx +4 -0
- package/kit/gates/color-from-token/green/Panel.vue +4 -0
- package/kit/gates/color-from-token/green/card.css +5 -0
- package/kit/gates/color-from-token/green/notes.md +2 -0
- package/kit/gates/color-from-token/green/tokens.css +7 -0
- package/kit/gates/color-from-token/red/Button.tsx +4 -0
- package/kit/gates/color-from-token/red/Panel.vue +4 -0
- package/kit/gates/color-from-token/red/card.css +5 -0
- package/kit/gates/commit-explains-itself/check.sh +25 -2
- package/kit/gates/duplicate-code/check.sh +13 -4
- package/kit/gates/duplicate-code/gate.yml +11 -2
- package/kit/gates/gate-has-samples/check.sh +9 -3
- package/kit/gates/gate-not-weakened/README.md +54 -0
- package/kit/gates/gate-not-weakened/check.sh +72 -0
- package/kit/gates/gate-not-weakened/gate.yml +15 -0
- package/kit/gates/gate-not-weakened/green/checkout.ts +8 -0
- package/kit/gates/gate-not-weakened/green/payments.py +6 -0
- package/kit/gates/gate-not-weakened/green/release.sh +2 -0
- package/kit/gates/gate-not-weakened/red/checkout.ts +9 -0
- package/kit/gates/gate-not-weakened/red/payments.py +6 -0
- package/kit/gates/gate-not-weakened/red/release.sh +2 -0
- package/kit/gates/gates-are-runnable/check.sh +7 -1
- package/kit/gates/gates-run-in-ci/check.sh +7 -1
- package/kit/gates/lesson-has-outcome/check.sh +6 -1
- package/kit/gates/promise-has-gate/README.md +50 -0
- package/kit/gates/promise-has-gate/check.sh +88 -0
- package/kit/gates/promise-has-gate/gate.yml +14 -0
- package/kit/gates/promise-has-gate/green/.aqk.yml +6 -0
- package/kit/gates/promise-has-gate/green/AGENTS.md +7 -0
- package/kit/gates/promise-has-gate/red/.aqk.yml +6 -0
- package/kit/gates/promise-has-gate/red/AGENTS.md +7 -0
- package/kit/gates/test-has-assertion/README.md +47 -0
- package/kit/gates/test-has-assertion/check.sh +194 -0
- package/kit/gates/test-has-assertion/gate.yml +15 -0
- package/kit/gates/test-has-assertion/green/checkout.test.ts +9 -0
- package/kit/gates/test-has-assertion/green/test_billing.py +17 -0
- package/kit/gates/test-has-assertion/red/checkout.test.ts +8 -0
- package/kit/gates/test-has-assertion/red/test_billing.py +14 -0
- package/kit/ratchet/ratchet.sh +9 -2
- package/kit/rules/general.md +14 -0
- package/llms.txt +58 -0
- package/package.json +4 -2
- package/tool/commands/doctor.mjs +106 -11
- package/tool/commands/gates.mjs +18 -3
- package/tool/commands/project.mjs +13 -4
- package/tool/commands/report.mjs +2 -2
- package/tool/i18n/en.mjs +55 -0
- package/tool/i18n/ru.mjs +61 -0
- package/tool/i18n/templates-en.mjs +9 -9
- package/tool/i18n/templates-ru.mjs +9 -9
- package/tool/lib/baseline.mjs +87 -0
- package/tool/lib/core.mjs +10 -2
- package/tool/lib/manifest.mjs +69 -2
- package/tool/lib/repo.mjs +7 -0
- package/tool/lib/scope.mjs +96 -0
- package/tool/program.mjs +1 -0
- package/tool/selfcheck/gates.sh +29 -5
- package/tool/selfcheck/lifecycle.mjs +29 -0
- package/tool/selfcheck/mutation.sh +95 -0
- package/tool/selfcheck/smoke.sh +269 -8
- package/tool/selfcheck/syntax.sh +9 -1
- package/tool/selfcheck/units.mjs +160 -1
package/tool/selfcheck/smoke.sh
CHANGED
|
@@ -384,18 +384,22 @@ rm -rf "$GODIR"
|
|
|
384
384
|
|
|
385
385
|
# --- 24. просьба про звезду и обратную связь — один раз на машину -----------
|
|
386
386
|
# Печатается один раз на установку (не на проект): второй init на этой же HOME её не повторяет.
|
|
387
|
+
# USERPROFILE задаётся рядом с HOME: `os.homedir()` на Windows читает именно его, и без этого
|
|
388
|
+
# отметка уезжала в настоящий домашний каталог раннера — к этой проверке она там уже лежала от
|
|
389
|
+
# предыдущих прогонов init, и просьба не печаталась. Изоляция, которая не изолирует, хуже её
|
|
390
|
+
# отсутствия: проверка краснела не на дефекте.
|
|
387
391
|
# Отметка живёт вне репозитория — внутри .aqk/ она либо закоммитится в чужой проект, либо
|
|
388
392
|
# потеряется при повторном init --force.
|
|
389
393
|
FAKEHOME="$(mktemp -d)"
|
|
390
394
|
D1="$(mktemp -d)"
|
|
391
|
-
OUT1=$( cd "$D1" && HOME="$FAKEHOME" node "$CLI" init 2>&1 )
|
|
395
|
+
OUT1=$( cd "$D1" && HOME="$FAKEHOME" USERPROFILE="$FAKEHOME" node "$CLI" init 2>&1 )
|
|
392
396
|
if printf '%s' "$OUT1" | grep -qi 'звезд'; then
|
|
393
397
|
ok "первый init на новой машине зовёт поставить звезду"
|
|
394
398
|
else
|
|
395
399
|
bad "первый init не упомянул звезду/обратную связь" "$OUT1"
|
|
396
400
|
fi
|
|
397
401
|
D2="$(mktemp -d)"
|
|
398
|
-
OUT2=$( cd "$D2" && HOME="$FAKEHOME" node "$CLI" init 2>&1 )
|
|
402
|
+
OUT2=$( cd "$D2" && HOME="$FAKEHOME" USERPROFILE="$FAKEHOME" node "$CLI" init 2>&1 )
|
|
399
403
|
if printf '%s' "$OUT2" | grep -qi 'звезд'; then
|
|
400
404
|
bad "init повторил просьбу про звезду на той же машине" "второй проект, та же HOME"
|
|
401
405
|
else
|
|
@@ -428,7 +432,9 @@ rm -f /tmp/aqk-broken-doc-links.$$
|
|
|
428
432
|
# --- 26. doctor печатает версию комплекта ------------------------------------
|
|
429
433
|
# Баг-репорт без версии нечем привязать к коммиту — заметили, заполняя .github/ISSUE_TEMPLATE/,
|
|
430
434
|
# где просили версию из шапки doctor, а шапка её не печатала вовсе.
|
|
431
|
-
|
|
435
|
+
# Путь отдаётся оболочкой, а Node на Windows не понимает «/d/a/…» из Git Bash. Читаем из
|
|
436
|
+
# текущего каталога, а не подставляем абсолютный путь в код.
|
|
437
|
+
PKGVER=$( cd "$ROOT" && node -p "require('./package.json').version" )
|
|
432
438
|
DOCVER=$( cd "$ROOT" && node "$CLI" doctor 2>&1 | head -3)
|
|
433
439
|
if printf '%s' "$DOCVER" | grep -qF "$PKGVER"; then
|
|
434
440
|
ok "doctor печатает версию комплекта ($PKGVER)"
|
|
@@ -532,16 +538,25 @@ fi
|
|
|
532
538
|
rm -rf "$SHDIR" "$CEDIR"
|
|
533
539
|
|
|
534
540
|
# --- 32. вывод действительно на двух языках -----------------------------------
|
|
541
|
+
# Утечку русского ищем СЛОВАМИ, а не диапазоном [а-яА-ЯёЁ]. Диапазон непереносим: в сборке grep
|
|
542
|
+
# из MSYS он сравнивает байты, и «—», «·», ««»» из обычной типографики попадают в него — на
|
|
543
|
+
# Windows проверка насчитывала пять «кириллических» строк в чисто английском выводе. Литеральные
|
|
544
|
+
# слова совпадают одинаково везде, что тот же прогон и подтвердил.
|
|
535
545
|
# Сверка ключей каталогов (units.mjs) доказывает, что строки не разошлись, но не доказывает,
|
|
536
546
|
# что выбор языка вообще доехал до вывода. Это проверяется только запуском.
|
|
537
547
|
EN_OUT=$(AQK_LANG=en node "$CLI" 2>&1)
|
|
538
548
|
RU_OUT=$(AQK_LANG=ru node "$CLI" 2>&1)
|
|
539
549
|
if printf '%s' "$EN_OUT" | grep -q "install a gate from the catalogue" &&
|
|
540
|
-
! printf '%s' "$EN_OUT" | grep -
|
|
550
|
+
! printf '%s' "$EN_OUT" | grep -qE 'гейт|каталог|проверк|уровен|репозитор' &&
|
|
541
551
|
printf '%s' "$RU_OUT" | grep -q "поставить гейт из каталога"; then
|
|
542
552
|
ok "справка печатается на двух языках, в английской нет кириллицы"
|
|
543
553
|
else
|
|
544
|
-
|
|
554
|
+
# Диагностика по каждому условию отдельно. Прежняя печатала первые строки вывода — по ним
|
|
555
|
+
# видно, что вывод английский, и совершенно не видно, какая из трёх сверок не сошлась.
|
|
556
|
+
EN_HAS=$(printf '%s' "$EN_OUT" | grep -c "install a gate from the catalogue")
|
|
557
|
+
EN_CYR=$(printf '%s' "$EN_OUT" | grep -cE 'гейт|каталог|проверк|уровен|репозитор')
|
|
558
|
+
RU_HAS=$(printf '%s' "$RU_OUT" | grep -c "поставить гейт из каталога")
|
|
559
|
+
bad "выбор языка не доехал до вывода" "англ.фраза=$EN_HAS кириллица_в_англ=$EN_CYR рус.фраза=$RU_HAS"
|
|
545
560
|
fi
|
|
546
561
|
|
|
547
562
|
# --- 33. ссылка на репозиторий ведёт в репозиторий -----------------------------
|
|
@@ -549,7 +564,7 @@ fi
|
|
|
549
564
|
# «github:владелец/репозиторий»), просьба про звезду поехала на github.com/agent-quality-kit —
|
|
550
565
|
# несуществующую страницу. Единственное место, где мы просим человека о чём-то, вело в никуда.
|
|
551
566
|
FBDIR="$(mktemp -d)"; FBPROJ="$(mktemp -d)"
|
|
552
|
-
FB_OUT=$( cd "$FBPROJ" && git init -q . && HOME="$FBDIR" node "$CLI" init 2>&1 )
|
|
567
|
+
FB_OUT=$( cd "$FBPROJ" && git init -q . && HOME="$FBDIR" USERPROFILE="$FBDIR" node "$CLI" init 2>&1 )
|
|
553
568
|
if printf '%s' "$FB_OUT" | grep -qE 'https://github\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+'; then
|
|
554
569
|
ok "просьба про звезду ведёт на репозиторий, а не на github.com/<имя пакета>"
|
|
555
570
|
else
|
|
@@ -565,13 +580,19 @@ REPDIR="$(mktemp -d)"
|
|
|
565
580
|
(
|
|
566
581
|
cd "$REPDIR" && git init -q . && mkdir -p src &&
|
|
567
582
|
printf 'def f():\n print("debug")\n' > src/a.py &&
|
|
568
|
-
node "$CLI" start
|
|
583
|
+
node "$CLI" start > /tmp/aqk-start.log 2>&1
|
|
569
584
|
)
|
|
570
585
|
REP_OUT=$( cd "$REPDIR" && node "$CLI" report 2>&1 ); REP_CODE=$?
|
|
571
586
|
if [ "$REP_CODE" -ne 0 ] && printf '%s' "$REP_OUT" | grep -q '❌ no-print-in-prod'; then
|
|
572
587
|
ok "report краснеет кодом возврата и называет упавший гейт"
|
|
573
588
|
else
|
|
574
|
-
|
|
589
|
+
# Код возврата отчёта не говорит, ПОЧЕМУ он ноль: гейт не сработал, не установился или
|
|
590
|
+
# установился не тот. Спрашиваем сам гейт напрямую — это и есть разница между «отчёт врёт»
|
|
591
|
+
# и «проверка не ловит на этой системе».
|
|
592
|
+
G_LS=$( cd "$REPDIR" && ls gates 2>&1 | tr '\n' ' ' )
|
|
593
|
+
G_DECL=$( cd "$REPDIR" && sed -n '/^gates:/,$p' .aqk.yml 2>/dev/null | grep -cE '^[[:space:]]+[A-Za-z0-9_-]+:' )
|
|
594
|
+
G_OUT=$( cd "$REPDIR" && bash gates/no-print-in-prod/check.sh . 2>&1 | head -2 ); G_CODE=$?
|
|
595
|
+
bad "report не отличает красное от зелёного" "код отчёта $REP_CODE; гейт напрямую: код $G_CODE, вывод «$(printf '%s' "$G_OUT" | tr '\n' ' ')»; в gates/: «$G_LS»; объявлено гейтов: $G_DECL; хвост start: «$(tail -4 /tmp/aqk-start.log 2>/dev/null | tr '\n' ' ')»"
|
|
575
596
|
fi
|
|
576
597
|
if [ -f "$REPDIR/.aqk/report.md" ] && grep -q '^## ' "$REPDIR/.aqk/report.md"; then
|
|
577
598
|
ok "report сохраняет .aqk/report.md"
|
|
@@ -696,6 +717,246 @@ else
|
|
|
696
717
|
fi
|
|
697
718
|
rm -rf "$BDIR"
|
|
698
719
|
|
|
720
|
+
# --- 41. порог различает «ступень ниже» и «упал гейт» -------------------------
|
|
721
|
+
# ЗАЧЕМ. При упавшем гейте печаталось «Порог AQK-1 НЕ пройден: сейчас AQK-1» — утверждение,
|
|
722
|
+
# противоречащее само себе. Человек шёл чинить манифест, а падал гейт. Это две разные
|
|
723
|
+
# развилки, и сообщение обязано их различать, иначе оно отправляет чинить не то.
|
|
724
|
+
TDIR="$(mktemp -d)"
|
|
725
|
+
( cd "$TDIR" && git init -q . && node "$CLI" init >/dev/null 2>&1 )
|
|
726
|
+
printf 'gates:\n always-fails: "false"\n' >> "$TDIR/.aqk.yml"
|
|
727
|
+
OUT_GATE="$( cd "$TDIR" && node "$CLI" doctor --run --min 1 2>&1 )"; RC_GATE=$?
|
|
728
|
+
# Ступень ниже порога: пустой манифест без входа и правил.
|
|
729
|
+
EDIR="$(mktemp -d)"; ( cd "$EDIR" && git init -q . && printf 'aqk: 1\n' > .aqk.yml )
|
|
730
|
+
OUT_LVL="$( cd "$EDIR" && node "$CLI" doctor --min 3 2>&1 )"; RC_LVL=$?
|
|
731
|
+
if [ "$RC_GATE" -ne 0 ] && [ "$RC_LVL" -ne 0 ] &&
|
|
732
|
+
printf '%s' "$OUT_GATE" | grep -q 'always-fails' &&
|
|
733
|
+
! printf '%s' "$OUT_GATE" | grep -qE '(НЕ пройден|NOT passed): (сейчас|currently) AQK-1' &&
|
|
734
|
+
printf '%s' "$OUT_LVL" | grep -qE '(НЕ пройден|NOT passed)'; then
|
|
735
|
+
ok "порог различает упавший гейт и недобранную ступень"
|
|
736
|
+
else
|
|
737
|
+
bad "сообщение о пороге не различает две развилки" "гейт: $(printf '%s' "$OUT_GATE" | tail -2 | tr '\n' ' ')"
|
|
738
|
+
fi
|
|
739
|
+
rm -rf "$TDIR" "$EDIR"
|
|
740
|
+
|
|
741
|
+
# --- 42. duplicate-code: пара упорядочена, а не как отдал обход ---------------
|
|
742
|
+
# ЗАЧЕМ. Ключ пары складывался в порядке, в котором файлы отдал find, а он разный на разных
|
|
743
|
+
# системах (здесь — по хешу имени, не по алфавиту и не по времени создания). Реестр, снятый
|
|
744
|
+
# на одной машине, краснел в конвейере целиком: те же дубли читались как новые. Хуже: храповик
|
|
745
|
+
# объявлял их исправленными и вычёркивал — реестр портился. Инвариант: пара внутри строки
|
|
746
|
+
# всегда лексикографическая, тогда ключ одинаков на любой системе.
|
|
747
|
+
DD="$(mktemp -d)"
|
|
748
|
+
BLOCK='def f():\n a = 1\n b = 2\n c = 3\n d = 4\n e = 5\n g = 6\n h = 7\n return a\n'
|
|
749
|
+
printf "$BLOCK" > "$DD/zz.py"; printf "$BLOCK" > "$DD/aa.py"
|
|
750
|
+
LINE="$(bash "$ROOT/kit/gates/duplicate-code/check.sh" "$DD" 2>&1 | sed "s#$DD/##g" | grep 'одинаков' | head -1)"
|
|
751
|
+
FIRST="${LINE%% и *}"; REST="${LINE#* и }"; SECOND="${REST%%:*}"
|
|
752
|
+
if [ -n "$LINE" ] && [ "${FIRST%%:*}" \< "$SECOND" ]; then
|
|
753
|
+
ok "duplicate-code упорядочивает пару лексикографически"
|
|
754
|
+
else
|
|
755
|
+
bad "пара идёт в порядке обхода — реестр разъедется между машинами" "$LINE"
|
|
756
|
+
fi
|
|
757
|
+
rm -rf "$DD"
|
|
758
|
+
|
|
759
|
+
# --- 43. храповик: ключ переживает сдвиг строки в ПЕРВОМ файле пары -----------
|
|
760
|
+
# ЗАЧЕМ. keys() убирал номер строки шаблоном ':<число>:', а у первого файла пары за номером
|
|
761
|
+
# идёт ' и '. Номер оставался в ключе, и сдвиг кода в первом файле читался как новое
|
|
762
|
+
# нарушение — ровно то, от чего храповик защищает.
|
|
763
|
+
RDIR="$(mktemp -d)"
|
|
764
|
+
printf '# реестр\na.tsx: и b.tsx: одинаковый кусок\n' > "$RDIR/reg.txt"
|
|
765
|
+
bash "$ROOT/kit/ratchet/ratchet.sh" "$RDIR/reg.txt" \
|
|
766
|
+
printf 'a.tsx:171 и b.tsx:188: одинаковый кусок\n' >/dev/null 2>&1; RC_SHIFT=$?
|
|
767
|
+
if [ "$RC_SHIFT" -eq 0 ]; then
|
|
768
|
+
ok "храповик: номер строки убран у обоих файлов пары"
|
|
769
|
+
else
|
|
770
|
+
bad "сдвиг строки в первом файле пары читается как новое нарушение" "код $RC_SHIFT"
|
|
771
|
+
fi
|
|
772
|
+
rm -rf "$RDIR"
|
|
773
|
+
|
|
774
|
+
# --- 44. манифест называет опечатку в имени поля -------------------------------
|
|
775
|
+
# ЗАЧЕМ. Разбор принимает любое имя поля. `gate:` вместо `gates:` молча означало «гейтов не
|
|
776
|
+
# объявлено»: вердикт выходил неверный, а причина не называлась. Человек шёл искать ошибку в
|
|
777
|
+
# проекте, а она была в одной букве манифеста. Тишина неотличима от успеха — тот самый класс,
|
|
778
|
+
# против которого построен стандарт, только внутри самой программы.
|
|
779
|
+
MDIR="$(mktemp -d)"
|
|
780
|
+
( cd "$MDIR" && git init -q . && node "$CLI" init >/dev/null 2>&1 )
|
|
781
|
+
sed -i 's/^gates:/gate:/' "$MDIR/.aqk.yml"
|
|
782
|
+
OUT_TYPO="$( cd "$MDIR" && node "$CLI" doctor 2>&1 )"
|
|
783
|
+
OUT_OK="$( cd "$MDIR" && sed -i 's/^gate:/gates:/' .aqk.yml && node "$CLI" doctor 2>&1 )"
|
|
784
|
+
if printf '%s' "$OUT_TYPO" | grep -qE '(does not know|не знает).*gate' &&
|
|
785
|
+
! printf '%s' "$OUT_OK" | grep -qE '(does not know|не знает)'; then
|
|
786
|
+
ok "манифест называет неизвестное поле и молчит на верном"
|
|
787
|
+
else
|
|
788
|
+
bad "опечатка в поле манифеста проходит молча" "$(printf '%s' "$OUT_TYPO" | grep -i 'know\|знает' | head -1)"
|
|
789
|
+
fi
|
|
790
|
+
rm -rf "$MDIR"
|
|
791
|
+
|
|
792
|
+
# --- 45. родной рецепт не читает то, что не читают переносимые -----------------
|
|
793
|
+
# ЗАЧЕМ. own_samples_filter знал про образцы гейтов и .aqkignore, но не про SKIP_NAMES:
|
|
794
|
+
# их применяли только переносимые проверки при обходе, а родному инструменту список не
|
|
795
|
+
# доставался вовсе. На живом проекте (Django + React, 2750 файлов кода) первой находкой
|
|
796
|
+
# duplicate-code оказались методички САМОГО комплекта в .aqk/docs — родной jscpd прошёлся
|
|
797
|
+
# по каталогу, который положил init. Вывод — 5597 строк. Такой гейт выключают целиком,
|
|
798
|
+
# ровно как сказано в шапке _skip.sh про 94% чужих находок.
|
|
799
|
+
NDIR="$(mktemp -d)"
|
|
800
|
+
mkdir -p "$NDIR/.aqk/docs" "$NDIR/node_modules/pkg" "$NDIR/src"
|
|
801
|
+
printf 'нарушение\n' > "$NDIR/.aqk/docs/guide.md"
|
|
802
|
+
printf 'нарушение\n' > "$NDIR/node_modules/pkg/index.js"
|
|
803
|
+
printf 'нарушение\n' > "$NDIR/src/mine.py"
|
|
804
|
+
# «Инструмент» печатает пути и возвращает отказ — как настоящий родной линтер.
|
|
805
|
+
# Пути в цветовых кодах — как их печатает jscpd: имя каталога идёт не после «/» и не с начала
|
|
806
|
+
# строки, а сразу за escape-последовательностью. Фильтр по границе пути их не видел, и на живом
|
|
807
|
+
# проекте вывод сократился с 5597 строк до 5505 — то есть не сократился.
|
|
808
|
+
cat > "$NDIR/fake-tool.sh" <<'EOT'
|
|
809
|
+
printf ' - \033[1m\033[32m.aqk/docs/guide.md:markdown\033[39m\033[22m [8:1 - 20:5]\n'
|
|
810
|
+
printf ' - \033[1m\033[32mnode_modules/pkg/index.js:javascript\033[39m\033[22m [1:1 - 9:2]\n'
|
|
811
|
+
printf ' - \033[1m\033[32msrc/mine.py:python\033[39m\033[22m [1:1 - 9:2]\n'
|
|
812
|
+
exit 1
|
|
813
|
+
EOT
|
|
814
|
+
OUT_N="$(cd "$NDIR" && sh "$ROOT/kit/gates/_native.sh" . sh ./fake-tool.sh 2>&1)"
|
|
815
|
+
if printf '%s' "$OUT_N" | grep -q 'src/mine.py' &&
|
|
816
|
+
! printf '%s' "$OUT_N" | grep -q '\.aqk/docs' &&
|
|
817
|
+
! printf '%s' "$OUT_N" | grep -q 'node_modules'; then
|
|
818
|
+
ok "родной рецепт молчит про .aqk и node_modules, но видит свой код"
|
|
819
|
+
else
|
|
820
|
+
bad "родной инструмент выдаёт то, что переносимые не читают" "$(printf '%s' "$OUT_N" | tr '\n' ' ')"
|
|
821
|
+
fi
|
|
822
|
+
rm -rf "$NDIR"
|
|
823
|
+
|
|
824
|
+
# --- 46. commit-explains-itself и синтетический merge-коммит -------------------
|
|
825
|
+
# ЗАЧЕМ. При разборе предложения изменений GitHub выкладывает не коммит автора, а синтетический
|
|
826
|
+
# merge-коммит с сообщением «Merge <sha> into <sha>». Гейт читал именно его, не находил разделов
|
|
827
|
+
# отчёта и краснел — на КАЖДОМ предложении изменений в КАЖДОМ проекте, куда его поставили.
|
|
828
|
+
# Поймано настоящим прогоном конвейера на этой же ветке, а не рассуждением.
|
|
829
|
+
CDIR="$(mktemp -d)"
|
|
830
|
+
(
|
|
831
|
+
cd "$CDIR" && git init -q . && git config user.email a@b.c && git config user.name a
|
|
832
|
+
printf 'один\n' > f.txt && git add -A
|
|
833
|
+
git commit -q -m "feat: первый" -m "Сделано: завёл файл" -m "Не уверен: ни в чём"
|
|
834
|
+
git checkout -q -b feature
|
|
835
|
+
printf 'два\n' >> f.txt && git add -A
|
|
836
|
+
git commit -q -m "feat: второй" -m "Сделано: дописал строку" -m "Не уверен: ни в чём"
|
|
837
|
+
git checkout -q master 2>/dev/null || git checkout -q main
|
|
838
|
+
# Ровно та форма сообщения, которую делает GitHub для ветки предложения изменений.
|
|
839
|
+
git merge -q --no-ff feature -m "Merge $(git rev-parse --short feature) into $(git rev-parse --short HEAD)"
|
|
840
|
+
) >/dev/null 2>&1
|
|
841
|
+
OUT_M="$(bash "$ROOT/kit/gates/commit-explains-itself/check.sh" "$CDIR" 2>&1)"; RC_M=$?
|
|
842
|
+
# Вторая форма — та, что делает кнопка Merge на сайте. Слова другие, случай тот же: сообщение
|
|
843
|
+
# сочинил не автор. Шаблон «Merge … into …» её не ловил, и main покраснел бы после первого же
|
|
844
|
+
# вливания через кнопку.
|
|
845
|
+
CDIR2="$(mktemp -d)"
|
|
846
|
+
(
|
|
847
|
+
cd "$CDIR2" && git init -q . && git config user.email a@b.c && git config user.name a
|
|
848
|
+
printf 'один\n' > f.txt && git add -A
|
|
849
|
+
git commit -q -m "feat: первый" -m "Сделано: завёл файл" -m "Не уверен: ни в чём"
|
|
850
|
+
git checkout -q -b feature
|
|
851
|
+
printf 'два\n' >> f.txt && git add -A
|
|
852
|
+
git commit -q -m "feat: второй" -m "Сделано: дописал строку" -m "Не уверен: ни в чём"
|
|
853
|
+
git checkout -q master 2>/dev/null || git checkout -q main
|
|
854
|
+
git merge -q --no-ff feature -m "Merge pull request #15 from owner/feature"
|
|
855
|
+
) >/dev/null 2>&1
|
|
856
|
+
bash "$ROOT/kit/gates/commit-explains-itself/check.sh" "$CDIR2" >/dev/null 2>&1; RC_PR=$?
|
|
857
|
+
rm -rf "$CDIR2"
|
|
858
|
+
if [ "$RC_M" -eq 0 ] && [ "$RC_PR" -eq 0 ]; then
|
|
859
|
+
ok "commit-explains-itself смотрит на коммит автора, а не на merge-коммит конвейера"
|
|
860
|
+
else
|
|
861
|
+
bad "гейт краснеет на слитом предложении изменений" "checkout-форма: $RC_M, кнопка Merge: $RC_PR"
|
|
862
|
+
fi
|
|
863
|
+
rm -rf "$CDIR"
|
|
864
|
+
|
|
865
|
+
# --- 47. doctor --baseline ставит галочки прогоном, а не по памяти -------------
|
|
866
|
+
# ЗАЧЕМ. Методичка про обязательный минимум — 50 пунктов — была единственным местом, где
|
|
867
|
+
# комплект просил верить на слово, что человек её прочитал и сверился. Ручной проход по живому
|
|
868
|
+
# проекту нашёл настоящее (логирование не задано, задачи конвейера не запускались ни разу),
|
|
869
|
+
# но дисциплина не масштабируется. Проверяем главное: значок ставит признак, а не автор.
|
|
870
|
+
BDIR2="$(mktemp -d)"
|
|
871
|
+
( cd "$BDIR2" && git init -q . && node "$CLI" init >/dev/null 2>&1 )
|
|
872
|
+
OUT_EMPTY="$( cd "$BDIR2" && node "$CLI" doctor --baseline 2>&1 )"
|
|
873
|
+
# Кладём общепринятые признаки трёх РАЗНЫХ экосистем: нейтральность к стеку — условие, а не
|
|
874
|
+
# пожелание. Проверка, знающая только про npm, объявила бы половину мира несоответствующей.
|
|
875
|
+
printf 'x\n' > "$BDIR2/Cargo.lock"; printf 'x\n' > "$BDIR2/ruff.toml"; printf 'x\n' > "$BDIR2/Dockerfile"
|
|
876
|
+
OUT_FULL="$( cd "$BDIR2" && node "$CLI" doctor --baseline 2>&1 )"
|
|
877
|
+
BEFORE=$(printf '%s' "$OUT_EMPTY" | grep -c '✔' || true)
|
|
878
|
+
AFTER=$(printf '%s' "$OUT_FULL" | grep -c '✔' || true)
|
|
879
|
+
if printf '%s' "$OUT_FULL" | grep -qE 'cargo.lock' &&
|
|
880
|
+
printf '%s' "$OUT_FULL" | grep -qE 'ruff.toml' &&
|
|
881
|
+
printf '%s' "$OUT_FULL" | grep -qE 'dockerfile' &&
|
|
882
|
+
[ "$AFTER" -gt "$BEFORE" ]; then
|
|
883
|
+
ok "doctor --baseline засчитывает признаки разных экосистем и называет, чем подтверждено"
|
|
884
|
+
else
|
|
885
|
+
bad "baseline не видит признаков или не называет доказательство" "было ✔ $BEFORE, стало $AFTER"
|
|
886
|
+
fi
|
|
887
|
+
rm -rf "$BDIR2"
|
|
888
|
+
|
|
889
|
+
# --- 48. start не бросает установку из-за одной записи ------------------------
|
|
890
|
+
# ЗАЧЕМ. Записи вроде dead-code нужен настоящий инструмент; переносимого рецепта у неё нет.
|
|
891
|
+
# На машине без него установка ПАДАЛА целиком: человек получал три сторожа вместо двенадцати и
|
|
892
|
+
# ни слова про остальные девять. Найдено прогоном на Windows, где нет ни ruff, ни vulture.
|
|
893
|
+
# Воспроизводим без Windows: урезаем PATH до одного node — инструментов не видно так же.
|
|
894
|
+
NRDIR="$(mktemp -d)"; NRBIN="$(mktemp -d)"
|
|
895
|
+
ln -sf "$(command -v node)" "$NRBIN/node"
|
|
896
|
+
( cd "$NRDIR" && git init -q . && mkdir -p src && printf 'def f():\n print("debug")\n' > src/a.py )
|
|
897
|
+
NR_OUT=$( cd "$NRDIR" && PATH="$NRBIN" node "$CLI" start 2>&1 ); NR_CODE=$?
|
|
898
|
+
NR_GATES=$( ls "$NRDIR/gates" 2>/dev/null | grep -cv '^_' )
|
|
899
|
+
if [ "$NR_CODE" -eq 0 ] && [ "$NR_GATES" -ge 5 ] &&
|
|
900
|
+
[ -f "$NRDIR/gates/no-print-in-prod/check.sh" ]; then
|
|
901
|
+
ok "start пропускает запись без пригодного инструмента и ставит остальные ($NR_GATES)"
|
|
902
|
+
else
|
|
903
|
+
bad "start бросил установку из-за одной записи" "код $NR_CODE, поставлено $NR_GATES, хвост: $(printf '%s' "$NR_OUT" | tail -2 | tr '\n' ' ')"
|
|
904
|
+
fi
|
|
905
|
+
rm -rf "$NRDIR" "$NRBIN"
|
|
906
|
+
|
|
907
|
+
# --- 49. выведенную запись не ставят, а называют преемника --------------------
|
|
908
|
+
# ЗАЧЕМ. Зрелость записи считается по доказательству, и объявить её нельзя — кроме одного
|
|
909
|
+
# состояния: `deprecated`. Оно объявляется, и весь его смысл в отказе: запись, которую всё ещё
|
|
910
|
+
# можно поставить одной командой, не выведена, а просто помечена. Проверяем сам отказ и то, что
|
|
911
|
+
# папка гейта в проекте НЕ появилась: половина установки хуже, чем её отсутствие.
|
|
912
|
+
#
|
|
913
|
+
# Каталог мутируем в КОПИИ пакета, а не в этом репозитории: проверка, которая правит собственные
|
|
914
|
+
# исходники, однажды упадёт посередине и оставит дерево грязным.
|
|
915
|
+
DEPKG="$(mktemp -d)"; DEPRJ="$(mktemp -d)"
|
|
916
|
+
cp -r "$ROOT/tool" "$ROOT/kit" "$ROOT/package.json" "$DEPKG/" 2>/dev/null
|
|
917
|
+
printf 'lifecycle: deprecated\nsuperseded_by: no-print-in-prod\n' >> "$DEPKG/kit/gates/todo-without-task/gate.yml"
|
|
918
|
+
( cd "$DEPRJ" && git init -q . && printf 'x = 1\n' > a.py && node "$DEPKG/tool/program.mjs" init >/dev/null 2>&1 )
|
|
919
|
+
DE_OUT=$( cd "$DEPRJ" && node "$DEPKG/tool/program.mjs" add todo-without-task 2>&1 ); DE_CODE=$?
|
|
920
|
+
if [ "$DE_CODE" -ne 0 ] &&
|
|
921
|
+
printf '%s' "$DE_OUT" | grep -q 'no-print-in-prod' &&
|
|
922
|
+
[ ! -d "$DEPRJ/gates/todo-without-task" ]; then
|
|
923
|
+
ok "add отказывает в выведенной записи и называет ту, что её заменяет"
|
|
924
|
+
else
|
|
925
|
+
bad "выведенная запись установилась или преемник не назван" "код $DE_CODE, папка: $([ -d "$DEPRJ/gates/todo-without-task" ] && echo есть || echo нет)"
|
|
926
|
+
fi
|
|
927
|
+
rm -rf "$DEPKG" "$DEPRJ"
|
|
928
|
+
|
|
929
|
+
# --- 50. --since показывает только то, что внёс диф ---------------------------
|
|
930
|
+
# ЗАЧЕМ. Первый прогон в живом проекте показывает долг за все годы. Стену красного не разбирают
|
|
931
|
+
# — проверку выключают целиком. Проверяем три исхода разом: старый долг молчит, новый краснеет,
|
|
932
|
+
# а гейт, который печатает вердикт без путей, НЕ становится зелёным от того, что его нечем сузить.
|
|
933
|
+
SCDIR="$(mktemp -d)"
|
|
934
|
+
(
|
|
935
|
+
cd "$SCDIR" && git init -q . && git config user.email t@t && git config user.name t
|
|
936
|
+
mkdir -p src && printf 'def old():\n print("старый долг")\n' > src/old.py
|
|
937
|
+
node "$CLI" init >/dev/null 2>&1
|
|
938
|
+
node "$CLI" add no-print-in-prod >/dev/null 2>&1
|
|
939
|
+
git add -A && git commit -qm "база" >/dev/null 2>&1
|
|
940
|
+
printf 'def fresh():\n print("новый долг")\n' > src/fresh.py
|
|
941
|
+
)
|
|
942
|
+
SC_WIDE=$( cd "$SCDIR" && node "$CLI" doctor --run 2>&1 )
|
|
943
|
+
SC_NARROW=$( cd "$SCDIR" && node "$CLI" doctor --run --since HEAD 2>&1 )
|
|
944
|
+
if printf '%s' "$SC_WIDE" | grep -q 'old.py' &&
|
|
945
|
+
printf '%s' "$SC_NARROW" | grep -q 'fresh.py' &&
|
|
946
|
+
! printf '%s' "$SC_NARROW" | grep -q 'old.py'; then
|
|
947
|
+
ok "--since прячет старый долг и показывает внесённый дифом"
|
|
948
|
+
else
|
|
949
|
+
bad "--since сузил не то" "широкий: $(printf '%s' "$SC_WIDE" | grep -c 'py:'), узкий: $(printf '%s' "$SC_NARROW" | grep -c 'py:')"
|
|
950
|
+
fi
|
|
951
|
+
# Несуществующая ссылка обязана быть отказом, а не тихим «сравнили с ничем».
|
|
952
|
+
SC_BAD=$( cd "$SCDIR" && node "$CLI" doctor --run --since net-takoy-vetki 2>&1 ); SC_BADCODE=$?
|
|
953
|
+
if [ "$SC_BADCODE" -ne 0 ] && printf '%s' "$SC_BAD" | grep -qi 'net-takoy-vetki'; then
|
|
954
|
+
ok "--since с несуществующей ссылкой — отказ, а не тихое сравнение с ничем"
|
|
955
|
+
else
|
|
956
|
+
bad "--since проглотил неверную ссылку" "код $SC_BADCODE"
|
|
957
|
+
fi
|
|
958
|
+
rm -rf "$SCDIR"
|
|
959
|
+
|
|
699
960
|
# --- итог -------------------------------------------------------------------
|
|
700
961
|
printf '\n'
|
|
701
962
|
if [ "$FAIL" -eq 0 ]; then
|
package/tool/selfcheck/syntax.sh
CHANGED
|
@@ -15,8 +15,16 @@ BAD=0
|
|
|
15
15
|
for F in $(git ls-files 'tool/*.mjs' 'tool/**/*.mjs' 2>/dev/null); do
|
|
16
16
|
node --check "$F" || BAD=1
|
|
17
17
|
done
|
|
18
|
+
# Разбираем тем интерпретатором, который объявлен в шебанге, а не всегда bash. `bash -n`
|
|
19
|
+
# принимает bash-измы («<<<», массивы, «local») в файле, который объявлен как `sh`, — и
|
|
20
|
+
# переносимость ломается молча, на чужой машине с dash. Так и вышло: четыре гейта, объявленные
|
|
21
|
+
# `#!/usr/bin/env sh`, получили «<<<» и прошли проверку синтаксиса зелёными.
|
|
18
22
|
for F in $(git ls-files '*.sh' 2>/dev/null); do
|
|
19
|
-
|
|
23
|
+
case "$(head -1 "$F")" in
|
|
24
|
+
*bash*) SH=bash ;;
|
|
25
|
+
*) SH=sh ;;
|
|
26
|
+
esac
|
|
27
|
+
"$SH" -n "$F" || BAD=1
|
|
20
28
|
done
|
|
21
29
|
|
|
22
30
|
[ "$BAD" -eq 0 ] || echo " почини: файл не разбирается — до него не доедет ни одна другая проверка."
|
package/tool/selfcheck/units.mjs
CHANGED
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
|
|
12
12
|
import test from "node:test";
|
|
13
13
|
import assert from "node:assert/strict";
|
|
14
|
-
import { parseManifest, manifestWithGate } from "../lib/manifest.mjs";
|
|
14
|
+
import { parseManifest, manifestWithGate, unknownKeys, entryLifecycle } from "../lib/manifest.mjs";
|
|
15
15
|
import { triggerVerdict, recipeFor, stems, overlap, EXT_LANG, whichSync } from "../lib/repo.mjs";
|
|
16
|
+
import { scopeOutput } from "../lib/scope.mjs";
|
|
17
|
+
import { assessBaseline, ITEMS, BASELINE_TOTAL } from "../lib/baseline.mjs";
|
|
16
18
|
import { CATALOGS, pickLang, L } from "../i18n/index.mjs";
|
|
17
19
|
import { badgeMarkdown, BADGE_RE, placesToCheck } from "../commands/badge.mjs";
|
|
18
20
|
import { dirname } from "node:path";
|
|
@@ -199,3 +201,160 @@ test("значок ищется в точке входа и в README, без п
|
|
|
199
201
|
assert.equal(places.filter((p) => p === "README.md").length, 1);
|
|
200
202
|
assert.ok(placesToCheck({}).includes("README.md"), "без entry README всё равно проверяется");
|
|
201
203
|
});
|
|
204
|
+
|
|
205
|
+
// ЗАЧЕМ. Опечатка в имени поля молча означала «поля нет»: `gate:` вместо `gates:` давало
|
|
206
|
+
// вердикт «гейтов не объявлено», а не «в манифесте опечатка». Тишина неотличима от успеха —
|
|
207
|
+
// тот самый дефект, ради которого весь стандарт существует, только внутри нас самих.
|
|
208
|
+
test("опечатка в поле манифеста называется, а не молчит", () => {
|
|
209
|
+
assert.deepEqual(unknownKeys(parseManifest("aqk: 1\ngate:\n smoke: \"bash x.sh\"\n")), ["gate"]);
|
|
210
|
+
assert.deepEqual(unknownKeys(parseManifest("aqk: 1\nrules: kit/rules\nlessons: incidents\n")), []);
|
|
211
|
+
// Пустой и отсутствующий манифест — не повод ругаться на поля.
|
|
212
|
+
assert.deepEqual(unknownKeys(null), []);
|
|
213
|
+
assert.deepEqual(unknownKeys({}), []);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ЗАЧЕМ. Разбор резал строку по «#» безусловно, в том числе внутри кавычек. Команда с решёткой
|
|
217
|
+
// — `--format "...,c#,..."`, `grep '#!'`, любой цвет `#fff` — молча обрезалась, и гейт запускал
|
|
218
|
+
// НЕ ТУ команду, которая объявлена. Объявленное и исполняемое разошлись бы беззвучно: ровно
|
|
219
|
+
// тот класс, ради которого стандарт существует. Найдено при правке рецепта duplicate-code.
|
|
220
|
+
test("решётка внутри кавычек не считается комментарием", () => {
|
|
221
|
+
const m = parseManifest('gates:\n dup: "npx jscpd --format \"java,c#,php\" ."\n');
|
|
222
|
+
assert.equal(m.gates.dup, 'npx jscpd --format "java,c#,php" .');
|
|
223
|
+
// Настоящий комментарий после команды по-прежнему срезается.
|
|
224
|
+
const c = parseManifest('gates:\n x: "bash a.sh" # пояснение\n');
|
|
225
|
+
assert.equal(c.gates.x, "bash a.sh");
|
|
226
|
+
// И комментарий на отдельной строке.
|
|
227
|
+
assert.deepEqual(Object.keys(parseManifest("# только комментарий\naqk: 1\n")), ["aqk"]);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// ЗАЧЕМ. Пункты baseline проверяются НАЛИЧИЕМ признака, и признак обязан быть семейством, а не
|
|
231
|
+
// одним именем: список, знающий только про npm, объявил бы половину мира несоответствующей.
|
|
232
|
+
// Проверяем именно нейтральность — что пункт засчитывается по маркеру любой экосистемы.
|
|
233
|
+
test("baseline: признак засчитывается по любой экосистеме", () => {
|
|
234
|
+
const by = (files) => Object.fromEntries(assessBaseline({ files }).map((r) => [r.key, r]));
|
|
235
|
+
for (const lock of ["package-lock.json", "poetry.lock", "go.sum", "Cargo.lock", "Gemfile.lock", "composer.lock", "mix.lock"]) {
|
|
236
|
+
assert.equal(by([lock]).lockfile.ok, true, lock);
|
|
237
|
+
assert.deepEqual(by([lock]).lockfile.by, { kind: "file", value: lock.toLowerCase() });
|
|
238
|
+
}
|
|
239
|
+
for (const lint of [".eslintrc.json", "ruff.toml", ".golangci.yml", "clippy.toml", ".rubocop.yml", "phpstan.neon", ".swiftlint.yml"]) {
|
|
240
|
+
assert.equal(by([lint]).linter.ok, true, lint);
|
|
241
|
+
}
|
|
242
|
+
// Пустой репозиторий: ни одного признака, и ни одной ложной галочки.
|
|
243
|
+
assert.equal(assessBaseline({}).every((r) => r.ok === false), true);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("baseline: гейт, факт и поле манифеста засчитываются наравне с файлом", () => {
|
|
247
|
+
const one = (arg) => Object.fromEntries(assessBaseline(arg).map((r) => [r.key, r]));
|
|
248
|
+
assert.equal(one({ gateKeys: ["secrets-not-in-code"] }).secretScan.ok, true);
|
|
249
|
+
assert.equal(one({ facts: { has_ci: true } }).pipeline.ok, true);
|
|
250
|
+
assert.equal(one({ manifest: { entry: ["AGENTS.md"] } }).machineReadable.ok, true);
|
|
251
|
+
assert.equal(one({ manifest: { entry: [] } }).machineReadable.ok, false);
|
|
252
|
+
assert.equal(one({ depsText: '"@sentry/node": "^7"' }).errorTracker.ok, true);
|
|
253
|
+
assert.equal(one({ depsText: "sentry-sdk==2.0" }).errorTracker.ok, true);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Число пунктов в методичке — не выдумка кода: если методичка вырастет, а число останется,
|
|
257
|
+
// отчёт начнёт врать о том, сколько осталось человеку.
|
|
258
|
+
test("baseline: заявленное число пунктов совпадает с методичкой", async () => {
|
|
259
|
+
const { readFile } = await import("node:fs/promises");
|
|
260
|
+
const { fileURLToPath } = await import("node:url");
|
|
261
|
+
const doc = await readFile(fileURLToPath(new URL("../../kit/docs/ai/project-baseline.md", import.meta.url)), "utf8");
|
|
262
|
+
const nums = [...doc.matchAll(/^(\d+)\. \*\*/gm)].map((m) => Number(m[1]));
|
|
263
|
+
assert.equal(Math.max(...nums), BASELINE_TOTAL);
|
|
264
|
+
assert.equal(ITEMS.every((i) => i.n <= BASELINE_TOTAL), true);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ЗАЧЕМ. Запись про внешний вид, показанная бэкенду или утилите командной строки, — это совет
|
|
268
|
+
// не по адресу; таким записям перестают верить, и вместе с ними всему каталогу. Признак
|
|
269
|
+
// интерфейса отличает проект со стилями от проекта на тех же языках без них.
|
|
270
|
+
test("триггер по интерфейсу отделяет фронтенд от бэкенда на том же языке", () => {
|
|
271
|
+
const rec = { trigger: { has_ui: true } };
|
|
272
|
+
const base = { langs: new Set(["typescript"]), files: 100, gateKeys: [] };
|
|
273
|
+
assert.equal(triggerVerdict(rec, { ...base, has_ui: true }).applies, true);
|
|
274
|
+
assert.equal(triggerVerdict(rec, { ...base, has_ui: false }).applies, false);
|
|
275
|
+
// Причина сокрытия называется, а не молчит: иначе «не показано» неотличимо от «нечего показать».
|
|
276
|
+
assert.equal(typeof triggerVerdict(rec, { ...base, has_ui: false }).why, "string");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// --- зрелость записи ---------------------------------------------------------
|
|
280
|
+
// ЗАЧЕМ. У всех трёх соседей поле зрелости есть, и у всех троих его ЗАПОЛНЯЕТ АВТОР: `lifecycle`
|
|
281
|
+
// у зондов Scorecard, `future`/`obsolete` у критериев значка OpenSSF. Поле, которое объявляет
|
|
282
|
+
// автор, означает доверие к автору, а не факт, — ровно то, против чего построен весь стандарт.
|
|
283
|
+
// Поэтому зрелость здесь ВЫЧИСЛЯЕТСЯ из доказательства, а объявить её нельзя.
|
|
284
|
+
test("зрелость записи считается по доказательству, а не по объявлению", () => {
|
|
285
|
+
const proven = entryLifecycle({ proof: "incidents/README.md, 2026-08-27 «печать в проде»" });
|
|
286
|
+
assert.equal(proven.state, "stable");
|
|
287
|
+
assert.equal(proven.problem, null);
|
|
288
|
+
|
|
289
|
+
const claimed = entryLifecycle({ proof: "это общепринятая хорошая практика" });
|
|
290
|
+
assert.equal(claimed.state, "experimental");
|
|
291
|
+
assert.equal(claimed.problem, null);
|
|
292
|
+
// Причина обязательна: «запись условная» без объяснения неотличимо от придирки.
|
|
293
|
+
assert.equal(typeof claimed.why, "string");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("объявить себя зрелым нельзя — это самооценка", () => {
|
|
297
|
+
for (const claim of ["stable", "experimental"]) {
|
|
298
|
+
const r = entryLifecycle({ lifecycle: claim, proof: "incidents/README.md, 2026-01-01" });
|
|
299
|
+
assert.notEqual(r.problem, null);
|
|
300
|
+
// Вердикт всё равно считается сам: объявление не влияет ни на что, кроме отказа.
|
|
301
|
+
assert.equal(r.state, "stable");
|
|
302
|
+
}
|
|
303
|
+
assert.notEqual(entryLifecycle({ lifecycle: "beta", proof: "incidents/x" }).problem, null);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Единственное состояние, которое ОБЪЯВЛЯЕТСЯ: из фактов записи «её больше не ставят» не
|
|
307
|
+
// выводится никак. Цена объявления — обязательная замена: запись, выведенная в никуда,
|
|
308
|
+
// оставляет человека без ответа на вопрос «а что теперь».
|
|
309
|
+
test("выведенная запись обязана назвать замену", () => {
|
|
310
|
+
const noReplacement = entryLifecycle({ lifecycle: "deprecated", proof: "incidents/x" });
|
|
311
|
+
assert.equal(noReplacement.state, "deprecated");
|
|
312
|
+
assert.notEqual(noReplacement.problem, null);
|
|
313
|
+
|
|
314
|
+
const ok = entryLifecycle({ lifecycle: "deprecated", superseded_by: "no-print-in-prod", proof: "incidents/x" });
|
|
315
|
+
assert.equal(ok.state, "deprecated");
|
|
316
|
+
assert.equal(ok.supersededBy, "no-print-in-prod");
|
|
317
|
+
assert.equal(ok.problem, null);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
// --- сужение вывода до дифа --------------------------------------------------
|
|
322
|
+
// ЗАЧЕМ. Первый прогон на живом проекте даёт тысячи находок из кода, который писали годами.
|
|
323
|
+
// Человек видит стену красного и выключает инструмент целиком — это причина номер один, по
|
|
324
|
+
// которой такие проверки снимают. Три независимых проекта из нашего разбора умеют показывать
|
|
325
|
+
// только внесённое дифом (reviewdog, ratchets `--since`, четыре режима шума у react-doctor).
|
|
326
|
+
test("сужение по дифу: находка вне диапазона отбрасывается, внутри — остаётся", () => {
|
|
327
|
+
const files = new Set(["src/new.py"]);
|
|
328
|
+
const r = scopeOutput(["src/new.py:3: печать", "src/old.py:9: печать"], files);
|
|
329
|
+
assert.deepEqual(r.kept, ["src/new.py:3: печать"]);
|
|
330
|
+
assert.equal(r.findings, 1);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("сужение по дифу: «./путь» и «путь» — один и тот же файл", () => {
|
|
334
|
+
const r = scopeOutput(["./src/new.py:3: печать", "src\\new.py:4: печать"], new Set(["src/new.py"]));
|
|
335
|
+
assert.equal(r.findings, 2);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Тот же урок, что стоил починки в _native.sh: родные инструменты печатают путь ВНУТРИ
|
|
339
|
+
// escape-последовательности, и сравнение по границе пути его не видит. Тогда «вывод
|
|
340
|
+
// сократился с 5597 до 5505 строк» выглядело как работающая правка.
|
|
341
|
+
test("сужение по дифу: цвет снимается до сравнения путей", () => {
|
|
342
|
+
const esc = String.fromCharCode(27);
|
|
343
|
+
const line = esc + "[32m" + "src/new.py" + esc + "[0m" + ":3: печать";
|
|
344
|
+
assert.equal(scopeOutput([line], new Set(["src/new.py"])).findings, 1);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test("сужение по дифу: строка без пути остаётся, но находкой не считается", () => {
|
|
348
|
+
const r = scopeOutput(["Итого: 4 нарушения", "src/old.py:1: печать"], new Set(["src/new.py"]));
|
|
349
|
+
assert.equal(r.findings, 0);
|
|
350
|
+
assert.equal(r.kept.includes("Итого: 4 нарушения"), true);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// САМОЕ ВАЖНОЕ ЗДЕСЬ. Гейт, который печатает вердикт без путей (проверка коммита, проверка
|
|
354
|
+
// конфига конвейера), сузить дифом нельзя. Молча признать его успешным — это ровно та тишина,
|
|
355
|
+
// против которой построен весь стандарт, только теперь внутри нашего же флага.
|
|
356
|
+
test("сужение по дифу: гейт без путей в выводе не сужается и остаётся красным", () => {
|
|
357
|
+
const r = scopeOutput(["коммит не несёт раздела «Сделано:»"], new Set(["src/new.py"]));
|
|
358
|
+
assert.equal(r.scopable, false);
|
|
359
|
+
assert.equal(scopeOutput(["src/old.py:1: печать"], new Set(["src/new.py"])).scopable, true);
|
|
360
|
+
});
|