agent-quality-kit 0.6.0 → 0.8.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 +61 -2
- package/README.ru.md +61 -2
- package/kit/docs/ai/agent-harness-playbook.md +1 -1
- package/kit/docs/ready-made-rules.md +29 -4
- package/kit/gates/README.md +40 -0
- package/kit/gates/_skip.sh +61 -1
- package/kit/gates/ci-actually-fails/README.md +12 -0
- package/kit/gates/ci-actually-fails/check.sh +26 -3
- package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +16 -0
- package/kit/gates/ci-actually-fails/red/.github/workflows/soft.yml +15 -0
- package/kit/gates/ci-not-hijackable/README.md +56 -0
- package/kit/gates/ci-not-hijackable/check.sh +73 -0
- package/kit/gates/ci-not-hijackable/gate.yml +19 -0
- package/kit/gates/ci-not-hijackable/green/.github/workflows/triage.yml +19 -0
- package/kit/gates/ci-not-hijackable/red/.github/workflows/triage.yml +18 -0
- package/kit/gates/color-from-token/check.sh +19 -3
- package/kit/gates/color-from-token/green/Button.tsx +2 -0
- package/kit/gates/commit-explains-itself/README.md +13 -3
- package/kit/gates/commit-explains-itself/check.sh +8 -4
- package/kit/gates/complexity-limit/README.md +5 -0
- package/kit/gates/complexity-limit/check.sh +27 -9
- package/kit/gates/complexity-limit/green/test_fixtures.py +14 -0
- package/kit/gates/deps-are-pinned/README.md +14 -1
- package/kit/gates/deps-are-pinned/check.sh +6 -1
- package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/pyproject.toml +12 -0
- package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/requirements.txt +3 -0
- package/kit/gates/deps-are-pinned/red/pyproject-loose/pyproject.toml +12 -0
- package/kit/gates/deps-are-pinned/red/pyproject-loose/requirements.txt +3 -0
- package/kit/gates/duplicate-code/README.md +11 -2
- package/kit/gates/duplicate-code/check.sh +36 -5
- package/kit/gates/duplicate-code/gate.yml +8 -0
- package/kit/gates/duplicate-code/green/imports_a.go +20 -0
- package/kit/gates/duplicate-code/green/imports_b.go +19 -0
- package/kit/gates/entry-links-exist/README.md +5 -0
- package/kit/gates/entry-links-exist/check.sh +10 -1
- package/kit/gates/entry-links-exist/green/AGENTS.md +5 -0
- package/kit/gates/file-size-limit/README.md +9 -2
- package/kit/gates/file-size-limit/check.sh +14 -2
- package/kit/gates/gate-not-weakened/check.sh +13 -1
- package/kit/gates/hook-actually-fires/README.md +74 -0
- package/kit/gates/hook-actually-fires/check.sh +183 -0
- package/kit/gates/hook-actually-fires/gate.yml +15 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/hooks.json +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/settings.json +74 -0
- package/kit/gates/hook-actually-fires/green/.claude/settings.local.json +74 -0
- package/kit/gates/hook-actually-fires/red/.claude/hooks/hooks.json +4 -0
- package/kit/gates/hook-actually-fires/red/.claude/settings.json +53 -0
- package/kit/gates/no-phantom-package/README.md +84 -0
- package/kit/gates/no-phantom-package/check.sh +161 -0
- package/kit/gates/no-phantom-package/gate.yml +20 -0
- package/kit/gates/no-phantom-package/green/AGENTS.md +15 -0
- package/kit/gates/no-phantom-package/red/AGENTS.md +15 -0
- package/kit/gates/no-print-in-prod/README.md +33 -39
- package/kit/gates/no-print-in-prod/gate.yml +14 -6
- package/kit/gates/personal-config-not-shared/README.md +66 -0
- package/kit/gates/personal-config-not-shared/check.sh +103 -0
- package/kit/gates/personal-config-not-shared/gate.yml +16 -0
- package/kit/gates/personal-config-not-shared/green/.aqk-tracked +9 -0
- package/kit/gates/personal-config-not-shared/red/.aqk-tracked +6 -0
- package/kit/gates/secrets-not-in-code/check.sh +29 -4
- package/kit/gates/secrets-not-in-code/green/testdata/certificate/key.pem +3 -0
- package/kit/gates/swallowed-error/README.md +36 -18
- package/kit/gates/swallowed-error/gate.yml +13 -3
- package/kit/gates/test-has-assertion/check.sh +13 -1
- package/kit/gates/test-not-adjusted/README.md +79 -0
- package/kit/gates/test-not-adjusted/check.sh +136 -0
- package/kit/gates/test-not-adjusted/gate.yml +19 -0
- package/kit/gates/test-not-adjusted/green/after/calc.py +6 -0
- package/kit/gates/test-not-adjusted/green/after/tests/test_calc.py +9 -0
- package/kit/gates/test-not-adjusted/green/before/calc.py +2 -0
- package/kit/gates/test-not-adjusted/green/before/tests/test_calc.py +5 -0
- package/kit/gates/test-not-adjusted/red/after/calc.py +2 -0
- package/kit/gates/test-not-adjusted/red/after/tests/test_calc.py +5 -0
- package/kit/gates/test-not-adjusted/red/before/calc.py +2 -0
- package/kit/gates/test-not-adjusted/red/before/tests/test_calc.py +7 -0
- package/kit/gates/todo-without-task/README.md +6 -0
- package/kit/gates/todo-without-task/check.sh +14 -2
- package/kit/gates/todo-without-task/green/app.py +1 -0
- package/kit/ratchet/ratchet.sh +70 -2
- package/kit/rules/general.md +9 -0
- package/kit/rules-en/general.md +82 -0
- package/kit/rules-en/security.md +33 -0
- package/kit/rules-en/testing.md +48 -0
- package/llms.txt +22 -1
- package/package.json +6 -2
- package/tool/commands/badge.mjs +7 -1
- package/tool/commands/context.mjs +260 -0
- package/tool/commands/doctor.mjs +72 -25
- package/tool/commands/gates.mjs +10 -4
- package/tool/commands/learn.mjs +159 -0
- package/tool/commands/project.mjs +9 -1
- package/tool/commands/prove.mjs +67 -0
- package/tool/commands/report.mjs +37 -2
- package/tool/i18n/en-docs.mjs +154 -0
- package/tool/i18n/en.mjs +70 -90
- package/tool/i18n/ru-docs.mjs +156 -0
- package/tool/i18n/ru.mjs +69 -90
- package/tool/i18n/templates-en.mjs +1 -1
- package/tool/i18n/templates-ru.mjs +1 -1
- package/tool/lib/core.mjs +37 -2
- package/tool/lib/evidence.mjs +124 -0
- package/tool/lib/manifest.mjs +63 -5
- package/tool/lib/prove.mjs +172 -0
- package/tool/lib/repo.mjs +31 -2
- package/tool/lib/scope.mjs +46 -2
- package/tool/lib/templates.mjs +3 -0
- package/tool/program.mjs +23 -22
- package/tool/selfcheck/gates.sh +66 -0
- package/tool/selfcheck/mutation.sh +21 -1
- package/tool/selfcheck/smoke.sh +519 -39
- package/tool/selfcheck/units-context.mjs +186 -0
- package/tool/selfcheck/units-evidence.mjs +83 -0
- package/tool/selfcheck/units-learn.mjs +88 -0
- package/tool/selfcheck/units-level.mjs +122 -0
- package/tool/selfcheck/units.mjs +114 -2
- package/kit/gates/no-print-in-prod/check.sh +0 -38
- package/kit/gates/no-print-in-prod/green/docs.ts +0 -15
- package/kit/gates/no-print-in-prod/green/main.go +0 -8
- package/kit/gates/no-print-in-prod/green/main.rs +0 -4
- package/kit/gates/no-print-in-prod/red/main.go +0 -8
- package/kit/gates/no-print-in-prod/red/main.rs +0 -4
- package/kit/gates/swallowed-error/check.sh +0 -54
- package/kit/gates/swallowed-error/green/run.js +0 -8
- package/kit/gates/swallowed-error/red/run.js +0 -3
- /package/kit/gates/commit-explains-itself/green/{COMMIT_MSG → .aqk-commit-msg} +0 -0
- /package/kit/gates/commit-explains-itself/red/{COMMIT_MSG → .aqk-commit-msg} +0 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Конфигурация конвейера не отдаёт чужому коду права и секреты.
|
|
3
|
+
#
|
|
4
|
+
# ЗАЧЕМ. Конвейер выполняется с правами репозитория и с доступом к его секретам. Три способа
|
|
5
|
+
# отдать их постороннему живут не в коде, а в двадцати строках yaml: триггер `pull_request_target`
|
|
6
|
+
# вместе с выкачиванием ветки автора PR; действие, закреплённое подвижной МЕТКОЙ, которую владелец
|
|
7
|
+
# действия может перевести на другой код; права `write-all`, выданные всему рабочему потоку.
|
|
8
|
+
# Ни одну из трёх обычная проверка кода не увидит: это не код, это настройка.
|
|
9
|
+
#
|
|
10
|
+
# Работу делает zizmor (MIT, 6459 звёзд, статический разбор GitHub Actions). Мы отвечаем за
|
|
11
|
+
# порог и за то, чтобы «не проверено» не выдавалось за «чисто».
|
|
12
|
+
#
|
|
13
|
+
# ПОЧЕМУ ПОРОГ ИМЕННО ТАКОЙ. Замер 2026-09-08 по шести настоящим репозиториям: express, flask и
|
|
14
|
+
# uv держат НОЛЬ находок высокой и средней строгости; gin (18), ripgrep (19) и httpx (4) — нет.
|
|
15
|
+
# Порог взят с тех, кто его держит, а не выдуман: он достижим, и это доказано чужой практикой.
|
|
16
|
+
# Мы сами на момент заведения записи были на неправильной стороне — 13 высоких и 5 средних.
|
|
17
|
+
DIR="${1:-.}"
|
|
18
|
+
WF="$DIR/.github/workflows"
|
|
19
|
+
|
|
20
|
+
# Нет конвейера — нечего проверять. Это не успех и не провал, это отсутствие предмета.
|
|
21
|
+
[ -d "$WF" ] || exit 0
|
|
22
|
+
|
|
23
|
+
if ! command -v zizmor >/dev/null 2>&1; then
|
|
24
|
+
echo "не найден zizmor — эта проверка делегирована ему"
|
|
25
|
+
echo " почини: pipx install zizmor (или uv tool install zizmor)"
|
|
26
|
+
exit 2
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# --no-exit-codes разводит два разных события, которые иначе слиплись бы в один ненулевой код:
|
|
30
|
+
# «нашлись находки» и «инструмент не отработал». Первое читается из вывода, второе — из кода.
|
|
31
|
+
# NO_COLOR и снятие управляющих последовательностей — вместе, а не по отдельности. Внутри
|
|
32
|
+
# GitHub Actions zizmor КРАСИТ вывод (там цвет поддержан), и итоговая строка начинается с
|
|
33
|
+
# escape-последовательности: правило «^[0-9]+ findings» её не видит, и гейт объявлял, что формат
|
|
34
|
+
# сменился. Локально этого не воспроизвести — вне конвейера цвет выключается сам. Поймано
|
|
35
|
+
# прогоном в конвейере 2026-09-08; тот же класс уже записан у нас в scope.mjs: «цвет снимается ДО
|
|
36
|
+
# поиска». NO_COLOR — соглашение, его может не знать следующая версия; sed — страховка, которая
|
|
37
|
+
# в отличие от NO_COLOR ни от кого не зависит.
|
|
38
|
+
ESC=$(printf '\033')
|
|
39
|
+
OUT=$(NO_COLOR=1 zizmor --no-online-audits --no-exit-codes --min-severity medium --format plain "$WF" 2>&1 \
|
|
40
|
+
| sed "s/${ESC}\[[0-9;]*[a-zA-Z]//g")
|
|
41
|
+
CODE=$?
|
|
42
|
+
if [ "$CODE" -ne 0 ]; then
|
|
43
|
+
echo "zizmor не отработал (код $CODE) — проверка не состоялась, это не вердикт «чисто»"
|
|
44
|
+
printf '%s\n' "$OUT" | grep -iE "error:|panic|not found" | head -3 | sed 's/^/ /'
|
|
45
|
+
echo " почини: прогони «zizmor .github/workflows» руками и посмотри, на чём он споткнулся"
|
|
46
|
+
exit 2
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Итоговая строка — единственное место, где сказано, сколько чего нашлось. Её отсутствие значит,
|
|
50
|
+
# что формат сменился; молчать об этом нельзя, иначе смена формата станет вечным зелёным.
|
|
51
|
+
SUM=$(printf '%s\n' "$OUT" | grep -E "^(No findings to report|[0-9]+ findings)" | tail -1)
|
|
52
|
+
if [ -z "$SUM" ]; then
|
|
53
|
+
echo "ответ zizmor не разобран: итоговой строки в нём нет"
|
|
54
|
+
# Печатаем то, что пришло на самом деле. Без этого причина видна только тому, у кого есть та
|
|
55
|
+
# же машина: первый отказ этой ветки случился в конвейере, а локально не воспроизводился, и
|
|
56
|
+
# разбирать пришлось догадками. Сообщение, не показывающее свой ввод, лечится вторым прогоном.
|
|
57
|
+
echo " вот последнее, что он напечатал:"
|
|
58
|
+
printf '%s\n' "$OUT" | tail -3 | sed 's/^/ /'
|
|
59
|
+
echo " почини: сверь версию zizmor с той, что названа в gate.yml"
|
|
60
|
+
exit 2
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
case "$SUM" in
|
|
64
|
+
"No findings to report"*) exit 0 ;;
|
|
65
|
+
esac
|
|
66
|
+
|
|
67
|
+
printf '%s\n' "$OUT" | grep -E "^(error|warning)\[" | head -20
|
|
68
|
+
N=$(printf '%s\n' "$OUT" | grep -cE "^(error|warning)\[")
|
|
69
|
+
[ "$N" -gt 20 ] && echo " … и ещё $((N - 20))"
|
|
70
|
+
echo " почини: закрепи действия по SHA вместо метки, сузь permissions до нужного задания,"
|
|
71
|
+
echo " замени pull_request_target на pull_request там, где выполняется код автора PR."
|
|
72
|
+
echo " подробности по каждой находке: https://docs.zizmor.sh/audits/"
|
|
73
|
+
exit 1
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
intent: конвейер не отдаёт чужому коду свои права и секреты
|
|
2
|
+
intent_en: the pipeline does not hand its permissions and secrets to somebody else's code
|
|
3
|
+
|
|
4
|
+
# Там, где конвейер есть. Без него отдавать нечего.
|
|
5
|
+
trigger:
|
|
6
|
+
has_ci: true
|
|
7
|
+
|
|
8
|
+
recipes:
|
|
9
|
+
any: bash {gate}/check.sh {dir}
|
|
10
|
+
|
|
11
|
+
# Программа, без которой запись не работает. По первому слову команды этого не видно: обёртка
|
|
12
|
+
# начинается с `bash`. Версия названа: обёртка читает итоговую строку вывода zizmor, и смена
|
|
13
|
+
# формата обязана быть видимой, а не тихой.
|
|
14
|
+
requires: zizmor
|
|
15
|
+
|
|
16
|
+
proof: incidents/README.md, 2026-09-08 «конвейер отдавал права по подвижной метке» — замер по
|
|
17
|
+
шести настоящим репозиториям показал, что express, flask и uv держат ноль находок высокой и
|
|
18
|
+
средней строгости, а gin (18), ripgrep (19) и httpx (4) нет; сам комплект на момент заведения
|
|
19
|
+
записи имел 13 высоких и 5 средних, включая десять действий, закреплённых меткой вместо SHA
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: разбор входящих
|
|
2
|
+
|
|
3
|
+
# pull_request вместо pull_request_target: код автора PR выполняется без доступа к секретам
|
|
4
|
+
# основного репозитория.
|
|
5
|
+
on:
|
|
6
|
+
pull_request:
|
|
7
|
+
types: [opened]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
triage:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
17
|
+
with:
|
|
18
|
+
persist-credentials: false
|
|
19
|
+
- run: echo "разбор без прав на запись"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: разбор входящих
|
|
2
|
+
|
|
3
|
+
# pull_request_target выполняется в контексте ОСНОВНОЙ ветки и с доступом к секретам,
|
|
4
|
+
# а код берётся из ветки автора PR. Классическая дыра.
|
|
5
|
+
on:
|
|
6
|
+
pull_request_target:
|
|
7
|
+
types: [opened]
|
|
8
|
+
|
|
9
|
+
permissions: write-all
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
triage:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
with:
|
|
17
|
+
ref: ${{ github.event.pull_request.head.sha }}
|
|
18
|
+
- run: npm install && npm run build
|
|
@@ -5,7 +5,19 @@
|
|
|
5
5
|
# который в тёмной теме остаётся светлым пятном, — и это видно не автору, а пользователю,
|
|
6
6
|
# который переключил тему. Дефект тихий: в теме автора всё выглядит правильно.
|
|
7
7
|
DIR="${1:-.}"
|
|
8
|
-
|
|
8
|
+
# Существование файла проверяется ДО `.`, а не запасной веткой после. Прежняя строка
|
|
9
|
+
# `. файл 2>/dev/null || запасной_вариант` выглядела страховкой и ею не была: под `sh` (dash)
|
|
10
|
+
# неудачный `.` завершает скрипт немедленно, и ветка после `||` не выполняется никогда; под
|
|
11
|
+
# `bash`, которым гейты и запускаются из манифеста, она выполняется, но подставляет только
|
|
12
|
+
# ПЕРЕМЕННУЮ — функции обхода остаются неопределёнными, конвейер печатает пустоту, и проверка
|
|
13
|
+
# выходит с НУЛЁМ. Замерено 2026-09-08 на файле с настоящим нарушением: гейт сказал «чисто».
|
|
14
|
+
SKIP_LIB="$(dirname "$0")/../_skip.sh"
|
|
15
|
+
if [ ! -f "$SKIP_LIB" ]; then
|
|
16
|
+
echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
|
|
17
|
+
echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
|
|
18
|
+
exit 2
|
|
19
|
+
fi
|
|
20
|
+
. "$SKIP_LIB"
|
|
9
21
|
|
|
10
22
|
# Расширения, где живёт ТЕМИЗИРУЕМЫЙ интерфейс. Своё, а не общий CODE_EXT: там нет ни css, ни
|
|
11
23
|
# vue — они не код в смысле «отладочная печать», но именно в них живёт цвет.
|
|
@@ -28,7 +40,7 @@ EXT_RE="$(printf '%s' "$COLOR_EXT" | tr ' ' '|')"
|
|
|
28
40
|
HITS=$(find "$DIR" $(skip_find "$DIR") -type f -print 2>/dev/null \
|
|
29
41
|
| grep -E "\.($EXT_RE)$" \
|
|
30
42
|
| grep -viE "(^|/)[^/]*($SOURCE_RE)[^/]*\.($EXT_RE)$" \
|
|
31
|
-
|
|
|
43
|
+
| drop_generated \
|
|
32
44
|
| LC_ALL=C sort \
|
|
33
45
|
| xargs -r awk '
|
|
34
46
|
# Блочные комментарии вырезаются ПО СОСТОЯНИЮ, а не построчно. Однострочный фильтр
|
|
@@ -51,7 +63,11 @@ HITS=$(find "$DIR" $(skip_find "$DIR") -type f -print 2>/dev/null \
|
|
|
51
63
|
if (e == 0) { line = substr(line, 1, s - 1); inblock = 1; break }
|
|
52
64
|
line = substr(line, 1, s - 1) substr(rest, e + 2)
|
|
53
65
|
}
|
|
54
|
-
|
|
66
|
+
# Хвост — «не буква, не цифра, не дефис», а не просто «не шестнадцатеричный символ».
|
|
67
|
+
# Якорь ссылки «#defining-entry-points» начинается с «#def», за которым идёт «i»: по
|
|
68
|
+
# прежнему правилу это был цвет. Замер 2026-09-08 по шести чужим репозиториям: в uv
|
|
69
|
+
# ложным оказалось именно это, в docs/js/extra.js.
|
|
70
|
+
if (line ~ /#[0-9a-fA-F]{8}([^0-9a-zA-Z_-]|$)|#[0-9a-fA-F]{6}([^0-9a-zA-Z_-]|$)|#[0-9a-fA-F]{3}([^0-9a-zA-Z_-]|$)/)
|
|
55
71
|
print FILENAME ":" FNR ":" $0
|
|
56
72
|
}
|
|
57
73
|
' 2>/dev/null \
|
|
@@ -2,3 +2,5 @@ export function Button({ children }: { children: React.ReactNode }) {
|
|
|
2
2
|
// Цвет приходит из темы: перекрашивается вместе с ней.
|
|
3
3
|
return <button className="bg-[var(--color-accent)] text-[var(--color-on-accent)]">{children}</button>;
|
|
4
4
|
}
|
|
5
|
+
|
|
6
|
+
const DOC_ANCHOR = "concepts/projects/#defining-entry-points";
|
|
@@ -12,8 +12,18 @@
|
|
|
12
12
|
**Почему машина, а не внимательность.** Написать отчёт «на будущее» — первое, что пропускают под
|
|
13
13
|
давлением дедлайна. Гейт делает это ценой, а не пожеланием.
|
|
14
14
|
|
|
15
|
-
**Готовый
|
|
16
|
-
|
|
15
|
+
**Готовый аналог есть, и раньше здесь было написано «не искал».** Искали 2026-09-07, при ревизии
|
|
16
|
+
каталога. Историю коммитов держат [`gitlint`](https://jorisroovers.com/gitlint/) и
|
|
17
|
+
[`commitlint`](https://commitlint.js.org/): проверяют форму заголовка, тип по Conventional
|
|
18
|
+
Commits, длину строк, пустую строку между заголовком и телом; у `gitlint` есть даже
|
|
19
|
+
`body-min-length` и возможность дописать своё правило на Python.
|
|
20
|
+
|
|
21
|
+
**Почему рецепта под них здесь нет.** Оба проверяют, что тело **есть** и как оно оформлено. Эта
|
|
22
|
+
запись требует другого: чтобы в теле стояли два названных раздела — что сделано и **в чём агент
|
|
23
|
+
не уверен**. Второго нет ни в Conventional Commits, ни во встроенных правилах обоих. Написать
|
|
24
|
+
своё правило `gitlint` можно, но это код на Python в проекте, который может быть не на Python, —
|
|
25
|
+
и он всё равно наш, только в чужой обёртке. Если `commitlint` у вас уже стоит — он закрывает
|
|
26
|
+
форму заголовка, чего не делаем мы; записи это не отменяет.
|
|
17
27
|
|
|
18
28
|
**Чего НЕ ловит.** Не проверяет качество отчёта, только его наличие — «Сделано: починил» и «Не
|
|
19
29
|
уверен: не знаю» формально пройдут. Не проверяет, что отчёт правдив. Это ограничение того же
|
|
@@ -39,7 +49,7 @@
|
|
|
39
49
|
|
|
40
50
|
Гейт всё-таки местный: подсказка «допиши в тело коммита» выполнима до пуша, а не после.
|
|
41
51
|
|
|
42
|
-
**Образцы.** `red
|
|
52
|
+
**Образцы.** `red/.aqk-commit-msg` — обычное тело коммита без отчёта. `green/.aqk-commit-msg` — то же самое
|
|
43
53
|
плюс `Сделано:` и `Не уверен:`. Образцы — текстовые файлы, а не настоящий git: арбитр в реальном
|
|
44
54
|
проекте читает `git log -1`, а вложенный `.git` внутри каталога комплекта создал бы embedded-
|
|
45
55
|
репозиторий, который сам по себе стал бы проблемой версионирования.
|
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
# оставляет след для человека, читающего историю позже.
|
|
5
5
|
DIR="${1:-.}"
|
|
6
6
|
|
|
7
|
-
# Образцы (gates.sh) читают тело коммита из
|
|
7
|
+
# Образцы (gates.sh) читают тело коммита из `.aqk-commit-msg` — реальный git log там взять неоткуда, а
|
|
8
8
|
# вложенный .git внутри каталога комплекта сам по себе создал бы embedded-репозиторий.
|
|
9
|
-
# В настоящем проекте
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
# В настоящем проекте такого файла не бывает — читается последний реальный коммит.
|
|
10
|
+
# Имя с точкой намеренно: `COMMIT_MSG` в корне проекта — вещь возможная (так зовут заготовку
|
|
11
|
+
# сообщения многие обёртки над git), и такой файл молча подменял бы собой настоящий коммит.
|
|
12
|
+
# Тот же дефект нашло ревью у `personal-config-not-shared` 2026-09-07; здесь он был с самого
|
|
13
|
+
# начала и не всплывал.
|
|
14
|
+
if [ -f "$DIR/.aqk-commit-msg" ]; then
|
|
15
|
+
MSG=$(cat "$DIR/.aqk-commit-msg")
|
|
12
16
|
else
|
|
13
17
|
if ! (cd "$DIR" 2>/dev/null && git rev-parse --git-dir >/dev/null 2>&1); then
|
|
14
18
|
echo "не git-репозиторий — проверять нечего"
|
|
@@ -33,5 +33,10 @@
|
|
|
33
33
|
считает только глубину. Не различает функции внутри файла: меряется худшее место файла, а не
|
|
34
34
|
каждая функция отдельно. Готовые правила умеют и то, и другое.
|
|
35
35
|
|
|
36
|
+
**Тесты не проверяются.** Глубокая вложенность в тесте — это обход таблицы ожиданий, а не
|
|
37
|
+
сложная логика. Замер по `fastapi` дал 303 находки, из которых **101 в `tests/`**; после
|
|
38
|
+
исключения тестов осталось 10. Гейт, который краснеет в основном на тестах, выключают целиком —
|
|
39
|
+
тот же довод и по той же причине, что в `duplicate-code`.
|
|
40
|
+
|
|
36
41
|
**Образцы.** `red/` — восемь уровней вложенности. `green/` — тот же смысл, разбитый на две
|
|
37
42
|
функции с ранним выходом.
|
|
@@ -6,12 +6,31 @@
|
|
|
6
6
|
# ЗАЧЕМ ВООБЩЕ. 29 ветвлений в одной функции — это код, который никто не держит в голове
|
|
7
7
|
# целиком: ни человек, ни агент. Агент в таком месте начинает переписывать вместо правки.
|
|
8
8
|
DIR="${1:-.}"
|
|
9
|
-
|
|
9
|
+
# Существование файла проверяется ДО `.`, а не запасной веткой после. Прежняя строка
|
|
10
|
+
# `. файл 2>/dev/null || запасной_вариант` выглядела страховкой и ею не была: под `sh` (dash)
|
|
11
|
+
# неудачный `.` завершает скрипт немедленно, и ветка после `||` не выполняется никогда; под
|
|
12
|
+
# `bash`, которым гейты и запускаются из манифеста, она выполняется, но подставляет только
|
|
13
|
+
# ПЕРЕМЕННУЮ — функции обхода остаются неопределёнными, конвейер печатает пустоту, и проверка
|
|
14
|
+
# выходит с НУЛЁМ. Замерено 2026-09-08 на файле с настоящим нарушением: гейт сказал «чисто».
|
|
15
|
+
SKIP_LIB="$(dirname "$0")/../_skip.sh"
|
|
16
|
+
if [ ! -f "$SKIP_LIB" ]; then
|
|
17
|
+
echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
|
|
18
|
+
echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
|
|
19
|
+
exit 2
|
|
20
|
+
fi
|
|
21
|
+
. "$SKIP_LIB"
|
|
10
22
|
MAX="${AQK_MAX_DEPTH:-5}"
|
|
11
23
|
|
|
24
|
+
# Тесты исключены намеренно — тем же списком, что и в duplicate-code. Глубокая вложенность в
|
|
25
|
+
# тесте это обход таблицы ожиданий, а не сложная логика: замер по fastapi дал 303 находки, из
|
|
26
|
+
# которых 101 в tests/. Гейт, который краснеет в основном на тестах, выключают целиком.
|
|
27
|
+
TESTS="-name test -prune -o -name tests -prune -o -name spec -prune -o -name __tests__ -prune -o"
|
|
28
|
+
|
|
12
29
|
# shellcheck disable=SC2046
|
|
13
|
-
find "$DIR" $(skip_find "$DIR") -type f
|
|
14
|
-
|
|
30
|
+
find "$DIR" $(skip_find "$DIR") $TESTS -type f \
|
|
31
|
+
! -name 'test_*' ! -name '*_test.*' ! -name '*.test.*' ! -name '*.spec.*' \
|
|
32
|
+
-print 2>/dev/null | only_code | own_samples_filter "$DIR" \
|
|
33
|
+
| drop_generated \
|
|
15
34
|
| xargs -r awk -v MAX="$MAX" '
|
|
16
35
|
# Один обход на все файлы: процесс на каждый файл дал 19 секунд на 4000 файлов.
|
|
17
36
|
# Разметка вложена по природе: пять уровней тегов — это не сложная логика, а обычная
|
|
@@ -21,12 +40,11 @@ find "$DIR" $(skip_find "$DIR") -type f -print 2>/dev/null | only_code | own_sam
|
|
|
21
40
|
FNR == 1 { flush(); worst = 0; wl = 0; wf = FILENAME }
|
|
22
41
|
/^[[:space:]]*$/ { next }
|
|
23
42
|
{
|
|
24
|
-
# ширина отступа: табуляция считается за четыре
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
43
|
+
# ширина отступа: табуляция считается за четыре пробела. Отступ берётся ОДНИМ
|
|
44
|
+
# match, а не посимвольным циклом с substr: цикл выделял новую строку на каждый
|
|
45
|
+
# символ отступа и стоил больше, чем весь остальной разбор. Замер 2026-09-08 на uv.
|
|
46
|
+
match($0, /^[ \t]*/); ind = substr($0, 1, RLENGTH)
|
|
47
|
+
n = gsub(/\t/, "", ind) * 4 + length(ind)
|
|
30
48
|
depth = int(n / 4)
|
|
31
49
|
if (depth > worst) { worst = depth; wl = FNR }
|
|
32
50
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Тест с глубокой вложенностью — не сложная логика, а разбор дерева ожиданий. Найдено замером
|
|
2
|
+
# по fastapi: 101 находка из 303 пришлась на tests/, и все они — таблицы ожидаемых ответов,
|
|
3
|
+
# которые никто не станет «упрощать». Гейт, который краснеет в основном на тестах, выключат.
|
|
4
|
+
def test_openapi_schema(client):
|
|
5
|
+
response = client.get("/openapi.json")
|
|
6
|
+
assert response.status_code == 200
|
|
7
|
+
schema = response.json()
|
|
8
|
+
for path, methods in schema["paths"].items():
|
|
9
|
+
for method, spec in methods.items():
|
|
10
|
+
for code, resp in spec["responses"].items():
|
|
11
|
+
if "content" in resp:
|
|
12
|
+
for media, body in resp["content"].items():
|
|
13
|
+
if "schema" in body:
|
|
14
|
+
assert body["schema"] is not None
|
|
@@ -26,4 +26,17 @@
|
|
|
26
26
|
**Готового аналога нет** — ни в линтерах, ни в пакетных менеджерах: они
|
|
27
27
|
умеют создать файл версий, но не умеют требовать, чтобы он существовал и лежал в репозитории.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
**Закрепляют не только poetry и uv.** Для `pyproject.toml` файлом закрепления считается и
|
|
30
|
+
`requirements.txt` — но только если он сам проходит проверку, то есть все версии в нём стоят
|
|
31
|
+
через `==`. Иначе «есть requirements.txt» стало бы способом обойти гейт пустым файлом.
|
|
32
|
+
Найдено замером по `httpx`: там инструменты закреплены до патча прямо в `requirements.txt`,
|
|
33
|
+
а гейт требовал ещё и `poetry.lock`, которого в этом укладе не бывает вовсе.
|
|
34
|
+
|
|
35
|
+
**Не различает библиотеку и приложение.** Библиотека намеренно оставляет
|
|
36
|
+
границы своих зависимостей широкими — закрепив их у себя, она ломает сборку всем, кто её
|
|
37
|
+
ставит. Гейт смотрит только на то, закреплено ли окружение самого проекта, и правильно молчит,
|
|
38
|
+
когда библиотека закрепила инструменты и не закрепила зависимости.
|
|
39
|
+
|
|
40
|
+
**Образцы.** `red/` — объявление зависимостей без закрепления, плюс `pyproject.toml` рядом с
|
|
41
|
+
незакреплённым `requirements.txt`. `green/` — с закреплением, включая уклад «pyproject плюс
|
|
42
|
+
requirements.txt с точными версиями».
|
|
@@ -30,7 +30,12 @@ need() {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
need package.json package-lock.json yarn.lock pnpm-lock.yaml npm-shrinkwrap.json
|
|
33
|
-
|
|
33
|
+
# requirements.txt считается закреплением для pyproject.toml наравне с файлами блокировки:
|
|
34
|
+
# закрепляют не только poetry и uv. Замер по httpx: инструменты там закреплены до патча
|
|
35
|
+
# прямо в requirements.txt, а гейт требовал ещё и poetry.lock, которого в этом укладе не
|
|
36
|
+
# бывает вовсе. Файл засчитывается только если он сам проходит проверку ниже — иначе
|
|
37
|
+
# «есть requirements.txt» стало бы способом обойти гейт пустым файлом.
|
|
38
|
+
need pyproject.toml poetry.lock uv.lock pdm.lock requirements.txt
|
|
34
39
|
need go.mod go.sum
|
|
35
40
|
need Cargo.toml Cargo.lock
|
|
36
41
|
need Gemfile Gemfile.lock
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Питоновская библиотека, которая закрепляет версии не файлом блокировки, а точными версиями
|
|
2
|
+
# в requirements.txt. Так делает httpx и весь класс проектов, живущих без poetry и uv:
|
|
3
|
+
# инструменты закреплены до патча, а границы зависимостей самой библиотеки оставлены широкими
|
|
4
|
+
# намеренно — библиотека, закрепившая их у себя, ломает сборку всем, кто её ставит.
|
|
5
|
+
[project]
|
|
6
|
+
name = "sample-lib"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
dependencies = ["httpx"]
|
|
9
|
+
|
|
10
|
+
[build-system]
|
|
11
|
+
requires = ["setuptools"]
|
|
12
|
+
build-backend = "setuptools.build_meta"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Питоновская библиотека, которая закрепляет версии не файлом блокировки, а точными версиями
|
|
2
|
+
# в requirements.txt. Так делает httpx и весь класс проектов, живущих без poetry и uv:
|
|
3
|
+
# инструменты закреплены до патча, а границы зависимостей самой библиотеки оставлены широкими
|
|
4
|
+
# намеренно — библиотека, закрепившая их у себя, ломает сборку всем, кто её ставит.
|
|
5
|
+
[project]
|
|
6
|
+
name = "sample-lib"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
dependencies = ["httpx"]
|
|
9
|
+
|
|
10
|
+
[build-system]
|
|
11
|
+
requires = ["setuptools"]
|
|
12
|
+
build-backend = "setuptools.build_meta"
|
|
@@ -12,13 +12,22 @@
|
|
|
12
12
|
**Почему агенты плодят дубли особенно охотно.** Скопировать из соседнего файла дешевле, чем
|
|
13
13
|
найти общее место и вынести туда: копия точно работает, вынесение может что-то сломать.
|
|
14
14
|
|
|
15
|
-
**Готовый аналог
|
|
16
|
-
|
|
15
|
+
**Готовый аналог есть, и под каждый стек свой.** `jscpd` для фронта — считает **похожесть**, а не
|
|
16
|
+
только точное совпадение. `pylint --enable=R0801` для Python — сравнивает разобранный код, и
|
|
17
|
+
переименованная переменная его не обманет. Рецепт под Python появился только 2026-09-07: до
|
|
18
|
+
ревизии каталога его не было вовсе, и питоновский проект получал нашу побайтовую самоделку,
|
|
19
|
+
хотя родной инструмент стоял у него уже установленным.
|
|
17
20
|
|
|
18
21
|
**Переносимая проверка грубее.** Она ищет одинаковые восемь строк подряд после снятия отступов —
|
|
19
22
|
совпадение байт в байт. Копия с переименованной переменной её не насторожит. Это запасной
|
|
20
23
|
вариант, а не замена.
|
|
21
24
|
|
|
25
|
+
**Одинаковые импорты — не дубль.** Окно, в котором нет ни одной строки кроме ввоза (`import`,
|
|
26
|
+
`use`, `require`, путь в кавычках внутри `import (…)` в Go) и одиноких скобок, находкой не
|
|
27
|
+
считается. Найдено замером по `cobra`: гейт краснел на `doc/man_docs.go` и `doc/md_docs.go`,
|
|
28
|
+
где совпадали восемь строк подряд из списка импортов и ни одной строки логики. В Go, Java и Rust
|
|
29
|
+
ввоз стоит по одной строке на строку, и в соседних файлах одного пакета он совпадает всегда.
|
|
30
|
+
|
|
22
31
|
**Тесты не проверяются.** Повтор в тестах часто осознанный: читаемость важнее сухости, и три
|
|
23
32
|
похожих теста лучше одного хитрого. Случай «три однотипных — свести в один с набором входов»
|
|
24
33
|
решается по правилам тестирования и глазами. На живом проекте **все двадцать находок были в
|
|
@@ -5,7 +5,19 @@
|
|
|
5
5
|
# ЗАЧЕМ. Дубль опаснее длины: правку вносят в одну копию из четырёх, три остаются со старым
|
|
6
6
|
# поведением, и расхождение всплывает через недели в другом месте.
|
|
7
7
|
DIR="${1:-.}"
|
|
8
|
-
|
|
8
|
+
# Существование файла проверяется ДО `.`, а не запасной веткой после. Прежняя строка
|
|
9
|
+
# `. файл 2>/dev/null || запасной_вариант` выглядела страховкой и ею не была: под `sh` (dash)
|
|
10
|
+
# неудачный `.` завершает скрипт немедленно, и ветка после `||` не выполняется никогда; под
|
|
11
|
+
# `bash`, которым гейты и запускаются из манифеста, она выполняется, но подставляет только
|
|
12
|
+
# ПЕРЕМЕННУЮ — функции обхода остаются неопределёнными, конвейер печатает пустоту, и проверка
|
|
13
|
+
# выходит с НУЛЁМ. Замерено 2026-09-08 на файле с настоящим нарушением: гейт сказал «чисто».
|
|
14
|
+
SKIP_LIB="$(dirname "$0")/../_skip.sh"
|
|
15
|
+
if [ ! -f "$SKIP_LIB" ]; then
|
|
16
|
+
echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
|
|
17
|
+
echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
|
|
18
|
+
exit 2
|
|
19
|
+
fi
|
|
20
|
+
. "$SKIP_LIB"
|
|
9
21
|
WIN="${AQK_DUP_LINES:-8}"
|
|
10
22
|
|
|
11
23
|
# Тесты исключены намеренно. Повтор в тестах часто осознанный: читаемость там важнее сухости,
|
|
@@ -18,18 +30,37 @@ TESTS="-name test -prune -o -name tests -prune -o -name spec -prune -o -name __t
|
|
|
18
30
|
find "$DIR" $(skip_find "$DIR") $TESTS -type f \
|
|
19
31
|
! -name 'test_*' ! -name '*_test.*' ! -name '*.test.*' ! -name '*.spec.*' \
|
|
20
32
|
-print 2>/dev/null | only_code | own_samples_filter "$DIR" \
|
|
21
|
-
|
|
|
33
|
+
| drop_generated \
|
|
22
34
|
| LC_ALL=C sort \
|
|
23
35
|
| xargs -r env LC_ALL=C awk -v WIN="$WIN" '
|
|
24
|
-
|
|
36
|
+
# Строка-объявление ввоза: `import`, `from … import`, `use …;`, путь в кавычках внутри
|
|
37
|
+
# блока `import (…)` в Go, `require(…)`, а также одинокие скобки и точки с запятой.
|
|
38
|
+
# ЗАЧЕМ. Восемь одинаковых строк ввоза подряд — форма языка, а не размноженный код: в Go,
|
|
39
|
+
# Java и Rust список ввоза стоит по одной строке и в соседних файлах одного пакета
|
|
40
|
+
# совпадает целиком. Замер по cobra: обе находки в doc/ были ровно этим — ни одной
|
|
41
|
+
# строки логики. Окно, где нет ни одной строки кроме ввоза, дублем не считается.
|
|
42
|
+
function isimport(s) {
|
|
43
|
+
return s ~ /^(import|from|use|export|require|package|#include|using)([[:space:](]|$)/ ||
|
|
44
|
+
s ~ /^[A-Za-z_][A-Za-z0-9_]*[[:space:]]+"[^"]*"$/ ||
|
|
45
|
+
s ~ /^[_.]?[[:space:]]*"[^"]*"[,;]?$/ ||
|
|
46
|
+
s ~ /^(const|let|var)[[:space:]].*require\(/ ||
|
|
47
|
+
s ~ /^[(){}\[\];,]+$/
|
|
48
|
+
}
|
|
49
|
+
FNR == 1 { n = 0; delete buf; delete imp }
|
|
25
50
|
{
|
|
26
51
|
line = $0
|
|
27
52
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", line)
|
|
28
53
|
if (line == "" || line ~ /^([#]|\/\/)/) next # пустые и комментарии не считаем
|
|
29
54
|
buf[++n] = line
|
|
55
|
+
imp[n] = isimport(line)
|
|
30
56
|
if (n >= WIN) {
|
|
31
|
-
|
|
32
|
-
|
|
57
|
+
# Ключ и признак «есть ли код» собираются ОДНИМ проходом по окну. Разнести их на
|
|
58
|
+
# два цикла выглядело ускорением — окно из одного ввоза отбрасывалось бы до сборки
|
|
59
|
+
# ключа. Замерено 2026-09-08 на uv: стало 2791 мс против 1770. Второй обход окна
|
|
60
|
+
# дороже, чем сборка ключа, которую он экономит; правка откачена по замеру.
|
|
61
|
+
key = ""; code = 0
|
|
62
|
+
for (i = n - WIN + 1; i <= n; i++) { key = key buf[i] "\x1e"; if (!imp[i]) code = 1 }
|
|
63
|
+
if (!code) next # окно целиком из ввоза — не дубль
|
|
33
64
|
if (key in seen && seen[key] != FILENAME ":" (FNR - WIN + 1)) {
|
|
34
65
|
print seen[key] " и " FILENAME ":" (FNR - WIN + 1) ": одинаковые " WIN " строк"
|
|
35
66
|
} else if (!(key in seen)) {
|
|
@@ -16,7 +16,15 @@ recipes:
|
|
|
16
16
|
#
|
|
17
17
|
# «c#» в списке нет намеренно: разбор манифеста режет строку по «#», и рецепт обрывался бы
|
|
18
18
|
# на «c». Обёртку _native.sh не пишем — её добавляет сама программа при установке гейта.
|
|
19
|
+
#
|
|
20
|
+
# Рецепт стоял только под javascript и typescript, хотя список --format покрывает и Python,
|
|
21
|
+
# и Go. Питоновский проект получал переносимую самоделку вместо готового инструмента —
|
|
22
|
+
# найдено ревизией каталога 2026-09-07, замером по пяти стекам.
|
|
19
23
|
javascript: npx --yes jscpd@5 --min-lines 8 --threshold 1 --format "javascript,jsx,typescript,tsx,vue,python,go,ruby,java,php,rust,kotlin,swift,scala" {dir}
|
|
20
24
|
typescript: npx --yes jscpd@5 --min-lines 8 --threshold 1 --format "javascript,jsx,typescript,tsx,vue,python,go,ruby,java,php,rust,kotlin,swift,scala" {dir}
|
|
25
|
+
# Родной инструмент стека, а не jscpd: питоновскому проекту незачем ставить Node ради одной
|
|
26
|
+
# проверки. R0801 сравнивает не байты, а разобранный код — переименованная переменная его
|
|
27
|
+
# не обманет, в отличие от переносимого рецепта.
|
|
28
|
+
python: pylint --disable=all --enable=R0801 --min-similarity-lines=8 {dir}
|
|
21
29
|
|
|
22
30
|
proof: incidents/README.md — «2026-08-25 разбор 1069 коммитов»: число моделей захардкожено в четырёх местах четырьмя разными значениями
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Одинаковый блок импортов в двух файлах одного пакета — не дубль кода, а форма языка.
|
|
2
|
+
// Найдено замером по cobra: гейт краснел на doc/man_docs.go и doc/md_docs.go, где совпадали
|
|
3
|
+
// восемь строк подряд из списка импортов, и ни одной строки логики.
|
|
4
|
+
package doc
|
|
5
|
+
|
|
6
|
+
import (
|
|
7
|
+
"bytes"
|
|
8
|
+
"fmt"
|
|
9
|
+
"io"
|
|
10
|
+
"os"
|
|
11
|
+
"path/filepath"
|
|
12
|
+
"sort"
|
|
13
|
+
"strconv"
|
|
14
|
+
"strings"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
func RenderMan(w io.Writer, name string) error {
|
|
18
|
+
_, err := fmt.Fprintf(w, "man page for %s", name)
|
|
19
|
+
return err
|
|
20
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
package doc
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"fmt"
|
|
6
|
+
"io"
|
|
7
|
+
"os"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"sort"
|
|
10
|
+
"strconv"
|
|
11
|
+
"strings"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
func RenderMarkdown(w io.Writer, name string) error {
|
|
15
|
+
var buf bytes.Buffer
|
|
16
|
+
buf.WriteString(strings.ToUpper(name))
|
|
17
|
+
_, err := w.Write(buf.Bytes())
|
|
18
|
+
return err
|
|
19
|
+
}
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
выбираются **по языку проекта**, а эти инструменты к языку не привязаны. Если такой инструмент у
|
|
16
16
|
вас стоит — он лучше нашего.
|
|
17
17
|
|
|
18
|
+
**Адрес страницы сайта не считается битой ссылкой.** Путь, кончающийся косой чертой —
|
|
19
|
+
`[руководство](tutorial/#install)`, — это адрес на опубликованном сайте, а не файл в
|
|
20
|
+
репозитории. Так ссылаются mkdocs, docusaurus и jekyll. Найдено замером по `fastapi`: гейт
|
|
21
|
+
объявлял битой рабочую ссылку из их README.
|
|
22
|
+
|
|
18
23
|
**Чего НЕ ловит.** Только файлы `*.md` в самом каталоге, без обхода вложенных: намерение записи —
|
|
19
24
|
точка входа, а не вся документация. Не проверяет внешние адреса (сеть) и якоря внутри файла.
|
|
20
25
|
|
|
@@ -7,13 +7,22 @@ MISS=0
|
|
|
7
7
|
for MD in "$DIR"/*.md; do
|
|
8
8
|
[ -f "$MD" ] || continue
|
|
9
9
|
# вытащить цели ссылок вида [текст](путь)
|
|
10
|
-
|
|
10
|
+
# Строки со вставками в обратных кавычках чистятся ДО разбора: «`[name](url)`» — это пример
|
|
11
|
+
# оформления ссылки, а не ссылка. Замер 2026-09-08: в uv/STYLE.md именно так и было, и гейт
|
|
12
|
+
# требовал создать файл с именем «url».
|
|
13
|
+
TARGETS=$(sed 's/`[^`]*`//g' "$MD" | sed -n 's/.*](\([^)]*\)).*/\1/p')
|
|
11
14
|
for T in $TARGETS; do
|
|
12
15
|
# автоссылки бывают обёрнуты как <(https://...)> — искать http где угодно внутри,
|
|
13
16
|
# не только в начале строки.
|
|
14
17
|
case "$T" in *http://*|*https://*|\#*|mailto:*) continue ;; esac
|
|
15
18
|
T=${T%%#*}
|
|
16
19
|
[ -z "$T" ] && continue
|
|
20
|
+
# Путь, кончающийся косой чертой, — адрес страницы опубликованного сайта, а не файл на
|
|
21
|
+
# диске. Так ссылаются mkdocs, docusaurus и jekyll: `[руководство](tutorial/#install)`
|
|
22
|
+
# работает у читателя и не существует в репозитории. Найдено замером по fastapi — гейт
|
|
23
|
+
# объявлял битой рабочую ссылку из их README. Проверять такие адреса умеют lychee и
|
|
24
|
+
# markdown-link-check: они ходят в сеть, а мы смотрим только на диск.
|
|
25
|
+
case "$T" in */) continue ;; esac
|
|
17
26
|
if [ ! -e "$DIR/$T" ]; then
|
|
18
27
|
echo "$MD: ссылка в никуда — $T"
|
|
19
28
|
echo " почини: создай файл или убери ссылку. Документ, обещающий несуществующее, хуже отсутствующего."
|
|
@@ -3,3 +3,8 @@
|
|
|
3
3
|
Стандарты: [общие правила](rules/general.md).
|
|
4
4
|
Внешняя ссылка: [semver](https://semver.org).
|
|
5
5
|
Автоссылка в скобках, как в CHANGELOG.md gin: [#1](<(https://example.com/pull/1)>).
|
|
6
|
+
Ссылка на страницу сайта документации: [руководство](tutorial/#install) — путь опубликованного
|
|
7
|
+
сайта, а не файл на диске. Найдено замером по fastapi: `[installation guide](tutorial/#install-fastapi)`
|
|
8
|
+
в README читалась как битая, хотя на сайте работает. Так ссылаются mkdocs, docusaurus и jekyll.
|
|
9
|
+
|
|
10
|
+
Пример оформления ссылки: `[name](url)` — это пример, а не ссылка.
|