@rt-tools/agent-kit 0.3.0 → 0.4.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.
Files changed (201) hide show
  1. package/README.md +194 -30
  2. package/assets/agents/business-analyst.md +74 -0
  3. package/assets/agents/project-manager.md +70 -0
  4. package/assets/agents/qa-engineer.md +72 -0
  5. package/assets/agents/skill-curator.md +110 -0
  6. package/assets/agents/spec-critic.md +44 -0
  7. package/assets/agents/spec-writer.md +50 -0
  8. package/assets/checks/board.github.mjs +286 -0
  9. package/assets/checks/check-board.github.mjs +188 -0
  10. package/assets/checks/check-doc-paths.mjs +163 -0
  11. package/assets/checks/check-dupes.mjs +277 -0
  12. package/assets/checks/check-lib-layers.mjs +573 -0
  13. package/assets/checks/check-reuse.mjs +208 -0
  14. package/assets/checks/check-schema-drift.mjs +186 -0
  15. package/assets/checks/check-specs.mjs +1007 -0
  16. package/assets/checks/check-styles.mjs +109 -0
  17. package/assets/checks/rt-kit-checks.config.mjs +134 -0
  18. package/assets/checks/task-new.github.mjs +198 -0
  19. package/assets/commands/skill-curator.md +70 -0
  20. package/assets/defaults/gate-map.sh +100 -0
  21. package/assets/defaults/project.sh +179 -0
  22. package/assets/hooks/browser-device-id.sh +0 -0
  23. package/assets/hooks/browser-guard-device-id.sh +2 -1
  24. package/assets/hooks/browser-guard-no-asking.sh +27 -0
  25. package/assets/hooks/browser-guard-no-listing.sh +2 -1
  26. package/assets/hooks/browser-guard-no-other-drivers.sh +2 -1
  27. package/assets/hooks/browser-guard-require-select.sh +2 -1
  28. package/assets/hooks/commit-msg.sh +1 -1
  29. package/assets/hooks/constitution-index.sh +5 -4
  30. package/assets/hooks/dev-server-guard.sh +8 -6
  31. package/assets/hooks/docs-guard.sh +223 -37
  32. package/assets/hooks/git-guard-delivery.sh +86 -29
  33. package/assets/hooks/git-guard-main.sh +1 -0
  34. package/assets/hooks/git-guard-push-tests.sh +34 -13
  35. package/assets/hooks/glossary-load.sh +23 -0
  36. package/assets/hooks/lint-after-edit.sh +155 -30
  37. package/assets/hooks/qa-dataid-guard.sh +72 -32
  38. package/assets/hooks/reuse-first-guard.sh +105 -34
  39. package/assets/hooks/skill-gate-rearm.sh +1 -0
  40. package/assets/hooks/skill-gate.sh +75 -15
  41. package/assets/hooks/skill-loaded.sh +1 -0
  42. package/assets/hooks/sql-guard.sh +606 -56
  43. package/assets/hooks/task-context-load.sh +100 -0
  44. package/assets/hooks/task-flow-guard.sh +107 -0
  45. package/assets/laws/{access.md → application/access.md} +1 -4
  46. package/assets/laws/{locales.md → application/locales.md} +1 -3
  47. package/assets/laws/application/money.md +41 -0
  48. package/assets/laws/application/ownership.md +32 -0
  49. package/assets/laws/{search-visibility.md → application/search-visibility.md} +1 -1
  50. package/assets/laws/code-structure.md +7 -6
  51. package/assets/laws/delivery.md +53 -3
  52. package/assets/laws/entity-editing.md +49 -55
  53. package/assets/laws/entity-models.md +4 -14
  54. package/assets/laws/frontend-application.md +5 -5
  55. package/assets/laws/lib-imports.md +14 -1
  56. package/assets/laws/lists.md +33 -0
  57. package/assets/laws/navigation.md +40 -0
  58. package/assets/laws/project-documentation.md +17 -8
  59. package/assets/laws/reuse-first.md +26 -21
  60. package/assets/laws/shared-code.md +13 -1
  61. package/assets/laws/verifiability.md +17 -1
  62. package/assets/laws/work-conduct.md +48 -0
  63. package/assets/patterns/admin-lists-screen.md +131 -0
  64. package/assets/patterns/admin-nav-item.md +71 -0
  65. package/assets/patterns/angular-patterns-state.md +29 -22
  66. package/assets/patterns/api-layer-pair.md +40 -30
  67. package/assets/patterns/browser-verification-measure.md +41 -38
  68. package/assets/patterns/browser-verification-stand.md +106 -42
  69. package/assets/patterns/component-structure-new.md +33 -32
  70. package/assets/patterns/dependencies-upgrade.md +65 -0
  71. package/assets/patterns/doc-style-sweep.md +65 -28
  72. package/assets/patterns/doc-style-write.md +36 -33
  73. package/assets/patterns/entity-aside.md +136 -0
  74. package/assets/patterns/entity-models-new.md +124 -0
  75. package/assets/patterns/entity-store.md +91 -0
  76. package/assets/patterns/git-workflow-commit.azure.md +259 -0
  77. package/assets/patterns/git-workflow-commit.github.md +333 -0
  78. package/assets/patterns/git-workflow-commit.gitlab.md +283 -0
  79. package/assets/patterns/git-workflow-merge.md +42 -25
  80. package/assets/patterns/git-workflow-migration.md +61 -31
  81. package/assets/patterns/git-workflow-restart.md +20 -20
  82. package/assets/patterns/lib-layers-move.md +50 -32
  83. package/assets/patterns/lib-layers-new.md +41 -29
  84. package/assets/patterns/ownership-scope-resolve.md +69 -0
  85. package/assets/patterns/permissions-procedure.md +35 -33
  86. package/assets/patterns/platform-access-di.md +39 -25
  87. package/assets/patterns/pricing-quote.md +71 -0
  88. package/assets/patterns/reuse-first-extend.md +22 -22
  89. package/assets/patterns/seo-page.md +52 -40
  90. package/assets/patterns/seo-verify.md +48 -29
  91. package/assets/patterns/shared-code-new.md +37 -31
  92. package/assets/patterns/spec-driven-domain.md +44 -37
  93. package/assets/patterns/spec-driven-rule.md +55 -40
  94. package/assets/patterns/styling-bem-component.md +43 -32
  95. package/assets/patterns/styling-bem-layout.md +30 -24
  96. package/assets/patterns/task-flow-close.md +90 -0
  97. package/assets/patterns/task-flow-resume.md +94 -0
  98. package/assets/patterns/task-flow-start.md +117 -0
  99. package/assets/patterns/testing-e2e.md +53 -51
  100. package/assets/patterns/testing-unit.md +70 -46
  101. package/assets/patterns/translations-key.md +32 -19
  102. package/assets/patterns/ts-procedure.md +24 -25
  103. package/assets/rules/angular-patterns.md +46 -27
  104. package/assets/rules/api-layer.md +46 -28
  105. package/assets/rules/browser-verification.md +66 -48
  106. package/assets/rules/component-structure.md +43 -27
  107. package/assets/rules/dependencies.md +66 -0
  108. package/assets/rules/doc-style.md +81 -39
  109. package/assets/rules/entity-conventions.md +78 -0
  110. package/assets/rules/entity-models.md +70 -0
  111. package/assets/rules/git-workflow.azure.md +116 -0
  112. package/assets/rules/git-workflow.github.md +123 -0
  113. package/assets/rules/git-workflow.gitlab.md +113 -0
  114. package/assets/rules/lib-layers.md +56 -30
  115. package/assets/rules/lists.md +73 -0
  116. package/assets/rules/navigation.md +78 -0
  117. package/assets/rules/ownership-scope.md +63 -0
  118. package/assets/rules/permissions.md +43 -25
  119. package/assets/rules/platform-access.md +57 -29
  120. package/assets/rules/pricing.md +64 -0
  121. package/assets/rules/reuse-first.md +57 -43
  122. package/assets/rules/seo.md +51 -30
  123. package/assets/rules/shared-code.md +51 -26
  124. package/assets/rules/spec-driven.md +96 -50
  125. package/assets/rules/styling-bem.md +54 -39
  126. package/assets/rules/task-flow.md +110 -0
  127. package/assets/rules/testing.md +78 -47
  128. package/assets/rules/translations.md +48 -31
  129. package/assets/rules/typescript-conventions.md +57 -27
  130. package/assets/skills/agent-kit.md +81 -0
  131. package/assets/skills/write-a-skill.md +108 -0
  132. package/assets/templates/gate-map.sh +23 -15
  133. package/assets/templates/implementation.md +14 -8
  134. package/assets/templates/pattern.md +1 -1
  135. package/assets/templates/project.sh +32 -19
  136. package/assets/templates/rule.md +1 -1
  137. package/assets/variants.json +20 -0
  138. package/assets/workflows/feature.js +134 -0
  139. package/assets/workflows/plan.js +150 -0
  140. package/bin/agent-kit.d.ts.map +1 -1
  141. package/bin/agent-kit.js +78 -5
  142. package/bin/agent-kit.js.map +1 -1
  143. package/bin/prompt.d.ts +5 -0
  144. package/bin/prompt.d.ts.map +1 -1
  145. package/bin/prompt.js +19 -7
  146. package/bin/prompt.js.map +1 -1
  147. package/index.d.ts +1 -0
  148. package/index.d.ts.map +1 -1
  149. package/index.js +1 -0
  150. package/index.js.map +1 -1
  151. package/lib/assets.d.ts +8 -3
  152. package/lib/assets.d.ts.map +1 -1
  153. package/lib/assets.js +13 -3
  154. package/lib/assets.js.map +1 -1
  155. package/lib/catalog.d.ts +52 -5
  156. package/lib/catalog.d.ts.map +1 -1
  157. package/lib/catalog.js +104 -16
  158. package/lib/catalog.js.map +1 -1
  159. package/lib/commands.d.ts +22 -1
  160. package/lib/commands.d.ts.map +1 -1
  161. package/lib/commands.js +202 -14
  162. package/lib/commands.js.map +1 -1
  163. package/lib/companion.d.ts +5 -1
  164. package/lib/companion.d.ts.map +1 -1
  165. package/lib/companion.js +29 -2
  166. package/lib/companion.js.map +1 -1
  167. package/lib/config.d.ts +26 -9
  168. package/lib/config.d.ts.map +1 -1
  169. package/lib/config.js +41 -15
  170. package/lib/config.js.map +1 -1
  171. package/lib/freshness.d.ts +14 -0
  172. package/lib/freshness.d.ts.map +1 -0
  173. package/lib/freshness.js +116 -0
  174. package/lib/freshness.js.map +1 -0
  175. package/lib/hooks-map.d.ts +24 -0
  176. package/lib/hooks-map.d.ts.map +1 -0
  177. package/lib/hooks-map.js +72 -0
  178. package/lib/hooks-map.js.map +1 -0
  179. package/lib/integrity.d.ts +36 -0
  180. package/lib/integrity.d.ts.map +1 -0
  181. package/lib/integrity.js +44 -0
  182. package/lib/integrity.js.map +1 -0
  183. package/lib/picker.d.ts +11 -1
  184. package/lib/picker.d.ts.map +1 -1
  185. package/lib/picker.js +44 -6
  186. package/lib/picker.js.map +1 -1
  187. package/lib/sync.d.ts +26 -0
  188. package/lib/sync.d.ts.map +1 -1
  189. package/lib/sync.js +59 -4
  190. package/lib/sync.js.map +1 -1
  191. package/lib/variants.d.ts +44 -0
  192. package/lib/variants.d.ts.map +1 -0
  193. package/lib/variants.js +82 -0
  194. package/lib/variants.js.map +1 -0
  195. package/package.json +1 -1
  196. package/rt-tools-agent-kit-0.4.0.tgz +0 -0
  197. package/assets/laws/admin-lists.md +0 -35
  198. package/assets/laws/admin-navigation.md +0 -38
  199. package/assets/patterns/git-workflow-commit.md +0 -175
  200. package/assets/rules/git-workflow.md +0 -106
  201. package/rt-tools-agent-kit-0.3.0.tgz +0 -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
