@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.
Files changed (62) hide show
  1. package/CONTRIBUTING.md +102 -0
  2. package/README.md +2 -0
  3. package/bin/cli.js +4 -2
  4. package/fragments/BUGBOT.md +23 -0
  5. package/package.json +10 -2
  6. package/presets/claude/next/BUGBOT.md +16 -0
  7. package/presets/claude/next/CLAUDE.md +5 -1
  8. package/presets/claude/next/agents/README.md +16 -15
  9. package/presets/claude/next/agents/code-reviewer.md +1 -1
  10. package/presets/claude/next/agents/playwright-test-generator.md +16 -65
  11. package/presets/claude/next/agents/playwright-test-healer.md +16 -51
  12. package/presets/claude/next/agents/playwright-test-planner.md +15 -55
  13. package/presets/claude/next/agents/task-analyst.md +1 -1
  14. package/presets/claude/next/agents/task-router.md +24 -0
  15. package/presets/claude/next/agents/tech-writer.md +1 -1
  16. package/presets/claude/next/hooks.json +17 -0
  17. package/presets/claude/next/rules/README.md +6 -2
  18. package/presets/claude/next/rules/architecture/README.md +2 -1
  19. package/presets/claude/next/rules/architecture/architecture-boundaries-ui.md +15 -0
  20. package/presets/claude/next/rules/architecture/architecture-boundaries.md +4 -1
  21. package/presets/claude/next/rules/architecture/feature-delivery-workflow.md +1 -1
  22. package/presets/claude/next/rules/architecture/layer-barrel-exports.md +1 -1
  23. package/presets/claude/next/rules/stack/README.md +2 -1
  24. package/presets/claude/next/rules/stack/navigation-router-ui.md +15 -0
  25. package/presets/claude/next/rules/stack/navigation-router.md +2 -1
  26. package/presets/claude/next/rules/stack/next-app-core.md +1 -1
  27. package/presets/claude/next/rules/tooling-and-review/agent-team-orchestrator.md +6 -0
  28. package/presets/claude/next/rules/tooling-and-review/code-review-mr.md +22 -59
  29. package/presets/claude/next/rules/ui-and-accessibility/README.md +1 -2
  30. package/presets/claude/next/rules/ui-and-accessibility/react-ui.md +4 -3
  31. package/presets/claude/next/team/README.md +1 -0
  32. package/presets/claude/next/team/fixtures/bugfix-standard.json +15 -0
  33. package/presets/claude/next/team/fixtures/feature-full.json +16 -0
  34. package/presets/claude/next/team/fixtures/feature-light.json +17 -0
  35. package/presets/cursor/next/AGENTS.md +2 -9
  36. package/presets/cursor/next/BUGBOT.md +2 -0
  37. package/presets/cursor/next/agents/README.md +16 -15
  38. package/presets/cursor/next/agents/code-reviewer.md +1 -1
  39. package/presets/cursor/next/agents/task-analyst.md +1 -1
  40. package/presets/cursor/next/agents/task-router.md +24 -0
  41. package/presets/cursor/next/agents/tech-writer.md +1 -1
  42. package/presets/cursor/next/rules/README.md +21 -1
  43. package/presets/cursor/next/rules/architecture-boundaries-ui.mdc +15 -0
  44. package/presets/cursor/next/rules/architecture-boundaries.mdc +1 -1
  45. package/presets/cursor/next/rules/code-review-mr.mdc +20 -46
  46. package/presets/cursor/next/rules/feature-delivery-workflow.mdc +1 -1
  47. package/presets/cursor/next/rules/layer-barrel-exports.mdc +1 -1
  48. package/presets/cursor/next/rules/navigation-router-stack.mdc +1 -1
  49. package/presets/cursor/next/rules/navigation-router-ui.mdc +16 -0
  50. package/presets/cursor/next/rules/next-app-core.mdc +1 -1
  51. package/presets/cursor/next/rules/no-cross-component-styles-import.mdc +3 -53
  52. package/presets/cursor/next/rules/react-ui.mdc +4 -3
  53. package/presets/cursor/next/team/README.md +1 -0
  54. package/presets/cursor/next/team/fixtures/bugfix-standard.json +15 -0
  55. package/presets/cursor/next/team/fixtures/feature-full.json +16 -0
  56. package/presets/cursor/next/team/fixtures/feature-light.json +17 -0
  57. package/scripts/README.md +84 -0
  58. package/scripts/golden-prompts.json +276 -0
  59. package/scripts/preset-manifest.json +72 -0
  60. package/scripts/regression-results/.gitkeep +0 -0
  61. package/scripts/validate-preset.sh +484 -0
  62. 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
- - Проще рефакторинг.