@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.
- 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 +286 -0
- package/assets/checks/check-board.github.mjs +188 -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 +1007 -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 +100 -0
- package/assets/defaults/project.sh +179 -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 +86 -29
- 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/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 +107 -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 +17 -8
- package/assets/laws/reuse-first.md +26 -21
- package/assets/laws/shared-code.md +13 -1
- package/assets/laws/verifiability.md +17 -1
- package/assets/laws/work-conduct.md +48 -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 +333 -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 +44 -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 +90 -0
- package/assets/patterns/task-flow-resume.md +94 -0
- package/assets/patterns/task-flow-start.md +117 -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 +46 -27
- package/assets/rules/api-layer.md +46 -28
- package/assets/rules/browser-verification.md +66 -48
- package/assets/rules/component-structure.md +43 -27
- package/assets/rules/dependencies.md +66 -0
- package/assets/rules/doc-style.md +81 -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 +96 -50
- package/assets/rules/styling-bem.md +54 -39
- package/assets/rules/task-flow.md +110 -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 +81 -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 +1 -1
- 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 +24 -0
- package/lib/hooks-map.d.ts.map +1 -0
- package/lib/hooks-map.js +72 -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.4.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,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'
|
|
@@ -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
|
-
# Что считается переизобретением, знает профиль
|
|
12
|
-
#
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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'
|