- # Чем линтуется файл, знает профиль проекта: {{projectProfile}}, функция `rt_lint_for <файл>`.
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
- profile="${CLAUDE_PROJECT_DIR:-.}/{{projectProfile}}"
54
- [ -f "$profile" ] || exit 0
55
- # shellcheck disable=SC1090
56
- . "$profile" 2>/dev/null || exit 0
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
- files=""
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
- files="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
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 "$files" ] && exit 0
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
- [ -f "$file" ] || continue
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
- out="$(eval "$lint" 2>&1)" || report="${report}${out}"$'\n'
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
- $files
192
+ $candidates
87
193
  EOF
88
194
 
89
195
  [ -z "$report" ] && exit 0
90
196
 
91
- jq -n --arg r "Замечания линтера по только что тронутым файлам чинить до конца задачи, включая те, что лежали в файле раньше:"$'\n'"$report" \
92
- '{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$r}}' 2>/dev/null
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
- # Гард якоря для спек. PreToolUse на правке шаблонов.
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
- # Что здесь не проверяется, знает профиль проекта: {{projectProfile}}, переменная RT_QA_SKIP_RE
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
- target="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
37
- case "$target" in
38
- *.component.html|*.html) ;;
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
- profile="${CLAUDE_PROJECT_DIR:-.}/{{projectProfile}}"
43
- if [ -f "$profile" ]; then
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
- if [ -n "${RT_QA_SKIP_RE:-}" ] && printf '%s' "$target" | grep -qE "$RT_QA_SKIP_RE"; then
47
- exit 0
48
- fi
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? // empty,
55
- .tool_input.text? // empty,
56
- .tool_input.new_string? // empty,
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
- offenders="$(printf '%s' "$added" | perl -0ne '
63
- my @bad;
64
- while (/<([a-zA-Z][a-zA-Z0-9-]*)((?:"[^"]*"|\x27[^\x27]*\x27|[^>"\x27])*)>/gs) {
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
- next if $attrs =~ /qa-dataid/ || $attrs =~ /qa-skip/;
67
- my $is_component = $tag =~ /-/;
68
- my $has_event = $attrs =~ /\(\s*[a-zA-Z][\w.]*\s*\)\s*=/;
69
- push @bad, "<$tag" if $is_component || $has_event;
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 "$offenders" ] && exit 0
114
+ [ -z "$missing" ] && exit 0
75
115
 
