-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvalidate-skills.sh
More file actions
executable file
·901 lines (809 loc) · 38.6 KB
/
validate-skills.sh
File metadata and controls
executable file
·901 lines (809 loc) · 38.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
#!/usr/bin/env bash
# Validate all skills for structural correctness.
# Does NOT require a live cluster — checks file structure and frontmatter only.
#
# Usage:
# ./scripts/validate-skills.sh
#
# Exit codes: 0 = all pass, 1 = one or more failures
set -euo pipefail
SKILLS_DIR="$(cd "$(dirname "$0")/../skills" && pwd)"
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PASS=0; FAIL=0; WARN=0
red() { echo -e "\033[31m$*\033[0m"; }
green() { echo -e "\033[32m$*\033[0m"; }
yellow() { echo -e "\033[33m$*\033[0m"; }
check() {
case "$3" in
ok) PASS=$((PASS+1)) ;;
warn) yellow " ⚠ $1: $2"; WARN=$((WARN+1)) ;;
*) red " ✗ $1: $2"; FAIL=$((FAIL+1)) ;;
esac
}
LANG_SUFFIXES="python java go dotnet nodejs rust scala php ruby"
is_lang_skill() { for l in $LANG_SUFFIXES; do [[ "$1" == *"-$l" ]] && return 0; done; return 1; }
skill_lang() { for l in $LANG_SUFFIXES; do [[ "$1" == *"-$l" ]] && echo "$l" && return; done; echo ""; }
PYTHON_PATTERNS="from couchbase import|import couchbase|pip install"
JAVA_PATTERNS="import com\.couchbase|pom\.xml"
GO_PATTERNS="gocb/v2"
NODEJS_PATTERNS="require\('couchbase'\)|from 'couchbase'|couchbase\.connect\("
DOTNET_PATTERNS="^using Couchbase|CouchbaseNetClient|await Cluster\.ConnectAsync"
PHP_PATTERNS="\\\$cluster->|\\\$collection->upsert|\\\$collection->get|new Cluster\("
RUBY_PATTERNS="require 'couchbase'|Couchbase::Cluster\.connect"
RUST_PATTERNS="use couchbase::|couchbase::Cluster"
SCALA_PATTERNS="import com\.couchbase\.client\.scala"
VALID_LANGS="python go java dotnet nodejs rust scala php ruby kotlin swift sql javascript typescript bash sh csharp"
# Build known-skills set from the working tree
KNOWN_SKILLS_FILE=$(mktemp)
trap "rm -f $KNOWN_SKILLS_FILE" EXIT
ls "$SKILLS_DIR"/ > "$KNOWN_SKILLS_FILE"
skill_exists() { grep -qxF "$1" "$KNOWN_SKILLS_FILE"; }
# ── Per-skill validator ───────────────────────────────────────────────────────
validate_skill() {
local skill_dir="$1"
local skill_file="$skill_dir/SKILL.md"
local skill
skill=$(basename "$skill_dir")
echo "── $skill"
# Group 1: File structure
if [ ! -f "$skill_file" ]; then
check "$skill" "SKILL.md missing" "fail"; return
fi
[ -d "$skill_dir/examples" ] \
&& check "$skill" "examples/ present" "ok" \
|| check "$skill" "no examples/ directory" "warn"
# Group 2: Required frontmatter fields
local name
name=$(grep "^name:" "$skill_file" | head -1 | sed 's/name: *//')
[ "$name" = "$skill" ] \
&& check "$skill" "name matches directory" "ok" \
|| check "$skill" "name '$name' does not match directory '$skill'" "fail"
local summary
summary=$(grep "^summary:" "$skill_file" | head -1 | sed 's/summary: *//')
[ -n "$summary" ] \
&& check "$skill" "summary present" "ok" \
|| check "$skill" "summary missing or empty" "fail"
# Forbidden frontmatter fields (must live in discovery.yaml, not SKILL.md)
for forbidden in triggers use_when specificity priority last_verified; do
grep -q "^${forbidden}:" "$skill_file" \
&& check "$skill" "${forbidden}: must be in discovery.yaml, not SKILL.md" "fail" \
|| true
done
# Group 3: Optional frontmatter fields
# 3a. compatibility: must be a scalar string, not a list
if grep -q "^compatibility:" "$skill_file"; then
local compat_val
compat_val=$(grep "^compatibility:" "$skill_file" | head -1 | sed 's/^compatibility:[[:space:]]*//')
if [ -z "$compat_val" ]; then
check "$skill" "compatibility: is a list — must be a scalar string (e.g. 'Python SDK 4.x')" "warn"
fi
fi
# 3b. Staleness: warn if SKILL.md has not been committed in >180 days.
# Skips gracefully for untracked/new files (git log returns empty).
local last_commit_ts
last_commit_ts=$(git -C "$REPO_ROOT" log -1 --format="%ct" -- "$skill_file" 2>/dev/null || true)
if [ -n "$last_commit_ts" ]; then
local now_ts age_days last_date
now_ts=$(date +%s)
age_days=$(( (now_ts - last_commit_ts) / 86400 ))
last_date=$(git -C "$REPO_ROOT" log -1 --format="%cs" -- "$skill_file" 2>/dev/null || echo "unknown")
if [ "$age_days" -gt 180 ]; then
check "$skill" "SKILL.md last committed $age_days days ago ($last_date) — review for staleness" "warn"
fi
fi
# 3c. deprecated_by: target must exist if set
if grep -q "deprecated_by:" "$skill_file"; then
local dep_target
dep_target=$(grep "deprecated_by:" "$skill_file" | head -1 | sed 's/.*deprecated_by: *//' | tr -d '"')
skill_exists "$dep_target" \
&& check "$skill" "deprecated_by: '$dep_target' exists" "ok" \
|| check "$skill" "deprecated_by: '$dep_target' does not exist" "fail"
fi
# 3d. deprecated_since: must be a valid X.Y or X.Y.Z version string if present
if grep -q "deprecated_since:" "$skill_file"; then
local dep_since
dep_since=$(grep "deprecated_since:" "$skill_file" | head -1 | sed 's/.*deprecated_since:[[:space:]]*//' | tr -d '"')
echo "$dep_since" | grep -qE '^[0-9]+\.[0-9]+(\.[0-9]+)?$' \
&& check "$skill" "deprecated_since: '$dep_since' is a valid version" "ok" \
|| check "$skill" "deprecated_since: '$dep_since' is not a valid X.Y or X.Y.Z version string" "fail"
fi
# 3f. allowed-tools: Bash must be justified by bash code blocks
if grep -q "^allowed-tools:.*Bash" "$skill_file"; then
local bb
bb=$(grep -cE '```(bash|sh|shell)' "$skill_file" 2>/dev/null || echo 0)
[ "$bb" -eq 0 ] \
&& check "$skill" "allowed-tools: Bash declared but no bash/sh/shell code blocks found" "warn" \
|| check "$skill" "allowed-tools: Bash justified ($bb blocks)" "ok"
fi
# 3g. compatibility: recommended for language-specific skills
if is_lang_skill "$skill"; then
grep -q "^compatibility:" "$skill_file" \
&& check "$skill" "compatibility present" "ok" \
|| check "$skill" "compatibility missing for language-specific skill" "warn"
fi
# 3h. last_verified: warn if missing; warn if >180 days old (YYYY-MM format)
local lv_raw lv_ts now_ts lv_age_days
lv_raw=$(grep "last_verified:" "$skill_file" | head -1 | sed 's/.*last_verified:[[:space:]]*//' | tr -d '"')
if [ -z "$lv_raw" ]; then
check "$skill" "last_verified missing from metadata — add 'last_verified: \"YYYY-MM\"' under metadata:" "warn"
else
# Parse YYYY-MM as first day of that month for age calculation
lv_ts=$(date -d "${lv_raw}-01" +%s 2>/dev/null || true)
if [ -n "$lv_ts" ]; then
now_ts=$(date +%s)
lv_age_days=$(( (now_ts - lv_ts) / 86400 ))
if [ "$lv_age_days" -gt 180 ]; then
check "$skill" "last_verified ($lv_raw) is ${lv_age_days} days old — review content for SDK/API changes" "warn"
else
check "$skill" "last_verified ($lv_raw) is current" "ok"
fi
fi
fi
# Group 4: Handoff block
grep -q "handoff:" "$skill_file" \
&& check "$skill" "handoff entries present" "ok" \
|| check "$skill" "no handoff entries" "warn"
if grep -q "^ handoff:" "$skill_file"; then
grep -q "do not edit manually\|generated from routing.yaml" "$skill_file" \
&& check "$skill" "handoff: marked as routing.yaml-derived" "ok" \
|| check "$skill" "handoff: missing 'do not edit manually' marker — run sync-handoffs.sh" "warn"
fi
while IFS= read -r target; do
target=$(echo "$target" | tr -d ' \r')
[ -z "$target" ] && continue
skill_exists "$target" \
&& check "$skill" "handoff target '$target' exists" "ok" \
|| check "$skill" "handoff target '$target' does not exist" "fail"
done < <(grep -A1 "condition:" "$skill_file" | grep "skill:" | sed 's/.*skill: //' || true)
# Duplicate handoff conditions
local dup_conditions
dup_conditions=$(grep "condition:" "$skill_file" | sed 's/.*condition: *//' | sort | uniq -d)
if [ -n "$dup_conditions" ]; then
while IFS= read -r dup; do
check "$skill" "duplicate handoff condition: '$dup'" "warn"
done <<< "$dup_conditions"
fi
# Group 5: Reference links
# 5a. Local references/ links
while IFS= read -r ref; do
[ -f "$skill_dir/$ref" ] \
&& check "$skill" "reference $ref resolves" "ok" \
|| check "$skill" "broken reference: $ref" "fail"
done < <(grep -oP '\(references/[^)]+\.md\)' "$skill_file" | tr -d '()' || true)
# 5b. shared/ links (../../shared/...)
while IFS= read -r ref; do
target="$REPO_ROOT/${ref#../../}"
[ -f "$target" ] \
&& check "$skill" "shared link $ref resolves" "ok" \
|| check "$skill" "broken shared link: $ref" "fail"
done < <(grep -oP '\(../../shared/[^)]+\)' "$skill_file" | tr -d '()' || true)
# 5c. templates/ links (../../templates/...)
while IFS= read -r ref; do
target="$REPO_ROOT/${ref#../../}"
[ -f "$target" ] \
&& check "$skill" "template link $ref resolves" "ok" \
|| check "$skill" "broken template link: $ref" "fail"
done < <(grep -oP '\(../../templates/[^)]+\)' "$skill_file" | tr -d '()' || true)
# Group 6: Examples / eval contract
local ef="$skill_dir/examples/examples.md"
if [ -f "$ef" ]; then
grep -q "^cases:" "$ef" \
&& check "$skill" "examples.md has eval frontmatter" "ok" \
|| check "$skill" "examples.md missing eval frontmatter (cases:)" "warn"
local case_count expect_count
case_count=$(grep -c "^ - id:" "$ef" 2>/dev/null || true); case_count=${case_count:-0}
expect_count=$(grep -c "^ expect:" "$ef" 2>/dev/null || true); expect_count=${expect_count:-0}
if [ "$case_count" -gt 0 ] && [ "$expect_count" -lt "$case_count" ]; then
check "$skill" "some eval cases missing expect ($expect_count/$case_count have it)" "warn"
fi
while IFS= read -r thresh_line; do
local thresh
thresh=$(echo "$thresh_line" | awk '{print $2}')
echo "$thresh" | grep -qE '^[0-9]+$' \
&& check "$skill" "threshold: $thresh valid" "ok" \
|| check "$skill" "threshold: '$thresh' must be a positive integer" "fail"
done < <(grep "^ threshold:" "$ef" 2>/dev/null || true)
while IFS= read -r cb_line; do
local cb_lang
cb_lang=$(echo "$cb_line" | awk '{print $2}')
echo "$VALID_LANGS" | grep -qw "$cb_lang" \
&& check "$skill" "code_block: $cb_lang valid" "ok" \
|| check "$skill" "code_block: '$cb_lang' not a recognised language identifier" "warn"
done < <(grep "^ code_block:" "$ef" 2>/dev/null || true)
if is_lang_skill "$skill"; then
local lang; lang=$(skill_lang "$skill")
# Scan only the markdown body (after the closing --- of the YAML frontmatter)
# to avoid false positives from reject: terms in the YAML block.
local md_body
md_body=$(awk 'BEGIN{n=0} /^---$/{n++; if(n==2){found=1; next}} found{print}' "$ef" 2>/dev/null)
if [ "$lang" != "python" ]; then
local h; h=$(echo "$md_body" | grep -oE "$PYTHON_PATTERNS" 2>/dev/null | wc -l)
[ "$h" -gt 0 ] && check "$skill" "examples contain Python patterns ($h hits)" "warn"
fi
if [ "$lang" != "java" ] && [ "$lang" != "scala" ]; then
local h; h=$(echo "$md_body" | grep -oE "$JAVA_PATTERNS" 2>/dev/null | wc -l)
[ "$h" -gt 0 ] && check "$skill" "examples contain Java patterns ($h hits)" "warn"
fi
if [ "$lang" != "go" ]; then
local h; h=$(echo "$md_body" | grep -oE "$GO_PATTERNS" 2>/dev/null | wc -l)
[ "$h" -gt 0 ] && check "$skill" "examples contain Go patterns ($h hits)" "warn"
fi
if [ "$lang" != "nodejs" ]; then
local h; h=$(echo "$md_body" | grep -oE "$NODEJS_PATTERNS" 2>/dev/null | wc -l)
[ "$h" -gt 0 ] && check "$skill" "examples contain Node.js patterns ($h hits)" "warn"
fi
if [ "$lang" != "dotnet" ]; then
local h; h=$(echo "$md_body" | grep -oE "$DOTNET_PATTERNS" 2>/dev/null | wc -l)
[ "$h" -gt 0 ] && check "$skill" "examples contain .NET patterns ($h hits)" "warn"
fi
if [ "$lang" != "php" ]; then
local h; h=$(echo "$md_body" | grep -oE "$PHP_PATTERNS" 2>/dev/null | wc -l)
[ "$h" -gt 0 ] && check "$skill" "examples contain PHP patterns ($h hits)" "warn"
fi
if [ "$lang" != "ruby" ]; then
local h; h=$(echo "$md_body" | grep -oE "$RUBY_PATTERNS" 2>/dev/null | wc -l)
[ "$h" -gt 0 ] && check "$skill" "examples contain Ruby patterns ($h hits)" "warn"
fi
if [ "$lang" != "rust" ]; then
local h; h=$(echo "$md_body" | grep -oE "$RUST_PATTERNS" 2>/dev/null | wc -l)
[ "$h" -gt 0 ] && check "$skill" "examples contain Rust patterns ($h hits)" "warn"
fi
if [ "$lang" != "scala" ] && [ "$lang" != "java" ]; then
local h; h=$(echo "$md_body" | grep -oE "$SCALA_PATTERNS" 2>/dev/null | wc -l)
[ "$h" -gt 0 ] && check "$skill" "examples contain Scala patterns ($h hits)" "warn"
fi
fi
fi
# Group 7: Size limits
local lines; lines=$(wc -l < "$skill_file")
if [ "$lines" -gt 600 ]; then
check "$skill" "SKILL.md is $lines lines (>600 — move content to references/)" "fail"
elif [ "$lines" -gt 400 ]; then
check "$skill" "SKILL.md is $lines lines (>400 — consider moving content to references/)" "warn"
fi
}
# ── routing.yaml cycle detection ─────────────────────────────────────────────
validate_routing_cycles() {
local routing="$REPO_ROOT/routing.yaml"
[ -f "$routing" ] || return
# Allowlisted bidirectional pairs — intentional A<->B navigation.
# Format: "SKILL_A|SKILL_B" with SKILL_A < SKILL_B alphabetically.
# Any 2-node cycle NOT on this list is a routing error and will fail.
local ALLOWLIST="
analytics-dotnet|server-querying-dotnet
analytics-go|server-querying-go
analytics-java|server-querying-java
analytics-nodejs|server-querying-nodejs
analytics-php|server-querying-php
analytics-python|columnar-analytics
analytics-python|server-querying-python
analytics-ruby|server-querying-ruby
analytics-scala|server-querying-scala
app-services|capella
app-services|mobile-data-modeling
app-services|mobile-sync-android
backup|disaster-recovery
caching-patterns|getting-started
caching-patterns|server-data-modeling
capella-quickstart|local-dev-setup
cluster-ops|monitoring
cluster-ops|security
cluster-ops|xdcr
columnar-analytics|server-query-optimizer
columnar-analytics|server-querying-python
error-handling|getting-started
error-handling|monitoring
error-handling|sdk-patterns-python
error-handling|transactions-python
eventing|kafka
getting-started|local-dev-setup
getting-started|migration
getting-started|server-data-modeling
getting-started|sqlpp-language
local-dev-setup|testing-patterns
local-dev-setup|vscode-extension
migration|server-data-modeling
mobile-android|mobile-ios
mobile-android|mobile-logging-android
mobile-android|mobile-p2p-sync-android
mobile-android|mobile-testing-android
mobile-android|mobile-vector-search-android
mobile-conflict-resolution-android|mobile-conflict-resolution-ios
mobile-conflict-resolution-android|mobile-data-modeling
mobile-conflict-resolution-android|mobile-sync-android
mobile-conflict-resolution-ios|mobile-sync-ios
mobile-data-modeling|server-data-modeling
mobile-ios|mobile-logging-ios
mobile-ios|mobile-p2p-sync-ios
mobile-ios|mobile-testing-ios
mobile-ios|mobile-vector-search-ios
mobile-logging-android|mobile-logging-ios
mobile-p2p-sync-android|mobile-p2p-sync-ios
mobile-p2p-sync-android|mobile-sync-android
mobile-p2p-sync-ios|mobile-sync-ios
mobile-sync-android|mobile-sync-ios
mobile-testing-android|mobile-testing-ios
mobile-vector-search-android|mobile-vector-search-ios
sdk-patterns-python|server-connection-python
sdk-patterns-dotnet|server-querying-dotnet
sdk-patterns-dotnet|transactions-dotnet
sdk-patterns-go|server-querying-go
sdk-patterns-go|transactions-go
sdk-patterns-java|server-querying-java
sdk-patterns-java|transactions-java
sdk-patterns-nodejs|server-querying-nodejs
sdk-patterns-nodejs|transactions-nodejs
sdk-patterns-php|server-querying-php
sdk-patterns-php|transactions-php
sdk-patterns-python|server-querying-python
sdk-patterns-python|transactions-python
sdk-patterns-ruby|server-querying-ruby
sdk-patterns-rust|server-querying-rust
sdk-patterns-scala|server-querying-scala
sdk-patterns-scala|transactions-scala
search-concepts|search-python
search-dotnet|server-connection-dotnet
search-dotnet|server-querying-dotnet
search-go|server-connection-go
search-go|server-querying-go
search-java|server-connection-java
search-java|server-querying-java
search-nodejs|server-connection-nodejs
search-nodejs|server-querying-nodejs
search-php|server-connection-php
search-php|server-querying-php
search-python|server-connection-python
search-python|server-querying-python
search-ruby|server-connection-ruby
search-ruby|server-querying-ruby
search-rust|server-connection-rust
search-rust|server-querying-rust
search-scala|server-connection-scala
search-scala|server-querying-scala
sdk-patterns-dotnet|search-dotnet
sdk-patterns-go|search-go
sdk-patterns-java|search-java
sdk-patterns-nodejs|search-nodejs
sdk-patterns-php|search-php
sdk-patterns-python|search-python
sdk-patterns-ruby|search-ruby
sdk-patterns-rust|search-rust
sdk-patterns-scala|search-scala
server-connection-dotnet|server-querying-dotnet
server-connection-dotnet|transactions-dotnet
server-connection-go|server-querying-go
server-connection-go|transactions-go
server-connection-java|server-querying-java
server-connection-java|transactions-java
server-connection-nodejs|server-querying-nodejs
server-connection-nodejs|transactions-nodejs
server-connection-php|server-querying-php
server-connection-php|transactions-php
server-connection-python|server-querying-python
server-connection-python|transactions-python
server-connection-ruby|server-querying-ruby
server-connection-rust|server-querying-rust
server-connection-scala|server-querying-scala
server-connection-scala|transactions-scala
server-data-modeling|server-query-optimizer
server-query-optimizer|server-querying-python
server-querying-dotnet|transactions-dotnet
server-querying-go|transactions-go
server-querying-java|transactions-java
server-querying-nodejs|transactions-nodejs
server-querying-php|transactions-php
server-querying-python|transactions-python
server-querying-scala|transactions-scala
analytics-rust|server-connection-rust
couchbase|getting-started
fle|fle-python
fle|security
fle-dotnet|server-connection-dotnet
fle-go|server-connection-go
fle-java|server-connection-java
fle-nodejs|server-connection-nodejs
fle-php|server-connection-php
fle-python|server-connection-python
server-connection-ruby|transactions-ruby
server-connection-rust|transactions-rust
server-connection-dotnet|testing-patterns-dotnet
server-connection-go|testing-patterns-go
server-connection-java|testing-patterns-java
server-connection-nodejs|testing-patterns-nodejs
server-connection-php|testing-patterns-php
server-connection-python|testing-patterns-python
server-connection-ruby|testing-patterns-ruby
server-connection-rust|testing-patterns-rust
server-connection-scala|testing-patterns-scala
testing-patterns|testing-patterns-python
"
# Build edge list: "from to" lines
local edges_tmp
edges_tmp=$(mktemp)
awk '/^ - from:/{from=$3} /^ to:/{print from " " $2}' "$routing" > "$edges_tmp"
# Detect 2-node cycles: A->B and B->A both present
local cycles_tmp
cycles_tmp=$(mktemp)
while IFS=' ' read -r a b; do
[ -z "$a" ] || [ -z "$b" ] && continue
if grep -qxF "$b $a" "$edges_tmp"; then
# Skip self-loops (a == b) — not a 2-node cycle
[ "$a" = "$b" ] && continue
# Canonical form: alphabetically smaller first
if [[ "$a" < "$b" ]]; then
echo "$a|$b"
else
echo "$b|$a"
fi
fi
done < "$edges_tmp" | sort -u > "$cycles_tmp"
local total=0 allowed=0 warned=0
while IFS= read -r pair; do
[ -z "$pair" ] && continue
total=$((total+1))
if echo "$ALLOWLIST" | grep -qxF "$pair"; then
check "routing.yaml" "2-node cycle '$pair' is allowlisted" "ok"
allowed=$((allowed+1))
else
check "routing.yaml" "2-node cycle detected: $pair — add to allowlist or make edges unidirectional" "fail"
warned=$((warned+1))
fi
done < "$cycles_tmp"
# Stale-entry check: every allowlist entry must correspond to an actual cycle.
while IFS= read -r entry; do
[ -z "$entry" ] && continue
if ! grep -qxF "$entry" "$cycles_tmp"; then
check "routing.yaml" "allowlist entry '$entry' has no matching cycle — remove it" "fail"
fi
done < <(echo "$ALLOWLIST" | grep -v '^$')
rm -f "$edges_tmp" "$cycles_tmp"
}
# ── routing.yaml validation ───────────────────────────────────────────────────
validate_routing() {
local routing="$REPO_ROOT/routing.yaml"
[ -f "$routing" ] || return
# 1. All 'to:' targets exist
while IFS= read -r target; do
target=$(echo "$target" | tr -d ' \r')
[ -z "$target" ] && continue
skill_exists "$target" \
&& check "routing.yaml" "target '$target' exists" "ok" \
|| check "routing.yaml" "target '$target' does not exist" "fail"
done < <(grep '^\s*to:' "$routing" | sed 's/.*to: //' || true)
# 2. All variant_group members exist
while IFS= read -r vg_member; do
vg_member=$(echo "$vg_member" | tr -d ' \r')
[ -z "$vg_member" ] && continue
skill_exists "$vg_member" \
&& check "routing.yaml" "variant_group '$vg_member' exists" "ok" \
|| check "routing.yaml" "variant_group '$vg_member' does not exist" "fail"
done < <(awk '/^ variant_group:/{in_vg=1;next} in_vg && /^ - /{print $2} in_vg && !/^ - /{in_vg=0}' "$routing" || true)
# 2b. All variant_group lists for the same skill family must have identical membership
local vg_families_tmp vg_results_tmp
vg_families_tmp=$(mktemp)
vg_results_tmp=$(mktemp)
awk '
/^ variant_group:/ { in_vg=1; members=""; next }
in_vg && /^ - / { m=$2; members=(members=="" ? m : members " " m); next }
in_vg && !/^ - / {
in_vg=0
if (members != "") {
first=members; sub(/ .*/, "", first)
family=first; sub(/-[^-]+$/, "", family)
print family "|" members
}
}
' "$routing" | sort -u > "$vg_families_tmp"
# Detect families with more than one distinct member list
awk -F'|' '{print $1}' "$vg_families_tmp" | sort -u | while read -r fam; do
count=$(grep -c "^${fam}|" "$vg_families_tmp" || true)
if [ "$count" -gt 1 ]; then
echo "FAIL|routing.yaml|variant_group '$fam' has inconsistent membership across edges"
else
echo "OK|routing.yaml|variant_group '$fam' membership consistent"
fi
done > "$vg_results_tmp"
while IFS='|' read -r result ctx msg; do
[ "$result" = "FAIL" ] && check "$ctx" "$msg" "fail" || check "$ctx" "$msg" "ok"
done < "$vg_results_tmp"
rm -f "$vg_families_tmp" "$vg_results_tmp"
# 2c. variant_group: lists must match canonical groups: definitions
local groups_tmp vg_check_tmp
groups_tmp=$(mktemp)
vg_check_tmp=$(mktemp)
# Parse groups: section into "group_name|member1 member2 ..." lines
awk '
/^groups:/ { in_groups=1; next }
/^routes:/ { in_groups=0 }
in_groups && /^ [a-z]/ {
if (group!="" && members!="") print group "|" members
group=substr($1,1,length($1)-1); members=""; next
}
in_groups && /^ - / { m=$2; members=(members=="" ? m : members " " m) }
END { if (group!="" && members!="") print group "|" members }
' "$routing" > "$groups_tmp"
# Collect unique variant_group lists from routes section
awk '
/^ variant_group:/ { in_vg=1; members=""; next }
in_vg && /^ - / { m=$2; members=(members=="" ? m : members " " m); next }
in_vg && !/^ - / {
in_vg=0
if (members != "") print members
}
END { if (in_vg && members != "") print members }
' "$routing" | sort -u > "$vg_check_tmp"
# Compare each unique variant_group list against canonical groups (order-insensitive)
while IFS= read -r vg_members; do
[ -z "$vg_members" ] && continue
first=$(echo "$vg_members" | awk '{print $1}')
family=$(echo "$first" | sed 's/-[^-]*$//')
canonical=$(grep "^${family}|" "$groups_tmp" | cut -d'|' -f2 || true)
[ -z "$canonical" ] && continue # no canonical group defined — skip
# Sort both sides for order-insensitive comparison
vg_sorted=$(echo "$vg_members" | tr ' ' '\n' | sort | tr '\n' ' ' | sed 's/ $//')
can_sorted=$(echo "$canonical" | tr ' ' '\n' | sort | tr '\n' ' ' | sed 's/ $//')
if [ "$vg_sorted" = "$can_sorted" ]; then
check "routing.yaml" "variant_group for '$family' matches canonical groups: definition" "ok"
else
check "routing.yaml" "variant_group for '$family' does not match canonical groups: definition" "fail"
fi
done < "$vg_check_tmp"
rm -f "$groups_tmp" "$vg_check_tmp"
# 3. Every main-branch skill has at least one incoming edge
while IFS= read -r skill_dir; do
local skill_name
skill_name=$(basename "$skill_dir")
[ -f "$skill_dir/SKILL.md" ] || continue
local incoming
incoming=$(awk '/^ to:/{print $2}' "$routing" | grep -c "^${skill_name}$" || true)
[ "$incoming" -eq 0 ] \
&& check "$skill_name" "no incoming routing edges — only reachable via cold-start triggers" "warn"
done < <(find "$SKILLS_DIR" -mindepth 1 -maxdepth 1 -type d | sort)
# 4. SKILL.md handoff: blocks in sync with routing.yaml
parse_routing_edges() {
awk '
/^ - from:/ { from = $3; to = ""; cond = ""; etype = "" }
/^ to:/ { to = $2 }
/^ condition:/ { sub(/^ condition: /, ""); gsub(/^"|"$/, ""); cond = $0 }
/^ type:/ {
etype = $2
if (from != "" && to != "") {
# Language-switch variant edges are omitted — handled by the
# plain-text "Other languages" section in each skill body.
is_lang_switch = (etype == "variant" && cond == "user switches to a different language")
if (!is_lang_switch) {
print from "|" to "|" etype
}
}
to = ""; cond = ""; etype = ""
}
' "$routing"
}
while IFS= read -r from_skill; do
from_skill=$(echo "$from_skill" | tr -d ' \r')
[ -z "$from_skill" ] && continue
local skill_file="$SKILLS_DIR/$from_skill/SKILL.md"
[ -f "$skill_file" ] || continue
# Check skill targets match
local expected current
expected=$(parse_routing_edges | awk -F'|' -v s="$from_skill" '$1==s{print $2}' | sort -u)
current=$(awk '
/^ handoff:/ { in_h=1; next }
in_h && /^ skill:/ { print $2 }
in_h && !/^ / { in_h=0 }
' "$skill_file" | sort -u)
[ "$expected" = "$current" ] \
&& check "$from_skill" "handoff: in sync with routing.yaml" "ok" \
|| check "$from_skill" "handoff: out of sync with routing.yaml (run sync-handoffs.sh --apply)" "fail"
# Check type: variant entries match routing.yaml
local expected_variants current_variants
expected_variants=$(parse_routing_edges | awk -F'|' -v s="$from_skill" '$1==s && $3=="variant"{print $2}' | sort -u)
current_variants=$(awk '
/^ handoff:/ { in_h=1; skill=""; is_variant=0; next }
in_h && /^ type: variant/ { is_variant=1; next }
in_h && /^ skill:/ { skill=$2 }
in_h && /^ - / {
if (skill != "" && is_variant) print skill
skill=""; is_variant=0
}
in_h && !/^ / {
if (skill != "" && is_variant) print skill
in_h=0
}
' "$skill_file" | sort -u)
[ "$expected_variants" = "$current_variants" ] \
&& check "$from_skill" "handoff: type:variant entries match routing.yaml" "ok" \
|| check "$from_skill" "handoff: type:variant mismatch with routing.yaml (run sync-handoffs.sh --apply)" "fail"
done < <(awk '/^ - from:/ { print $3 }' "$routing" | sort -u)
# 5. Cycle detection — self-loops and 2-cycles on non-variant edges.
# Variant edges are intentionally bidirectional (language-switch groups).
# Self-loops and 2-cycles on topic/language/platform edges indicate
# accidental infinite routing loops.
local nonvariant_edges cycle_results
nonvariant_edges=$(mktemp)
cycle_results=$(mktemp)
# Extract non-variant edges: parse each edge block, emit "FROM TO" only when type != variant
awk '
/^ - from:/ { from=$3; to=""; etype="" }
/^ to:/ { to=$2 }
/^ type:/ { etype=$2 }
# Flush on next edge block start
/^ - from:/ && NR > 1 {
if (prev_from != "" && prev_to != "" && prev_etype != "variant")
print prev_from " " prev_to
prev_from=from; prev_to=to; prev_etype=etype
next
}
END {
if (from != "" && to != "" && etype != "variant") print from " " to
}
' "$routing" | sort -u > "$nonvariant_edges"
# Self-loops: A -> A
awk '$1 == $2 { print "self-loop: " $1 }' "$nonvariant_edges" >> "$cycle_results"
# 2-cycles: A->B and B->A (emit canonical pair A<B once)
awk 'NR==FNR { seen[$1 SUBSEP $2]=1; next }
seen[$2 SUBSEP $1] && $1 < $2 { print "2-cycle: " $1 " <-> " $2 }
' "$nonvariant_edges" "$nonvariant_edges" >> "$cycle_results"
if [ -s "$cycle_results" ]; then
while IFS= read -r line; do
check "routing.yaml" "routing loop on non-variant edge ($line)" "warn"
done < "$cycle_results"
else
check "routing.yaml" "routing graph has no self-loops or 2-cycles on non-variant edges" "ok"
fi
rm -f "$nonvariant_edges" "$cycle_results"
}
# ── Group 0: discovery.yaml validation ───────────────────────────────────────
validate_discovery() {
local discovery="$REPO_ROOT/discovery.yaml"
echo "── discovery.yaml"
if [ ! -f "$discovery" ]; then
check "discovery.yaml" "file missing — create discovery.yaml at repo root" "fail"
return
fi
# Every skill directory must have an entry
while IFS= read -r skill_name; do
[ -f "$SKILLS_DIR/$skill_name/SKILL.md" ] || continue
if grep -q "^ - skill: $skill_name$" "$discovery"; then
check "discovery.yaml" "skill '$skill_name' has entry" "ok"
else
check "discovery.yaml" "skill '$skill_name' missing from discovery.yaml" "fail"
fi
done < <(ls "$SKILLS_DIR")
# No skill in discovery.yaml should be absent from skills/
while IFS= read -r entry_skill; do
entry_skill=$(echo "$entry_skill" | sed 's/.*skill: //' | tr -d ' \r')
[ -z "$entry_skill" ] && continue
skill_exists "$entry_skill" \
&& check "discovery.yaml" "entry '$entry_skill' has skill directory" "ok" \
|| check "discovery.yaml" "entry '$entry_skill' has no skill directory" "fail"
done < <(grep "^ - skill:" "$discovery" || true)
# Required fields per entry: use_when, priority, triggers (min 3)
# Parse into a temp file to avoid subshell variable scoping issues
local entry_tmp
entry_tmp=$(mktemp)
awk '
/^ - skill:/ {
if (skill != "") print skill "|" has_uw "|" has_pr "|" tc "|" bad_pr
skill=$3; has_uw=0; has_pr=0; tc=0; bad_pr=""; in_t=0
}
/^ use_when:/ { has_uw=1 }
/^ priority:/ { has_pr=1; pr=$2; if (pr!="primary" && pr!="secondary") bad_pr=pr }
/^ triggers:/ { in_t=1; next }
in_t && /^ - / { tc++ }
in_t && !/^ - / { in_t=0 }
END { if (skill != "") print skill "|" has_uw "|" has_pr "|" tc "|" bad_pr }
' "$discovery" > "$entry_tmp"
while IFS='|' read -r sk uw pr tc bad_pr; do
[ -z "$sk" ] && continue
[ "$uw" = "1" ] \
&& check "discovery.yaml[$sk]" "use_when present" "ok" \
|| check "discovery.yaml[$sk]" "use_when missing" "fail"
[ "$pr" = "1" ] \
&& check "discovery.yaml[$sk]" "priority present" "ok" \
|| check "discovery.yaml[$sk]" "priority missing" "fail"
[ -n "$bad_pr" ] \
&& check "discovery.yaml[$sk]" "priority '$bad_pr' invalid (must be primary or secondary)" "fail"
[ "$tc" -ge 3 ] \
&& check "discovery.yaml[$sk]" "$tc triggers" "ok" \
|| check "discovery.yaml[$sk]" "only $tc triggers (minimum 3)" "fail"
done < "$entry_tmp"
rm -f "$entry_tmp"
# Duplicate trigger check
local dupes
dupes=$(awk '/^ - "/{t=$0; gsub(/^[[:space:]]*- "/, "", t); gsub(/"[[:space:]]*$/, "", t); print tolower(t)}' "$discovery" | sort | uniq -d)
if [ -n "$dupes" ]; then
while IFS= read -r dup; do
check "discovery.yaml" "duplicate trigger: '$dup'" "fail"
done <<< "$dupes"
else
check "discovery.yaml" "no duplicate triggers" "ok"
fi
}
# ── Working tree ──────────────────────────────────────────────────────────────
# ── Section coverage check ────────────────────────────────────────────────────
# Warns when a language-family skill is missing a canonical section that the
# majority of its siblings have. Uses prefix matching so minor title variations
# (e.g. "SQL++ Transactions" vs "SQL++ Inside a Transaction") still match.
#
# Format: FAMILY_PREFIX REQUIRED_SECTION_PREFIX [ALT_PREFIX]
# A section passes if any heading starts with REQUIRED_SECTION_PREFIX or ALT_PREFIX.
validate_section_coverage() {
local skill_file="$1"
local skill
skill=$(basename "$(dirname "$skill_file")")
# Determine which family this skill belongs to
local family=""
case "$skill" in
transactions-*) family="transactions" ;;
server-connection-*) family="server-connection" ;;
sdk-patterns-*) family="sdk-patterns" ;;
esac
[ -z "$family" ] && return 0
# Read all headings from the skill file
local headings
headings=$(grep "^## " "$skill_file" 2>/dev/null || true)
# Helper: check if any heading starts with a given prefix (or alt prefix)
section_present() {
local prefix="$1" alt="${2:-}"
if echo "$headings" | grep -qF "## $prefix"; then
return 0
fi
if [ -n "$alt" ] && echo "$headings" | grep -qF "## $alt"; then
return 0
fi
return 1
}
case "$family" in
transactions)
section_present "Bank Transfer" "Basic Transaction" \
|| check "$skill" "section-coverage: missing opening transaction example (## Bank Transfer Example or ## Basic Transaction)" "warn"
section_present "SQL++" \
|| check "$skill" "section-coverage: missing SQL++ section (## SQL++ ...)" "warn"
section_present "Error Handling" \
|| check "$skill" "section-coverage: missing ## Error Handling section" "warn"
section_present "Design Rules" \
|| check "$skill" "section-coverage: missing ## Design Rules section" "warn"
;;
server-connection)
section_present "The One Rule" "Connect" \
|| check "$skill" "section-coverage: missing connection section (## The One Rule or ## Connect)" "warn"
# Accept any singleton-style heading: Singleton, Module-Level Singleton, ASP.NET Core, Standalone Singleton
if ! echo "$headings" | grep -qiE "## (Singleton|Module-Level Singleton|Standalone Singleton|ASP\.NET Core)"; then
check "$skill" "section-coverage: missing ## Singleton pattern section" "warn"
fi
section_present "Sub-Document" \
|| check "$skill" "section-coverage: missing ## Sub-Document section" "warn"
section_present "Common Errors" "Error Handling" \
|| check "$skill" "section-coverage: missing ## Common Errors or ## Error Handling section" "warn"
;;
sdk-patterns)
section_present "CAS" \
|| check "$skill" "section-coverage: missing ## CAS section" "warn"
section_present "Bulk Operations" "Bulk Ops" \
|| check "$skill" "section-coverage: missing ## Bulk Operations section" "warn"
section_present "Atomic Counters" \
|| check "$skill" "section-coverage: missing ## Atomic Counters section" "warn"
section_present "Error Handling" \
|| check "$skill" "section-coverage: missing ## Error Handling section" "warn"
;;
esac
}
echo ""
echo "Validating working tree..."
echo ""
echo "Group 0: discovery.yaml"
validate_discovery
echo ""
for skill_dir in "$SKILLS_DIR"/*/; do
validate_skill "$skill_dir" || true
validate_section_coverage "$skill_dir/SKILL.md" || true
done
echo ""
validate_routing
echo ""
validate_routing_cycles
# ── Final summary ─────────────────────────────────────────────────────────────
echo ""
echo "Results: $(green "$PASS passed") | $(yellow "$WARN warnings") | $(red "$FAIL failed")"
[ "$FAIL" -eq 0 ] && exit 0 || exit 1