Skip to content

Commit 4f3b478

Browse files
speedbunnyclaude
andauthored
add brand validation script to skills (anthropics#250)
* Create validate_brand.py Missing validate_brand.py * fix: improve validate_brand.py compatibility with repository standards - Update example brand guidelines to use correct Acme Corporation standards matching SKILL.md - Add comprehensive error handling to load_guidelines_from_json function - Add get_acme_corporation_guidelines() helper function for default guidelines - Fix test content to use proper brand name capitalization - Improve documentation with detailed docstrings These changes ensure consistency with apply_brand.py and the SKILL.md reference documentation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent 1dd5c48 commit 4f3b478

File tree

1 file changed

+316
-0
lines changed

1 file changed

+316
-0
lines changed
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Brand Validation Script
4+
Validates content against brand guidelines including colors, fonts, tone, and messaging.
5+
"""
6+
7+
import re
8+
import json
9+
from typing import Dict, List, Tuple, Optional
10+
from dataclasses import dataclass, asdict
11+
12+
13+
@dataclass
14+
class BrandGuidelines:
15+
"""Brand guidelines configuration"""
16+
brand_name: str
17+
primary_colors: List[str]
18+
secondary_colors: List[str]
19+
fonts: List[str]
20+
tone_keywords: List[str]
21+
prohibited_words: List[str]
22+
tagline: Optional[str] = None
23+
logo_usage_rules: Optional[Dict] = None
24+
25+
26+
@dataclass
27+
class ValidationResult:
28+
"""Result of brand validation"""
29+
passed: bool
30+
score: float
31+
violations: List[str]
32+
warnings: List[str]
33+
suggestions: List[str]
34+
35+
36+
class BrandValidator:
37+
"""Validates content against brand guidelines"""
38+
39+
def __init__(self, guidelines: BrandGuidelines):
40+
self.guidelines = guidelines
41+
42+
def validate_colors(self, content: str) -> Tuple[List[str], List[str]]:
43+
"""
44+
Validate color usage in content (hex codes, RGB, color names)
45+
Returns: (violations, warnings)
46+
"""
47+
violations = []
48+
warnings = []
49+
50+
# Find hex colors
51+
hex_pattern = r'#[0-9A-Fa-f]{6}|#[0-9A-Fa-f]{3}'
52+
found_colors = re.findall(hex_pattern, content)
53+
54+
# Find RGB colors
55+
rgb_pattern = r'rgb\s*\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*\)'
56+
found_colors.extend(re.findall(rgb_pattern, content, re.IGNORECASE))
57+
58+
approved_colors = self.guidelines.primary_colors + self.guidelines.secondary_colors
59+
60+
for color in found_colors:
61+
if color.upper() not in [c.upper() for c in approved_colors]:
62+
violations.append(f"Unapproved color used: {color}")
63+
64+
return violations, warnings
65+
66+
def validate_fonts(self, content: str) -> Tuple[List[str], List[str]]:
67+
"""
68+
Validate font usage in content
69+
Returns: (violations, warnings)
70+
"""
71+
violations = []
72+
warnings = []
73+
74+
# Common font specification patterns
75+
font_patterns = [
76+
r'font-family\s*:\s*["\']?([^;"\']+)["\']?',
77+
r'font:\s*[^;]*\s+([A-Za-z][A-Za-z\s]+)(?:,|;|\s+\d)',
78+
]
79+
80+
found_fonts = []
81+
for pattern in font_patterns:
82+
matches = re.findall(pattern, content, re.IGNORECASE)
83+
found_fonts.extend(matches)
84+
85+
approved_fonts_lower = [f.lower() for f in self.guidelines.fonts]
86+
87+
for font in found_fonts:
88+
font_clean = font.strip().lower()
89+
# Check if any approved font is in the found font string
90+
if not any(approved.lower() in font_clean for approved in self.guidelines.fonts):
91+
violations.append(f"Unapproved font used: {font}")
92+
93+
return violations, warnings
94+
95+
def validate_tone(self, content: str) -> Tuple[List[str], List[str]]:
96+
"""
97+
Validate tone and messaging
98+
Returns: (violations, warnings)
99+
"""
100+
violations = []
101+
warnings = []
102+
103+
# Check for prohibited words
104+
content_lower = content.lower()
105+
for word in self.guidelines.prohibited_words:
106+
if word.lower() in content_lower:
107+
violations.append(f"Prohibited word/phrase used: '{word}'")
108+
109+
# Check for tone keywords (should have at least some)
110+
tone_matches = sum(1 for keyword in self.guidelines.tone_keywords
111+
if keyword.lower() in content_lower)
112+
113+
if tone_matches == 0 and len(content) > 100:
114+
warnings.append(
115+
f"Content may not align with brand tone. "
116+
f"Consider using terms like: {', '.join(self.guidelines.tone_keywords[:5])}"
117+
)
118+
119+
return violations, warnings
120+
121+
def validate_brand_name(self, content: str) -> Tuple[List[str], List[str]]:
122+
"""
123+
Validate brand name usage and capitalization
124+
Returns: (violations, warnings)
125+
"""
126+
violations = []
127+
warnings = []
128+
129+
# Find all variations of the brand name
130+
brand_pattern = re.compile(re.escape(self.guidelines.brand_name), re.IGNORECASE)
131+
matches = brand_pattern.findall(content)
132+
133+
for match in matches:
134+
if match != self.guidelines.brand_name:
135+
violations.append(
136+
f"Incorrect brand name capitalization: '{match}' "
137+
f"should be '{self.guidelines.brand_name}'"
138+
)
139+
140+
return violations, warnings
141+
142+
def calculate_score(self, violations: List[str], warnings: List[str]) -> float:
143+
"""Calculate compliance score (0-100)"""
144+
violation_penalty = len(violations) * 10
145+
warning_penalty = len(warnings) * 3
146+
147+
score = max(0, 100 - violation_penalty - warning_penalty)
148+
return round(score, 2)
149+
150+
def generate_suggestions(self, violations: List[str], warnings: List[str]) -> List[str]:
151+
"""Generate helpful suggestions based on violations and warnings"""
152+
suggestions = []
153+
154+
if any("color" in v.lower() for v in violations):
155+
suggestions.append(
156+
f"Use approved colors: Primary: {', '.join(self.guidelines.primary_colors[:3])}"
157+
)
158+
159+
if any("font" in v.lower() for v in violations):
160+
suggestions.append(
161+
f"Use approved fonts: {', '.join(self.guidelines.fonts)}"
162+
)
163+
164+
if any("tone" in w.lower() for w in warnings):
165+
suggestions.append(
166+
f"Incorporate brand tone keywords: {', '.join(self.guidelines.tone_keywords[:5])}"
167+
)
168+
169+
if any("brand name" in v.lower() for v in violations):
170+
suggestions.append(
171+
f"Always capitalize brand name as: {self.guidelines.brand_name}"
172+
)
173+
174+
return suggestions
175+
176+
def validate(self, content: str) -> ValidationResult:
177+
"""
178+
Perform complete brand validation
179+
Returns: ValidationResult
180+
"""
181+
all_violations = []
182+
all_warnings = []
183+
184+
# Run all validation checks
185+
color_v, color_w = self.validate_colors(content)
186+
all_violations.extend(color_v)
187+
all_warnings.extend(color_w)
188+
189+
font_v, font_w = self.validate_fonts(content)
190+
all_violations.extend(font_v)
191+
all_warnings.extend(font_w)
192+
193+
tone_v, tone_w = self.validate_tone(content)
194+
all_violations.extend(tone_v)
195+
all_warnings.extend(tone_w)
196+
197+
brand_v, brand_w = self.validate_brand_name(content)
198+
all_violations.extend(brand_v)
199+
all_warnings.extend(brand_w)
200+
201+
# Calculate score and generate suggestions
202+
score = self.calculate_score(all_violations, all_warnings)
203+
suggestions = self.generate_suggestions(all_violations, all_warnings)
204+
205+
return ValidationResult(
206+
passed=len(all_violations) == 0,
207+
score=score,
208+
violations=all_violations,
209+
warnings=all_warnings,
210+
suggestions=suggestions
211+
)
212+
213+
214+
def load_guidelines_from_json(filepath: str) -> BrandGuidelines:
215+
"""
216+
Load brand guidelines from JSON file
217+
218+
Args:
219+
filepath: Path to JSON file containing brand guidelines
220+
221+
Returns:
222+
BrandGuidelines object
223+
224+
Raises:
225+
FileNotFoundError: If the file doesn't exist
226+
json.JSONDecodeError: If the file contains invalid JSON
227+
TypeError: If required fields are missing
228+
"""
229+
try:
230+
with open(filepath, 'r') as f:
231+
data = json.load(f)
232+
return BrandGuidelines(**data)
233+
except FileNotFoundError:
234+
raise FileNotFoundError(f"Brand guidelines file not found: {filepath}")
235+
except json.JSONDecodeError as e:
236+
raise json.JSONDecodeError(
237+
f"Invalid JSON in brand guidelines file: {e.msg}", e.doc, e.pos
238+
)
239+
except TypeError as e:
240+
raise TypeError(f"Missing required fields in brand guidelines: {e}")
241+
242+
243+
def get_acme_corporation_guidelines() -> BrandGuidelines:
244+
"""
245+
Get default Acme Corporation brand guidelines.
246+
247+
These guidelines match the standards defined in the SKILL.md reference.
248+
Users should customize these for their own organization.
249+
250+
Returns:
251+
BrandGuidelines object with Acme Corporation standards
252+
"""
253+
return BrandGuidelines(
254+
brand_name="Acme Corporation",
255+
primary_colors=["#0066CC", "#003366", "#FFFFFF"], # Acme Blue, Acme Navy, White
256+
secondary_colors=["#28A745", "#FFC107", "#DC3545", "#6C757D", "#F8F9FA"], # Success Green, Warning Amber, Error Red, Neutral Gray, Light Gray
257+
fonts=["Segoe UI", "system-ui", "-apple-system", "sans-serif"],
258+
tone_keywords=["innovation", "excellence", "professional", "solutions", "trusted", "reliable"],
259+
prohibited_words=["cheap", "outdated", "inferior", "unprofessional", "sloppy"],
260+
tagline="Innovation Through Excellence"
261+
)
262+
263+
264+
def main():
265+
"""Example usage demonstrating brand validation"""
266+
# Load Acme Corporation brand guidelines
267+
# Users should customize this for their own organization
268+
guidelines = get_acme_corporation_guidelines()
269+
270+
# Example content to validate (intentionally contains violations for demonstration)
271+
test_content = """
272+
Welcome to acme corporation!
273+
274+
We are a cheap solution provider with outdated technology.
275+
276+
Our innovation and excellence in professional solutions are trusted by many.
277+
278+
Contact us at: font-family: 'Comic Sans MS'
279+
Color scheme: #FF0000
280+
Background: rgb(255, 0, 0)
281+
"""
282+
283+
# Validate
284+
validator = BrandValidator(guidelines)
285+
result = validator.validate(test_content)
286+
287+
# Print results
288+
print("=" * 60)
289+
print(f"BRAND VALIDATION REPORT")
290+
print("=" * 60)
291+
print(f"\nOverall Status: {'✓ PASSED' if result.passed else '✗ FAILED'}")
292+
print(f"Compliance Score: {result.score}/100")
293+
294+
if result.violations:
295+
print(f"\n❌ VIOLATIONS ({len(result.violations)}):")
296+
for i, violation in enumerate(result.violations, 1):
297+
print(f" {i}. {violation}")
298+
299+
if result.warnings:
300+
print(f"\n⚠️ WARNINGS ({len(result.warnings)}):")
301+
for i, warning in enumerate(result.warnings, 1):
302+
print(f" {i}. {warning}")
303+
304+
if result.suggestions:
305+
print(f"\n💡 SUGGESTIONS:")
306+
for i, suggestion in enumerate(result.suggestions, 1):
307+
print(f" {i}. {suggestion}")
308+
309+
print("\n" + "=" * 60)
310+
311+
# Return JSON for programmatic use
312+
return asdict(result)
313+
314+
315+
if __name__ == "__main__":
316+
main()

0 commit comments

Comments
 (0)