76
- reason="Интерактивный элемент без якоря для спек: ${offenders}. Спеки адресуют элементы только им классы меняются вместе с вёрсткой, а поиск по тексту ломается на переводах. Значение — через дефис, по смыслу элемента; повторяющиеся элементы списка носят один якорь и различаются атрибутами данных. Декоративный элемент помечается атрибутом пропуска на самом теге."
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'
@@ -1,35 +1,51 @@
1
1
  #!/usr/bin/env bash
2
+ # rt-hook: PreToolUse Edit|Write|MultiEdit|mcp__webstorm__create_new_file
2
3
  # Гард «ничего не пишется с нуля». PreToolUse на правке кода и разметки.
3
4
  #
4
- # Линтеры знают правила, но не знают ИНВЕНТАРЬ: линтер стилей поймает сырой цвет, линтер кода
5
- # нетипизированное значение, но ни один из них не знает, что готовое сообщение уже написано,
6
- # и молча пропустит свою область оповещения с текстом, кнопкой и стилями. Гард закрывает ровно
5
+ # Линтеры знают правила, но не знают ИНВЕНТАРЬ: линтер стилей поймает сырой цвет, линтер кода
6
+ # нетипизированное значение, но ни один из них не знает, что готовое сообщение уже написано, и
7
+ # молча пропустит свою область оповещения с текстом, кнопкой и стилями. Гард закрывает ровно
7
8
  # эту дыру: сверяет то, что пишется, с тем, что в дереве уже есть.
