agy-superpowers 5.1.2 → 5.1.4
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/LICENSE +1 -1
- package/README.md +198 -175
- package/package.json +1 -1
- package/template/agent/.shared/mobile-uiux-promax/data/accessibility.csv +25 -0
- package/template/agent/.shared/mobile-uiux-promax/data/animation.csv +22 -0
- package/template/agent/.shared/mobile-uiux-promax/data/components.csv +21 -0
- package/template/agent/.shared/mobile-uiux-promax/data/gestures.csv +26 -0
- package/template/agent/.shared/mobile-uiux-promax/data/layout.csv +21 -0
- package/template/agent/.shared/mobile-uiux-promax/data/navigation.csv +27 -0
- package/template/agent/.shared/mobile-uiux-promax/data/onboarding.csv +17 -0
- package/template/agent/.shared/mobile-uiux-promax/data/platform.csv +22 -0
- package/template/agent/.shared/mobile-uiux-promax/data/stacks/flutter.csv +19 -0
- package/template/agent/.shared/mobile-uiux-promax/data/stacks/jetpack-compose.csv +18 -0
- package/template/agent/.shared/mobile-uiux-promax/data/stacks/react-native.csv +20 -0
- package/template/agent/.shared/mobile-uiux-promax/data/stacks/swiftui.csv +18 -0
- package/template/agent/.shared/mobile-uiux-promax/data/ux-laws.csv +16 -0
- package/template/agent/.shared/mobile-uiux-promax/scripts/mobile-search.py +157 -0
- package/template/agent/.shared/ui-ux-pro-max/data/charts.csv +26 -0
- package/template/agent/.shared/ui-ux-pro-max/data/colors.csv +97 -0
- package/template/agent/.shared/ui-ux-pro-max/data/landing.csv +31 -0
- package/template/agent/.shared/ui-ux-pro-max/data/products.csv +97 -0
- package/template/agent/.shared/ui-ux-pro-max/data/prompts.csv +24 -0
- package/template/agent/.shared/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/template/agent/.shared/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/template/agent/.shared/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/template/agent/.shared/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/template/agent/.shared/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/template/agent/.shared/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/template/agent/.shared/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/template/agent/.shared/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/template/agent/.shared/ui-ux-pro-max/data/styles.csv +59 -0
- package/template/agent/.shared/ui-ux-pro-max/data/typography.csv +58 -0
- package/template/agent/.shared/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/template/agent/.shared/ui-ux-pro-max/scripts/__pycache__/core.cpython-313.pyc +0 -0
- package/template/agent/.shared/ui-ux-pro-max/scripts/core.py +236 -0
- package/template/agent/.shared/ui-ux-pro-max/scripts/search.py +61 -0
- package/template/agent/.tests/TESTS.md +119 -0
- package/template/agent/.tests/mobile-uiux-promax/test_search.py +266 -0
- package/template/agent/.tests/run_tests.py +86 -0
- package/template/agent/patches/skills-patches.md +24 -0
- package/template/agent/rules/git-policy.md +25 -0
- package/template/agent/skills/brainstorming/SKILL.md +57 -0
- package/template/agent/skills/finishing-a-development-branch/SKILL.md +18 -6
- package/template/agent/skills/frontend-design/SKILL.md +147 -0
- package/template/agent/skills/frontend-design/reference/color-and-contrast.md +117 -0
- package/template/agent/skills/frontend-design/reference/interaction-design.md +159 -0
- package/template/agent/skills/frontend-design/reference/motion-design.md +150 -0
- package/template/agent/skills/frontend-design/reference/responsive-design.md +161 -0
- package/template/agent/skills/frontend-design/reference/spatial-design.md +122 -0
- package/template/agent/skills/frontend-design/reference/typography.md +124 -0
- package/template/agent/skills/frontend-design/reference/ux-writing.md +127 -0
- package/template/agent/skills/mobile-uiux-promax/SKILL.md +139 -0
- package/template/agent/skills/using-git-worktrees/SKILL.md +3 -1
- package/template/agent/skills/verification-before-completion/SKILL.md +11 -0
- package/template/agent/workflows/mobile-uiux-promax.md +137 -0
- package/template/agent/workflows/ui-ux-pro-max.md +231 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
UI/UX Pro Max Core - BM25 search engine for UI/UX style guides
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import csv
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from math import log
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
|
|
13
|
+
# ============ CONFIGURATION ============
|
|
14
|
+
DATA_DIR = Path(__file__).parent.parent / "data"
|
|
15
|
+
MAX_RESULTS = 3
|
|
16
|
+
|
|
17
|
+
CSV_CONFIG = {
|
|
18
|
+
"style": {
|
|
19
|
+
"file": "styles.csv",
|
|
20
|
+
"search_cols": ["Style Category", "Keywords", "Best For", "Type"],
|
|
21
|
+
"output_cols": ["Style Category", "Type", "Keywords", "Primary Colors", "Effects & Animation", "Best For", "Performance", "Accessibility", "Framework Compatibility", "Complexity"]
|
|
22
|
+
},
|
|
23
|
+
"prompt": {
|
|
24
|
+
"file": "prompts.csv",
|
|
25
|
+
"search_cols": ["Style Category", "AI Prompt Keywords (Copy-Paste Ready)", "CSS/Technical Keywords"],
|
|
26
|
+
"output_cols": ["Style Category", "AI Prompt Keywords (Copy-Paste Ready)", "CSS/Technical Keywords", "Implementation Checklist"]
|
|
27
|
+
},
|
|
28
|
+
"color": {
|
|
29
|
+
"file": "colors.csv",
|
|
30
|
+
"search_cols": ["Product Type", "Keywords", "Notes"],
|
|
31
|
+
"output_cols": ["Product Type", "Keywords", "Primary (Hex)", "Secondary (Hex)", "CTA (Hex)", "Background (Hex)", "Text (Hex)", "Border (Hex)", "Notes"]
|
|
32
|
+
},
|
|
33
|
+
"chart": {
|
|
34
|
+
"file": "charts.csv",
|
|
35
|
+
"search_cols": ["Data Type", "Keywords", "Best Chart Type", "Accessibility Notes"],
|
|
36
|
+
"output_cols": ["Data Type", "Keywords", "Best Chart Type", "Secondary Options", "Color Guidance", "Accessibility Notes", "Library Recommendation", "Interactive Level"]
|
|
37
|
+
},
|
|
38
|
+
"landing": {
|
|
39
|
+
"file": "landing.csv",
|
|
40
|
+
"search_cols": ["Pattern Name", "Keywords", "Conversion Optimization", "Section Order"],
|
|
41
|
+
"output_cols": ["Pattern Name", "Keywords", "Section Order", "Primary CTA Placement", "Color Strategy", "Conversion Optimization"]
|
|
42
|
+
},
|
|
43
|
+
"product": {
|
|
44
|
+
"file": "products.csv",
|
|
45
|
+
"search_cols": ["Product Type", "Keywords", "Primary Style Recommendation", "Key Considerations"],
|
|
46
|
+
"output_cols": ["Product Type", "Keywords", "Primary Style Recommendation", "Secondary Styles", "Landing Page Pattern", "Dashboard Style (if applicable)", "Color Palette Focus"]
|
|
47
|
+
},
|
|
48
|
+
"ux": {
|
|
49
|
+
"file": "ux-guidelines.csv",
|
|
50
|
+
"search_cols": ["Category", "Issue", "Description", "Platform"],
|
|
51
|
+
"output_cols": ["Category", "Issue", "Platform", "Description", "Do", "Don't", "Code Example Good", "Code Example Bad", "Severity"]
|
|
52
|
+
},
|
|
53
|
+
"typography": {
|
|
54
|
+
"file": "typography.csv",
|
|
55
|
+
"search_cols": ["Font Pairing Name", "Category", "Mood/Style Keywords", "Best For", "Heading Font", "Body Font"],
|
|
56
|
+
"output_cols": ["Font Pairing Name", "Category", "Heading Font", "Body Font", "Mood/Style Keywords", "Best For", "Google Fonts URL", "CSS Import", "Tailwind Config", "Notes"]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
STACK_CONFIG = {
|
|
61
|
+
"html-tailwind": {"file": "stacks/html-tailwind.csv"},
|
|
62
|
+
"react": {"file": "stacks/react.csv"},
|
|
63
|
+
"nextjs": {"file": "stacks/nextjs.csv"},
|
|
64
|
+
"vue": {"file": "stacks/vue.csv"},
|
|
65
|
+
"svelte": {"file": "stacks/svelte.csv"},
|
|
66
|
+
"swiftui": {"file": "stacks/swiftui.csv"},
|
|
67
|
+
"react-native": {"file": "stacks/react-native.csv"},
|
|
68
|
+
"flutter": {"file": "stacks/flutter.csv"}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Common columns for all stacks
|
|
72
|
+
_STACK_COLS = {
|
|
73
|
+
"search_cols": ["Category", "Guideline", "Description", "Do", "Don't"],
|
|
74
|
+
"output_cols": ["Category", "Guideline", "Description", "Do", "Don't", "Code Good", "Code Bad", "Severity", "Docs URL"]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
AVAILABLE_STACKS = list(STACK_CONFIG.keys())
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ============ BM25 IMPLEMENTATION ============
|
|
81
|
+
class BM25:
|
|
82
|
+
"""BM25 ranking algorithm for text search"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, k1=1.5, b=0.75):
|
|
85
|
+
self.k1 = k1
|
|
86
|
+
self.b = b
|
|
87
|
+
self.corpus = []
|
|
88
|
+
self.doc_lengths = []
|
|
89
|
+
self.avgdl = 0
|
|
90
|
+
self.idf = {}
|
|
91
|
+
self.doc_freqs = defaultdict(int)
|
|
92
|
+
self.N = 0
|
|
93
|
+
|
|
94
|
+
def tokenize(self, text):
|
|
95
|
+
"""Lowercase, split, remove punctuation, filter short words"""
|
|
96
|
+
text = re.sub(r'[^\w\s]', ' ', str(text).lower())
|
|
97
|
+
return [w for w in text.split() if len(w) > 2]
|
|
98
|
+
|
|
99
|
+
def fit(self, documents):
|
|
100
|
+
"""Build BM25 index from documents"""
|
|
101
|
+
self.corpus = [self.tokenize(doc) for doc in documents]
|
|
102
|
+
self.N = len(self.corpus)
|
|
103
|
+
if self.N == 0:
|
|
104
|
+
return
|
|
105
|
+
self.doc_lengths = [len(doc) for doc in self.corpus]
|
|
106
|
+
self.avgdl = sum(self.doc_lengths) / self.N
|
|
107
|
+
|
|
108
|
+
for doc in self.corpus:
|
|
109
|
+
seen = set()
|
|
110
|
+
for word in doc:
|
|
111
|
+
if word not in seen:
|
|
112
|
+
self.doc_freqs[word] += 1
|
|
113
|
+
seen.add(word)
|
|
114
|
+
|
|
115
|
+
for word, freq in self.doc_freqs.items():
|
|
116
|
+
self.idf[word] = log((self.N - freq + 0.5) / (freq + 0.5) + 1)
|
|
117
|
+
|
|
118
|
+
def score(self, query):
|
|
119
|
+
"""Score all documents against query"""
|
|
120
|
+
query_tokens = self.tokenize(query)
|
|
121
|
+
scores = []
|
|
122
|
+
|
|
123
|
+
for idx, doc in enumerate(self.corpus):
|
|
124
|
+
score = 0
|
|
125
|
+
doc_len = self.doc_lengths[idx]
|
|
126
|
+
term_freqs = defaultdict(int)
|
|
127
|
+
for word in doc:
|
|
128
|
+
term_freqs[word] += 1
|
|
129
|
+
|
|
130
|
+
for token in query_tokens:
|
|
131
|
+
if token in self.idf:
|
|
132
|
+
tf = term_freqs[token]
|
|
133
|
+
idf = self.idf[token]
|
|
134
|
+
numerator = tf * (self.k1 + 1)
|
|
135
|
+
denominator = tf + self.k1 * (1 - self.b + self.b * doc_len / self.avgdl)
|
|
136
|
+
score += idf * numerator / denominator
|
|
137
|
+
|
|
138
|
+
scores.append((idx, score))
|
|
139
|
+
|
|
140
|
+
return sorted(scores, key=lambda x: x[1], reverse=True)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ============ SEARCH FUNCTIONS ============
|
|
144
|
+
def _load_csv(filepath):
|
|
145
|
+
"""Load CSV and return list of dicts"""
|
|
146
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
147
|
+
return list(csv.DictReader(f))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _search_csv(filepath, search_cols, output_cols, query, max_results):
|
|
151
|
+
"""Core search function using BM25"""
|
|
152
|
+
if not filepath.exists():
|
|
153
|
+
return []
|
|
154
|
+
|
|
155
|
+
data = _load_csv(filepath)
|
|
156
|
+
|
|
157
|
+
# Build documents from search columns
|
|
158
|
+
documents = [" ".join(str(row.get(col, "")) for col in search_cols) for row in data]
|
|
159
|
+
|
|
160
|
+
# BM25 search
|
|
161
|
+
bm25 = BM25()
|
|
162
|
+
bm25.fit(documents)
|
|
163
|
+
ranked = bm25.score(query)
|
|
164
|
+
|
|
165
|
+
# Get top results with score > 0
|
|
166
|
+
results = []
|
|
167
|
+
for idx, score in ranked[:max_results]:
|
|
168
|
+
if score > 0:
|
|
169
|
+
row = data[idx]
|
|
170
|
+
results.append({col: row.get(col, "") for col in output_cols if col in row})
|
|
171
|
+
|
|
172
|
+
return results
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def detect_domain(query):
|
|
176
|
+
"""Auto-detect the most relevant domain from query"""
|
|
177
|
+
query_lower = query.lower()
|
|
178
|
+
|
|
179
|
+
domain_keywords = {
|
|
180
|
+
"color": ["color", "palette", "hex", "#", "rgb"],
|
|
181
|
+
"chart": ["chart", "graph", "visualization", "trend", "bar", "pie", "scatter", "heatmap", "funnel"],
|
|
182
|
+
"landing": ["landing", "page", "cta", "conversion", "hero", "testimonial", "pricing", "section"],
|
|
183
|
+
"product": ["saas", "ecommerce", "e-commerce", "fintech", "healthcare", "gaming", "portfolio", "crypto", "dashboard"],
|
|
184
|
+
"prompt": ["prompt", "css", "implementation", "variable", "checklist", "tailwind"],
|
|
185
|
+
"style": ["style", "design", "ui", "minimalism", "glassmorphism", "neumorphism", "brutalism", "dark mode", "flat", "aurora"],
|
|
186
|
+
"ux": ["ux", "usability", "accessibility", "wcag", "touch", "scroll", "animation", "keyboard", "navigation", "mobile"],
|
|
187
|
+
"typography": ["font", "typography", "heading", "serif", "sans"]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
scores = {domain: sum(1 for kw in keywords if kw in query_lower) for domain, keywords in domain_keywords.items()}
|
|
191
|
+
best = max(scores, key=scores.get)
|
|
192
|
+
return best if scores[best] > 0 else "style"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def search(query, domain=None, max_results=MAX_RESULTS):
|
|
196
|
+
"""Main search function with auto-domain detection"""
|
|
197
|
+
if domain is None:
|
|
198
|
+
domain = detect_domain(query)
|
|
199
|
+
|
|
200
|
+
config = CSV_CONFIG.get(domain, CSV_CONFIG["style"])
|
|
201
|
+
filepath = DATA_DIR / config["file"]
|
|
202
|
+
|
|
203
|
+
if not filepath.exists():
|
|
204
|
+
return {"error": f"File not found: {filepath}", "domain": domain}
|
|
205
|
+
|
|
206
|
+
results = _search_csv(filepath, config["search_cols"], config["output_cols"], query, max_results)
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
"domain": domain,
|
|
210
|
+
"query": query,
|
|
211
|
+
"file": config["file"],
|
|
212
|
+
"count": len(results),
|
|
213
|
+
"results": results
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def search_stack(query, stack, max_results=MAX_RESULTS):
|
|
218
|
+
"""Search stack-specific guidelines"""
|
|
219
|
+
if stack not in STACK_CONFIG:
|
|
220
|
+
return {"error": f"Unknown stack: {stack}. Available: {', '.join(AVAILABLE_STACKS)}"}
|
|
221
|
+
|
|
222
|
+
filepath = DATA_DIR / STACK_CONFIG[stack]["file"]
|
|
223
|
+
|
|
224
|
+
if not filepath.exists():
|
|
225
|
+
return {"error": f"Stack file not found: {filepath}", "stack": stack}
|
|
226
|
+
|
|
227
|
+
results = _search_csv(filepath, _STACK_COLS["search_cols"], _STACK_COLS["output_cols"], query, max_results)
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
"domain": "stack",
|
|
231
|
+
"stack": stack,
|
|
232
|
+
"query": query,
|
|
233
|
+
"file": STACK_CONFIG[stack]["file"],
|
|
234
|
+
"count": len(results),
|
|
235
|
+
"results": results
|
|
236
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
UI/UX Pro Max Search - BM25 search engine for UI/UX style guides
|
|
5
|
+
Usage: python search.py "<query>" [--domain <domain>] [--stack <stack>] [--max-results 3]
|
|
6
|
+
|
|
7
|
+
Domains: style, prompt, color, chart, landing, product, ux, typography
|
|
8
|
+
Stacks: html-tailwind, react, nextjs
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
from core import CSV_CONFIG, AVAILABLE_STACKS, MAX_RESULTS, search, search_stack
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def format_output(result):
|
|
16
|
+
"""Format results for Claude consumption (token-optimized)"""
|
|
17
|
+
if "error" in result:
|
|
18
|
+
return f"Error: {result['error']}"
|
|
19
|
+
|
|
20
|
+
output = []
|
|
21
|
+
if result.get("stack"):
|
|
22
|
+
output.append(f"## UI Pro Max Stack Guidelines")
|
|
23
|
+
output.append(f"**Stack:** {result['stack']} | **Query:** {result['query']}")
|
|
24
|
+
else:
|
|
25
|
+
output.append(f"## UI Pro Max Search Results")
|
|
26
|
+
output.append(f"**Domain:** {result['domain']} | **Query:** {result['query']}")
|
|
27
|
+
output.append(f"**Source:** {result['file']} | **Found:** {result['count']} results\n")
|
|
28
|
+
|
|
29
|
+
for i, row in enumerate(result['results'], 1):
|
|
30
|
+
output.append(f"### Result {i}")
|
|
31
|
+
for key, value in row.items():
|
|
32
|
+
value_str = str(value)
|
|
33
|
+
if len(value_str) > 300:
|
|
34
|
+
value_str = value_str[:300] + "..."
|
|
35
|
+
output.append(f"- **{key}:** {value_str}")
|
|
36
|
+
output.append("")
|
|
37
|
+
|
|
38
|
+
return "\n".join(output)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
parser = argparse.ArgumentParser(description="UI Pro Max Search")
|
|
43
|
+
parser.add_argument("query", help="Search query")
|
|
44
|
+
parser.add_argument("--domain", "-d", choices=list(CSV_CONFIG.keys()), help="Search domain")
|
|
45
|
+
parser.add_argument("--stack", "-s", choices=AVAILABLE_STACKS, help="Stack-specific search (html-tailwind, react, nextjs)")
|
|
46
|
+
parser.add_argument("--max-results", "-n", type=int, default=MAX_RESULTS, help="Max results (default: 3)")
|
|
47
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
48
|
+
|
|
49
|
+
args = parser.parse_args()
|
|
50
|
+
|
|
51
|
+
# Stack search takes priority
|
|
52
|
+
if args.stack:
|
|
53
|
+
result = search_stack(args.query, args.stack, args.max_results)
|
|
54
|
+
else:
|
|
55
|
+
result = search(args.query, args.domain, args.max_results)
|
|
56
|
+
|
|
57
|
+
if args.json:
|
|
58
|
+
import json
|
|
59
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
60
|
+
else:
|
|
61
|
+
print(format_output(result))
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# `.agent/.tests/` — Test Convention
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
Lightweight local-CI for AI-modified tools. When the AI modifies any `.agent/skills/<skill>/`
|
|
5
|
+
or `.agent/.shared/<tool>/`, it runs the relevant test suite before claiming work is done.
|
|
6
|
+
|
|
7
|
+
## Directory Layout
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
.agent/.tests/
|
|
11
|
+
├── run_tests.py ← entry point
|
|
12
|
+
├── TESTS.md ← this file
|
|
13
|
+
└── <skill-name>/
|
|
14
|
+
├── test_search.py ← one file per concern
|
|
15
|
+
└── test_data_integrity.py
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Trigger Rules (when AI auto-runs)
|
|
19
|
+
|
|
20
|
+
| Change made to | Tests to run |
|
|
21
|
+
|----------------|-------------|
|
|
22
|
+
| `.agent/.shared/<tool>/` | `.agent/.tests/<tool>/` |
|
|
23
|
+
| `.agent/skills/<skill>/SKILL.md` | `.agent/.tests/<skill>/` (if exists) |
|
|
24
|
+
| Any CSV in a data folder | The skill that owns that folder |
|
|
25
|
+
| `.agent/.tests/run_tests.py` | `--all` |
|
|
26
|
+
|
|
27
|
+
## Running
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
python3 .agent/.tests/run_tests.py <skill-name>
|
|
31
|
+
python3 .agent/.tests/run_tests.py --all
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Writing a New Test File
|
|
35
|
+
|
|
36
|
+
### Required contract
|
|
37
|
+
- Filename: `test_*.py`
|
|
38
|
+
- Standalone Python 3 script (stdlib only — no pytest/unittest required)
|
|
39
|
+
- Exit 0 = all pass, exit 1 = any fail
|
|
40
|
+
- Print `✅ label` or `❌ label` for each test case
|
|
41
|
+
|
|
42
|
+
### Minimal template
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
#!/usr/bin/env python3
|
|
46
|
+
"""Tests for <skill-name>"""
|
|
47
|
+
import subprocess, sys
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
|
|
50
|
+
# Project root: 3 levels up (.agent/.tests/<skill>/test_*.py)
|
|
51
|
+
BASE = str(Path(__file__).resolve().parents[3])
|
|
52
|
+
passed, failed = [], []
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def check(label, output, expected_kw, returncode=0):
|
|
56
|
+
ok_code = output.returncode == returncode
|
|
57
|
+
ok_kw = expected_kw.lower() in (output.stdout + output.stderr).lower() if expected_kw else True
|
|
58
|
+
if ok_code and ok_kw:
|
|
59
|
+
passed.append(label)
|
|
60
|
+
print(f" ✅ {label}")
|
|
61
|
+
else:
|
|
62
|
+
failed.append(label)
|
|
63
|
+
print(f" ❌ {label}")
|
|
64
|
+
if not ok_kw:
|
|
65
|
+
print(f" → expected keyword: '{expected_kw}'")
|
|
66
|
+
print(f" → stdout[:300]: {(output.stdout + output.stderr)[:300]!r}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ─── GROUPS ───────────────────────────────────────────────
|
|
70
|
+
print("\n📍 GROUP 1: <Group Name>")
|
|
71
|
+
# r = subprocess.run(["python3", "path/to/script.py", "query", "--flag", "value"],
|
|
72
|
+
# capture_output=True, text=True, cwd=BASE)
|
|
73
|
+
# check("test label", r, "expected keyword in output")
|
|
74
|
+
|
|
75
|
+
# ─── FINAL REPORT ─────────────────────────────────────────
|
|
76
|
+
total = len(passed) + len(failed)
|
|
77
|
+
pct = int(100 * len(passed) / total) if total else 0
|
|
78
|
+
print(f"\n{'═' * 55}")
|
|
79
|
+
print(f" TOTAL: {total} PASSED: {len(passed)} ({pct}%) FAILED: {len(failed)}")
|
|
80
|
+
if failed:
|
|
81
|
+
print(f"\n ❌ FAILED TESTS:")
|
|
82
|
+
for f in failed:
|
|
83
|
+
print(f" - {f}")
|
|
84
|
+
print(f"{'═' * 55}")
|
|
85
|
+
sys.exit(0 if not failed else 1)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Data integrity pattern
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
import csv
|
|
92
|
+
from pathlib import Path
|
|
93
|
+
|
|
94
|
+
DATA_DIR = Path(__file__).resolve().parents[3] / ".agent/.shared/<tool>/data"
|
|
95
|
+
|
|
96
|
+
print("\n📍 DATA INTEGRITY")
|
|
97
|
+
for fname, min_rows in {
|
|
98
|
+
"navigation.csv": 20,
|
|
99
|
+
"gestures.csv": 20,
|
|
100
|
+
}.items():
|
|
101
|
+
fpath = DATA_DIR / fname
|
|
102
|
+
if not fpath.exists():
|
|
103
|
+
failed.append(f"file exists: {fname}")
|
|
104
|
+
print(f" ❌ MISSING: {fname}")
|
|
105
|
+
continue
|
|
106
|
+
with open(fpath) as f:
|
|
107
|
+
rows = list(csv.reader(f))
|
|
108
|
+
data_rows = len(rows) - 1
|
|
109
|
+
ok = data_rows >= min_rows
|
|
110
|
+
(passed if ok else failed).append(f"row count: {fname}")
|
|
111
|
+
print(f" {'✅' if ok else '❌'} {fname}: {data_rows} rows (min {min_rows})")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Skill Mapping Reference
|
|
115
|
+
|
|
116
|
+
| Test folder | Triggered by |
|
|
117
|
+
|-------------|-------------|
|
|
118
|
+
| `mobile-uiux-promax/` | `.agent/.shared/mobile-uiux-promax/` · `.agent/skills/mobile-uiux-promax/` |
|
|
119
|
+
| *(add new rows here as new skills gain test suites)* | |
|