📦 deps(thirdparty): update snapshots

This commit is contained in:
ci[bot]
2026-07-18 00:02:59 +00:00
parent 82f7c6e56a
commit 47ce7f78dc
1446 changed files with 141041 additions and 6442 deletions
+231 -41
View File
@@ -77,6 +77,14 @@ CSV_CONFIG = {
}
}
# Output columns whose content (code samples, checklists) must never be
# hard-truncated for display -- truncating mid-snippet destroys the value.
UNTRUNCATED_COLS = {
"Code Example Good", "Code Example Bad", "Code Good", "Code Bad",
"Implementation Checklist", "Design System Variables", "CSS Import",
"Tailwind Config", "GSAP Snippet",
}
STACK_CONFIG = {
"react": {"file": "stacks/react.csv"},
"nextjs": {"file": "stacks/nextjs.csv"},
@@ -111,6 +119,44 @@ _STACK_COLS = {
AVAILABLE_STACKS = list(STACK_CONFIG.keys())
# ============ TOKENIZATION ============
# Common two-letter/three-letter words that add noise without adding search
# signal. Deliberately short -- domain-relevant short tokens (ui, ux, ai,
# css, 3d, js, os, md, gsap) must stay searchable, which is why we don't
# filter purely by length.
_STOPWORDS = {
"to", "in", "on", "at", "is", "of", "by", "or", "an", "if", "no", "so",
"do", "be", "we", "it", "as", "the", "and", "for", "are", "was",
}
# Query/corpus normalization so common spelling variants match each other.
# Keep this a plain dict (stdlib only, no fuzzy-matching dependency).
_SYNONYMS = {
"e-commerce": "ecommerce",
"dark-mode": "dark",
"darkmode": "dark",
"light-mode": "light",
"lightmode": "light",
"a11y": "accessibility",
"nav": "navigation",
"sign-up": "signup",
"log-in": "login",
"colour": "color",
"colours": "colors",
"customisation": "customization",
"organisation": "organization",
"behaviour": "behavior",
"ux/ui": "ux ui",
}
def _normalize(text):
"""Apply synonym substitution before tokenizing."""
for variant, canonical in _SYNONYMS.items():
text = text.replace(variant, canonical)
return text
# ============ BM25 IMPLEMENTATION ============
class BM25:
"""BM25 ranking algorithm for text search"""
@@ -124,11 +170,13 @@ class BM25:
self.idf = {}
self.doc_freqs = defaultdict(int)
self.N = 0
self._term_freqs = [] # precomputed per-doc term frequencies
def tokenize(self, text):
"""Lowercase, split, remove punctuation, filter short words"""
text = re.sub(r'[^\w\s]', ' ', str(text).lower())
return [w for w in text.split() if len(w) >= 2]
"""Lowercase, normalize synonyms, split, remove punctuation, filter stopwords"""
text = _normalize(str(text).lower())
text = re.sub(r'[^\w\s]', ' ', text)
return [w for w in text.split() if len(w) >= 2 and w not in _STOPWORDS]
def fit(self, documents):
"""Build BM25 index from documents"""
@@ -139,12 +187,14 @@ class BM25:
self.doc_lengths = [len(doc) for doc in self.corpus]
self.avgdl = sum(self.doc_lengths) / self.N
self._term_freqs = []
for doc in self.corpus:
seen = set()
tf = defaultdict(int)
for word in doc:
if word not in seen:
self.doc_freqs[word] += 1
seen.add(word)
tf[word] += 1
self._term_freqs.append(tf)
for word in tf:
self.doc_freqs[word] += 1
for word, freq in self.doc_freqs.items():
self.idf[word] = log((self.N - freq + 0.5) / (freq + 0.5) + 1)
@@ -154,16 +204,14 @@ class BM25:
query_tokens = self.tokenize(query)
scores = []
for idx, doc in enumerate(self.corpus):
for idx in range(self.N):
score = 0
doc_len = self.doc_lengths[idx]
term_freqs = defaultdict(int)
for word in doc:
term_freqs[word] += 1
term_freqs = self._term_freqs[idx]
for token in query_tokens:
if token in self.idf:
tf = term_freqs[token]
tf = term_freqs.get(token, 0)
idf = self.idf[token]
numerator = tf * (self.k1 + 1)
denominator = tf + self.k1 * (1 - self.b + self.b * doc_len / self.avgdl)
@@ -173,48 +221,139 @@ class BM25:
return sorted(scores, key=lambda x: x[1], reverse=True)
def vocabulary(self):
"""All indexed terms, for suggestion/typo-recovery purposes."""
return list(self.idf.keys())
# ============ CSV / INDEX CACHE ============
# Data files are small and reused across multiple domain searches within a
# single --design-system run; avoid re-reading + re-indexing the same file
# repeatedly in one process.
_csv_cache = {} # filepath -> (mtime, rows)
_bm25_cache = {} # (filepath, tuple(search_cols)) -> (mtime, BM25 instance)
# ============ SEARCH FUNCTIONS ============
def _load_csv(filepath):
"""Load CSV and return list of dicts"""
"""Load CSV and return list of dicts, with mtime-based caching."""
mtime = filepath.stat().st_mtime
cached = _csv_cache.get(filepath)
if cached and cached[0] == mtime:
return cached[1]
with open(filepath, 'r', encoding='utf-8') as f:
return list(csv.DictReader(f))
rows = list(csv.DictReader(f))
_csv_cache[filepath] = (mtime, rows)
return rows
def _search_csv(filepath, search_cols, output_cols, query, max_results):
"""Core search function using BM25"""
if not filepath.exists():
return []
def _get_bm25(filepath, search_cols, data):
"""Fitted BM25 index for this file+columns, with mtime-based caching."""
key = (filepath, tuple(search_cols))
mtime = filepath.stat().st_mtime
cached = _bm25_cache.get(key)
if cached and cached[0] == mtime:
return cached[1]
data = _load_csv(filepath)
# Build documents from search columns
documents = [" ".join(str(row.get(col, "")) for col in search_cols) for row in data]
# BM25 search
bm25 = BM25()
bm25.fit(documents)
_bm25_cache[key] = (mtime, bm25)
return bm25
# ============ SEARCH FUNCTIONS ============
def _search_csv(filepath, search_cols, output_cols, query, max_results):
"""Core search function using BM25. Returns (results, bm25_or_none)."""
if not filepath.exists():
return [], None
try:
data = _load_csv(filepath)
except (csv.Error, OSError, UnicodeDecodeError) as e:
return [{"_error": f"Failed to read {filepath.name}: {e}"}], None
if not data:
return [], None
bm25 = _get_bm25(filepath, search_cols, data)
ranked = bm25.score(query)
# Get top results with score > 0
results = []
for idx, score in ranked[:max_results]:
if score > 0:
row = data[idx]
results.append({col: row.get(col, "") for col in output_cols if col in row})
return results
return results, bm25
def detect_domain(query):
"""Auto-detect the most relevant domain from query"""
query_lower = query.lower()
def _suggest_terms(bm25, query, limit=6):
"""Nearest known vocabulary terms for a query that returned 0 hits,
so the caller can retry instead of silently reporting nothing."""
if bm25 is None:
return []
query_tokens = set(bm25.tokenize(query))
if not query_tokens:
return []
domain_keywords = {
candidates = []
for term in bm25.vocabulary():
for qt in query_tokens:
if term.startswith(qt[:3]) or qt.startswith(term[:3]):
candidates.append(term)
break
# Stable de-dup, most frequent terms first (doc_freqs available via idf keys only,
# so just de-dup preserving discovery order).
seen = set()
ordered = []
for term in candidates:
if term not in seen:
seen.add(term)
ordered.append(term)
return ordered[:limit]
# Load the product-domain keyword list from products.csv at import time so
# it stays in sync with the data instead of needing manual updates to a
# hardcoded list. Falls back to a small built-in seed if the file is
# missing (e.g. package built without data/).
def _load_product_keywords():
seed = ["saas", "ecommerce", "e-commerce", "fintech", "healthcare", "gaming",
"portfolio", "crypto", "dashboard", "fitness", "marketplace"]
filepath = DATA_DIR / CSV_CONFIG["product"]["file"]
if not filepath.exists():
return seed
try:
rows = _load_csv(filepath)
except (csv.Error, OSError, UnicodeDecodeError):
return seed
keywords = set(seed)
for row in rows:
raw = row.get("Keywords", "")
for kw in re.split(r"[,;]", raw):
kw = kw.strip().lower()
if kw and len(kw) >= 3:
keywords.add(kw)
return sorted(keywords, key=len, reverse=True)
_DOMAIN_KEYWORDS = None
def _domain_keywords():
global _DOMAIN_KEYWORDS
if _DOMAIN_KEYWORDS is not None:
return _DOMAIN_KEYWORDS
_DOMAIN_KEYWORDS = {
"color": ["color", "palette", "hex", "#", "rgb", "token", "semantic", "accent", "destructive", "muted", "foreground"],
"chart": ["chart", "graph", "visualization", "trend", "bar", "pie", "scatter", "heatmap", "funnel"],
"landing": ["landing", "page", "cta", "conversion", "hero", "testimonial", "pricing", "section"],
"product": ["saas", "ecommerce", "e-commerce", "fintech", "healthcare", "gaming", "portfolio", "crypto", "dashboard", "fitness", "restaurant", "hotel", "travel", "music", "education", "learning", "legal", "insurance", "medical", "beauty", "pharmacy", "dental", "pet", "dating", "wedding", "recipe", "delivery", "ride", "booking", "calendar", "timer", "tracker", "diary", "note", "chat", "messenger", "crm", "invoice", "parking", "transit", "vpn", "alarm", "weather", "sleep", "meditation", "fasting", "habit", "grocery", "meme", "wardrobe", "plant care", "reading", "flashcard", "puzzle", "trivia", "arcade", "photography", "streaming", "podcast", "newsletter", "marketplace", "freelancer", "coworking", "airline", "museum", "theater", "church", "non-profit", "charity", "kindergarten", "daycare", "senior care", "veterinary", "florist", "bakery", "brewery", "construction", "automotive", "real estate", "logistics", "agriculture", "coding bootcamp"],
"product": _load_product_keywords(),
"style": ["style", "design", "ui", "minimalism", "glassmorphism", "neumorphism", "brutalism", "dark mode", "flat", "aurora", "prompt", "css", "implementation", "variable", "checklist", "tailwind"],
"ux": ["ux", "usability", "accessibility", "wcag", "touch", "scroll", "animation", "keyboard", "navigation", "mobile"],
"typography": ["font pairing", "typography pairing", "heading font", "body font"],
@@ -224,16 +363,57 @@ def detect_domain(query):
"react": ["react", "next.js", "nextjs", "suspense", "memo", "usecallback", "useeffect", "rerender", "bundle", "waterfall", "barrel", "dynamic import", "rsc", "server component"],
"web": ["aria", "focus", "outline", "semantic", "virtualize", "autocomplete", "form", "input type", "preconnect"]
}
return _DOMAIN_KEYWORDS
scores = {domain: sum(1 for kw in keywords if re.search(r'\b' + re.escape(kw) + r'\b', query_lower)) for domain, keywords in domain_keywords.items()}
best = max(scores, key=scores.get)
return best if scores[best] > 0 else "style"
# Domains checked in this fixed order when scores tie, so results are
# deterministic instead of depending on dict/hash ordering.
_DOMAIN_TIEBREAK_ORDER = [
"ux", "product", "style", "color", "typography", "google-fonts",
"chart", "landing", "icons", "gsap", "react", "web",
]
def detect_domain(query, return_scores=False):
"""Auto-detect the most relevant domain from query.
Matches are weighted by keyword length (multi-word/longer phrases are
more specific and score higher than short generic words). Ties are
broken by a fixed domain priority order, not dict/insertion order.
"""
query_lower = query.lower()
domain_keywords = _domain_keywords()
scores = {}
for domain, keywords in domain_keywords.items():
total = 0.0
for kw in keywords:
if re.search(r'\b' + re.escape(kw) + r'\b', query_lower):
# weight = 1 point per word in the keyword phrase
total += max(1, len(kw.split()))
scores[domain] = total
ranked = sorted(
scores.items(),
key=lambda item: (item[1], -_DOMAIN_TIEBREAK_ORDER.index(item[0])
if item[0] in _DOMAIN_TIEBREAK_ORDER else -999),
reverse=True,
)
best_domain, best_score = ranked[0]
result = best_domain if best_score > 0 else "style"
if return_scores:
runner_up = ranked[1][0] if len(ranked) > 1 and ranked[1][1] > 0 else None
return result, runner_up
return result
def search(query, domain=None, max_results=MAX_RESULTS):
"""Main search function with auto-domain detection"""
auto_detected = domain is None
runner_up = None
if domain is None:
domain = detect_domain(query)
domain, runner_up = detect_domain(query, return_scores=True)
config = CSV_CONFIG.get(domain, CSV_CONFIG["style"])
filepath = DATA_DIR / config["file"]
@@ -241,15 +421,22 @@ def search(query, domain=None, max_results=MAX_RESULTS):
if not filepath.exists():
return {"error": f"File not found: {filepath}", "domain": domain}
results = _search_csv(filepath, config["search_cols"], config["output_cols"], query, max_results)
results, bm25 = _search_csv(filepath, config["search_cols"], config["output_cols"], query, max_results)
return {
out = {
"domain": domain,
"query": query,
"file": config["file"],
"count": len(results),
"results": results
"results": results,
}
if auto_detected:
out["auto_detected"] = True
if runner_up:
out["runner_up_domain"] = runner_up
if not results:
out["suggestions"] = _suggest_terms(bm25, query)
return out
def search_stack(query, stack, max_results=MAX_RESULTS):
@@ -262,13 +449,16 @@ def search_stack(query, stack, max_results=MAX_RESULTS):
if not filepath.exists():
return {"error": f"Stack file not found: {filepath}", "stack": stack}
results = _search_csv(filepath, _STACK_COLS["search_cols"], _STACK_COLS["output_cols"], query, max_results)
results, bm25 = _search_csv(filepath, _STACK_COLS["search_cols"], _STACK_COLS["output_cols"], query, max_results)
return {
out = {
"domain": "stack",
"stack": stack,
"query": query,
"file": STACK_CONFIG[stack]["file"],
"count": len(results),
"results": results
"results": results,
}
if not results:
out["suggestions"] = _suggest_terms(bm25, query)
return out
@@ -7,10 +7,12 @@ to generate comprehensive design system recommendations.
Usage:
from design_system import generate_design_system
result = generate_design_system("SaaS dashboard", "My Project")
print(result["text"])
# With persistence (Master + Overrides pattern)
result = generate_design_system("SaaS dashboard", "My Project", persist=True)
result = generate_design_system("SaaS dashboard", "My Project", persist=True, page="dashboard")
result = generate_design_system("SaaS dashboard", "My Project", persist=True, output_dir="/path/to/project")
result["persistence"] # {"status": "success"|"skipped_exists", "created_files": [...], ...}
result = generate_design_system("SaaS dashboard", "My Project", persist=True, page="dashboard", output_dir="/path/to/project")
"""
import csv
@@ -665,7 +667,8 @@ def format_markdown(design_system: dict) -> str:
# ============ MAIN ENTRY POINT ============
def generate_design_system(query: str, project_name: str = None, output_format: str = "ascii",
persist: bool = False, page: str = None, output_dir: str = None,
variance: int = None, motion: int = None, density: int = None) -> str:
variance: int = None, motion: int = None, density: int = None,
force: bool = False) -> dict:
"""
Main entry point for design system generation.
@@ -679,20 +682,28 @@ def generate_design_system(query: str, project_name: str = None, output_format:
variance: Optional 1-10 DESIGN_VARIANCE dial (1=centered/minimal, 10=bold/asymmetric)
motion: Optional 1-10 MOTION_INTENSITY dial, pulls a matching GSAP snippet from motion.csv
density: Optional 1-10 VISUAL_DENSITY dial, overrides the spacing scale (1=spacious, 10=dense)
force: If True, overwrite an existing MASTER.md; otherwise persistence
is skipped (with a status message) when one already exists
Returns:
Formatted design system string
dict with keys: "text" (formatted design system string), "design_system"
(raw dict, useful for --json callers), and "persistence" (result of
persist_design_system(), or None if persist=False)
"""
generator = DesignSystemGenerator()
design_system = generator.generate(query, project_name, variance=variance, motion=motion, density=density)
# Persist to files if requested
persistence_result = None
if persist:
persist_design_system(design_system, page, output_dir, query)
persistence_result = persist_design_system(design_system, page, output_dir, query, force=force)
if output_format == "markdown":
return format_markdown(design_system)
return format_ascii_box(design_system)
text = format_markdown(design_system) if output_format == "markdown" else format_ascii_box(design_system)
return {
"text": text,
"design_system": design_system,
"persistence": persistence_result,
}
# ============ PERSISTENCE FUNCTIONS ============
@@ -707,42 +718,61 @@ def safe_slug(name, fallback: str = "default") -> str:
return slug or fallback
def persist_design_system(design_system: dict, page: str = None, output_dir: str = None, page_query: str = None) -> dict:
def persist_design_system(design_system: dict, page: str = None, output_dir: str = None,
page_query: str = None, force: bool = False) -> dict:
"""
Persist design system to design-system/<project>/ folder using Master + Overrides pattern.
Args:
design_system: The generated design system dictionary
page: Optional page name for page-specific override file
output_dir: Optional output directory (defaults to current working directory)
page_query: Optional query string for intelligent page override generation
force: If True, overwrite an existing MASTER.md. If False (default) and
MASTER.md already exists, persistence is skipped so prior design
decisions aren't silently discarded.
Returns:
dict with created file paths and status
dict with created file paths and status. status is "skipped_exists" if
MASTER.md already existed and force was not set.
"""
base_dir = Path(output_dir) if output_dir else Path.cwd()
# Use project name for project-specific folder. Coalesce falsy values
# (missing key, explicit None, or "") so slugification can't crash.
project_slug = safe_slug(design_system.get("project_name") or "default")
# (missing key, explicit None, or "") so the .lower() below can't crash.
project_name = design_system.get("project_name") or "default"
project_slug = safe_slug(project_name)
design_system_dir = base_dir / "design-system" / project_slug
pages_dir = design_system_dir / "pages"
master_file = design_system_dir / "MASTER.md"
if master_file.exists() and not force:
return {
"status": "skipped_exists",
"design_system_dir": str(design_system_dir),
"master_file": str(master_file),
"created_files": [],
"message": (
f"{master_file} already exists and was not modified. "
"Read it first to check for prior design decisions, then "
"re-run with force=True / --force to overwrite."
),
}
created_files = []
# Create directories
design_system_dir.mkdir(parents=True, exist_ok=True)
pages_dir.mkdir(parents=True, exist_ok=True)
master_file = design_system_dir / "MASTER.md"
# Generate and write MASTER.md
master_content = format_master_md(design_system)
with open(master_file, 'w', encoding='utf-8') as f:
f.write(master_content)
created_files.append(str(master_file))
# If page is specified, create page override file with intelligent content
if page:
page_file = pages_dir / f"{safe_slug(page, 'page')}.md"
@@ -750,10 +780,11 @@ def persist_design_system(design_system: dict, page: str = None, output_dir: str
with open(page_file, 'w', encoding='utf-8') as f:
f.write(page_content)
created_files.append(str(page_file))
return {
"status": "success",
"design_system_dir": str(design_system_dir),
"master_file": str(master_file),
"created_files": created_files
}
@@ -1337,4 +1368,4 @@ if __name__ == "__main__":
args = parser.parse_args()
result = generate_design_system(args.query, args.project_name, args.format)
print(result)
print(result["text"])
+74 -39
View File
@@ -4,11 +4,12 @@
UI/UX Pro Max Search - BM25 search engine for UI/UX style guides
Usage: python search.py "<query>" [--domain <domain>] [--stack <stack>] [--max-results 3]
python search.py "<query>" --design-system [-p "Project Name"]
python search.py "<query>" --design-system --persist [-p "Project Name"] [--page "dashboard"]
python search.py "<query>" --design-system --persist [-p "Project Name"] --output-dir "<project-root>" [--page "dashboard"]
python search.py "<query>" --design-system --variance 8 --motion 9 --density 7
Domains: style, prompt, color, chart, landing, product, ux, typography, google-fonts, gsap
Stacks: react, nextjs, vue, svelte, astro, swiftui, react-native, flutter, nuxtjs, nuxt-ui, html-tailwind, shadcn, jetpack-compose, threejs, angular, laravel, javafx, wpf, winui, avalonia, uno, uwp
Domains: style, color, chart, landing, product, ux, typography, google-fonts, icons, gsap, react, web
Stacks: react, nextjs, vue, svelte, astro, swiftui, react-native, flutter, nuxtjs, nuxt-ui,
html-tailwind, shadcn, jetpack-compose, threejs, angular, laravel
Design dials (1-10, only with --design-system):
--variance DESIGN_VARIANCE: 1=centered/minimal, 10=bold/asymmetric
@@ -16,15 +17,20 @@ Design dials (1-10, only with --design-system):
--density VISUAL_DENSITY: 1=spacious, 10=dense/dashboard; overrides the spacing scale
Persistence (Master + Overrides pattern):
--persist Save design system to design-system/MASTER.md
--page Also create a page-specific override file in design-system/pages/
--persist Save design system to design-system/<project-slug>/MASTER.md
--output-dir Directory the design-system/ folder is created under (defaults to cwd --
always pass this explicitly, pointed at the project root)
--page Also create a page-specific override file in design-system/<project-slug>/pages/
--force Overwrite an existing MASTER.md (without this, persistence is skipped
if MASTER.md already exists, so prior design decisions aren't lost)
"""
import argparse
import json as json_module
import sys
import io
from core import CSV_CONFIG, AVAILABLE_STACKS, MAX_RESULTS, search, search_stack
from design_system import generate_design_system, persist_design_system, safe_slug
from core import CSV_CONFIG, AVAILABLE_STACKS, MAX_RESULTS, UNTRUNCATED_COLS, search, search_stack
from design_system import generate_design_system
# Force UTF-8 for stdout/stderr to handle emojis on Windows (cp1252 default)
if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8':
@@ -32,27 +38,47 @@ if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8':
if sys.stderr.encoding and sys.stderr.encoding.lower() != 'utf-8':
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
TRUNCATE_AT = 300
def format_output(result):
def format_output(result, full=False):
"""Format results for Claude consumption (token-optimized)"""
if "error" in result:
return f"Error: {result['error']}"
output = []
if result.get("stack"):
output.append(f"## UI Pro Max Stack Guidelines")
output.append("## UI Pro Max Stack Guidelines")
output.append(f"**Stack:** {result['stack']} | **Query:** {result['query']}")
else:
output.append(f"## UI Pro Max Search Results")
output.append(f"**Domain:** {result['domain']} | **Query:** {result['query']}")
output.append("## UI Pro Max Search Results")
domain_note = result['domain']
if result.get("auto_detected"):
domain_note += " (auto-detected"
if result.get("runner_up_domain"):
domain_note += f", runner-up: {result['runner_up_domain']}"
domain_note += ")"
output.append(f"**Domain:** {domain_note} | **Query:** {result['query']}")
output.append(f"**Source:** {result['file']} | **Found:** {result['count']} results\n")
if result['count'] == 0:
output.append(
"No matches. This is not a match with an empty value -- the query "
"did not hit the database. Retry with broader/different keywords "
"before falling back to general defaults, and say explicitly that "
"no database match was found if you do fall back."
)
suggestions = result.get("suggestions") or []
if suggestions:
output.append(f"**Closest known terms:** {', '.join(suggestions)}")
return "\n".join(output)
for i, row in enumerate(result['results'], 1):
output.append(f"### Result {i}")
for key, value in row.items():
value_str = str(value)
if len(value_str) > 300:
value_str = value_str[:300] + "..."
if not full and key not in UNTRUNCATED_COLS and len(value_str) > TRUNCATE_AT:
value_str = value_str[:TRUNCATE_AT] + "..."
output.append(f"- **{key}:** {value_str}")
output.append("")
@@ -66,14 +92,16 @@ if __name__ == "__main__":
parser.add_argument("--stack", "-s", choices=AVAILABLE_STACKS, help=f"Stack-specific search. Available: {', '.join(AVAILABLE_STACKS)}")
parser.add_argument("--max-results", "-n", type=int, default=MAX_RESULTS, help="Max results (default: 3)")
parser.add_argument("--json", action="store_true", help="Output as JSON")
parser.add_argument("--full", action="store_true", help="Do not truncate long field values in text output")
# Design system generation
parser.add_argument("--design-system", "-ds", action="store_true", help="Generate complete design system recommendation")
parser.add_argument("--project-name", "-p", type=str, default=None, help="Project name for design system output")
parser.add_argument("--format", "-f", choices=["ascii", "markdown"], default="ascii", help="Output format for design system")
parser.add_argument("--format", "-f", choices=["ascii", "markdown"], default="ascii", help="Output format for design system (ignored if --json)")
# Persistence (Master + Overrides pattern)
parser.add_argument("--persist", action="store_true", help="Save design system to design-system/MASTER.md (creates hierarchical structure)")
parser.add_argument("--page", type=str, default=None, help="Create page-specific override file in design-system/pages/")
parser.add_argument("--output-dir", "-o", type=str, default=None, help="Output directory for persisted files (default: current directory)")
parser.add_argument("--persist", action="store_true", help="Save design system to design-system/<project-slug>/MASTER.md (creates hierarchical structure)")
parser.add_argument("--page", type=str, default=None, help="Create page-specific override file in design-system/<project-slug>/pages/")
parser.add_argument("--output-dir", "-o", type=str, default=None, help="Output directory for persisted files (default: current directory -- pass this explicitly, pointed at the project root)")
parser.add_argument("--force", action="store_true", help="Overwrite an existing MASTER.md when persisting (default: skip if it already exists)")
# Design dials (1-10), only applied with --design-system
parser.add_argument("--variance", type=int, choices=range(1, 11), metavar="1-10", help="DESIGN_VARIANCE dial: 1=centered/minimal, 10=bold/asymmetric (only with --design-system)")
parser.add_argument("--motion", type=int, choices=range(1, 11), metavar="1-10", help="MOTION_INTENSITY dial: 1=subtle, 10=complex; pulls a matching GSAP snippet from motion.csv (only with --design-system)")
@@ -92,36 +120,43 @@ if __name__ == "__main__":
output_dir=args.output_dir,
variance=args.variance,
motion=args.motion,
density=args.density
density=args.density,
force=args.force,
)
print(result)
# Print persistence confirmation
if args.persist:
project_slug = safe_slug(args.project_name or args.query.upper())
print("\n" + "=" * 60)
print(f"✅ Design system persisted to design-system/{project_slug}/")
print(f" 📄 design-system/{project_slug}/MASTER.md (Global Source of Truth)")
if args.page:
page_filename = safe_slug(args.page, 'page')
print(f" 📄 design-system/{project_slug}/pages/{page_filename}.md (Page Overrides)")
print("")
print(f"📖 Usage: When building a page, check design-system/{project_slug}/pages/[page].md first.")
print(f" If exists, its rules override MASTER.md. Otherwise, use MASTER.md.")
print("=" * 60)
if args.json:
print(json_module.dumps(
{"design_system": result["design_system"], "persistence": result["persistence"]},
indent=2, ensure_ascii=False,
))
else:
print(result["text"])
if args.persist:
persistence = result["persistence"] or {}
print("\n" + "=" * 60)
if persistence.get("status") == "skipped_exists":
print(f"⚠️ {persistence.get('message', 'MASTER.md already exists; not overwritten.')}")
else:
ds_dir = persistence.get("design_system_dir", "design-system/<project>")
print(f"✅ Design system persisted to {ds_dir}/")
for f in persistence.get("created_files", []):
print(f" 📄 {f}")
print("")
print(f"📖 Usage: When building a page, check {ds_dir}/pages/[page].md first.")
print(" If it exists, its rules override MASTER.md. Otherwise, use MASTER.md.")
print("=" * 60)
# Stack search
elif args.stack:
result = search_stack(args.query, args.stack, args.max_results)
if args.json:
import json
print(json.dumps(result, indent=2, ensure_ascii=False))
print(json_module.dumps(result, indent=2, ensure_ascii=False))
else:
print(format_output(result))
print(format_output(result, full=args.full))
# Domain search
else:
result = search(args.query, args.domain, args.max_results)
if args.json:
import json
print(json.dumps(result, indent=2, ensure_ascii=False))
print(json_module.dumps(result, indent=2, ensure_ascii=False))
else:
print(format_output(result))
print(format_output(result, full=args.full))
@@ -0,0 +1,134 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Stdlib-only regression tests for core.py / design_system.py (unittest, not
pytest -- this project ships with zero external dependencies and the tests
shouldn't add one).
Run with:
python -m unittest discover -s scripts/tests -v
or directly:
python scripts/tests/test_core.py
"""
import sys
import tempfile
import unittest
from pathlib import Path
SCRIPTS_DIR = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(SCRIPTS_DIR))
from core import BM25, detect_domain, search, search_stack, CSV_CONFIG, AVAILABLE_STACKS
from design_system import generate_design_system, persist_design_system, DesignSystemGenerator
class TestTokenizer(unittest.TestCase):
def test_short_domain_terms_are_kept(self):
bm25 = BM25()
tokens = bm25.tokenize("UI and UX design with 3D and AI")
self.assertIn("ui", tokens)
self.assertIn("3d", tokens)
self.assertIn("ai", tokens)
def test_stopwords_removed(self):
bm25 = BM25()
tokens = bm25.tokenize("this is for the team to do")
for stopword in ("is", "for", "the", "to", "do"):
self.assertNotIn(stopword, tokens)
def test_synonym_normalization(self):
bm25 = BM25()
self.assertEqual(bm25.tokenize("e-commerce store"), bm25.tokenize("ecommerce store"))
self.assertEqual(bm25.tokenize("dark-mode toggle"), bm25.tokenize("dark toggle"))
class TestSearchDomains(unittest.TestCase):
"""Known query -> expected top-domain sanity checks (not exact-row pinning,
since data can grow; these assert the engine still finds *something*
relevant for each domain's core vocabulary)."""
def test_ui_is_searchable_in_style_domain(self):
result = search("ui minimalism", domain="style", max_results=1)
self.assertGreater(result["count"], 0, "literal 'ui' token must be searchable, not filtered by tokenizer")
def test_accessibility_query_hits_ux(self):
result = search("accessibility contrast wcag keyboard", domain="ux", max_results=3)
self.assertGreater(result["count"], 0)
def test_zero_result_query_reports_suggestions_not_error(self):
result = search("zzqqxx totally made up gibberish", domain="ux", max_results=2)
self.assertEqual(result["count"], 0)
self.assertIn("suggestions", result)
self.assertNotIn("error", result)
def test_every_configured_domain_file_exists_and_is_searchable(self):
for domain, config in CSV_CONFIG.items():
with self.subTest(domain=domain):
result = search("design", domain=domain, max_results=1)
self.assertNotIn("error", result, f"domain '{domain}' failed: {result.get('error')}")
def test_every_stack_file_exists_and_is_searchable(self):
for stack in AVAILABLE_STACKS:
with self.subTest(stack=stack):
result = search_stack("performance", stack, max_results=1)
self.assertNotIn("error", result, f"stack '{stack}' failed: {result.get('error')}")
class TestDomainDetection(unittest.TestCase):
def test_style_keywords_route_to_style(self):
self.assertEqual(detect_domain("glassmorphism dark ui"), "style")
def test_accessibility_keywords_route_to_ux(self):
self.assertEqual(detect_domain("accessibility contrast wcag"), "ux")
def test_ambiguous_query_returns_runner_up(self):
domain, runner_up = detect_domain("font pairing elegant crypto", return_scores=True)
self.assertIsNotNone(domain)
# runner_up may be None if the winning domain has no close second --
# this just verifies the call shape works without raising.
def test_empty_query_falls_back_to_style(self):
self.assertEqual(detect_domain("...!!!???"), "style")
class TestPersistence(unittest.TestCase):
def test_persist_then_skip_then_force(self):
with tempfile.TemporaryDirectory() as tmp:
result = generate_design_system("saas dashboard", "Test Project", persist=True, output_dir=tmp)
self.assertEqual(result["persistence"]["status"], "success")
master = Path(result["persistence"]["master_file"])
self.assertTrue(master.exists())
original_content = master.read_text(encoding="utf-8")
# Second persist without force must not overwrite.
result2 = generate_design_system("saas dashboard", "Test Project", persist=True, output_dir=tmp)
self.assertEqual(result2["persistence"]["status"], "skipped_exists")
self.assertEqual(master.read_text(encoding="utf-8"), original_content)
# With force=True it must overwrite.
result3 = generate_design_system("ecommerce luxury", "Test Project", persist=True, output_dir=tmp, force=True)
self.assertEqual(result3["persistence"]["status"], "success")
def test_persist_writes_only_under_output_dir(self):
with tempfile.TemporaryDirectory() as tmp:
generate_design_system("saas dashboard", "Scoped Project", persist=True, output_dir=tmp)
expected = Path(tmp) / "design-system" / "scoped-project" / "MASTER.md"
self.assertTrue(expected.exists())
class TestReasoningMatch(unittest.TestCase):
def test_known_category_matches_exactly(self):
gen = DesignSystemGenerator()
rule = gen._find_reasoning_rule("SaaS (General)")
self.assertTrue(rule, "exact-match category lookup should not fall through to fuzzy matching")
def test_unknown_category_falls_back_gracefully(self):
gen = DesignSystemGenerator()
rule = gen._find_reasoning_rule("Totally Unknown Category XYZ")
# Should not raise; may return {} which _apply_reasoning handles with defaults.
self.assertIsInstance(rule, dict)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,114 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Data integrity guardrail for ui-ux-pro-max. Stdlib-only, no pytest dependency,
so it can run as a standalone pre-publish/CI check:
python validate_data.py
Checks, per configured domain/stack CSV:
- file exists
- header row contains every column referenced in search_cols/output_cols
- no duplicate primary-key values (first column) within a file
- any "Decision_Rules"-style JSON column parses as JSON
Exits 0 with no output on success; exits 1 and prints every problem found
on failure (fail-fast is the wrong call here -- a data change can break
several files at once, so we want the full list in one run).
"""
import csv
import json
import sys
from pathlib import Path
from core import CSV_CONFIG, STACK_CONFIG, _STACK_COLS, DATA_DIR
# REASONING_FILE lives in design_system.py, not core.py -- redeclared here to
# avoid a circular import (design_system.py imports core.py).
REASONING_FILE = "ui-reasoning.csv"
JSON_COLUMNS = {"Decision_Rules"}
def _read_rows(filepath):
with open(filepath, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
return reader.fieldnames or [], list(reader)
def _check_file(label, filepath, search_cols, output_cols, problems):
if not filepath.exists():
problems.append(f"[{label}] missing file: {filepath}")
return
try:
headers, rows = _read_rows(filepath)
except (csv.Error, UnicodeDecodeError, OSError) as e:
problems.append(f"[{label}] failed to parse {filepath.name}: {e}")
return
header_set = set(headers)
for col in set(search_cols) | set(output_cols):
if col not in header_set:
problems.append(f"[{label}] {filepath.name}: expected column '{col}' not found in header")
# Only check for duplicates against an actual identifier column ("No" is
# the sequential-index convention used across this dataset). The first
# CSV column is not reliably a unique key -- e.g. stack files use
# "Category", which legitimately repeats across many guideline rows.
if "No" in header_set:
seen = {}
for i, row in enumerate(rows, start=2): # +1 header, +1 to be 1-indexed
key = row.get("No", "")
if key in seen:
problems.append(
f"[{label}] {filepath.name}: duplicate 'No' value '{key}' on rows {seen[key]} and {i}"
)
else:
seen[key] = i
elif label.startswith("stack:"):
problems.append(
f"[{label}] {filepath.name}: missing 'No' index column present in other stack files "
"(schema drift -- harmless for search, but inconsistent with the rest of data/stacks/)"
)
for row_idx, row in enumerate(rows, start=2):
for col in JSON_COLUMNS:
if col in row and row[col]:
try:
json.loads(row[col])
except json.JSONDecodeError as e:
problems.append(
f"[{label}] {filepath.name} row {row_idx}: column '{col}' is not valid JSON: {e}"
)
def main():
problems = []
for domain, config in CSV_CONFIG.items():
_check_file(f"domain:{domain}", DATA_DIR / config["file"],
config["search_cols"], config["output_cols"], problems)
for stack, config in STACK_CONFIG.items():
_check_file(f"stack:{stack}", DATA_DIR / config["file"],
_STACK_COLS["search_cols"], _STACK_COLS["output_cols"], problems)
reasoning_path = DATA_DIR / REASONING_FILE
if reasoning_path.exists():
_check_file("reasoning", reasoning_path, ["UI_Category"], ["UI_Category", "Decision_Rules"], problems)
else:
problems.append(f"[reasoning] missing file: {reasoning_path}")
if problems:
print(f"FAILED: {len(problems)} data integrity issue(s) found:\n")
for p in problems:
print(f" - {p}")
sys.exit(1)
print(f"OK: validated {len(CSV_CONFIG)} domain files, {len(STACK_CONFIG)} stack files, and ui-reasoning.csv")
sys.exit(0)
if __name__ == "__main__":
main()