8
9
  #
9
10
  # Правило целиком — `reuse-first`.
10
11
  #
11
- # Что считается переизобретением, знает профиль проекта: {{projectProfile}}, функция
12
- # `rt_reinvented_in <файл>` — печатает по строке «образец<таб>чем заменить». Нет профиля или
13
- # нет функции — гард пропускает: инвентарь дерева пакет знать не может.
12
+ # Что считается переизобретением, знает профиль дерева: функция `rt_reinvented_in <файл>`
13
+ # печатает по строке на правило, четырьмя полями через табуляцию:
14
14
  #
15
- # Осознанный выход: строка с маркером отступления пропускается — для случаев, которых в
16
- # готовом действительно нет. Маркер объясняет, чего именно не хватает; «эти строки были здесь
17
- # раньше» причиной не считается, и оттого проверяется только НОВЫЙ текст.
15
+ # <над чем> <образец> <образец отмены> <чем заменить>
16
+ #
17
+ # «Над чем» `added` (только новый текст правки) или `whole` (правка вместе с содержимым
18
+ # файла). Второе нужно там, где признак виден только целиком: наследование основы и метка
19
+ # процедуры в точечную правку не попадают. «Образец отмены» гасит правило: маппер, который уже
20
+ # наследует общую основу, переизобретением не является; пустое поле не гасит ничего.
21
+ #
22
+ # Функция получает путь и вправе решать по нему сама — заглянуть в инвентарь кита, различить
23
+ # слой дерева, промолчать на файле, который уже существует. Нет профиля или нет функции — гард
24
+ # пропускает: инвентарь дерева пакет знать не может.
25
+ #
26
+ # Образцы разбирает `perl`: конъюнкция и отрицание пишутся заглядыванием вперёд, а без них
27
+ # самодельный оверлей не отличить от липкой шапки, а нативную кнопку — от кнопки кита.
28
+ #
29
+ # Осознанный выход: маркер отступления в тексте правки. Он объясняет, чего именно не хватает;
30
+ # «эти строки были здесь раньше» причиной не считается, и оттого проверяется только НОВЫЙ текст.
18
31
  #
