@bonesofspring/ai-rules 0.2.0 → 0.2.1
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/CONTRIBUTING.md +102 -0
- package/README.md +2 -0
- package/bin/cli.js +4 -2
- package/fragments/BUGBOT.md +23 -0
- package/package.json +10 -2
- package/presets/claude/next/BUGBOT.md +16 -0
- package/presets/claude/next/CLAUDE.md +5 -1
- package/presets/claude/next/agents/README.md +16 -15
- package/presets/claude/next/agents/code-reviewer.md +1 -1
- package/presets/claude/next/agents/playwright-test-generator.md +16 -65
- package/presets/claude/next/agents/playwright-test-healer.md +16 -51
- package/presets/claude/next/agents/playwright-test-planner.md +15 -55
- package/presets/claude/next/agents/task-analyst.md +1 -1
- package/presets/claude/next/agents/task-router.md +24 -0
- package/presets/claude/next/agents/tech-writer.md +1 -1
- package/presets/claude/next/hooks.json +17 -0
- package/presets/claude/next/rules/README.md +6 -2
- package/presets/claude/next/rules/architecture/README.md +2 -1
- package/presets/claude/next/rules/architecture/architecture-boundaries-ui.md +15 -0
- package/presets/claude/next/rules/architecture/architecture-boundaries.md +4 -1
- package/presets/claude/next/rules/architecture/feature-delivery-workflow.md +1 -1
- package/presets/claude/next/rules/architecture/layer-barrel-exports.md +1 -1
- package/presets/claude/next/rules/stack/README.md +2 -1
- package/presets/claude/next/rules/stack/navigation-router-ui.md +15 -0
- package/presets/claude/next/rules/stack/navigation-router.md +2 -1
- package/presets/claude/next/rules/stack/next-app-core.md +1 -1
- package/presets/claude/next/rules/tooling-and-review/agent-team-orchestrator.md +6 -0
- package/presets/claude/next/rules/tooling-and-review/code-review-mr.md +22 -59
- package/presets/claude/next/rules/ui-and-accessibility/README.md +1 -2
- package/presets/claude/next/rules/ui-and-accessibility/react-ui.md +4 -3
- package/presets/claude/next/team/README.md +1 -0
- package/presets/claude/next/team/fixtures/bugfix-standard.json +15 -0
- package/presets/claude/next/team/fixtures/feature-full.json +16 -0
- package/presets/claude/next/team/fixtures/feature-light.json +17 -0
- package/presets/cursor/next/AGENTS.md +2 -9
- package/presets/cursor/next/BUGBOT.md +2 -0
- package/presets/cursor/next/agents/README.md +16 -15
- package/presets/cursor/next/agents/code-reviewer.md +1 -1
- package/presets/cursor/next/agents/task-analyst.md +1 -1
- package/presets/cursor/next/agents/task-router.md +24 -0
- package/presets/cursor/next/agents/tech-writer.md +1 -1
- package/presets/cursor/next/rules/README.md +21 -1
- package/presets/cursor/next/rules/architecture-boundaries-ui.mdc +15 -0
- package/presets/cursor/next/rules/architecture-boundaries.mdc +1 -1
- package/presets/cursor/next/rules/code-review-mr.mdc +20 -46
- package/presets/cursor/next/rules/feature-delivery-workflow.mdc +1 -1
- package/presets/cursor/next/rules/layer-barrel-exports.mdc +1 -1
- package/presets/cursor/next/rules/navigation-router-stack.mdc +1 -1
- package/presets/cursor/next/rules/navigation-router-ui.mdc +16 -0
- package/presets/cursor/next/rules/next-app-core.mdc +1 -1
- package/presets/cursor/next/rules/no-cross-component-styles-import.mdc +3 -53
- package/presets/cursor/next/rules/react-ui.mdc +4 -3
- package/presets/cursor/next/team/README.md +1 -0
- package/presets/cursor/next/team/fixtures/bugfix-standard.json +15 -0
- package/presets/cursor/next/team/fixtures/feature-full.json +16 -0
- package/presets/cursor/next/team/fixtures/feature-light.json +17 -0
- package/scripts/README.md +84 -0
- package/scripts/golden-prompts.json +276 -0
- package/scripts/preset-manifest.json +72 -0
- package/scripts/regression-results/.gitkeep +0 -0
- package/scripts/validate-preset.sh +484 -0
- package/presets/claude/next/rules/ui-and-accessibility/component-styles.md +0 -56
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# validate-preset.sh — static regression checks for @bonesofspring/ai-rules presets.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# ./scripts/validate-preset.sh # full static validation
|
|
6
|
+
# ./scripts/validate-preset.sh --init-smoke # + init cursor/claude into temp dirs
|
|
7
|
+
# ./scripts/validate-preset.sh --check-golden # + validate golden-prompts.json schema/agents
|
|
8
|
+
# ./scripts/validate-preset.sh --print-golden # print manual router regression checklist
|
|
9
|
+
# ./scripts/validate-preset.sh --compare-pipeline <dir> # compare pipeline.json files to golden
|
|
10
|
+
#
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
+
PKG_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
15
|
+
CURSOR_PRESET="$PKG_ROOT/presets/cursor/next"
|
|
16
|
+
CLAUDE_PRESET="$PKG_ROOT/presets/claude/next"
|
|
17
|
+
MANIFEST="$SCRIPT_DIR/preset-manifest.json"
|
|
18
|
+
GOLDEN="$SCRIPT_DIR/golden-prompts.json"
|
|
19
|
+
|
|
20
|
+
RUN_INIT_SMOKE=false
|
|
21
|
+
CHECK_GOLDEN=false
|
|
22
|
+
PRINT_GOLDEN=false
|
|
23
|
+
COMPARE_PIPELINE_DIR=""
|
|
24
|
+
|
|
25
|
+
while [[ $# -gt 0 ]]; do
|
|
26
|
+
case "$1" in
|
|
27
|
+
--init-smoke) RUN_INIT_SMOKE=true ;;
|
|
28
|
+
--check-golden) CHECK_GOLDEN=true ;;
|
|
29
|
+
--print-golden) PRINT_GOLDEN=true ;;
|
|
30
|
+
--compare-pipeline)
|
|
31
|
+
shift
|
|
32
|
+
COMPARE_PIPELINE_DIR="${1:-}"
|
|
33
|
+
if [[ -z "$COMPARE_PIPELINE_DIR" ]]; then
|
|
34
|
+
echo "error: --compare-pipeline requires a directory" >&2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
CHECK_GOLDEN=true
|
|
38
|
+
;;
|
|
39
|
+
-h | --help)
|
|
40
|
+
sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'
|
|
41
|
+
exit 0
|
|
42
|
+
;;
|
|
43
|
+
*)
|
|
44
|
+
echo "error: unknown argument: $1" >&2
|
|
45
|
+
exit 1
|
|
46
|
+
;;
|
|
47
|
+
esac
|
|
48
|
+
shift
|
|
49
|
+
done
|
|
50
|
+
|
|
51
|
+
FAILURES=0
|
|
52
|
+
WARNINGS=0
|
|
53
|
+
|
|
54
|
+
pass() { printf ' \033[32m✓\033[0m %s\n' "$1"; }
|
|
55
|
+
fail() { printf ' \033[31m✗\033[0m %s\n' "$1"; FAILURES=$((FAILURES + 1)); }
|
|
56
|
+
warn() { printf ' \033[33m!\033[0m %s\n' "$1"; WARNINGS=$((WARNINGS + 1)); }
|
|
57
|
+
section() { printf '\n\033[1m%s\033[0m\n' "$1"; }
|
|
58
|
+
|
|
59
|
+
require_file() {
|
|
60
|
+
local f="$1"
|
|
61
|
+
if [[ ! -f "$f" ]]; then
|
|
62
|
+
fail "missing file: $f"
|
|
63
|
+
return 1
|
|
64
|
+
fi
|
|
65
|
+
return 0
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
require_dir() {
|
|
69
|
+
local d="$1"
|
|
70
|
+
if [[ ! -d "$d" ]]; then
|
|
71
|
+
fail "missing directory: $d"
|
|
72
|
+
return 1
|
|
73
|
+
fi
|
|
74
|
+
return 0
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
node_json() {
|
|
78
|
+
node -e "$1"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# --- print golden checklist (manual LLM regression) ---
|
|
82
|
+
if $PRINT_GOLDEN; then
|
|
83
|
+
section "Golden router regression checklist (manual)"
|
|
84
|
+
node - "$GOLDEN" <<'NODE'
|
|
85
|
+
const fs = require('fs');
|
|
86
|
+
const data = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
|
|
87
|
+
console.log('Run each prompt via /task in a fresh chat. Save pipeline.json as scripts/regression-results/<id>.json\n');
|
|
88
|
+
console.log('| # | id | prompt | intent | first agent |');
|
|
89
|
+
console.log('|---|-----|--------|--------|-------------|');
|
|
90
|
+
data.prompts.forEach((p, i) => {
|
|
91
|
+
const prompt = p.prompt.replace(/\|/g, '\\|').slice(0, 60);
|
|
92
|
+
const first = p.noPipeline ? '(no pipeline)' : (p.firstAgent || '—');
|
|
93
|
+
console.log(`| ${i + 1} | ${p.id} | ${prompt} | ${p.intent} | ${first} |`);
|
|
94
|
+
});
|
|
95
|
+
console.log('\nAfter run: ./scripts/validate-preset.sh --compare-pipeline scripts/regression-results/');
|
|
96
|
+
NODE
|
|
97
|
+
exit 0
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
section "CLI syntax"
|
|
101
|
+
if node --check "$PKG_ROOT/bin/cli.js" 2>/dev/null; then
|
|
102
|
+
pass "bin/cli.js parses"
|
|
103
|
+
else
|
|
104
|
+
fail "bin/cli.js syntax error"
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
section "Manifest"
|
|
108
|
+
require_file "$MANIFEST" || true
|
|
109
|
+
require_file "$GOLDEN" || true
|
|
110
|
+
|
|
111
|
+
section "Cursor always-on rules (session start)"
|
|
112
|
+
ALWAYS_ON_COUNT=0
|
|
113
|
+
ALWAYS_ON_BYTES=0
|
|
114
|
+
while IFS= read -r name; do
|
|
115
|
+
[[ -z "$name" ]] && continue
|
|
116
|
+
f="$CURSOR_PRESET/rules/$name"
|
|
117
|
+
if [[ ! -f "$f" ]]; then
|
|
118
|
+
fail "always-on rule missing: $name"
|
|
119
|
+
continue
|
|
120
|
+
fi
|
|
121
|
+
if ! grep -q 'alwaysApply: true' "$f"; then
|
|
122
|
+
fail "$name exists but alwaysApply: true not set"
|
|
123
|
+
else
|
|
124
|
+
ALWAYS_ON_COUNT=$((ALWAYS_ON_COUNT + 1))
|
|
125
|
+
size=$(wc -c <"$f" | tr -d ' ')
|
|
126
|
+
ALWAYS_ON_BYTES=$((ALWAYS_ON_BYTES + size))
|
|
127
|
+
pass "$name ($size bytes)"
|
|
128
|
+
fi
|
|
129
|
+
done < <(node_json "
|
|
130
|
+
const m = require('$MANIFEST');
|
|
131
|
+
m.alwaysOnCursor.forEach((n) => console.log(n));
|
|
132
|
+
")
|
|
133
|
+
|
|
134
|
+
EXTRA_ALWAYS=$(grep -l 'alwaysApply: true' "$CURSOR_PRESET"/rules/*.mdc 2>/dev/null | wc -l | tr -d ' ')
|
|
135
|
+
if [[ "$EXTRA_ALWAYS" -ne "$ALWAYS_ON_COUNT" ]]; then
|
|
136
|
+
fail "expected $ALWAYS_ON_COUNT alwaysApply rules, found $EXTRA_ALWAYS"
|
|
137
|
+
grep -l 'alwaysApply: true' "$CURSOR_PRESET"/rules/*.mdc 2>/dev/null | while read -r f; do
|
|
138
|
+
echo " - $(basename "$f")"
|
|
139
|
+
done
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
MAX_BYTES=$(node_json "console.log(require('$MANIFEST').alwaysOnMaxBytes)")
|
|
143
|
+
if [[ "$ALWAYS_ON_BYTES" -gt "$MAX_BYTES" ]]; then
|
|
144
|
+
fail "always-on bundle $ALWAYS_ON_BYTES bytes exceeds budget $MAX_BYTES"
|
|
145
|
+
else
|
|
146
|
+
pass "always-on bundle: $ALWAYS_ON_BYTES / $MAX_BYTES bytes"
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
section "Claude session-start rules (no paths:)"
|
|
150
|
+
CLAUDE_SESSION_COUNT=0
|
|
151
|
+
while IFS= read -r rel; do
|
|
152
|
+
[[ -z "$rel" ]] && continue
|
|
153
|
+
f="$CLAUDE_PRESET/rules/$rel"
|
|
154
|
+
if [[ ! -f "$f" ]]; then
|
|
155
|
+
fail "claude session-start rule missing: $rel"
|
|
156
|
+
continue
|
|
157
|
+
fi
|
|
158
|
+
if head -8 "$f" | grep -q '^paths:'; then
|
|
159
|
+
fail "$rel must not have paths: (session start)"
|
|
160
|
+
else
|
|
161
|
+
CLAUDE_SESSION_COUNT=$((CLAUDE_SESSION_COUNT + 1))
|
|
162
|
+
pass "$rel"
|
|
163
|
+
fi
|
|
164
|
+
done < <(node_json "
|
|
165
|
+
const m = require('$MANIFEST');
|
|
166
|
+
m.alwaysOnClaude.forEach((n) => console.log(n));
|
|
167
|
+
")
|
|
168
|
+
|
|
169
|
+
# Unexpected session-start files
|
|
170
|
+
while IFS= read -r f; do
|
|
171
|
+
[[ "$(basename "$f")" == "README.md" ]] && continue
|
|
172
|
+
rel="${f#"$CLAUDE_PRESET/rules/"}"
|
|
173
|
+
if ! node_json "
|
|
174
|
+
const m = require('$MANIFEST');
|
|
175
|
+
process.exit(m.alwaysOnClaude.includes('$rel') ? 0 : 1);
|
|
176
|
+
" 2>/dev/null; then
|
|
177
|
+
if ! head -8 "$f" | grep -q '^paths:'; then
|
|
178
|
+
fail "unexpected claude session-start file (no paths:): $rel"
|
|
179
|
+
fi
|
|
180
|
+
fi
|
|
181
|
+
done < <(find "$CLAUDE_PRESET/rules" -name '*.md' -type f 2>/dev/null)
|
|
182
|
+
|
|
183
|
+
section "Cursor ↔ Claude rule pairs"
|
|
184
|
+
while IFS=$'\t' read -r cursor claude; do
|
|
185
|
+
cf="$CURSOR_PRESET/rules/$cursor"
|
|
186
|
+
lf="$CLAUDE_PRESET/rules/$claude"
|
|
187
|
+
if [[ ! -f "$cf" ]]; then fail "cursor pair missing: $cursor"; fi
|
|
188
|
+
if [[ ! -f "$lf" ]]; then fail "claude pair missing: $claude"; fi
|
|
189
|
+
if [[ -f "$cf" && -f "$lf" ]]; then pass "$cursor ↔ $claude"; fi
|
|
190
|
+
done < <(node_json "
|
|
191
|
+
const m = require('$MANIFEST');
|
|
192
|
+
m.rulePairs.forEach(([c, l]) => console.log(c + '\t' + l));
|
|
193
|
+
")
|
|
194
|
+
|
|
195
|
+
section "Parity: agents, skills, fixtures"
|
|
196
|
+
while IFS= read -r dir; do
|
|
197
|
+
[[ -z "$dir" ]] && continue
|
|
198
|
+
cdir="$CURSOR_PRESET/$dir"
|
|
199
|
+
ldir="$CLAUDE_PRESET/$dir"
|
|
200
|
+
require_dir "$cdir" || continue
|
|
201
|
+
require_dir "$ldir" || continue
|
|
202
|
+
# Compare file names (excluding README.md)
|
|
203
|
+
diff_out=$(diff <(find "$cdir" -type f ! -name 'README.md' -exec basename {} \; | sort -u) \
|
|
204
|
+
<(find "$ldir" -type f ! -name 'README.md' -exec basename {} \; | sort -u) 2>/dev/null || true)
|
|
205
|
+
if [[ -n "$diff_out" ]]; then
|
|
206
|
+
fail "$dir file name mismatch"
|
|
207
|
+
echo "$diff_out" | sed 's/^/ /'
|
|
208
|
+
else
|
|
209
|
+
pass "$dir names match"
|
|
210
|
+
fi
|
|
211
|
+
done < <(node_json "
|
|
212
|
+
const m = require('$MANIFEST');
|
|
213
|
+
m.parityDirs.forEach((d) => console.log(d));
|
|
214
|
+
")
|
|
215
|
+
|
|
216
|
+
section "task-router parity (cursor ↔ claude)"
|
|
217
|
+
CR="$CURSOR_PRESET/agents/task-router.md"
|
|
218
|
+
CL="$CLAUDE_PRESET/agents/task-router.md"
|
|
219
|
+
if [[ -f "$CR" && -f "$CL" ]]; then
|
|
220
|
+
# Strip frontmatter and compare body similarity via line count delta
|
|
221
|
+
c_lines=$(tail -n +8 "$CR" | wc -l | tr -d ' ')
|
|
222
|
+
l_lines=$(tail -n +8 "$CL" | wc -l | tr -d ' ')
|
|
223
|
+
delta=$((c_lines > l_lines ? c_lines - l_lines : l_lines - c_lines))
|
|
224
|
+
if [[ "$delta" -gt 15 ]]; then
|
|
225
|
+
warn "task-router.md body length delta $delta lines (cursor=$c_lines, claude=$l_lines) — review parity"
|
|
226
|
+
else
|
|
227
|
+
pass "task-router.md body length similar (Δ=$delta lines)"
|
|
228
|
+
fi
|
|
229
|
+
else
|
|
230
|
+
fail "task-router.md missing in one or both presets"
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
section "BUGBOT sync (fragments → presets)"
|
|
234
|
+
while IFS= read -r line; do
|
|
235
|
+
src_rel="${line%%$'\t'*}"
|
|
236
|
+
tgt_rel="${line#*$'\t'}"
|
|
237
|
+
src="$PKG_ROOT/$src_rel"
|
|
238
|
+
tgt="$PKG_ROOT/$tgt_rel"
|
|
239
|
+
if [[ ! -f "$src" || ! -f "$tgt" ]]; then
|
|
240
|
+
fail "BUGBOT sync path missing: $src_rel → $tgt_rel"
|
|
241
|
+
continue
|
|
242
|
+
fi
|
|
243
|
+
# Compare numbered priorities only (preset bodies add platform-specific hints)
|
|
244
|
+
src_prio=$(sed -n '/^## Priorities$/,/^## /p' "$src" | grep -E '^[0-9]+\.' | sed 's/ (`.*`)//g' || true)
|
|
245
|
+
tgt_prio=$(sed -n '/^## Priorities$/,/^## /p' "$tgt" | grep -E '^[0-9]+\.' | sed 's/ (`.*`)//g' || true)
|
|
246
|
+
if [[ "$src_prio" != "$tgt_prio" ]]; then
|
|
247
|
+
fail "BUGBOT priorities differ: $tgt_rel"
|
|
248
|
+
diff <(echo "$src_prio") <(echo "$tgt_prio") | sed 's/^/ /' || true
|
|
249
|
+
else
|
|
250
|
+
pass "$tgt_rel priorities match fragment"
|
|
251
|
+
fi
|
|
252
|
+
done < <(node_json "
|
|
253
|
+
const m = require('$MANIFEST');
|
|
254
|
+
for (const item of m.syncedRootFiles) {
|
|
255
|
+
for (const t of item.targets) console.log(item.source + '\t' + t);
|
|
256
|
+
}
|
|
257
|
+
")
|
|
258
|
+
|
|
259
|
+
section "UI globs consolidation"
|
|
260
|
+
while IFS=$'\t' read -r id file mustMatch mustNotMatch; do
|
|
261
|
+
f="$CURSOR_PRESET/rules/$file"
|
|
262
|
+
[[ ! -f "$f" ]] && { fail "$id: missing $file"; continue; }
|
|
263
|
+
ok=true
|
|
264
|
+
if [[ -n "$mustMatch" && "$mustMatch" != "null" ]]; then
|
|
265
|
+
if ! grep -qE "$mustMatch" "$f"; then
|
|
266
|
+
fail "$id: expected pattern '$mustMatch' in $file"
|
|
267
|
+
ok=false
|
|
268
|
+
fi
|
|
269
|
+
fi
|
|
270
|
+
if [[ -n "$mustNotMatch" && "$mustNotMatch" != "null" ]]; then
|
|
271
|
+
if grep -qE "$mustNotMatch" "$f"; then
|
|
272
|
+
fail "$id: forbidden pattern '$mustNotMatch' found in $file"
|
|
273
|
+
ok=false
|
|
274
|
+
fi
|
|
275
|
+
fi
|
|
276
|
+
$ok && pass "$id"
|
|
277
|
+
done < <(node_json "
|
|
278
|
+
const m = require('$MANIFEST');
|
|
279
|
+
for (const c of m.uiGlobChecks) {
|
|
280
|
+
console.log([
|
|
281
|
+
c.id,
|
|
282
|
+
c.file,
|
|
283
|
+
c.mustMatch || '',
|
|
284
|
+
c.mustNotMatch || ''
|
|
285
|
+
].join('\t'));
|
|
286
|
+
}
|
|
287
|
+
")
|
|
288
|
+
|
|
289
|
+
section "Wide globs denylist (UI edit must not load)"
|
|
290
|
+
while IFS= read -r file; do
|
|
291
|
+
[[ -z "$file" ]] && continue
|
|
292
|
+
f="$CURSOR_PRESET/rules/$file"
|
|
293
|
+
[[ ! -f "$f" ]] && continue
|
|
294
|
+
# Flag app/src/** or app/src/**/* globs; allow narrowed patterns like app/src/**/index.ts
|
|
295
|
+
if grep -E '^globs:' "$f" | grep -E 'app/src/\*\*' | grep -vq 'index\.ts'; then
|
|
296
|
+
fail "$file has wide UI glob without index.ts narrowing"
|
|
297
|
+
else
|
|
298
|
+
pass "$file has no wide UI glob"
|
|
299
|
+
fi
|
|
300
|
+
done < <(node_json "
|
|
301
|
+
const m = require('$MANIFEST');
|
|
302
|
+
m.wideGlobDenylist.forEach((n) => console.log(n));
|
|
303
|
+
")
|
|
304
|
+
|
|
305
|
+
section "Team fixtures vs golden profiles"
|
|
306
|
+
for fixture in feature-full feature-light bugfix-standard; do
|
|
307
|
+
for tool in cursor claude; do
|
|
308
|
+
f="$PKG_ROOT/presets/$tool/next/team/fixtures/${fixture}.json"
|
|
309
|
+
if [[ ! -f "$f" ]]; then
|
|
310
|
+
fail "missing fixture: $f"
|
|
311
|
+
continue
|
|
312
|
+
fi
|
|
313
|
+
if ! node -e "
|
|
314
|
+
const j = require(process.argv[1]);
|
|
315
|
+
if (!j.intent || !Array.isArray(j.steps) || j.steps.length < 1) process.exit(1);
|
|
316
|
+
" "$f" 2>/dev/null; then
|
|
317
|
+
fail "invalid fixture JSON: $f"
|
|
318
|
+
else
|
|
319
|
+
pass "$tool/$fixture.json"
|
|
320
|
+
fi
|
|
321
|
+
done
|
|
322
|
+
done
|
|
323
|
+
|
|
324
|
+
if $CHECK_GOLDEN; then
|
|
325
|
+
section "Golden prompts schema & agent references"
|
|
326
|
+
node - "$GOLDEN" "$CURSOR_PRESET/agents" "$COMPARE_PIPELINE_DIR" <<'NODE'
|
|
327
|
+
const fs = require('fs');
|
|
328
|
+
const path = require('path');
|
|
329
|
+
|
|
330
|
+
const goldenPath = process.argv[2];
|
|
331
|
+
const agentsDir = process.argv[3];
|
|
332
|
+
const compareDir = process.argv[4] || '';
|
|
333
|
+
|
|
334
|
+
let failures = 0;
|
|
335
|
+
const fail = (msg) => { console.error(` \x1b[31m✗\x1b[0m ${msg}`); failures++; };
|
|
336
|
+
const pass = (msg) => console.log(` \x1b[32m✓\x1b[0m ${msg}`);
|
|
337
|
+
const warn = (msg) => console.log(` \x1b[33m!\x1b[0m ${msg}`);
|
|
338
|
+
|
|
339
|
+
const data = JSON.parse(fs.readFileSync(goldenPath, 'utf8'));
|
|
340
|
+
const agentFiles = fs.readdirSync(agentsDir).filter((f) => f.endsWith('.md') && f !== 'README.md');
|
|
341
|
+
const knownAgents = new Set(agentFiles.map((f) => f.replace(/\.md$/, '')));
|
|
342
|
+
|
|
343
|
+
const requiredFields = ['id', 'prompt', 'intent'];
|
|
344
|
+
|
|
345
|
+
for (const p of data.prompts) {
|
|
346
|
+
for (const field of requiredFields) {
|
|
347
|
+
if (!p[field]) fail(`prompt ${p.id || '?'} missing field: ${field}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (p.noPipeline) {
|
|
351
|
+
if (p.intent !== 'retro') fail(`${p.id}: noPipeline only valid for retro`);
|
|
352
|
+
pass(`${p.id}: retro (no pipeline)`);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (!p.firstAgent) fail(`${p.id}: missing firstAgent`);
|
|
357
|
+
else if (!knownAgents.has(p.firstAgent)) fail(`${p.id}: unknown firstAgent ${p.firstAgent}`);
|
|
358
|
+
else pass(`${p.id}: firstAgent ${p.firstAgent} exists`);
|
|
359
|
+
|
|
360
|
+
for (const agent of [...(p.requiredSequence || []), ...(p.mustInclude || [])]) {
|
|
361
|
+
if (!knownAgents.has(agent)) fail(`${p.id}: unknown agent ${agent}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
for (const sc of p.stepConstraints || []) {
|
|
365
|
+
if (!knownAgents.has(sc.agent)) fail(`${p.id}: stepConstraints unknown agent ${sc.agent}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (compareDir) {
|
|
370
|
+
console.log('');
|
|
371
|
+
console.log('\x1b[1mCompare pipeline.json to golden\x1b[0m');
|
|
372
|
+
for (const p of data.prompts) {
|
|
373
|
+
if (p.noPipeline) continue;
|
|
374
|
+
const pipelinePath = path.join(compareDir, `${p.id}.json`);
|
|
375
|
+
if (!fs.existsSync(pipelinePath)) {
|
|
376
|
+
warn(`${p.id}: no file ${pipelinePath} (skip)`);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const pipeline = JSON.parse(fs.readFileSync(pipelinePath, 'utf8'));
|
|
380
|
+
const agents = (pipeline.steps || []).map((s) => s.agent);
|
|
381
|
+
|
|
382
|
+
if (pipeline.intent && pipeline.intent !== p.intent) {
|
|
383
|
+
fail(`${p.id}: intent got ${pipeline.intent}, want ${p.intent}`);
|
|
384
|
+
}
|
|
385
|
+
if (p.profile && pipeline.profile && pipeline.profile !== p.profile) {
|
|
386
|
+
warn(`${p.id}: profile got ${pipeline.profile}, expected ${p.profile}`);
|
|
387
|
+
}
|
|
388
|
+
if (agents[0] !== p.firstAgent) {
|
|
389
|
+
fail(`${p.id}: first agent got ${agents[0]}, want ${p.firstAgent}`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// requiredSequence — subsequence check
|
|
393
|
+
let idx = 0;
|
|
394
|
+
for (const want of p.requiredSequence || []) {
|
|
395
|
+
while (idx < agents.length && agents[idx] !== want) idx++;
|
|
396
|
+
if (idx >= agents.length) {
|
|
397
|
+
fail(`${p.id}: requiredSequence missing ${want} (got: ${agents.join(' → ')})`);
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
idx++;
|
|
401
|
+
}
|
|
402
|
+
if ((p.requiredSequence || []).length && idx > 0) {
|
|
403
|
+
pass(`${p.id}: requiredSequence satisfied (${agents.join(' → ')})`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
for (const forbidden of p.mustNotInclude || []) {
|
|
407
|
+
if (agents.includes(forbidden)) {
|
|
408
|
+
fail(`${p.id}: mustNotInclude violated: ${forbidden}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
for (const sc of p.stepConstraints || []) {
|
|
413
|
+
const step = (pipeline.steps || []).find((s) => s.agent === sc.agent);
|
|
414
|
+
if (!step) {
|
|
415
|
+
warn(`${p.id}: stepConstraints agent ${sc.agent} not in pipeline`);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (sc.skipIf && step.skipIf !== sc.skipIf) {
|
|
419
|
+
fail(`${p.id}: ${sc.agent}.skipIf got ${step.skipIf}, want ${sc.skipIf}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (Array.isArray(p.humanGates) && Array.isArray(pipeline.humanGates)) {
|
|
424
|
+
for (const gate of p.humanGates) {
|
|
425
|
+
if (!pipeline.humanGates.includes(gate)) {
|
|
426
|
+
fail(`${p.id}: missing humanGate ${gate}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (p.humanGates.length) pass(`${p.id}: humanGates OK`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
process.exit(failures > 0 ? 1 : 0);
|
|
435
|
+
NODE
|
|
436
|
+
golden_exit=$?
|
|
437
|
+
if [[ "$golden_exit" -ne 0 ]]; then
|
|
438
|
+
FAILURES=$((FAILURES + golden_exit))
|
|
439
|
+
fi
|
|
440
|
+
fi
|
|
441
|
+
|
|
442
|
+
if $RUN_INIT_SMOKE; then
|
|
443
|
+
section "Init smoke (cursor + claude)"
|
|
444
|
+
TMP_BASE=$(mktemp -d "${TMPDIR:-/tmp}/ai-rules-validate.XXXXXX")
|
|
445
|
+
cleanup() { rm -rf "$TMP_BASE"; }
|
|
446
|
+
trap cleanup EXIT
|
|
447
|
+
|
|
448
|
+
for tool in cursor claude; do
|
|
449
|
+
target="$TMP_BASE/$tool-project"
|
|
450
|
+
mkdir -p "$target"
|
|
451
|
+
if node "$PKG_ROOT/bin/cli.js" init "$tool" --preset next --cwd "$target" >/dev/null 2>&1; then
|
|
452
|
+
pass "init $tool → $target"
|
|
453
|
+
# Spot-check critical paths
|
|
454
|
+
case "$tool" in
|
|
455
|
+
cursor)
|
|
456
|
+
for p in .cursor/rules/next-app-core.mdc .cursor/agents/task-router.md .cursor/team/fixtures/feature-full.json .cursor/BUGBOT.md .cursor/AGENTS.md; do
|
|
457
|
+
[[ -f "$target/$p" ]] && pass " $p" || fail " missing after init: $p"
|
|
458
|
+
done
|
|
459
|
+
;;
|
|
460
|
+
claude)
|
|
461
|
+
for p in .claude/rules/stack/next-app-core.md .claude/agents/task-router.md .claude/team/fixtures/feature-full.json .claude/CLAUDE.md; do
|
|
462
|
+
[[ -f "$target/$p" ]] && pass " $p" || fail " missing after init: $p"
|
|
463
|
+
done
|
|
464
|
+
;;
|
|
465
|
+
esac
|
|
466
|
+
else
|
|
467
|
+
fail "init $tool failed"
|
|
468
|
+
fi
|
|
469
|
+
done
|
|
470
|
+
fi
|
|
471
|
+
|
|
472
|
+
section "Summary"
|
|
473
|
+
if [[ "$FAILURES" -eq 0 ]]; then
|
|
474
|
+
printf '\033[32mPASS\033[0m — %s warnings\n' "$WARNINGS"
|
|
475
|
+
echo ""
|
|
476
|
+
echo "Manual router regression:"
|
|
477
|
+
echo " ./scripts/validate-preset.sh --print-golden"
|
|
478
|
+
echo " # save outputs to scripts/regression-results/<id>.json"
|
|
479
|
+
echo " ./scripts/validate-preset.sh --check-golden --compare-pipeline scripts/regression-results/"
|
|
480
|
+
exit 0
|
|
481
|
+
else
|
|
482
|
+
printf '\033[31mFAIL\033[0m — %s error(s), %s warning(s)\n' "$FAILURES" "$WARNINGS"
|
|
483
|
+
exit 1
|
|
484
|
+
fi
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
paths:
|
|
3
|
-
- app/src/ui/**/*.ts
|
|
4
|
-
- app/src/ui/**/*.tsx
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Стили только рядом с компонентом
|
|
8
|
-
|
|
9
|
-
## Имя корневого styled-элемента: `Root`
|
|
10
|
-
|
|
11
|
-
- **Корневой** styled-элемент — **`Root`**.
|
|
12
|
-
- В разметке: `<s.Root>...</s.Root>` при `import * as s from './styles'`.
|
|
13
|
-
- Вложенные — `Title`, `List`, `Item` и т.д.
|
|
14
|
-
- Единственная внешняя styled-обёртка всего JSX — **`Root`**, не `Container` / `Wrapper`.
|
|
15
|
-
|
|
16
|
-
```tsx
|
|
17
|
-
// ✅ Хорошо
|
|
18
|
-
export const Root = styled.div` ... `
|
|
19
|
-
// в компоненте: <s.Root>...</s.Root>
|
|
20
|
-
|
|
21
|
-
// ❌ Плохо для единственной обёртки
|
|
22
|
-
export const Service = styled.div` ... `
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## Правило
|
|
26
|
-
|
|
27
|
-
**Запрещено** импортировать модули стилей **другого** UI-компонента или блока.
|
|
28
|
-
|
|
29
|
-
Под «модулями стилей»:
|
|
30
|
-
|
|
31
|
-
- `styles.ts` / `styles.tsx` рядом с компонентом;
|
|
32
|
-
- `*.module.css`, `*.module.scss`;
|
|
33
|
-
- файлы, экспортирующие styled-примитивы одного компонента.
|
|
34
|
-
|
|
35
|
-
## Разрешено
|
|
36
|
-
|
|
37
|
-
- `import * as s from './styles'` — в той же папке.
|
|
38
|
-
- Общие примитивы дизайн-системы, токены, `@/ui/components/...`.
|
|
39
|
-
- Повторное использование визуала через **сам компонент**, не через его `styles`.
|
|
40
|
-
|
|
41
|
-
## Примеры
|
|
42
|
-
|
|
43
|
-
```tsx
|
|
44
|
-
// ✅ Хорошо
|
|
45
|
-
import * as s from './styles'
|
|
46
|
-
|
|
47
|
-
// ❌ Плохо
|
|
48
|
-
import * as s from '../../styles'
|
|
49
|
-
import * as s from '../RequestForAnalysisServices/styles'
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
## Почему
|
|
53
|
-
|
|
54
|
-
- **`Root`** — быстрая ориентация в разметке.
|
|
55
|
-
- Стили и компонент меняются вместе.
|
|
56
|
-
- Проще рефакторинг.
|