@rt-tools/agent-kit 0.3.0 → 0.5.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 +194 -30
- package/assets/agents/business-analyst.md +74 -0
- package/assets/agents/project-manager.md +70 -0
- package/assets/agents/qa-engineer.md +72 -0
- package/assets/agents/skill-curator.md +110 -0
- package/assets/agents/spec-critic.md +44 -0
- package/assets/agents/spec-writer.md +50 -0
- package/assets/checks/board.github.mjs +329 -0
- package/assets/checks/check-board.github.mjs +181 -0
- package/assets/checks/check-doc-paths.mjs +163 -0
- package/assets/checks/check-dupes.mjs +277 -0
- package/assets/checks/check-lib-layers.mjs +573 -0
- package/assets/checks/check-reuse.mjs +208 -0
- package/assets/checks/check-schema-drift.mjs +186 -0
- package/assets/checks/check-specs.mjs +1086 -0
- package/assets/checks/check-styles.mjs +109 -0
- package/assets/checks/rt-kit-checks.config.mjs +134 -0
- package/assets/checks/task-new.github.mjs +198 -0
- package/assets/commands/skill-curator.md +70 -0
- package/assets/defaults/gate-map.sh +106 -0
- package/assets/defaults/project.sh +204 -0
- package/assets/hooks/browser-device-id.sh +0 -0
- package/assets/hooks/browser-guard-device-id.sh +2 -1
- package/assets/hooks/browser-guard-no-asking.sh +27 -0
- package/assets/hooks/browser-guard-no-listing.sh +2 -1
- package/assets/hooks/browser-guard-no-other-drivers.sh +2 -1
- package/assets/hooks/browser-guard-require-select.sh +2 -1
- package/assets/hooks/commit-msg.sh +1 -1
- package/assets/hooks/constitution-index.sh +5 -4
- package/assets/hooks/dev-server-guard.sh +8 -6
- package/assets/hooks/docs-guard.sh +223 -37
- package/assets/hooks/git-guard-delivery.sh +171 -31
- package/assets/hooks/git-guard-main.sh +1 -0
- package/assets/hooks/git-guard-push-tests.sh +34 -13
- package/assets/hooks/glossary-load.sh +23 -0
- package/assets/hooks/grill-gate.sh +96 -0
- package/assets/hooks/lint-after-edit.sh +155 -30
- package/assets/hooks/qa-dataid-guard.sh +72 -32
- package/assets/hooks/reuse-first-guard.sh +105 -34
- package/assets/hooks/skill-gate-rearm.sh +1 -0
- package/assets/hooks/skill-gate.sh +75 -15
- package/assets/hooks/skill-loaded.sh +1 -0
- package/assets/hooks/sql-guard.sh +606 -56
- package/assets/hooks/task-context-load.sh +100 -0
- package/assets/hooks/task-flow-guard.sh +118 -0
- package/assets/laws/{access.md → application/access.md} +1 -4
- package/assets/laws/{locales.md → application/locales.md} +1 -3
- package/assets/laws/application/money.md +41 -0
- package/assets/laws/application/ownership.md +32 -0
- package/assets/laws/{search-visibility.md → application/search-visibility.md} +1 -1
- package/assets/laws/code-structure.md +7 -6
- package/assets/laws/delivery.md +53 -3
- package/assets/laws/entity-editing.md +49 -55
- package/assets/laws/entity-models.md +4 -14
- package/assets/laws/frontend-application.md +5 -5
- package/assets/laws/lib-imports.md +14 -1
- package/assets/laws/lists.md +33 -0
- package/assets/laws/navigation.md +40 -0
- package/assets/laws/project-documentation.md +27 -8
- package/assets/laws/reuse-first.md +26 -21
- package/assets/laws/shared-code.md +13 -1
- package/assets/laws/verifiability.md +30 -1
- package/assets/laws/work-conduct.md +59 -0
- package/assets/patterns/admin-lists-screen.md +131 -0
- package/assets/patterns/admin-nav-item.md +71 -0
- package/assets/patterns/angular-patterns-state.md +29 -22
- package/assets/patterns/api-layer-pair.md +40 -30
- package/assets/patterns/browser-verification-measure.md +41 -38
- package/assets/patterns/browser-verification-stand.md +106 -42
- package/assets/patterns/component-structure-new.md +33 -32
- package/assets/patterns/dependencies-upgrade.md +65 -0
- package/assets/patterns/doc-style-sweep.md +65 -28
- package/assets/patterns/doc-style-write.md +36 -33
- package/assets/patterns/entity-aside.md +136 -0
- package/assets/patterns/entity-models-new.md +124 -0
- package/assets/patterns/entity-store.md +91 -0
- package/assets/patterns/git-workflow-commit.azure.md +259 -0
- package/assets/patterns/git-workflow-commit.github.md +337 -0
- package/assets/patterns/git-workflow-commit.gitlab.md +283 -0
- package/assets/patterns/git-workflow-merge.md +42 -25
- package/assets/patterns/git-workflow-migration.md +61 -31
- package/assets/patterns/git-workflow-restart.md +20 -20
- package/assets/patterns/lib-layers-move.md +50 -32
- package/assets/patterns/lib-layers-new.md +41 -29
- package/assets/patterns/ownership-scope-resolve.md +69 -0
- package/assets/patterns/permissions-procedure.md +35 -33
- package/assets/patterns/platform-access-di.md +39 -25
- package/assets/patterns/pricing-quote.md +71 -0
- package/assets/patterns/reuse-first-extend.md +22 -22
- package/assets/patterns/seo-page.md +52 -40
- package/assets/patterns/seo-verify.md +48 -29
- package/assets/patterns/shared-code-new.md +37 -31
- package/assets/patterns/spec-driven-domain.md +60 -37
- package/assets/patterns/spec-driven-rule.md +55 -40
- package/assets/patterns/styling-bem-component.md +43 -32
- package/assets/patterns/styling-bem-layout.md +30 -24
- package/assets/patterns/task-flow-close.md +154 -0
- package/assets/patterns/task-flow-resume.md +94 -0
- package/assets/patterns/task-flow-start.md +129 -0
- package/assets/patterns/testing-e2e.md +53 -51
- package/assets/patterns/testing-unit.md +70 -46
- package/assets/patterns/translations-key.md +32 -19
- package/assets/patterns/ts-procedure.md +24 -25
- package/assets/rules/angular-patterns.md +50 -27
- package/assets/rules/api-layer.md +46 -28
- package/assets/rules/browser-verification.md +67 -48
- package/assets/rules/component-structure.md +43 -27
- package/assets/rules/dependencies.md +66 -0
- package/assets/rules/doc-style.md +95 -39
- package/assets/rules/entity-conventions.md +78 -0
- package/assets/rules/entity-models.md +70 -0
- package/assets/rules/git-workflow.azure.md +116 -0
- package/assets/rules/git-workflow.github.md +123 -0
- package/assets/rules/git-workflow.gitlab.md +113 -0
- package/assets/rules/lib-layers.md +56 -30
- package/assets/rules/lists.md +73 -0
- package/assets/rules/navigation.md +78 -0
- package/assets/rules/ownership-scope.md +63 -0
- package/assets/rules/permissions.md +43 -25
- package/assets/rules/platform-access.md +57 -29
- package/assets/rules/pricing.md +64 -0
- package/assets/rules/reuse-first.md +57 -43
- package/assets/rules/seo.md +51 -30
- package/assets/rules/shared-code.md +51 -26
- package/assets/rules/spec-driven.md +107 -51
- package/assets/rules/styling-bem.md +54 -39
- package/assets/rules/task-flow.md +150 -0
- package/assets/rules/testing.md +78 -47
- package/assets/rules/translations.md +48 -31
- package/assets/rules/typescript-conventions.md +57 -27
- package/assets/skills/agent-kit.md +85 -0
- package/assets/skills/write-a-skill.md +108 -0
- package/assets/templates/gate-map.sh +23 -15
- package/assets/templates/implementation.md +14 -8
- package/assets/templates/pattern.md +1 -1
- package/assets/templates/project.sh +32 -19
- package/assets/templates/rule.md +2 -2
- package/assets/variants.json +20 -0
- package/assets/workflows/feature.js +134 -0
- package/assets/workflows/plan.js +150 -0
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +78 -5
- package/bin/agent-kit.js.map +1 -1
- package/bin/prompt.d.ts +5 -0
- package/bin/prompt.d.ts.map +1 -1
- package/bin/prompt.js +19 -7
- package/bin/prompt.js.map +1 -1
- package/index.d.ts +1 -0
- package/index.d.ts.map +1 -1
- package/index.js +1 -0
- package/index.js.map +1 -1
- package/lib/assets.d.ts +8 -3
- package/lib/assets.d.ts.map +1 -1
- package/lib/assets.js +13 -3
- package/lib/assets.js.map +1 -1
- package/lib/catalog.d.ts +52 -5
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +104 -16
- package/lib/catalog.js.map +1 -1
- package/lib/commands.d.ts +22 -1
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +202 -14
- package/lib/commands.js.map +1 -1
- package/lib/companion.d.ts +5 -1
- package/lib/companion.d.ts.map +1 -1
- package/lib/companion.js +29 -2
- package/lib/companion.js.map +1 -1
- package/lib/config.d.ts +26 -9
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +41 -15
- package/lib/config.js.map +1 -1
- package/lib/freshness.d.ts +14 -0
- package/lib/freshness.d.ts.map +1 -0
- package/lib/freshness.js +116 -0
- package/lib/freshness.js.map +1 -0
- package/lib/hooks-map.d.ts +27 -0
- package/lib/hooks-map.d.ts.map +1 -0
- package/lib/hooks-map.js +77 -0
- package/lib/hooks-map.js.map +1 -0
- package/lib/integrity.d.ts +36 -0
- package/lib/integrity.d.ts.map +1 -0
- package/lib/integrity.js +44 -0
- package/lib/integrity.js.map +1 -0
- package/lib/picker.d.ts +11 -1
- package/lib/picker.d.ts.map +1 -1
- package/lib/picker.js +44 -6
- package/lib/picker.js.map +1 -1
- package/lib/sync.d.ts +26 -0
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +59 -4
- package/lib/sync.js.map +1 -1
- package/lib/variants.d.ts +44 -0
- package/lib/variants.d.ts.map +1 -0
- package/lib/variants.js +82 -0
- package/lib/variants.js.map +1 -0
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.5.0.tgz +0 -0
- package/assets/laws/admin-lists.md +0 -35
- package/assets/laws/admin-navigation.md +0 -38
- package/assets/patterns/git-workflow-commit.md +0 -175
- package/assets/rules/git-workflow.md +0 -106
- package/rt-tools-agent-kit-0.3.0.tgz +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# rt-hook: PreToolUse Bash|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
|
|
2
3
|
# Гард проверок перед пушем. PreToolUse на вызове пуша.
|
|
3
4
|
#
|
|
4
5
|
# Пуш — это вход в конвейер: слияние в главную ветку запускает выкатку, и всё, что не
|
|
@@ -6,13 +7,20 @@
|
|
|
6
7
|
# выборочном прогоне и красные в конвейере: один раз прогонялись только сквозные спеки, другой
|
|
7
8
|
# — только затронутый проект.
|
|
8
9
|
#
|
|
9
|
-
# Гард не верит на слово: он сам гоняет то, что перечислил профиль
|
|
10
|
-
#
|
|
11
|
-
#
|
|
10
|
+
# Гард не верит на слово: он сам гоняет то, что перечислил профиль дерева, и пускает пуш только
|
|
11
|
+
# при нулевом коде возврата. Если прогонщик кэширует результат, набор на неизменившемся дереве
|
|
12
|
+
# занимает секунды, а после правки гоняется заново.
|
|
12
13
|
#
|
|
13
|
-
# Что гонять, знает профиль:
|
|
14
|
-
#
|
|
15
|
-
#
|
|
14
|
+
# Что гонять, знает профиль: функция `rt_push_checks <база>` — по команде на строку. База —
|
|
15
|
+
# ветка, относительно которой считается вклад; пустая означает, что удалённого нет. Функция
|
|
16
|
+
# зовётся из каталога, откуда идёт пуш, и вправе решать по нему сама: шаг, проверки которого в
|
|
17
|
+
# дереве нет, она просто не печатает. Нет профиля или нет функции — гард пропускает: список
|
|
18
|
+
# проверок пакет выдумать не может.
|
|
19
|
+
#
|
|
20
|
+
# База берётся у удалённого, а не у локальной главной ветки: локальная отстаёт или расходится
|
|
21
|
+
# молча. Это уже случалось — локальная стояла на слиянии, стёртом из истории силовым пушем, и
|
|
22
|
+
# набор от неё посчитался бы не тот. Нет сети или нет удалённого — база пустая, и профиль
|
|
23
|
+
# откатывается на полный прогон: гейт может оказаться строже нужного, но НИКОГДА не слабее.
|
|
16
24
|
#
|
|
17
25
|
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: не репозиторий, битый ввод, нет профиля — пропуск.
|
|
18
26
|
|
|
@@ -21,6 +29,7 @@ input="$(cat 2>/dev/null)"
|
|
|
21
29
|
|
|
22
30
|
tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
|
|
23
31
|
case "$tool" in
|
|
32
|
+
# Терминал среды и универсальный исполнитель кладут команду в то же поле.
|
|
24
33
|
Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool) ;;
|
|
25
34
|
*) exit 0 ;;
|
|
26
35
|
esac
|
|
@@ -40,13 +49,25 @@ workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
|
|
|
40
49
|
[ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
|
|
41
50
|
cd "$workdir" 2>/dev/null || exit 0
|
|
42
51
|
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
|
|
52
|
+
[ -f package.json ] || exit 0
|
|
43
53
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
#
|
|
47
|
-
|
|
54
|
+
# Профиль дерева: сперва умолчание пакета, поверх него — надстройка проекта, если она есть.
|
|
55
|
+
# Объявленная в надстройке функция замещает умолчание целиком и вправе позвать его обратно
|
|
56
|
+
# суффиксом `_default`. Нет ни того ни другого — хук пропускает: пустой гард лучше гарда,
|
|
57
|
+
# отбивающего наугад.
|
|
58
|
+
rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
59
|
+
for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../defaults/project.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/defaults/project.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/project.sh"; do
|
|
60
|
+
# shellcheck disable=SC1090
|
|
61
|
+
[ -f "$profile" ] && . "$profile" 2>/dev/null
|
|
62
|
+
done
|
|
48
63
|
command -v rt_push_checks >/dev/null 2>&1 || exit 0
|
|
49
64
|
|
|
65
|
+
main_branch="${RT_MAIN_BRANCH:-main}"
|
|
66
|
+
base=''
|
|
67
|
+
if git fetch --quiet origin "$main_branch" 2>/dev/null && git rev-parse --verify --quiet "origin/$main_branch" >/dev/null 2>&1; then
|
|
68
|
+
base="origin/$main_branch"
|
|
69
|
+
fi
|
|
70
|
+
|
|
50
71
|
failed=""
|
|
51
72
|
output=""
|
|
52
73
|
while IFS= read -r check; do
|
|
@@ -56,14 +77,14 @@ while IFS= read -r check; do
|
|
|
56
77
|
output="$out"
|
|
57
78
|
break
|
|
58
79
|
done <<EOF
|
|
59
|
-
$(rt_push_checks)
|
|
80
|
+
$(rt_push_checks "$base")
|
|
60
81
|
EOF
|
|
61
82
|
|
|
62
83
|
[ -z "$failed" ] && exit 0
|
|
63
84
|
|
|
64
85
|
# Хвост вывода, а не весь: у прогонщика он длинный, а нужна причина отказа.
|
|
65
|
-
tail_out="$(printf '%s' "$output" | tail -n 40)"
|
|
66
|
-
reason="
|
|
86
|
+
tail_out="$(printf '%s' "$output" | tail -n 40 | tr -d '\000')"
|
|
87
|
+
reason="BLOCKED: пуш без зелёного локального прогона. «${failed}» упала — почини и пушь снова, обходить гард нельзя. Пуш — вход в конвейер, и красное отсюда проверяется уже на проде. Хвост вывода:
|
|
67
88
|
|
|
68
89
|
${tail_out}"
|
|
69
90
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# rt-hook: SessionStart startup|resume|compact|clear
|
|
3
|
+
# SessionStart: словарь проекта уезжает в контекст целиком, на каждом запуске сессии.
|
|
4
|
+
#
|
|
5
|
+
# Памятью это не держится: словарь читают перед тем, как написать текст, а разговор с
|
|
6
|
+
# владельцем идёт без чтения — и слово из левой колонки «Так не пишем» всплывает в ответе,
|
|
7
|
+
# хотя в файлах его уже нет. Здесь словарь приходит до первой реплики и действует и на
|
|
8
|
+
# тексты, и на ответы.
|
|
9
|
+
#
|
|
10
|
+
# FAIL-OPEN: нет файла или нет `jq` — выходим молча. Сессия важнее словаря.
|
|
11
|
+
|
|
12
|
+
GLOSSARY="${CLAUDE_PROJECT_DIR:-.}/docs/GLOSSARY.md"
|
|
13
|
+
[ -f "$GLOSSARY" ] || exit 0
|
|
14
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
printf 'СЛОВАРЬ ПРОЕКТА — `docs/GLOSSARY.md`, ниже целиком.\n\n'
|
|
18
|
+
printf 'Слово отсюда употребляется в том значении, что здесь: и в файлах, и в ответах владельцу.\n'
|
|
19
|
+
printf 'Раздел «Так не пишем» действует так же — левая колонка не пишется и не произносится нигде.\n'
|
|
20
|
+
printf 'Слова, которого здесь нет, у читателя нет тоже: оно либо заводится здесь же той же правкой,\n'
|
|
21
|
+
printf 'либо заменяется тем, что уже есть.\n\n'
|
|
22
|
+
cat "$GLOSSARY"
|
|
23
|
+
} | jq -Rs '{hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:.}}'
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# rt-hook: Stop
|
|
3
|
+
# Гард разговора: ход, в котором владельцу задан вопрос, не заканчивается, пока за этот же ход
|
|
4
|
+
# не читались законы и правила. Stop.
|
|
5
|
+
#
|
|
6
|
+
# Зачем именно так. Требование «правила читаются до разговора» записано в правиле ведения
|
|
7
|
+
# работы, а исполнения у него не было: все прочие гарды судят правку файла или команду, а
|
|
8
|
+
# вопрос в чат ни тем, ни другим не является. Поймать его можно только на завершении хода —
|
|
9
|
+
# событие получает путь к записи хода и видит его целиком.
|
|
10
|
+
#
|
|
11
|
+
# Перехват инструмента меню вариантов эту дыру не закрывает: вопрос чаще задаётся прозой, и
|
|
12
|
+
# ровно так был задан тот, из-за которого гард заведён.
|
|
13
|
+
#
|
|
14
|
+
# Чтением правил считается любой из трёх путей: загрузка правила, чтение файла законов или
|
|
15
|
+
# правил, поиск по ним. Требовать именно загрузку значило бы гнать на неё там, где хватило
|
|
16
|
+
# одного поиска, — гард мешал бы работе вместо того, чтобы её выправлять.
|
|
17
|
+
#
|
|
18
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: при любой ошибке, отсутствии записи хода и повторном заходе ход
|
|
19
|
+
# РАЗРЕШАЕТСЯ (exit 0). Сломанный гард не имеет права заклинить разговор.
|
|
20
|
+
|
|
21
|
+
input="$(cat 2>/dev/null)"
|
|
22
|
+
[ -z "$input" ] && exit 0
|
|
23
|
+
|
|
24
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
25
|
+
|
|
26
|
+
# Повторный заход по тому же ходу не судится: иначе ход не кончится никогда — гард сказал своё
|
|
27
|
+
# один раз и отпускает.
|
|
28
|
+
active="$(printf '%s' "$input" | jq -r '.stop_hook_active // false' 2>/dev/null)"
|
|
29
|
+
[ "$active" = "true" ] && exit 0
|
|
30
|
+
|
|
31
|
+
transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null)"
|
|
32
|
+
[ -z "$transcript" ] && exit 0
|
|
33
|
+
[ -f "$transcript" ] || exit 0
|
|
34
|
+
|
|
35
|
+
# Профиль дерева: каталоги законов, правил и спеков у каждого свои, а знать их надо и для
|
|
36
|
+
# признака чтения, и для подсказки в отказе.
|
|
37
|
+
rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
38
|
+
for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../defaults/project.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/defaults/project.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/project.sh"; do
|
|
39
|
+
# shellcheck disable=SC1090
|
|
40
|
+
[ -f "$profile" ] && . "$profile" 2>/dev/null
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
# Подстановка без двоеточия намеренно: заданное пустым — это отказ дерева от требования, и
|
|
44
|
+
# подменять его умолчанием нельзя. Умолчание достаётся только тому, кто не задал переменной
|
|
45
|
+
# вовсе.
|
|
46
|
+
laws_dir="${RT_LAWS_DIR-docs/constitution}"
|
|
47
|
+
rules_dir="${RT_RULES_DIR-.claude/skills}"
|
|
48
|
+
specs_dir="${RT_SPECS_DIR-docs/specs}"
|
|
49
|
+
|
|
50
|
+
# Дерево, у которого нет ни законов, ни правил, требования не получает: читать нечего.
|
|
51
|
+
[ -z "$laws_dir" ] && [ -z "$rules_dir" ] && exit 0
|
|
52
|
+
|
|
53
|
+
# Образец, по которому вызов инструмента считается чтением правил. Каталоги идут в него как
|
|
54
|
+
# есть: точка в `.claude` совпадает с любым знаком и лишнего сюда не приводит.
|
|
55
|
+
read_re="$(printf '%s' "$laws_dir|$rules_dir|$specs_dir" | sed 's/^|*//; s/|*$//; s/||*/|/g')"
|
|
56
|
+
[ -z "$read_re" ] && exit 0
|
|
57
|
+
|
|
58
|
+
# Ход — это всё, что записано после последнего настоящего ввода владельца. Ответ инструмента
|
|
59
|
+
# приходит той же ролью `user`, поэтому строки с `tool_result` вводом не считаются: иначе ходом
|
|
60
|
+
# оказался бы кусок после последнего вызова инструмента, и чтение правил в его начале потерялось
|
|
61
|
+
# бы.
|
|
62
|
+
#
|
|
63
|
+
# Хвост в 400 строк: запись хода растёт всю сессию, а судится только последний ход.
|
|
64
|
+
verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg re "$read_re" '
|
|
65
|
+
def is_input:
|
|
66
|
+
.type == "user"
|
|
67
|
+
and (((.message.content // []) | if type == "array"
|
|
68
|
+
then ([.[] | select(.type == "tool_result")] | length)
|
|
69
|
+
else 0 end) == 0);
|
|
70
|
+
|
|
71
|
+
(map(is_input) | rindex(true)) as $i
|
|
72
|
+
| (if $i == null then . else .[$i + 1:] end) as $turn
|
|
73
|
+
| [$turn[] | select(.type == "assistant") | (.message.content // [])[] | select(.type == "text") | .text] as $texts
|
|
74
|
+
| [$turn[] | select(.type == "assistant") | (.message.content // [])[] | select(.type == "tool_use")] as $uses
|
|
75
|
+
| ($uses | map(
|
|
76
|
+
(.name == "Skill")
|
|
77
|
+
or ((.name // "") | test("^(Read|Grep|Glob)$")) and ((.input | tostring) | test($re))
|
|
78
|
+
or ((.name == "Bash") and ((.input.command // "") | test($re)))
|
|
79
|
+
) | any) as $read
|
|
80
|
+
| (($texts | join("\n")) | test("\\?[[:space:]]*$"; "m")) as $asked_prose
|
|
81
|
+
| ($uses | map(.name == "AskUserQuestion") | any) as $asked_menu
|
|
82
|
+
| if ($asked_prose or $asked_menu) and ($read | not) then "ask" else "pass" end
|
|
83
|
+
' 2>/dev/null)"
|
|
84
|
+
|
|
85
|
+
[ "$verdict" = "ask" ] || exit 0
|
|
86
|
+
|
|
87
|
+
reason="BLOCKED by grill-gate: в ответе есть вопрос владельцу, а законы и правила за этот ход не читались. Вопрос, ответ на который уже записан, владельцу не задаётся — правило ведения работы. Прогони поиск по словам темы и ответь по найденному; спрашивай только то, что документацией не покрыто:
|
|
88
|
+
|
|
89
|
+
grep -rn -i \"<слово темы>\" $laws_dir $rules_dir $specs_dir
|
|
90
|
+
|
|
91
|
+
Гард судит один ход: следующий заход не отбивается."
|
|
92
|
+
|
|
93
|
+
jq -n --arg r "$reason" '{decision:"block",reason:$r}' 2>/dev/null \
|
|
94
|
+
|| printf '{"decision":"block","reason":"grill-gate: прочитай законы и правила по теме, прежде чем спрашивать владельца."}\n'
|
|
95
|
+
|
|
96
|
+
exit 0
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# rt-hook: PostToolUse Edit|Write|MultiEdit|Bash|mcp__webstorm__create_new_file
|
|
2
3
|
# Линтер по следам правки. PostToolUse.
|
|
3
4
|
#
|
|
4
5
|
# Два входа. Правка файла — линтуется один изменённый файл. Перенос файла командой — линтуются
|
|
5
|
-
# файлы по адресу назначения: перенос меняет либу, а вместе с ней и границы, и импорт,
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# файлы по адресу назначения: перенос меняет либу, а вместе с ней и границы, и импорт, законный
|
|
7
|
+
# на прежнем месте, на новом уже запрещён. Ровно так запрещённый импорт уезжает в общую либу
|
|
8
|
+
# молча: файлы перекладываются командой, хук на неё не смотрит, и правило границ сработало бы —
|
|
9
|
+
# но его никто не запустил.
|
|
9
10
|
#
|
|
10
11
|
# Полный набор гоняет гард на пуше, но это конец работы: к моменту, когда правило срабатывает,
|
|
11
12
|
# поверх нарушения лежит десяток правок, и разбор превращается в археологию. Здесь тот же
|
|
@@ -15,11 +16,15 @@
|
|
|
15
16
|
# незаконченный промежуточный файл имеет право быть красным. Чинить их надо до конца задачи —
|
|
16
17
|
# все, включая лежавшие в файле раньше.
|
|
17
18
|
#
|
|
18
|
-
#
|
|
19
|
+
# Что здесь чем зовётся, знает профиль дерева:
|
|
20
|
+
# rt_lint_for — чем линтуется этот файл;
|
|
21
|
+
# rt_is_app_code — где лежит код, к которому линтеры вообще относятся;
|
|
22
|
+
# RT_LINT_SKIP_RE — что из него исключено;
|
|
23
|
+
# rt_push_checks — что гоняет гейт пуша. По нему же решается, догонит ли замечание позже:
|
|
24
|
+
# линтер, которого в гейте нет, ловит только этот хук.
|
|
19
25
|
# Нет профиля или нет функции — хук молчит.
|
|
20
26
|
#
|
|
21
|
-
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ и молча: нет линтера, файл вне
|
|
22
|
-
# вывод.
|
|
27
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ и молча: нет линтера, файл вне дерева, зелёный результат — пустой вывод.
|
|
23
28
|
|
|
24
29
|
# Сколько файлов линтуется за один перенос. Переезд либы трогает десятки файлов, и прогон по
|
|
25
30
|
# каждому превратил бы хук в минутную паузу; на нарушение границы хватает первых.
|
|
@@ -49,46 +54,166 @@ fi
|
|
|
49
54
|
workdir="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)"
|
|
50
55
|
[ -z "$workdir" ] && workdir="${CLAUDE_PROJECT_DIR:-.}"
|
|
51
56
|
cd "$workdir" 2>/dev/null || exit 0
|
|
57
|
+
[ -f package.json ] || exit 0
|
|
52
58
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
# Профиль дерева: сперва умолчание пакета, поверх него — надстройка проекта, если она есть.
|
|
60
|
+
rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
61
|
+
for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../defaults/project.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/defaults/project.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/project.sh"; do
|
|
62
|
+
# shellcheck disable=SC1090
|
|
63
|
+
[ -f "$profile" ] && . "$profile" 2>/dev/null
|
|
64
|
+
done
|
|
57
65
|
command -v rt_lint_for >/dev/null 2>&1 || exit 0
|
|
58
66
|
|
|
59
|
-
|
|
67
|
+
# --- какие файлы проверяем -------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
# Пути назначения всех переносов в команде. Команда бывает составной, поэтому режется по
|
|
70
|
+
# разделителям, и каждый кусок разбирается отдельно.
|
|
71
|
+
collect_moved() {
|
|
72
|
+
# Перевод строки в конце обязателен: чтение без него теряет последний кусок, а команда
|
|
73
|
+
# чаще всего состоит ровно из одного.
|
|
74
|
+
printf '%s\n' "$1" | tr ';&\n' '\n\n\n' | while IFS= read -r segment; do
|
|
75
|
+
case "$segment" in
|
|
76
|
+
*"git mv "*) ;;
|
|
77
|
+
*) continue ;;
|
|
78
|
+
esac
|
|
79
|
+
|
|
80
|
+
# shellcheck disable=SC2086 # разбиение по пробелам здесь и нужно
|
|
81
|
+
set -- ${segment#*git mv }
|
|
82
|
+
args=''
|
|
83
|
+
for arg in "$@"; do
|
|
84
|
+
case "$arg" in
|
|
85
|
+
-*) continue ;;
|
|
86
|
+
esac
|
|
87
|
+
args="${args}${args:+ }${arg}"
|
|
88
|
+
done
|
|
89
|
+
|
|
90
|
+
# shellcheck disable=SC2086
|
|
91
|
+
set -- $args
|
|
92
|
+
[ "$#" -lt 2 ] && continue
|
|
93
|
+
|
|
94
|
+
dest=''
|
|
95
|
+
for arg in "$@"; do dest="$arg"; done
|
|
96
|
+
|
|
97
|
+
# Назначение — каталог: имя файла остаётся прежним, меняется только путь.
|
|
98
|
+
if [ -d "$dest" ]; then
|
|
99
|
+
for arg in "$@"; do
|
|
100
|
+
[ "$arg" = "$dest" ] && continue
|
|
101
|
+
printf '%s/%s\n' "${dest%/}" "${arg##*/}"
|
|
102
|
+
done
|
|
103
|
+
else
|
|
104
|
+
printf '%s\n' "$dest"
|
|
105
|
+
fi
|
|
106
|
+
done
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Каталог разворачивается в лежащие в нём файлы: перенос умеет двигать целые слои.
|
|
110
|
+
expand() {
|
|
111
|
+
if [ -d "$1" ]; then
|
|
112
|
+
find "$1" -type f \( -name '*.ts' -o -name '*.html' -o -name '*.scss' \) 2>/dev/null
|
|
113
|
+
elif [ -f "$1" ]; then
|
|
114
|
+
printf '%s\n' "$1"
|
|
115
|
+
fi
|
|
116
|
+
}
|
|
117
|
+
|
|
60
118
|
if [ "$mode" = "edit" ]; then
|
|
61
|
-
|
|
119
|
+
path="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
|
|
120
|
+
[ -z "$path" ] && exit 0
|
|
121
|
+
# Путь от корня дерева приводится к абсолютному один раз, чтобы образцы не двоились.
|
|
122
|
+
case "$path" in
|
|
123
|
+
/*) ;;
|
|
124
|
+
*) path="${CLAUDE_PROJECT_DIR:-.}/$path" ;;
|
|
125
|
+
esac
|
|
126
|
+
candidates="$(expand "$path")"
|
|
62
127
|
else
|
|
63
|
-
|
|
64
|
-
# разделителям, и каждый кусок разбирается отдельно. Перевод строки в конце обязателен:
|
|
65
|
-
# чтение без него теряет последний кусок, а команда чаще всего состоит ровно из одного.
|
|
66
|
-
files="$(printf '%s\n' "$command_text" | tr ';&\n' '\n\n\n' | awk '
|
|
67
|
-
/git mv / {
|
|
68
|
-
sub(/.*git mv /, "");
|
|
69
|
-
n = split($0, parts, /[[:space:]]+/);
|
|
70
|
-
for (i = n; i >= 1; i--) {
|
|
71
|
-
if (parts[i] !~ /^-/ && parts[i] != "") { print parts[i]; break }
|
|
72
|
-
}
|
|
73
|
-
}' | head -n "$MAX_FILES")"
|
|
128
|
+
candidates="$(collect_moved "$command_text" | while IFS= read -r moved; do expand "$moved"; done)"
|
|
74
129
|
fi
|
|
75
130
|
|
|
76
|
-
[ -z "$
|
|
131
|
+
[ -z "$candidates" ] && exit 0
|
|
132
|
+
|
|
133
|
+
# --- отсев того, что линтерами дерева не покрыто ----------------------------------------
|
|
134
|
+
|
|
135
|
+
lintable() {
|
|
136
|
+
if command -v rt_is_app_code >/dev/null 2>&1; then
|
|
137
|
+
rt_is_app_code "$1" || return 1
|
|
138
|
+
fi
|
|
139
|
+
if [ -n "${RT_LINT_SKIP_RE:-}" ] && printf '%s' "$1" | grep -qE "$RT_LINT_SKIP_RE"; then
|
|
140
|
+
return 1
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
return 0
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# Имя линтера в команде — первое слово, которое не запускатель. Оно идёт в заголовок и по нему
|
|
147
|
+
# же ищется гейт пуша.
|
|
148
|
+
linter_name() {
|
|
149
|
+
printf '%s' "$1" | awk '{
|
|
150
|
+
for (i = 1; i <= NF; i++) {
|
|
151
|
+
if ($i ~ /^(npx|pnpm|yarn|bun|npm|exec|run|dlx|--no-install)$/) { continue }
|
|
152
|
+
print $i; exit
|
|
153
|
+
}
|
|
154
|
+
}'
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# --- прогон -----------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
push_checks="$(command -v rt_push_checks >/dev/null 2>&1 && rt_push_checks 2>/dev/null)"
|
|
77
160
|
|
|
78
161
|
report=""
|
|
162
|
+
linters=""
|
|
163
|
+
covered=1
|
|
164
|
+
checked=0
|
|
165
|
+
|
|
79
166
|
while IFS= read -r file; do
|
|
80
167
|
[ -z "$file" ] && continue
|
|
81
|
-
|
|
168
|
+
lintable "$file" || continue
|
|
169
|
+
[ "$checked" -ge "$MAX_FILES" ] && break
|
|
170
|
+
|
|
82
171
|
lint="$(rt_lint_for "$file" 2>/dev/null)"
|
|
83
172
|
[ -z "$lint" ] && continue
|
|
84
|
-
|
|
173
|
+
checked=$((checked + 1))
|
|
174
|
+
|
|
175
|
+
out="$(eval "$lint" 2>&1)" && continue
|
|
176
|
+
|
|
177
|
+
# Линтера нет или он упал сам по себе — это не замечание к файлу, молчим.
|
|
178
|
+
printf '%s' "$out" | grep -qiE 'command not found|could not determine executable|Cannot find module' && continue
|
|
179
|
+
|
|
180
|
+
linter="$(linter_name "$lint")"
|
|
181
|
+
case " $linters " in
|
|
182
|
+
*" $linter "*) ;;
|
|
183
|
+
*) linters="${linters}${linters:+ }${linter}" ;;
|
|
184
|
+
esac
|
|
185
|
+
printf '%s' "$push_checks" | grep -qF "$linter" || covered=0
|
|
186
|
+
|
|
187
|
+
report="${report}
|
|
188
|
+
${file}:
|
|
189
|
+
$(printf '%s' "$out" | head -c 2500 | tr -d '\000')
|
|
190
|
+
"
|
|
85
191
|
done <<EOF
|
|
86
|
-
$
|
|
192
|
+
$candidates
|
|
87
193
|
EOF
|
|
88
194
|
|
|
89
195
|
[ -z "$report" ] && exit 0
|
|
90
196
|
|
|
91
|
-
|
|
92
|
-
|
|
197
|
+
# Длинный вывод режем: важен факт и первые нарушения, остальное видно при полном прогоне.
|
|
198
|
+
report="$(printf '%s' "$report" | head -c 6000)"
|
|
199
|
+
|
|
200
|
+
if [ "$covered" -eq 1 ]; then
|
|
201
|
+
tail_line="Пуш всё равно не пройдёт, пока набор красный."
|
|
202
|
+
else
|
|
203
|
+
tail_line="Почини их сейчас: этот линтер в гейт пуша не входит, и отложенное замечание уедет в главную ветку молча."
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
if [ "$mode" = "move" ]; then
|
|
207
|
+
head_line="ЛИНТЕР (${linters}) НАШЁЛ ЗАМЕЧАНИЯ ПОСЛЕ ПЕРЕНОСА. Перенос меняет либу, а вместе с ней границы: импорт, законный на прежнем месте, на новом может быть запрещён."
|
|
208
|
+
else
|
|
209
|
+
head_line="ЛИНТЕР (${linters}) НАШЁЛ ЗАМЕЧАНИЯ:"
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
ctx="${head_line}
|
|
213
|
+
${report}
|
|
214
|
+
|
|
215
|
+
Почини их до конца задачи — правятся ВСЕ замечания в затронутом файле, и новые, и лежавшие раньше: накопленные нарушения глушат сигнал о свежих. ${tail_line}"
|
|
216
|
+
|
|
217
|
+
jq -n --arg c "$ctx" '{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$c}}' 2>/dev/null
|
|
93
218
|
|
|
94
219
|
exit 0
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
2
|
+
# rt-hook: PreToolUse Edit|Write|MultiEdit|mcp__webstorm__create_new_file
|
|
3
|
+
# Гард якоря для спек. PreToolUse на правке разметки.
|
|
3
4
|
#
|
|
4
5
|
# Спеки адресуют элементы только через этот атрибут. Классы оформления меняются вместе с
|
|
5
6
|
# вёрсткой, а поиск по роли и тексту ломается на переводах — приложение живёт во многих
|
|
@@ -8,17 +9,18 @@
|
|
|
8
9
|
# того, как кто-то заметит его отсутствие.
|
|
9
10
|
#
|
|
10
11
|
# Правило инвертировано: интерактивным считается ЛЮБОЙ составной тег (компонент) и любой тег с
|
|
11
|
-
# привязкой события, кроме помеченных
|
|
12
|
-
# — компонентов в
|
|
13
|
-
# проходит без якоря.
|
|
12
|
+
# привязкой события, кроме помеченных декоративными. Перечислять интерактивные поимённо не
|
|
13
|
+
# выходит — компонентов в ките больше сотни, и список устаревает молча: забытый в нём
|
|
14
|
+
# компонент проходит без якоря.
|
|
14
15
|
#
|
|
15
16
|
# Осознанный выход: атрибут пропуска НА САМОМ ТЕГЕ. Маркер, который ищется во всей правке,
|
|
16
17
|
# выключал бы проверку целиком — декоративная ссылка уносила бы с собой кнопку отправки из той
|
|
17
18
|
# же правки.
|
|
18
19
|
#
|
|
19
|
-
# Что здесь
|
|
20
|
-
#
|
|
21
|
-
#
|
|
20
|
+
# Что здесь чем зовётся, знает профиль дерева:
|
|
21
|
+
# RT_QA_SKIP_RE — пути, где якорь не требуется: витрина, корневая разметка, спеки;
|
|
22
|
+
# RT_QA_COMPONENT_RE — по какому имени тег опознаётся компонентом этого дерева;
|
|
23
|
+
# rt_qa_decorative — имена тегов, которые спека не нажимает.
|
|
22
24
|
#
|
|
23
25
|
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет разборщика, битый ввод, чужой инструмент — пропуск.
|
|
24
26
|
|
|
@@ -29,51 +31,89 @@ command -v perl >/dev/null 2>&1 || exit 0
|
|
|
29
31
|
|
|
30
32
|
tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
|
|
31
33
|
case "$tool" in
|
|
34
|
+
# Инструмент среды заводит файл теми же двумя данными, только называет их иначе — без этой
|
|
35
|
+
# ветки разметка заводилась мимо всех проверок.
|
|
32
36
|
Edit | Write | MultiEdit | mcp__webstorm__create_new_file) ;;
|
|
33
37
|
*) exit 0 ;;
|
|
34
38
|
esac
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
path="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
|
|
41
|
+
# Путь из среды приходит относительным корню дерева, а все образцы ниже написаны от каталога
|
|
42
|
+
# приложения. Приводим к одному виду один раз, чтобы правила не двоились.
|
|
43
|
+
case "$path" in
|
|
44
|
+
/*) ;;
|
|
45
|
+
?*) path="${CLAUDE_PROJECT_DIR:-.}/$path" ;;
|
|
46
|
+
esac
|
|
47
|
+
case "$path" in
|
|
48
|
+
*.html) ;;
|
|
39
49
|
*) exit 0 ;;
|
|
40
50
|
esac
|
|
41
51
|
|
|
42
|
-
|
|
43
|
-
|
|
52
|
+
# Профиль дерева: сперва умолчание пакета, поверх него — надстройка проекта, если она есть.
|
|
53
|
+
rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
54
|
+
for profile in "$rt_hooks_dir/../rt-kit/defaults/project.sh" "$rt_hooks_dir/../defaults/project.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/defaults/project.sh" "${CLAUDE_PROJECT_DIR:-.}/.claude/rt-kit/project.sh"; do
|
|
44
55
|
# shellcheck disable=SC1090
|
|
45
|
-
. "$profile" 2>/dev/null
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
[ -f "$profile" ] && . "$profile" 2>/dev/null
|
|
57
|
+
done
|
|
58
|
+
|
|
59
|
+
# Разметка приложения живёт там же, где его код. Без этой положительной проверки гард
|
|
60
|
+
# распространялся на любой файл разметки на диске — черновик вне дерева отклонялся требованием
|
|
61
|
+
# проставить якоря.
|
|
62
|
+
if command -v rt_is_app_code >/dev/null 2>&1; then
|
|
63
|
+
rt_is_app_code "$path" || exit 0
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
if [ -n "${RT_QA_SKIP_RE:-}" ] && printf '%s' "$path" | grep -qE "$RT_QA_SKIP_RE"; then
|
|
67
|
+
exit 0
|
|
49
68
|
fi
|
|
50
69
|
|
|
51
70
|
# Проверяется только НОВЫЙ текст: то, что уже лежало в файле, эта правка не заводила, и
|
|
52
71
|
# требовать якорь у чужой строки — значит отбивать правку соседней.
|
|
53
72
|
added="$(printf '%s' "$input" | jq -r '
|
|
54
|
-
[.tool_input.content
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
(.tool_input.edits? // [] | map(.new_string) | join("\n"))] | join("\n")' 2>/dev/null)"
|
|
73
|
+
[ .tool_input.content?, .tool_input.text?, .tool_input.new_string?, (.tool_input.edits[]?.new_string) ]
|
|
74
|
+
| map(select(. != null)) | join("\n")
|
|
75
|
+
' 2>/dev/null)"
|
|
58
76
|
[ -z "$added" ] && exit 0
|
|
59
77
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
78
|
+
decorative="$(command -v rt_qa_decorative >/dev/null 2>&1 && rt_qa_decorative 2>/dev/null)"
|
|
79
|
+
component_re="${RT_QA_COMPONENT_RE:--}"
|
|
80
|
+
|
|
81
|
+
# Открывающие теги разбираются ЦЕЛИКОМ: тег занимает несколько строк, и якорь часто стоит не в
|
|
82
|
+
# первой из них — построчная проверка врала бы.
|
|
83
|
+
missing="$(printf '%s' "$added" | RT_QA_DECORATIVE="$decorative" RT_QA_COMPONENT_RE="$component_re" perl -0777 -ne '
|
|
84
|
+
# Комментарии вырезаются до разбора: в них часто лежит разметка-пример («было <button …>,
|
|
85
|
+
# заменено на переход»). Без этого правка отклонялась из-за элемента, которого в дереве
|
|
86
|
+
# документа не будет, а обойти отказ можно было только испортив текст самого комментария.
|
|
87
|
+
s/<!--.*?-->//gs;
|
|
88
|
+
# Декоративное и структурное: спека такое не нажимает и по нему не проверяет состояние.
|
|
89
|
+
# Всё остальное считается интерактивным по умолчанию — кит растёт, и умолчание должно быть
|
|
90
|
+
# в сторону якоря, а не в сторону тишины. Попадание в список НЕ выдаёт индульгенцию: тег с
|
|
91
|
+
# привязкой события проверяется наравне с прочими.
|
|
92
|
+
my %decorative = map { $_ => 1 } split /\s+/, ($ENV{RT_QA_DECORATIVE} // "");
|
|
93
|
+
my $component = $ENV{RT_QA_COMPONENT_RE} || "-";
|
|
94
|
+
my %seen;
|
|
95
|
+
while (/<([a-zA-Z][\w-]*)((?:[^<>"\x27]|"[^"]*"|\x27[^\x27]*\x27)*)>/gs) {
|
|
65
96
|
my ($tag, $attrs) = ($1, $2);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
97
|
+
# Структурные теги каркаса узла документа не порождают: поставленный на них якорь не
|
|
98
|
+
# найдёт ни один селектор спеки. Требовать его — значит требовать неработающего.
|
|
99
|
+
next if $tag =~ /^(ng-container|ng-template|ng-content)$/;
|
|
100
|
+
next if $attrs =~ /\bqa-dataid\b/;
|
|
101
|
+
# Осознанный отказ помечается на самом теге, а не где-то в той же правке.
|
|
102
|
+
next if $attrs =~ /\bqa-skip\b/;
|
|
103
|
+
my $interactive =
|
|
104
|
+
$attrs =~ /\(\s*[a-zA-Z][\w.:-]*\s*\)\s*=/
|
|
105
|
+
|| $attrs =~ /\b(routerLink|rtButton|href)\b/
|
|
106
|
+
|| $tag =~ /^(button|a|input|select|textarea|form|dialog)$/
|
|
107
|
+
|| ($tag =~ /$component/ && !$decorative{$tag});
|
|
108
|
+
next unless $interactive;
|
|
109
|
+
next if $seen{$tag}++;
|
|
110
|
+
print "<$tag> ";
|
|
70
111
|
}
|
|
71
|
-
print join(", ", @bad) if @bad;
|
|
72
112
|
' 2>/dev/null)"
|
|
73
113
|
|
|
74
|
-
[ -z "$
|
|
114
|
+
[ -z "$missing" ] && exit 0
|
|
75
115
|
|
|
76
|
-
reason="
|
|
116
|
+
reason="BLOCKED: интерактивные элементы без якоря для спек в ${path##*/}: ${missing}. Спеки адресуют элементы только через этот атрибут: классы оформления меняются вместе с вёрсткой, а поиск по роли и тексту ломается на переводах — оба вида селекторов делают сквозные спеки хрупкими. Проставь якорь через дефис по смыслу элемента; повторяющиеся элементы списка носят ОДИН якорь и различаются атрибутами данных. Если элемент чисто декоративный и спека его никогда не тронет — поставь атрибут пропуска НА САМ ЭТОТ ТЕГ, соседние элементы правки при этом продолжают проверяться."
|
|
77
117
|
|
|
78
118
|
jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
|
|
79
119
|
|| printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Интерактивный элемент без якоря для спек."}}\n'
|