19
32
  # ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет разборщика, битый ввод, чужой инструмент — пропуск.
20
33
 
21
34
  input="$(cat 2>/dev/null)"
22
35
  [ -z "$input" ] && exit 0
23
36
  command -v jq >/dev/null 2>&1 || exit 0
37
+ command -v perl >/dev/null 2>&1 || exit 0
24
38
 
25
39
  tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
26
40
  case "$tool" in
41
+ # Инструмент среды заводит файл теми же двумя данными, только называет их иначе — без этой
42
+ # ветки файл заводился мимо всех проверок.
27
43
  Edit | Write | MultiEdit | mcp__webstorm__create_new_file) ;;
28
44
  *) exit 0 ;;
29
45
  esac
30
46
 
31
47
  path="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.pathInProject // empty' 2>/dev/null)"
32
- # Путь от корня проекта приводится к абсолютному один раз, чтобы образцы не двоились.
48
+ # Путь от корня дерева приводится к абсолютному один раз, чтобы образцы не двоились.
33
49
  case "$path" in
34
50
  /*) ;;
35
51
  ?*) path="${CLAUDE_PROJECT_DIR:-.}/$path" ;;
@@ -39,43 +55,98 @@ case "$path" in
39
55
  *) exit 0 ;;
40
56
  esac
41
57
 
42
- # Правила этого дерева действуют на файлы этого дерева: без проверки корня гард требовал бы
43
- # верстать готовым и в черновике за пределами репозитория.
44
- case "$path" in
45
- "${CLAUDE_PROJECT_DIR:-.}"/*) ;;
46
- *) exit 0 ;;
47
- esac
48
-
49
- profile="${CLAUDE_PROJECT_DIR:-.}/{{projectProfile}}"
50
- [ -f "$profile" ] || exit 0
51
- # shellcheck disable=SC1090
52
- . "$profile" 2>/dev/null || exit 0
58
+ # Профиль дерева: сперва умолчание пакета, поверх него надстройка проекта, если она есть.
59
+ rt_hooks_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
60
+ 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
61
+ # shellcheck disable=SC1090
62
+ [ -f "$profile" ] && . "$profile" 2>/dev/null
63
+ done
53
64
  command -v rt_reinvented_in >/dev/null 2>&1 || exit 0
54
65
 
66
+ # Правила этого дерева действуют на код этого дерева: без положительной проверки гард требовал
67
+ # бы собирать готовым и в черновике за пределами дерева.
68
+ if command -v rt_is_app_code >/dev/null 2>&1; then
69
+ rt_is_app_code "$path" || exit 0
70
+ fi
71
+
72
+ # Готовое собирается там, где ему и место: внутри кита нативные контролы и примитивы уместны,
73
+ # витрина и спеки — тоже не забота этого гарда.
74
+ if [ -n "${RT_REUSE_SKIP_RE:-}" ] && printf '%s' "$path" | grep -qE "$RT_REUSE_SKIP_RE"; then
75
+ exit 0
76
+ fi
77
+
55
78
  # Только новый текст: строка, уже лежавшая в файле, этой правкой не заводилась.
56
79
  added="$(printf '%s' "$input" | jq -r '
57
- [.tool_input.content? // empty,
58
- .tool_input.text? // empty,
59
- .tool_input.new_string? // empty,
60
- (.tool_input.edits? // [] | map(.new_string) | join("\n"))] | join("\n")' 2>/dev/null)"
80
+ [ .tool_input.content?, .tool_input.text?, .tool_input.new_string?, (.tool_input.edits[]?.new_string) ]
81
+ | map(select(. != null)) | join("\n")
82
+ ' 2>/dev/null)"
61
83
  [ -z "$added" ] && exit 0
62
84
 
63
- found=""
64
- while IFS="$(printf '\t')" read -r pattern replacement; do
85
+ # Явный отказ от правила: готового такого нет, автор это осознал и пометил. Считается по
86
+ # исходному тексту правки, до вычёркивания ниже: маркер обычно и стоит на строке, которая уже
87
+ # лежит в файле, и вычеркнутый маркер отбивал бы правку соседней строки.
88
+ case "$added" in
89
+ *native-ok*) exit 0 ;;
90
+ esac
91
+
92
+ # Перенос — не написание заново. Строку, которая уже лежит в файле, гард из проверяемого текста
93
+ # вычёркивает: блок, переехавший вместе с экраном из одного места файла в другое, отбивался
94
+ # наравне с новым, и маркер отступления приходилось ставить вслепую.
95
+ #
96
+ # Сверка идёт без отступов: при переезде блок меняет отступ, оставаясь тем же кодом.
97
+ if [ -f "$path" ]; then
98
+ added="$(
99
+ printf '%s' "$added" | awk '
100
+ NR == FNR {
101
+ line = $0
102
+ gsub(/^[ \t]+|[ \t]+$/, "", line)
103
+ if (line != "") { existing[line] = 1 }
104
+ next
105
+ }
106
+ {
107
+ line = $0
108
+ gsub(/^[ \t]+|[ \t]+$/, "", line)
109
+ if (line == "" || !(line in existing)) { print }
110
+ }
111
+ ' "$path" - 2>/dev/null
112
+ )"
113
+ [ -z "${added//[[:space:]]/}" ] && exit 0
114
+ fi
115
+
116
+ # Признак, который виден только целиком по файлу (наследование основы, метка процедуры),
117
+ # считается по итоговому содержимому: точечная правка приносит кусок без объявления класса.
118
+ whole="$added"
119
+ if [ -f "$path" ]; then
120
+ whole="$added
121
+ $(cat "$path" 2>/dev/null)"
122
+ fi
123
+
124
+ has_re() {
125
+ printf '%s' "$1" | RT_RE="$2" perl -0777 -ne 'exit(/$ENV{RT_RE}/s ? 0 : 1)' 2>/dev/null
126
+ }
127
+
128
+ found=''
129
+ while IFS="$(printf '\t')" read -r scope pattern cancel replacement; do
65
130
  [ -z "$pattern" ] && continue
66
- # Строка с маркером отступления снимается до сверки — вместе с ней снимается и повод.
67
- printf '%s\n' "$added" | grep -v 'native-ok' | grep -qE "$pattern" || continue
68
- found="${found} ${pattern} — есть готовое: ${replacement}"$'\n'
131
+ case "$scope" in
132
+ whole) text="$whole" ;;
133
+ *) text="$added" ;;
134
+ esac
135
+ has_re "$text" "$pattern" || continue
136
+ [ -n "$cancel" ] && has_re "$text" "$cancel" && continue
137
+ found="${found}
138
+ - ${replacement}"
69
139
  done <<EOF
70
- $(rt_reinvented_in "$path")
140
+ $(rt_reinvented_in "$path" 2>/dev/null)
71
141
  EOF
72
142
 
73
143
  [ -z "$found" ] && exit 0
74
144
 
75
- reason="Написано своё там, где готовое уже есть:
76
-
145
+ reason="BLOCKED: это уже написано. Файл: ${path##*/}
77
146
  ${found}
78
- Работа начинается с чтения готового, а не с чистого файла — правило reuse-first. Недостающий вариант заводится у готового, а не клонируется рядом: клон забирает правки на себя и расходится с оригиналом с первой же. Если готового здесь правда не хватает, поставь маркер отступления в той же строке и объясни, чего именно нет."
147
+
148
+ Порядок действий: 1) открой готовое — барель кита или базовый класс — и используй его; 2) найди в дереве экран, где этот случай уже собран, и повтори сборку; 3) если готового правда не хватает — расширяй его на месте, у готового, а не клонируй рядом: клон забирает правки на себя и расходится с оригиналом с первой же.
149
+ Свой примитив, своя основа и свои инлайновые стили заводятся только с явного одобрения владельца, и спрашивается это до первого написанного файла. Разовое исключение помечается маркером отступления в той же строке, с объяснением, чего именно нет в готовом. Переименованием файла это не обходится."
79
150
 
80
151
  jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
81
152
  || printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Написано своё там, где готовое уже есть."}}\n'
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bash
2
+ # rt-hook: SessionStart compact|clear
2
3
  # Взвод гейта заново. SessionStart(compact|clear).
3
4
  #
4
5
  # Гейт помнит загруженное правило по идентификатору сессии и дальше пропускает эту область