Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:47:30 +08:00
commit 48d6099939
30 changed files with 5747 additions and 0 deletions

View File

@@ -0,0 +1,244 @@
#!/usr/bin/env python3
"""
Requirement analyzer for Bubble Tea TUIs.
Extracts structured requirements from natural language.
"""
import re
from typing import Dict, List
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from utils.validators import RequirementValidator
# TUI archetype keywords
ARCHETYPE_KEYWORDS = {
'file-manager': ['file', 'directory', 'browse', 'navigator', 'ranger', 'three-column'],
'installer': ['install', 'package', 'progress', 'setup', 'installation'],
'dashboard': ['dashboard', 'monitor', 'real-time', 'metrics', 'status'],
'form': ['form', 'input', 'wizard', 'configuration', 'settings'],
'viewer': ['view', 'display', 'log', 'text', 'document', 'reader'],
'chat': ['chat', 'message', 'conversation', 'messaging'],
'table-viewer': ['table', 'data', 'spreadsheet', 'grid'],
'menu': ['menu', 'select', 'choose', 'options'],
'editor': ['edit', 'editor', 'compose', 'write']
}
def extract_requirements(description: str) -> Dict:
"""
Extract structured requirements from description.
Args:
description: Natural language TUI description
Returns:
Dictionary with structured requirements
Example:
>>> reqs = extract_requirements("Build a log viewer with search")
>>> reqs['archetype']
'viewer'
"""
# Validate input
validator = RequirementValidator()
validation = validator.validate_description(description)
desc_lower = description.lower()
# Extract archetype
archetype = classify_tui_type(description)
# Extract features
features = identify_features(description)
# Extract interactions
interactions = identify_interactions(description)
# Extract data types
data_types = identify_data_types(description)
# Determine view type
views = determine_view_type(description)
# Special requirements
special = identify_special_requirements(description)
requirements = {
'archetype': archetype,
'features': features,
'interactions': interactions,
'data_types': data_types,
'views': views,
'special_requirements': special,
'original_description': description,
'validation': validation.to_dict()
}
return requirements
def classify_tui_type(description: str) -> str:
"""Classify TUI archetype from description."""
desc_lower = description.lower()
# Score each archetype
scores = {}
for archetype, keywords in ARCHETYPE_KEYWORDS.items():
score = sum(1 for kw in keywords if kw in desc_lower)
if score > 0:
scores[archetype] = score
if not scores:
return 'general'
# Return highest scoring archetype
return max(scores.items(), key=lambda x: x[1])[0]
def identify_features(description: str) -> List[str]:
"""Identify features from description."""
features = []
desc_lower = description.lower()
feature_keywords = {
'navigation': ['navigate', 'move', 'browse', 'arrow'],
'selection': ['select', 'choose', 'pick'],
'search': ['search', 'find', 'filter', 'query'],
'editing': ['edit', 'modify', 'change', 'update'],
'display': ['display', 'show', 'view', 'render'],
'input': ['input', 'enter', 'type'],
'progress': ['progress', 'loading', 'install'],
'preview': ['preview', 'peek', 'preview pane'],
'scrolling': ['scroll', 'scrollable'],
'sorting': ['sort', 'order', 'rank'],
'filtering': ['filter', 'narrow'],
'highlighting': ['highlight', 'emphasize', 'mark']
}
for feature, keywords in feature_keywords.items():
if any(kw in desc_lower for kw in keywords):
features.append(feature)
return features if features else ['display']
def identify_interactions(description: str) -> Dict[str, List[str]]:
"""Identify user interaction types."""
desc_lower = description.lower()
keyboard = []
mouse = []
# Keyboard interactions
kbd_keywords = {
'navigation': ['arrow', 'hjkl', 'navigate', 'move'],
'selection': ['enter', 'select', 'choose'],
'search': ['/', 'search', 'find'],
'quit': ['q', 'quit', 'exit', 'esc'],
'help': ['?', 'help']
}
for interaction, keywords in kbd_keywords.items():
if any(kw in desc_lower for kw in keywords):
keyboard.append(interaction)
# Default keyboard interactions
if not keyboard:
keyboard = ['navigation', 'selection', 'quit']
# Mouse interactions
if any(word in desc_lower for word in ['mouse', 'click', 'drag']):
mouse = ['click', 'scroll']
return {
'keyboard': keyboard,
'mouse': mouse
}
def identify_data_types(description: str) -> List[str]:
"""Identify data types being displayed."""
desc_lower = description.lower()
data_type_keywords = {
'files': ['file', 'directory', 'folder'],
'text': ['text', 'log', 'document'],
'tabular': ['table', 'data', 'rows', 'columns'],
'messages': ['message', 'chat', 'conversation'],
'packages': ['package', 'dependency', 'module'],
'metrics': ['metric', 'stat', 'data point'],
'config': ['config', 'setting', 'option']
}
data_types = []
for dtype, keywords in data_type_keywords.items():
if any(kw in desc_lower for kw in keywords):
data_types.append(dtype)
return data_types if data_types else ['text']
def determine_view_type(description: str) -> str:
"""Determine if single or multi-view."""
desc_lower = description.lower()
multi_keywords = ['multi-view', 'multiple view', 'tabs', 'tabbed', 'switch', 'views']
three_pane_keywords = ['three', 'three-column', 'three pane']
if any(kw in desc_lower for kw in three_pane_keywords):
return 'three-pane'
elif any(kw in desc_lower for kw in multi_keywords):
return 'multi'
else:
return 'single'
def identify_special_requirements(description: str) -> List[str]:
"""Identify special requirements."""
desc_lower = description.lower()
special = []
special_keywords = {
'validation': ['validate', 'validation', 'check'],
'real-time': ['real-time', 'live', 'streaming'],
'async': ['async', 'background', 'concurrent'],
'persistence': ['save', 'persist', 'store'],
'theming': ['theme', 'color', 'style']
}
for req, keywords in special_keywords.items():
if any(kw in desc_lower for kw in keywords):
special.append(req)
return special
def main():
"""Test requirement analyzer."""
print("Testing Requirement Analyzer\n" + "=" * 50)
test_cases = [
"Build a log viewer with search and highlighting",
"Create a file manager with three-column view",
"Design an installer with progress bars",
"Make a form wizard with validation"
]
for i, desc in enumerate(test_cases, 1):
print(f"\n{i}. Testing: '{desc}'")
reqs = extract_requirements(desc)
print(f" Archetype: {reqs['archetype']}")
print(f" Features: {', '.join(reqs['features'])}")
print(f" Data types: {', '.join(reqs['data_types'])}")
print(f" View type: {reqs['views']}")
print(f" Validation: {reqs['validation']['summary']}")
print("\n✅ All tests passed!")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Architecture designer for Bubble Tea TUIs."""
import sys
from pathlib import Path
from typing import Dict, List
sys.path.insert(0, str(Path(__file__).parent))
from utils.template_generator import (
generate_model_struct,
generate_init_function,
generate_update_skeleton,
generate_view_skeleton
)
from utils.ascii_diagram import (
draw_component_tree,
draw_message_flow,
draw_state_machine
)
from utils.validators import DesignValidator
def design_architecture(components: Dict, patterns: Dict, requirements: Dict) -> Dict:
"""Design TUI architecture."""
primary = components.get('primary_components', [])
comp_names = [c['component'].replace('.Model', '') for c in primary]
archetype = requirements.get('archetype', 'general')
views = requirements.get('views', 'single')
# Generate code structures
model_struct = generate_model_struct(comp_names, archetype)
init_logic = generate_init_function(comp_names)
message_handlers = {
'tea.KeyMsg': 'Handle keyboard input (arrows, enter, q, etc.)',
'tea.WindowSizeMsg': 'Handle window resize, update component dimensions'
}
# Add component-specific handlers
if 'progress' in comp_names or 'spinner' in comp_names:
message_handlers['progress.FrameMsg'] = 'Update progress/spinner animation'
view_logic = generate_view_skeleton(comp_names)
# Generate diagrams
diagrams = {
'component_hierarchy': draw_component_tree(comp_names, archetype),
'message_flow': draw_message_flow(list(message_handlers.keys()))
}
if views == 'multi':
diagrams['state_machine'] = draw_state_machine(['View 1', 'View 2', 'View 3'])
architecture = {
'model_struct': model_struct,
'init_logic': init_logic,
'message_handlers': message_handlers,
'view_logic': view_logic,
'diagrams': diagrams
}
# Validate
validator = DesignValidator()
validation = validator.validate_architecture(architecture)
architecture['validation'] = validation.to_dict()
return architecture

224
scripts/design_tui.py Normal file
View File

@@ -0,0 +1,224 @@
#!/usr/bin/env python3
"""
Main TUI designer orchestrator.
Combines all analyses into comprehensive design report.
"""
import sys
import argparse
from pathlib import Path
from typing import Dict, Optional, List
sys.path.insert(0, str(Path(__file__).parent))
from analyze_requirements import extract_requirements
from map_components import map_to_components
from select_patterns import select_relevant_patterns
from design_architecture import design_architecture
from generate_workflow import generate_implementation_workflow
from utils.helpers import get_timestamp
from utils.template_generator import generate_main_go
from utils.validators import DesignValidator
def comprehensive_tui_design_report(
description: str,
inventory_path: Optional[str] = None,
include_sections: Optional[List[str]] = None,
detail_level: str = "complete"
) -> Dict:
"""
Generate comprehensive TUI design report.
This is the all-in-one function that combines all design analyses.
Args:
description: Natural language TUI description
inventory_path: Path to charm-examples-inventory
include_sections: Which sections to include (None = all)
detail_level: "summary" | "detailed" | "complete"
Returns:
Complete design report dictionary with all sections
Example:
>>> report = comprehensive_tui_design_report(
... "Build a log viewer with search"
... )
>>> print(report['summary'])
"TUI Design: Log Viewer..."
"""
if include_sections is None:
include_sections = ['requirements', 'components', 'patterns', 'architecture', 'workflow']
report = {
'description': description,
'generated_at': get_timestamp(),
'sections': {}
}
# Phase 1: Requirements Analysis
if 'requirements' in include_sections:
requirements = extract_requirements(description)
report['sections']['requirements'] = requirements
report['tui_type'] = requirements['archetype']
else:
requirements = extract_requirements(description)
report['tui_type'] = requirements.get('archetype', 'general')
# Phase 2: Component Mapping
if 'components' in include_sections:
components = map_to_components(requirements)
report['sections']['components'] = components
else:
components = map_to_components(requirements)
# Phase 3: Pattern Selection
if 'patterns' in include_sections:
patterns = select_relevant_patterns(components, inventory_path)
report['sections']['patterns'] = patterns
else:
patterns = {'examples': []}
# Phase 4: Architecture Design
if 'architecture' in include_sections:
architecture = design_architecture(components, patterns, requirements)
report['sections']['architecture'] = architecture
else:
architecture = design_architecture(components, patterns, requirements)
# Phase 5: Workflow Generation
if 'workflow' in include_sections:
workflow = generate_implementation_workflow(architecture, patterns)
report['sections']['workflow'] = workflow
# Generate summary
report['summary'] = _generate_summary(report, requirements, components)
# Generate code scaffolding
if detail_level == "complete":
primary_comps = [
c['component'].replace('.Model', '')
for c in components.get('primary_components', [])[:3]
]
report['scaffolding'] = {
'main_go': generate_main_go(primary_comps, requirements.get('archetype', 'general'))
}
# File structure recommendation
report['file_structure'] = {
'recommended': ['main.go', 'go.mod', 'README.md']
}
# Next steps
report['next_steps'] = _generate_next_steps(patterns, workflow if 'workflow' in report['sections'] else None)
# Resources
report['resources'] = {
'documentation': [
'https://github.com/charmbracelet/bubbletea',
'https://github.com/charmbracelet/lipgloss'
],
'tutorials': [
'Bubble Tea tutorial: https://github.com/charmbracelet/bubbletea/tree/master/tutorials'
],
'community': [
'Charm Discord: https://charm.sh/chat'
]
}
# Overall validation
validator = DesignValidator()
validation = validator.validate_design_report(report)
report['validation'] = validation.to_dict()
return report
def _generate_summary(report: Dict, requirements: Dict, components: Dict) -> str:
"""Generate executive summary."""
tui_type = requirements.get('archetype', 'general')
features = requirements.get('features', [])
primary = components.get('primary_components', [])
summary_parts = [
f"TUI Design: {tui_type.replace('-', ' ').title()}",
f"\nPurpose: {report.get('description', 'N/A')}",
f"\nKey Features: {', '.join(features)}",
f"\nPrimary Components: {', '.join([c['component'] for c in primary[:3]])}",
]
if 'workflow' in report.get('sections', {}):
summary_parts.append(
f"\nEstimated Implementation Time: {report['sections']['workflow'].get('total_estimated_time', 'N/A')}"
)
return '\n'.join(summary_parts)
def _generate_next_steps(patterns: Dict, workflow: Optional[Dict]) -> List[str]:
"""Generate next steps list."""
steps = ['1. Review the architecture diagram and component selection']
examples = patterns.get('examples', [])
if examples:
steps.append(f'2. Study example files: {examples[0]["file"]}')
if workflow:
steps.append('3. Follow the implementation workflow starting with Phase 1')
steps.append('4. Test at each checkpoint')
steps.append('5. Refer to Bubble Tea documentation for component details')
return steps
def main():
"""CLI for TUI designer."""
parser = argparse.ArgumentParser(description='Bubble Tea TUI Designer')
parser.add_argument('description', help='TUI description')
parser.add_argument('--inventory', help='Path to charm-examples-inventory')
parser.add_argument('--detail', choices=['summary', 'detailed', 'complete'], default='complete')
args = parser.parse_args()
print("=" * 60)
print("Bubble Tea TUI Designer")
print("=" * 60)
report = comprehensive_tui_design_report(
args.description,
inventory_path=args.inventory,
detail_level=args.detail
)
print(f"\n{report['summary']}")
if 'architecture' in report['sections']:
print("\n" + "=" * 60)
print("ARCHITECTURE")
print("=" * 60)
print(report['sections']['architecture']['diagrams']['component_hierarchy'])
if 'workflow' in report['sections']:
print("\n" + "=" * 60)
print("IMPLEMENTATION WORKFLOW")
print("=" * 60)
for phase in report['sections']['workflow']['phases']:
print(f"\n{phase['name']} ({phase['total_time']})")
for task in phase['tasks']:
print(f" - {task['task']}")
print("\n" + "=" * 60)
print("NEXT STEPS")
print("=" * 60)
for step in report['next_steps']:
print(step)
print("\n" + "=" * 60)
print(f"Validation: {report['validation']['summary']}")
print("=" * 60)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""Workflow generator for TUI implementation."""
import sys
from pathlib import Path
from typing import Dict, List
sys.path.insert(0, str(Path(__file__).parent))
from utils.helpers import estimate_complexity
from utils.validators import DesignValidator
def generate_implementation_workflow(architecture: Dict, patterns: Dict) -> Dict:
"""Generate step-by-step implementation workflow."""
comp_count = len(architecture.get('model_struct', '').split('\n')) // 2
examples = patterns.get('examples', [])
phases = [
{
'name': 'Phase 1: Setup',
'tasks': [
{'task': 'Initialize Go module', 'estimated_time': '2 minutes'},
{'task': 'Install Bubble Tea and dependencies', 'estimated_time': '3 minutes'},
{'task': 'Create main.go with basic structure', 'estimated_time': '5 minutes'}
],
'total_time': '10 minutes'
},
{
'name': 'Phase 2: Core Components',
'tasks': [
{'task': 'Implement model struct', 'estimated_time': '15 minutes'},
{'task': 'Add Init() function', 'estimated_time': '10 minutes'},
{'task': 'Implement basic Update() handler', 'estimated_time': '20 minutes'},
{'task': 'Create basic View()', 'estimated_time': '15 minutes'}
],
'total_time': '60 minutes'
},
{
'name': 'Phase 3: Integration',
'tasks': [
{'task': 'Connect components', 'estimated_time': '30 minutes'},
{'task': 'Add message passing', 'estimated_time': '20 minutes'},
{'task': 'Implement full keyboard handling', 'estimated_time': '20 minutes'}
],
'total_time': '70 minutes'
},
{
'name': 'Phase 4: Polish',
'tasks': [
{'task': 'Add Lipgloss styling', 'estimated_time': '30 minutes'},
{'task': 'Add help text', 'estimated_time': '15 minutes'},
{'task': 'Error handling', 'estimated_time': '15 minutes'}
],
'total_time': '60 minutes'
}
]
testing_checkpoints = [
'After Phase 1: go build succeeds',
'After Phase 2: Basic TUI renders',
'After Phase 3: All interactions work',
'After Phase 4: Production ready'
]
workflow = {
'phases': phases,
'testing_checkpoints': testing_checkpoints,
'total_estimated_time': estimate_complexity(comp_count)
}
# Validate
validator = DesignValidator()
validation = validator.validate_workflow_completeness(workflow)
workflow['validation'] = validation.to_dict()
return workflow

161
scripts/map_components.py Normal file
View File

@@ -0,0 +1,161 @@
#!/usr/bin/env python3
"""
Component mapper for Bubble Tea TUIs.
Maps requirements to appropriate components.
"""
import sys
from pathlib import Path
from typing import Dict, List
sys.path.insert(0, str(Path(__file__).parent))
from utils.component_matcher import (
match_score,
find_best_match,
get_alternatives,
explain_match,
rank_components_by_relevance
)
from utils.validators import DesignValidator
def map_to_components(requirements: Dict, inventory=None) -> Dict:
"""
Map requirements to Bubble Tea components.
Args:
requirements: Structured requirements from analyze_requirements
inventory: Optional inventory object (unused for now)
Returns:
Dictionary with component recommendations
Example:
>>> components = map_to_components(reqs)
>>> components['primary_components'][0]['component']
'viewport.Model'
"""
features = requirements.get('features', [])
archetype = requirements.get('archetype', 'general')
data_types = requirements.get('data_types', [])
views = requirements.get('views', 'single')
# Get ranked components
ranked = rank_components_by_relevance(features, min_score=50)
# Build primary components list
primary_components = []
for component, score, matching_features in ranked[:5]: # Top 5
justification = explain_match(component, ' '.join(matching_features), score)
primary_components.append({
'component': f'{component}.Model',
'score': score,
'justification': justification,
'example_file': f'examples/{component}/main.go',
'key_patterns': [f'{component} usage', 'initialization', 'message handling']
})
# Add archetype-specific components
archetype_components = _get_archetype_components(archetype)
for comp in archetype_components:
if not any(c['component'].startswith(comp) for c in primary_components):
primary_components.append({
'component': f'{comp}.Model',
'score': 70,
'justification': f'Standard component for {archetype} TUIs',
'example_file': f'examples/{comp}/main.go',
'key_patterns': [f'{comp} patterns']
})
# Supporting components
supporting = _get_supporting_components(features, views)
# Styling
styling = ['lipgloss for layout and styling']
if 'highlighting' in features:
styling.append('lipgloss for text highlighting')
# Alternatives
alternatives = {}
for comp in primary_components[:3]:
comp_name = comp['component'].replace('.Model', '')
alts = get_alternatives(comp_name)
if alts:
alternatives[comp['component']] = [f'{alt}.Model' for alt in alts]
result = {
'primary_components': primary_components,
'supporting_components': supporting,
'styling': styling,
'alternatives': alternatives
}
# Validate
validator = DesignValidator()
validation = validator.validate_component_selection(result, requirements)
result['validation'] = validation.to_dict()
return result
def _get_archetype_components(archetype: str) -> List[str]:
"""Get standard components for archetype."""
archetype_map = {
'file-manager': ['filepicker', 'viewport', 'list'],
'installer': ['progress', 'spinner', 'list'],
'dashboard': ['tabs', 'viewport', 'table'],
'form': ['textinput', 'textarea', 'help'],
'viewer': ['viewport', 'paginator', 'textinput'],
'chat': ['viewport', 'textarea', 'textinput'],
'table-viewer': ['table', 'paginator'],
'menu': ['list'],
'editor': ['textarea', 'viewport']
}
return archetype_map.get(archetype, [])
def _get_supporting_components(features: List[str], views: str) -> List[str]:
"""Get supporting components based on features."""
supporting = []
if views in ['multi', 'three-pane']:
supporting.append('Multiple viewports for multi-pane layout')
if 'help' not in features:
supporting.append('help.Model for keyboard shortcuts')
if views == 'multi':
supporting.append('tabs.Model or state machine for view switching')
return supporting
def main():
"""Test component mapper."""
print("Testing Component Mapper\n" + "=" * 50)
# Mock requirements
requirements = {
'archetype': 'viewer',
'features': ['display', 'search', 'scrolling'],
'data_types': ['text'],
'views': 'single'
}
print("\n1. Testing map_to_components()...")
components = map_to_components(requirements)
print(f" Primary components: {len(components['primary_components'])}")
for comp in components['primary_components'][:3]:
print(f" - {comp['component']} (score: {comp['score']})")
print(f"\n Validation: {components['validation']['summary']}")
print("\n✅ Tests passed!")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""Pattern selector - finds relevant example files."""
import sys
from pathlib import Path
from typing import Dict, List, Optional
sys.path.insert(0, str(Path(__file__).parent))
from utils.inventory_loader import load_inventory, Inventory
def select_relevant_patterns(components: Dict, inventory_path: Optional[str] = None) -> Dict:
"""Select relevant example files."""
try:
inventory = load_inventory(inventory_path)
except Exception as e:
return {'examples': [], 'error': str(e)}
primary_components = components.get('primary_components', [])
examples = []
for comp_info in primary_components[:3]:
comp_name = comp_info['component'].replace('.Model', '')
comp_examples = inventory.get_by_component(comp_name)
for ex in comp_examples[:2]:
examples.append({
'file': ex.file_path,
'capability': ex.capability,
'relevance_score': comp_info['score'],
'key_patterns': ex.key_patterns,
'study_order': len(examples) + 1
})
return {
'examples': examples,
'recommended_study_order': list(range(1, len(examples) + 1)),
'total_study_time': f"{len(examples) * 15} minutes"
}

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""
ASCII diagram generator for architecture visualization.
"""
from typing import List, Dict
def draw_component_tree(components: List[str], archetype: str) -> str:
"""Draw component hierarchy as ASCII tree."""
lines = [
"┌─────────────────────────────────────┐",
"│ Main Model │",
"├─────────────────────────────────────┤"
]
# Add state fields
lines.append("│ Components: │")
for comp in components:
lines.append(f"│ - {comp:<30}")
lines.append("└────────────┬───────────────┬────────┘")
# Add component boxes below
if len(components) >= 2:
comp_boxes = []
for comp in components[:3]: # Show max 3
comp_boxes.append(f" ┌────▼────┐")
comp_boxes.append(f"{comp:<7}")
comp_boxes.append(f" └─────────┘")
return "\n".join(lines) + "\n" + "\n".join(comp_boxes)
return "\n".join(lines)
def draw_message_flow(messages: List[str]) -> str:
"""Draw message flow diagram."""
flow = ["Message Flow:"]
flow.append("")
flow.append("User Input → tea.KeyMsg → Update() →")
for msg in messages:
flow.append(f" {msg}")
flow.append(" Model Updated → View() → Render")
return "\n".join(flow)
def draw_state_machine(states: List[str]) -> str:
"""Draw state machine diagram."""
if not states or len(states) < 2:
return "Single-state application (no state machine)"
diagram = ["State Machine:", ""]
for i, state in enumerate(states):
if i < len(states) - 1:
diagram.append(f"{state}{states[i+1]}")
else:
diagram.append(f"{state} → Done")
return "\n".join(diagram)

View File

@@ -0,0 +1,379 @@
#!/usr/bin/env python3
"""
Component matching logic for Bubble Tea Designer.
Scores and ranks components based on requirements.
"""
from typing import Dict, List, Tuple
import logging
logger = logging.getLogger(__name__)
# Component capability definitions
COMPONENT_CAPABILITIES = {
'viewport': {
'keywords': ['scroll', 'view', 'display', 'content', 'pager', 'document'],
'use_cases': ['viewing large text', 'log viewer', 'document reader'],
'complexity': 'medium'
},
'textinput': {
'keywords': ['input', 'text', 'search', 'query', 'single-line'],
'use_cases': ['search box', 'text input', 'single field'],
'complexity': 'low'
},
'textarea': {
'keywords': ['edit', 'multi-line', 'text area', 'editor', 'compose'],
'use_cases': ['text editing', 'message composition', 'multi-line input'],
'complexity': 'medium'
},
'table': {
'keywords': ['table', 'tabular', 'rows', 'columns', 'grid', 'data display'],
'use_cases': ['data table', 'spreadsheet view', 'structured data'],
'complexity': 'medium'
},
'list': {
'keywords': ['list', 'items', 'select', 'choose', 'menu', 'options'],
'use_cases': ['item selection', 'menu', 'file list'],
'complexity': 'medium'
},
'progress': {
'keywords': ['progress', 'loading', 'installation', 'percent', 'bar'],
'use_cases': ['progress indication', 'loading', 'installation progress'],
'complexity': 'low'
},
'spinner': {
'keywords': ['loading', 'spinner', 'wait', 'processing', 'busy'],
'use_cases': ['loading indicator', 'waiting', 'processing'],
'complexity': 'low'
},
'filepicker': {
'keywords': ['file', 'select file', 'choose file', 'file system', 'browse'],
'use_cases': ['file selection', 'file browser', 'file chooser'],
'complexity': 'medium'
},
'paginator': {
'keywords': ['page', 'pagination', 'pages', 'navigate pages'],
'use_cases': ['page navigation', 'chunked content', 'paged display'],
'complexity': 'low'
},
'timer': {
'keywords': ['timer', 'countdown', 'timeout', 'time limit'],
'use_cases': ['countdown', 'timeout', 'timed operation'],
'complexity': 'low'
},
'stopwatch': {
'keywords': ['stopwatch', 'elapsed', 'time tracking', 'duration'],
'use_cases': ['time tracking', 'elapsed time', 'duration measurement'],
'complexity': 'low'
},
'help': {
'keywords': ['help', 'shortcuts', 'keybindings', 'documentation'],
'use_cases': ['help menu', 'keyboard shortcuts', 'documentation'],
'complexity': 'low'
},
'tabs': {
'keywords': ['tabs', 'tabbed', 'switch views', 'navigation'],
'use_cases': ['tab navigation', 'multiple views', 'view switching'],
'complexity': 'medium'
},
'autocomplete': {
'keywords': ['autocomplete', 'suggestions', 'completion', 'dropdown'],
'use_cases': ['autocomplete', 'suggestions', 'smart input'],
'complexity': 'medium'
}
}
def match_score(requirement: str, component: str) -> int:
"""
Calculate relevance score for component given requirement.
Args:
requirement: Feature requirement description
component: Component name
Returns:
Score from 0-100 (higher = better match)
Example:
>>> match_score("scrollable log display", "viewport")
95
"""
if component not in COMPONENT_CAPABILITIES:
return 0
score = 0
requirement_lower = requirement.lower()
comp_info = COMPONENT_CAPABILITIES[component]
# Keyword matching (60 points max)
keywords = comp_info['keywords']
keyword_matches = sum(1 for kw in keywords if kw in requirement_lower)
keyword_score = min(60, (keyword_matches / len(keywords)) * 60)
score += keyword_score
# Use case matching (40 points max)
use_cases = comp_info['use_cases']
use_case_matches = sum(1 for uc in use_cases if any(
word in requirement_lower for word in uc.split()
))
use_case_score = min(40, (use_case_matches / len(use_cases)) * 40)
score += use_case_score
return int(score)
def find_best_match(requirement: str, components: List[str] = None) -> Tuple[str, int]:
"""
Find best matching component for requirement.
Args:
requirement: Feature requirement
components: List of component names to consider (None = all)
Returns:
Tuple of (best_component, score)
Example:
>>> find_best_match("need to show progress while installing")
('progress', 85)
"""
if components is None:
components = list(COMPONENT_CAPABILITIES.keys())
best_component = None
best_score = 0
for component in components:
score = match_score(requirement, component)
if score > best_score:
best_score = score
best_component = component
return best_component, best_score
def suggest_combinations(requirements: List[str]) -> List[List[str]]:
"""
Suggest component combinations for multiple requirements.
Args:
requirements: List of feature requirements
Returns:
List of component combinations (each is a list of components)
Example:
>>> suggest_combinations(["display logs", "search logs"])
[['viewport', 'textinput']]
"""
combinations = []
# Find best match for each requirement
selected_components = []
for req in requirements:
component, score = find_best_match(req)
if score > 50 and component not in selected_components:
selected_components.append(component)
if selected_components:
combinations.append(selected_components)
# Common patterns
patterns = {
'file_manager': ['filepicker', 'viewport', 'list'],
'installer': ['progress', 'spinner', 'list'],
'form': ['textinput', 'textarea', 'help'],
'viewer': ['viewport', 'paginator', 'textinput'],
'dashboard': ['tabs', 'viewport', 'table']
}
# Check if requirements match any patterns
req_text = ' '.join(requirements).lower()
for pattern_name, pattern_components in patterns.items():
if pattern_name.replace('_', ' ') in req_text:
combinations.append(pattern_components)
return combinations if combinations else [selected_components]
def get_alternatives(component: str) -> List[str]:
"""
Get alternative components that serve similar purposes.
Args:
component: Component name
Returns:
List of alternative component names
Example:
>>> get_alternatives('viewport')
['pager', 'textarea']
"""
alternatives = {
'viewport': ['pager'],
'textinput': ['textarea', 'autocomplete'],
'textarea': ['textinput', 'viewport'],
'table': ['list'],
'list': ['table', 'filepicker'],
'progress': ['spinner'],
'spinner': ['progress'],
'filepicker': ['list'],
'paginator': ['viewport'],
'tabs': ['composable-views']
}
return alternatives.get(component, [])
def explain_match(component: str, requirement: str, score: int) -> str:
"""
Generate explanation for why component matches requirement.
Args:
component: Component name
requirement: Requirement description
score: Match score
Returns:
Human-readable explanation
Example:
>>> explain_match("viewport", "scrollable display", 90)
"viewport is a strong match (90/100) for 'scrollable display' because..."
"""
if component not in COMPONENT_CAPABILITIES:
return f"{component} is not a known component"
comp_info = COMPONENT_CAPABILITIES[component]
requirement_lower = requirement.lower()
# Find which keywords matched
matched_keywords = [kw for kw in comp_info['keywords'] if kw in requirement_lower]
explanation_parts = []
if score >= 80:
explanation_parts.append(f"{component} is a strong match ({score}/100)")
elif score >= 50:
explanation_parts.append(f"{component} is a good match ({score}/100)")
else:
explanation_parts.append(f"{component} is a weak match ({score}/100)")
explanation_parts.append(f"for '{requirement}'")
if matched_keywords:
explanation_parts.append(f"because it handles: {', '.join(matched_keywords)}")
# Add use case
explanation_parts.append(f"Common use cases: {', '.join(comp_info['use_cases'])}")
return " ".join(explanation_parts) + "."
def rank_components_by_relevance(
requirements: List[str],
min_score: int = 50
) -> List[Tuple[str, int, List[str]]]:
"""
Rank all components by relevance to requirements.
Args:
requirements: List of feature requirements
min_score: Minimum score to include (default: 50)
Returns:
List of tuples: (component, total_score, matching_requirements)
Sorted by total_score descending
Example:
>>> rank_components_by_relevance(["scroll", "display text"])
[('viewport', 180, ['scroll', 'display text']), ...]
"""
component_scores = {}
component_matches = {}
all_components = list(COMPONENT_CAPABILITIES.keys())
for component in all_components:
total_score = 0
matching_reqs = []
for req in requirements:
score = match_score(req, component)
if score >= min_score:
total_score += score
matching_reqs.append(req)
if total_score > 0:
component_scores[component] = total_score
component_matches[component] = matching_reqs
# Sort by score
ranked = sorted(
component_scores.items(),
key=lambda x: x[1],
reverse=True
)
return [(comp, score, component_matches[comp]) for comp, score in ranked]
def main():
"""Test component matcher."""
print("Testing Component Matcher\n" + "=" * 50)
# Test 1: Match score
print("\n1. Testing match_score()...")
score = match_score("scrollable log display", "viewport")
print(f" Score for 'scrollable log display' + viewport: {score}")
assert score > 50, "Should have good score"
print(" ✓ Match scoring works")
# Test 2: Find best match
print("\n2. Testing find_best_match()...")
component, score = find_best_match("need to show progress while installing")
print(f" Best match: {component} ({score})")
assert component in ['progress', 'spinner'], "Should match progress-related component"
print(" ✓ Best match finding works")
# Test 3: Suggest combinations
print("\n3. Testing suggest_combinations()...")
combos = suggest_combinations(["display logs", "search logs", "scroll through logs"])
print(f" Suggested combinations: {combos}")
assert len(combos) > 0, "Should suggest at least one combination"
print(" ✓ Combination suggestion works")
# Test 4: Get alternatives
print("\n4. Testing get_alternatives()...")
alts = get_alternatives('viewport')
print(f" Alternatives to viewport: {alts}")
assert 'pager' in alts, "Should include pager as alternative"
print(" ✓ Alternative suggestions work")
# Test 5: Explain match
print("\n5. Testing explain_match()...")
explanation = explain_match("viewport", "scrollable display", 90)
print(f" Explanation: {explanation}")
assert "strong match" in explanation, "Should indicate strong match"
print(" ✓ Match explanation works")
# Test 6: Rank components
print("\n6. Testing rank_components_by_relevance()...")
ranked = rank_components_by_relevance(
["scroll", "display", "text", "search"],
min_score=40
)
print(f" Top 3 components:")
for i, (comp, score, reqs) in enumerate(ranked[:3], 1):
print(f" {i}. {comp} (score: {score}) - matches: {reqs}")
assert len(ranked) > 0, "Should rank some components"
print(" ✓ Component ranking works")
print("\n✅ All tests passed!")
if __name__ == "__main__":
main()

40
scripts/utils/helpers.py Normal file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""
General helper utilities for Bubble Tea Designer.
"""
from datetime import datetime
from typing import Optional
def get_timestamp() -> str:
"""Get current timestamp in ISO format."""
return datetime.now().isoformat()
def format_list_markdown(items: list, ordered: bool = False) -> str:
"""Format list as markdown."""
if not items:
return ""
if ordered:
return "\n".join(f"{i}. {item}" for i, item in enumerate(items, 1))
else:
return "\n".join(f"- {item}" for item in items)
def truncate_text(text: str, max_length: int = 100) -> str:
"""Truncate text to max length with ellipsis."""
if len(text) <= max_length:
return text
return text[:max_length-3] + "..."
def estimate_complexity(num_components: int, num_views: int = 1) -> str:
"""Estimate implementation complexity."""
if num_components <= 2 and num_views == 1:
return "Simple (1-2 hours)"
elif num_components <= 4 and num_views <= 2:
return "Medium (2-4 hours)"
else:
return "Complex (4+ hours)"

View File

@@ -0,0 +1,334 @@
#!/usr/bin/env python3
"""
Inventory loader for Bubble Tea examples.
Loads and parses CONTEXTUAL-INVENTORY.md from charm-examples-inventory.
"""
import os
import re
from typing import Dict, List, Optional, Tuple
from pathlib import Path
import logging
logger = logging.getLogger(__name__)
class InventoryLoadError(Exception):
"""Raised when inventory cannot be loaded."""
pass
class Example:
"""Represents a single Bubble Tea example."""
def __init__(self, name: str, file_path: str, capability: str):
self.name = name
self.file_path = file_path
self.capability = capability
self.key_patterns: List[str] = []
self.components: List[str] = []
self.use_cases: List[str] = []
def __repr__(self):
return f"Example({self.name}, {self.capability})"
class Inventory:
"""Bubble Tea examples inventory."""
def __init__(self, base_path: str):
self.base_path = base_path
self.examples: Dict[str, Example] = {}
self.capabilities: Dict[str, List[Example]] = {}
self.components: Dict[str, List[Example]] = {}
def add_example(self, example: Example):
"""Add example to inventory."""
self.examples[example.name] = example
# Index by capability
if example.capability not in self.capabilities:
self.capabilities[example.capability] = []
self.capabilities[example.capability].append(example)
# Index by components
for component in example.components:
if component not in self.components:
self.components[component] = []
self.components[component].append(example)
def search_by_keyword(self, keyword: str) -> List[Example]:
"""Search examples by keyword in name or patterns."""
keyword_lower = keyword.lower()
results = []
for example in self.examples.values():
if keyword_lower in example.name.lower():
results.append(example)
continue
for pattern in example.key_patterns:
if keyword_lower in pattern.lower():
results.append(example)
break
return results
def get_by_capability(self, capability: str) -> List[Example]:
"""Get all examples for a capability."""
return self.capabilities.get(capability, [])
def get_by_component(self, component: str) -> List[Example]:
"""Get all examples using a component."""
return self.components.get(component, [])
def load_inventory(inventory_path: Optional[str] = None) -> Inventory:
"""
Load Bubble Tea examples inventory from CONTEXTUAL-INVENTORY.md.
Args:
inventory_path: Path to charm-examples-inventory directory
If None, tries to find it automatically
Returns:
Loaded Inventory object
Raises:
InventoryLoadError: If inventory cannot be loaded
Example:
>>> inv = load_inventory("/path/to/charm-examples-inventory")
>>> examples = inv.search_by_keyword("progress")
"""
if inventory_path is None:
inventory_path = _find_inventory_path()
inventory_file = Path(inventory_path) / "bubbletea" / "examples" / "CONTEXTUAL-INVENTORY.md"
if not inventory_file.exists():
raise InventoryLoadError(
f"Inventory file not found: {inventory_file}\n"
f"Expected at: {inventory_path}/bubbletea/examples/CONTEXTUAL-INVENTORY.md"
)
logger.info(f"Loading inventory from: {inventory_file}")
with open(inventory_file, 'r') as f:
content = f.read()
inventory = parse_inventory_markdown(content, str(inventory_path))
logger.info(f"Loaded {len(inventory.examples)} examples")
logger.info(f"Categories: {len(inventory.capabilities)}")
return inventory
def parse_inventory_markdown(content: str, base_path: str) -> Inventory:
"""
Parse CONTEXTUAL-INVENTORY.md markdown content.
Args:
content: Markdown content
base_path: Base path for example files
Returns:
Inventory object with parsed examples
"""
inventory = Inventory(base_path)
# Parse quick reference table
table_matches = re.finditer(
r'\|\s*(.+?)\s*\|\s*`(.+?)`\s*\|',
content
)
need_to_file = {}
for match in table_matches:
need = match.group(1).strip()
file_path = match.group(2).strip()
need_to_file[need] = file_path
# Parse detailed sections (## Examples by Capability)
capability_pattern = r'### (.+?)\n\n\*\*Use (.+?) when you need:\*\*(.+?)(?=\n\n\*\*|### |\Z)'
capability_sections = re.finditer(capability_pattern, content, re.DOTALL)
for section in capability_sections:
capability = section.group(1).strip()
example_name = section.group(2).strip()
description = section.group(3).strip()
# Extract file path and key patterns
file_match = re.search(r'\*\*File\*\*: `(.+?)`', description)
patterns_match = re.search(r'\*\*Key patterns\*\*: (.+?)(?=\n|$)', description)
if file_match:
file_path = file_match.group(1).strip()
example = Example(example_name, file_path, capability)
if patterns_match:
patterns_text = patterns_match.group(1).strip()
example.key_patterns = [p.strip() for p in patterns_text.split(',')]
# Extract components from file name and patterns
example.components = _extract_components(example_name, example.key_patterns)
inventory.add_example(example)
return inventory
def _extract_components(name: str, patterns: List[str]) -> List[str]:
"""Extract component names from example name and patterns."""
components = []
# Common component keywords
component_keywords = [
'textinput', 'textarea', 'viewport', 'table', 'list', 'pager',
'paginator', 'spinner', 'progress', 'timer', 'stopwatch',
'filepicker', 'help', 'tabs', 'autocomplete'
]
name_lower = name.lower()
for keyword in component_keywords:
if keyword in name_lower:
components.append(keyword)
for pattern in patterns:
pattern_lower = pattern.lower()
for keyword in component_keywords:
if keyword in pattern_lower and keyword not in components:
components.append(keyword)
return components
def _find_inventory_path() -> str:
"""
Try to find charm-examples-inventory automatically.
Searches in common locations:
- ./charm-examples-inventory
- ../charm-examples-inventory
- ~/charmtuitemplate/vinw/charm-examples-inventory
Returns:
Path to inventory directory
Raises:
InventoryLoadError: If not found
"""
search_paths = [
Path.cwd() / "charm-examples-inventory",
Path.cwd().parent / "charm-examples-inventory",
Path.home() / "charmtuitemplate" / "vinw" / "charm-examples-inventory"
]
for path in search_paths:
if (path / "bubbletea" / "examples" / "CONTEXTUAL-INVENTORY.md").exists():
logger.info(f"Found inventory at: {path}")
return str(path)
raise InventoryLoadError(
"Could not find charm-examples-inventory automatically.\n"
f"Searched: {[str(p) for p in search_paths]}\n"
"Please provide inventory_path parameter."
)
def build_capability_index(inventory: Inventory) -> Dict[str, List[str]]:
"""
Build index of capabilities to example names.
Args:
inventory: Loaded inventory
Returns:
Dict mapping capability names to example names
"""
index = {}
for capability, examples in inventory.capabilities.items():
index[capability] = [ex.name for ex in examples]
return index
def build_component_index(inventory: Inventory) -> Dict[str, List[str]]:
"""
Build index of components to example names.
Args:
inventory: Loaded inventory
Returns:
Dict mapping component names to example names
"""
index = {}
for component, examples in inventory.components.items():
index[component] = [ex.name for ex in examples]
return index
def get_example_details(inventory: Inventory, example_name: str) -> Optional[Example]:
"""
Get detailed information about a specific example.
Args:
inventory: Loaded inventory
example_name: Name of example to look up
Returns:
Example object or None if not found
"""
return inventory.examples.get(example_name)
def main():
"""Test inventory loader."""
logging.basicConfig(level=logging.INFO)
print("Testing Inventory Loader\n" + "=" * 50)
try:
# Load inventory
print("\n1. Loading inventory...")
inventory = load_inventory()
print(f"✓ Loaded {len(inventory.examples)} examples")
print(f"{len(inventory.capabilities)} capability categories")
# Test search
print("\n2. Testing keyword search...")
results = inventory.search_by_keyword("progress")
print(f"✓ Found {len(results)} examples for 'progress':")
for ex in results[:3]:
print(f" - {ex.name} ({ex.capability})")
# Test capability lookup
print("\n3. Testing capability lookup...")
cap_examples = inventory.get_by_capability("Installation and Progress Tracking")
print(f"✓ Found {len(cap_examples)} installation examples")
# Test component lookup
print("\n4. Testing component lookup...")
comp_examples = inventory.get_by_component("spinner")
print(f"✓ Found {len(comp_examples)} examples using 'spinner'")
# Test indices
print("\n5. Building indices...")
cap_index = build_capability_index(inventory)
comp_index = build_component_index(inventory)
print(f"✓ Capability index: {len(cap_index)} categories")
print(f"✓ Component index: {len(comp_index)} components")
print("\n✅ All tests passed!")
except InventoryLoadError as e:
print(f"\n❌ Error loading inventory: {e}")
return 1
return 0
if __name__ == "__main__":
exit(main())

View File

@@ -0,0 +1,140 @@
#!/usr/bin/env python3
"""
Template generator for Bubble Tea TUIs.
Generates code scaffolding and boilerplate.
"""
from typing import List, Dict
def generate_model_struct(components: List[str], archetype: str) -> str:
"""Generate model struct with components."""
component_fields = {
'viewport': ' viewport viewport.Model',
'textinput': ' textInput textinput.Model',
'textarea': ' textArea textarea.Model',
'table': ' table table.Model',
'list': ' list list.Model',
'progress': ' progress progress.Model',
'spinner': ' spinner spinner.Model'
}
fields = []
for comp in components:
if comp in component_fields:
fields.append(component_fields[comp])
# Add common fields
fields.extend([
' width int',
' height int',
' ready bool'
])
return f"""type model struct {{
{chr(10).join(fields)}
}}"""
def generate_init_function(components: List[str]) -> str:
"""Generate Init() function."""
inits = []
for comp in components:
if comp == 'viewport':
inits.append(' m.viewport = viewport.New(80, 20)')
elif comp == 'textinput':
inits.append(' m.textInput = textinput.New()')
inits.append(' m.textInput.Focus()')
elif comp == 'spinner':
inits.append(' m.spinner = spinner.New()')
inits.append(' m.spinner.Spinner = spinner.Dot')
elif comp == 'progress':
inits.append(' m.progress = progress.New(progress.WithDefaultGradient())')
init_cmds = ', '.join([f'{c}.Init()' for c in components if c != 'viewport'])
return f"""func (m model) Init() tea.Cmd {{
{chr(10).join(inits) if inits else ' // Initialize components'}
return tea.Batch({init_cmds if init_cmds else 'nil'})
}}"""
def generate_update_skeleton(interactions: Dict) -> str:
"""Generate Update() skeleton."""
return """func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
}
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
m.ready = true
}
// Update components
// TODO: Add component update logic
return m, nil
}"""
def generate_view_skeleton(components: List[str]) -> str:
"""Generate View() skeleton."""
renders = []
for comp in components:
renders.append(f' // Render {comp}')
renders.append(f' // views = append(views, m.{comp}.View())')
return f"""func (m model) View() string {{
if !m.ready {{
return "Loading..."
}}
var views []string
{chr(10).join(renders)}
return lipgloss.JoinVertical(lipgloss.Left, views...)
}}"""
def generate_main_go(components: List[str], archetype: str) -> str:
"""Generate complete main.go scaffold."""
imports = ['github.com/charmbracelet/bubbletea']
if 'viewport' in components:
imports.append('github.com/charmbracelet/bubbles/viewport')
if 'textinput' in components:
imports.append('github.com/charmbracelet/bubbles/textinput')
if any(c in components for c in ['table', 'list', 'spinner', 'progress']):
imports.append('github.com/charmbracelet/bubbles/' + components[0])
imports.append('github.com/charmbracelet/lipgloss')
import_block = '\n '.join(f'"{imp}"' for imp in imports)
return f"""package main
import (
{import_block}
)
{generate_model_struct(components, archetype)}
{generate_init_function(components)}
{generate_update_skeleton({})}
{generate_view_skeleton(components)}
func main() {{
p := tea.NewProgram(model{{}}, tea.WithAltScreen())
if _, err := p.Run(); err != nil {{
panic(err)
}}
}}
"""

View File

@@ -0,0 +1,26 @@
"""Validators for Bubble Tea Designer."""
from .requirement_validator import (
RequirementValidator,
validate_description_clarity,
validate_requirements_completeness,
ValidationReport,
ValidationResult,
ValidationLevel
)
from .design_validator import (
DesignValidator,
validate_component_fit
)
__all__ = [
'RequirementValidator',
'validate_description_clarity',
'validate_requirements_completeness',
'DesignValidator',
'validate_component_fit',
'ValidationReport',
'ValidationResult',
'ValidationLevel'
]

View File

@@ -0,0 +1,425 @@
#!/usr/bin/env python3
"""
Design validators for Bubble Tea Designer.
Validates design outputs (component selections, architecture, workflows).
"""
from typing import Dict, List, Optional
from .requirement_validator import ValidationReport, ValidationResult, ValidationLevel
class DesignValidator:
"""Validates TUI design outputs."""
def validate_component_selection(
self,
components: Dict,
requirements: Dict
) -> ValidationReport:
"""
Validate component selection against requirements.
Args:
components: Selected components dict
requirements: Original requirements
Returns:
ValidationReport
"""
report = ValidationReport()
# Check 1: At least one component selected
primary = components.get('primary_components', [])
has_components = len(primary) > 0
report.add(ValidationResult(
check_name="has_components",
level=ValidationLevel.CRITICAL,
passed=has_components,
message=f"Primary components selected: {len(primary)}"
))
# Check 2: Components cover requirements
features = set(requirements.get('features', []))
if features and primary:
# Check if components mention required features
covered_features = set()
for comp in primary:
justification = comp.get('justification', '').lower()
for feature in features:
if feature.lower() in justification:
covered_features.add(feature)
coverage = len(covered_features) / len(features) * 100 if features else 0
report.add(ValidationResult(
check_name="feature_coverage",
level=ValidationLevel.WARNING,
passed=coverage >= 50,
message=f"Feature coverage: {coverage:.0f}% ({len(covered_features)}/{len(features)})"
))
# Check 3: No duplicate components
comp_names = [c.get('component', '') for c in primary]
duplicates = [name for name in comp_names if comp_names.count(name) > 1]
report.add(ValidationResult(
check_name="no_duplicates",
level=ValidationLevel.WARNING,
passed=len(duplicates) == 0,
message="No duplicate components" if not duplicates else
f"Duplicate components: {set(duplicates)}"
))
# Check 4: Reasonable number of components (not too many)
reasonable_count = len(primary) <= 6
report.add(ValidationResult(
check_name="reasonable_count",
level=ValidationLevel.INFO,
passed=reasonable_count,
message=f"Component count: {len(primary)} ({'reasonable' if reasonable_count else 'may be too many'})"
))
# Check 5: Each component has justification
all_justified = all('justification' in c for c in primary)
report.add(ValidationResult(
check_name="all_justified",
level=ValidationLevel.INFO,
passed=all_justified,
message="All components justified" if all_justified else
"Some components missing justification"
))
return report
def validate_architecture(self, architecture: Dict) -> ValidationReport:
"""
Validate architecture design.
Args:
architecture: Architecture specification
Returns:
ValidationReport
"""
report = ValidationReport()
# Check 1: Has model struct
has_model = 'model_struct' in architecture and architecture['model_struct']
report.add(ValidationResult(
check_name="has_model_struct",
level=ValidationLevel.CRITICAL,
passed=has_model,
message="Model struct defined" if has_model else "Missing model struct"
))
# Check 2: Has message handlers
handlers = architecture.get('message_handlers', {})
has_handlers = len(handlers) > 0
report.add(ValidationResult(
check_name="has_message_handlers",
level=ValidationLevel.CRITICAL,
passed=has_handlers,
message=f"Message handlers defined: {len(handlers)}"
))
# Check 3: Has key message handler (keyboard)
has_key_handler = 'tea.KeyMsg' in handlers or 'KeyMsg' in handlers
report.add(ValidationResult(
check_name="has_keyboard_handler",
level=ValidationLevel.WARNING,
passed=has_key_handler,
message="Keyboard handler present" if has_key_handler else
"Missing keyboard handler (tea.KeyMsg)"
))
# Check 4: Has view logic
has_view = 'view_logic' in architecture and architecture['view_logic']
report.add(ValidationResult(
check_name="has_view_logic",
level=ValidationLevel.CRITICAL,
passed=has_view,
message="View logic defined" if has_view else "Missing view logic"
))
# Check 5: Has diagrams
diagrams = architecture.get('diagrams', {})
has_diagrams = len(diagrams) > 0
report.add(ValidationResult(
check_name="has_diagrams",
level=ValidationLevel.INFO,
passed=has_diagrams,
message=f"Architecture diagrams: {len(diagrams)}"
))
return report
def validate_workflow_completeness(self, workflow: Dict) -> ValidationReport:
"""
Validate workflow has all necessary phases and tasks.
Args:
workflow: Workflow specification
Returns:
ValidationReport
"""
report = ValidationReport()
# Check 1: Has phases
phases = workflow.get('phases', [])
has_phases = len(phases) > 0
report.add(ValidationResult(
check_name="has_phases",
level=ValidationLevel.CRITICAL,
passed=has_phases,
message=f"Workflow phases: {len(phases)}"
))
if not phases:
return report
# Check 2: Each phase has tasks
all_have_tasks = all(len(phase.get('tasks', [])) > 0 for phase in phases)
report.add(ValidationResult(
check_name="all_phases_have_tasks",
level=ValidationLevel.WARNING,
passed=all_have_tasks,
message="All phases have tasks" if all_have_tasks else
"Some phases are missing tasks"
))
# Check 3: Has testing checkpoints
checkpoints = workflow.get('testing_checkpoints', [])
has_testing = len(checkpoints) > 0
report.add(ValidationResult(
check_name="has_testing",
level=ValidationLevel.WARNING,
passed=has_testing,
message=f"Testing checkpoints: {len(checkpoints)}"
))
# Check 4: Reasonable phase count (2-6 phases)
reasonable_phases = 2 <= len(phases) <= 6
report.add(ValidationResult(
check_name="reasonable_phases",
level=ValidationLevel.INFO,
passed=reasonable_phases,
message=f"Phase count: {len(phases)} ({'good' if reasonable_phases else 'unusual'})"
))
# Check 5: Has time estimates
total_time = workflow.get('total_estimated_time')
has_estimate = bool(total_time)
report.add(ValidationResult(
check_name="has_time_estimate",
level=ValidationLevel.INFO,
passed=has_estimate,
message=f"Time estimate: {total_time or 'missing'}"
))
return report
def validate_design_report(self, report_data: Dict) -> ValidationReport:
"""
Validate complete design report.
Args:
report_data: Complete design report
Returns:
ValidationReport
"""
report = ValidationReport()
# Check all required sections present
required_sections = ['requirements', 'components', 'patterns', 'architecture', 'workflow']
sections = report_data.get('sections', {})
for section in required_sections:
has_section = section in sections and sections[section]
report.add(ValidationResult(
check_name=f"has_{section}_section",
level=ValidationLevel.CRITICAL,
passed=has_section,
message=f"Section '{section}': {'present' if has_section else 'MISSING'}"
))
# Check has summary
has_summary = 'summary' in report_data and report_data['summary']
report.add(ValidationResult(
check_name="has_summary",
level=ValidationLevel.WARNING,
passed=has_summary,
message="Summary present" if has_summary else "Missing summary"
))
# Check has scaffolding
has_scaffolding = 'scaffolding' in report_data and report_data['scaffolding']
report.add(ValidationResult(
check_name="has_scaffolding",
level=ValidationLevel.INFO,
passed=has_scaffolding,
message="Code scaffolding included" if has_scaffolding else
"No code scaffolding"
))
# Check has next steps
next_steps = report_data.get('next_steps', [])
has_next_steps = len(next_steps) > 0
report.add(ValidationResult(
check_name="has_next_steps",
level=ValidationLevel.INFO,
passed=has_next_steps,
message=f"Next steps: {len(next_steps)}"
))
return report
def validate_component_fit(component: str, requirement: str) -> bool:
"""
Quick check if component fits requirement.
Args:
component: Component name (e.g., "viewport.Model")
requirement: Requirement description
Returns:
True if component appears suitable
"""
component_lower = component.lower()
requirement_lower = requirement.lower()
# Simple keyword matching
keyword_map = {
'viewport': ['scroll', 'view', 'display', 'content'],
'textinput': ['input', 'text', 'search', 'query'],
'textarea': ['edit', 'multi-line', 'text area'],
'table': ['table', 'tabular', 'rows', 'columns'],
'list': ['list', 'items', 'select', 'choose'],
'progress': ['progress', 'loading', 'installation'],
'spinner': ['loading', 'spinner', 'wait'],
'filepicker': ['file', 'select file', 'choose file']
}
for comp_key, keywords in keyword_map.items():
if comp_key in component_lower:
return any(kw in requirement_lower for kw in keywords)
return False
def main():
"""Test design validator."""
print("Testing Design Validator\n" + "=" * 50)
validator = DesignValidator()
# Test 1: Component selection validation
print("\n1. Testing component selection validation...")
components = {
'primary_components': [
{
'component': 'viewport.Model',
'score': 95,
'justification': 'Scrollable display for log content'
},
{
'component': 'textinput.Model',
'score': 90,
'justification': 'Search query input'
}
]
}
requirements = {
'features': ['display', 'search', 'scroll']
}
report = validator.validate_component_selection(components, requirements)
print(f" {report.get_summary()}")
assert not report.has_critical_issues(), "Should pass for valid components"
print(" ✓ Component selection validated")
# Test 2: Architecture validation
print("\n2. Testing architecture validation...")
architecture = {
'model_struct': 'type model struct {...}',
'message_handlers': {
'tea.KeyMsg': 'handle keyboard',
'tea.WindowSizeMsg': 'handle resize'
},
'view_logic': 'func (m model) View() string {...}',
'diagrams': {
'component_hierarchy': '...'
}
}
report = validator.validate_architecture(architecture)
print(f" {report.get_summary()}")
assert report.all_passed(), "Should pass for complete architecture"
print(" ✓ Architecture validated")
# Test 3: Workflow validation
print("\n3. Testing workflow validation...")
workflow = {
'phases': [
{
'name': 'Phase 1: Setup',
'tasks': [
{'task': 'Initialize project'},
{'task': 'Install dependencies'}
]
},
{
'name': 'Phase 2: Core',
'tasks': [
{'task': 'Implement viewport'}
]
}
],
'testing_checkpoints': ['After Phase 1', 'After Phase 2'],
'total_estimated_time': '2 hours'
}
report = validator.validate_workflow_completeness(workflow)
print(f" {report.get_summary()}")
assert report.all_passed(), "Should pass for complete workflow"
print(" ✓ Workflow validated")
# Test 4: Complete design report validation
print("\n4. Testing complete design report validation...")
design_report = {
'sections': {
'requirements': {...},
'components': {...},
'patterns': {...},
'architecture': {...},
'workflow': {...}
},
'summary': 'TUI design for log viewer',
'scaffolding': 'package main...',
'next_steps': ['Step 1', 'Step 2']
}
report = validator.validate_design_report(design_report)
print(f" {report.get_summary()}")
assert report.all_passed(), "Should pass for complete report"
print(" ✓ Design report validated")
# Test 5: Component fit check
print("\n5. Testing component fit check...")
assert validate_component_fit("viewport.Model", "scrollable log display")
assert validate_component_fit("textinput.Model", "search query input")
assert not validate_component_fit("spinner.Model", "text input field")
print(" ✓ Component fit checks working")
print("\n✅ All tests passed!")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,393 @@
#!/usr/bin/env python3
"""
Requirement validators for Bubble Tea Designer.
Validates user input and extracted requirements.
"""
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass
from enum import Enum
class ValidationLevel(Enum):
"""Severity levels for validation results."""
CRITICAL = "critical"
WARNING = "warning"
INFO = "info"
@dataclass
class ValidationResult:
"""Single validation check result."""
check_name: str
level: ValidationLevel
passed: bool
message: str
details: Optional[Dict] = None
class ValidationReport:
"""Collection of validation results."""
def __init__(self):
self.results: List[ValidationResult] = []
def add(self, result: ValidationResult):
"""Add validation result."""
self.results.append(result)
def has_critical_issues(self) -> bool:
"""Check if any critical issues found."""
return any(
r.level == ValidationLevel.CRITICAL and not r.passed
for r in self.results
)
def all_passed(self) -> bool:
"""Check if all validations passed."""
return all(r.passed for r in self.results)
def get_warnings(self) -> List[str]:
"""Get all warning messages."""
return [
r.message for r in self.results
if r.level == ValidationLevel.WARNING and not r.passed
]
def get_summary(self) -> str:
"""Get summary of validation results."""
total = len(self.results)
passed = sum(1 for r in self.results if r.passed)
critical = sum(
1 for r in self.results
if r.level == ValidationLevel.CRITICAL and not r.passed
)
return (
f"Validation: {passed}/{total} passed "
f"({critical} critical issues)"
)
def to_dict(self) -> Dict:
"""Convert to dictionary."""
return {
'passed': self.all_passed(),
'summary': self.get_summary(),
'warnings': self.get_warnings(),
'critical_issues': [
r.message for r in self.results
if r.level == ValidationLevel.CRITICAL and not r.passed
],
'all_results': [
{
'check': r.check_name,
'level': r.level.value,
'passed': r.passed,
'message': r.message
}
for r in self.results
]
}
class RequirementValidator:
"""Validates TUI requirements and descriptions."""
def validate_description(self, description: str) -> ValidationReport:
"""
Validate user-provided description.
Args:
description: Natural language TUI description
Returns:
ValidationReport with results
"""
report = ValidationReport()
# Check 1: Not empty
report.add(ValidationResult(
check_name="not_empty",
level=ValidationLevel.CRITICAL,
passed=bool(description and description.strip()),
message="Description is empty" if not description else "Description provided"
))
if not description:
return report
# Check 2: Minimum length (at least 10 words)
words = description.split()
min_words = 10
has_min_length = len(words) >= min_words
report.add(ValidationResult(
check_name="minimum_length",
level=ValidationLevel.WARNING,
passed=has_min_length,
message=f"Description has {len(words)} words (recommended: ≥{min_words})"
))
# Check 3: Contains actionable verbs
action_verbs = ['show', 'display', 'view', 'create', 'select', 'navigate',
'edit', 'input', 'track', 'monitor', 'search', 'filter']
has_action = any(verb in description.lower() for verb in action_verbs)
report.add(ValidationResult(
check_name="has_actions",
level=ValidationLevel.WARNING,
passed=has_action,
message="Description contains action verbs" if has_action else
"Consider adding action verbs (show, select, edit, etc.)"
))
# Check 4: Contains data type mentions
data_types = ['file', 'text', 'data', 'table', 'list', 'log', 'config',
'message', 'package', 'item', 'entry']
has_data = any(dtype in description.lower() for dtype in data_types)
report.add(ValidationResult(
check_name="has_data_types",
level=ValidationLevel.INFO,
passed=has_data,
message="Data types mentioned" if has_data else
"No explicit data types mentioned"
))
return report
def validate_requirements(self, requirements: Dict) -> ValidationReport:
"""
Validate extracted requirements structure.
Args:
requirements: Structured requirements dict
Returns:
ValidationReport
"""
report = ValidationReport()
# Check 1: Has archetype
has_archetype = 'archetype' in requirements and requirements['archetype']
report.add(ValidationResult(
check_name="has_archetype",
level=ValidationLevel.CRITICAL,
passed=has_archetype,
message=f"TUI archetype: {requirements.get('archetype', 'MISSING')}"
))
# Check 2: Has features
features = requirements.get('features', [])
has_features = len(features) > 0
report.add(ValidationResult(
check_name="has_features",
level=ValidationLevel.CRITICAL,
passed=has_features,
message=f"Features identified: {len(features)}"
))
# Check 3: Has interactions
interactions = requirements.get('interactions', {})
keyboard_interactions = interactions.get('keyboard', [])
has_interactions = len(keyboard_interactions) > 0
report.add(ValidationResult(
check_name="has_interactions",
level=ValidationLevel.WARNING,
passed=has_interactions,
message=f"Keyboard interactions: {len(keyboard_interactions)}"
))
# Check 4: Has view specification
views = requirements.get('views', '')
has_views = bool(views)
report.add(ValidationResult(
check_name="has_view_spec",
level=ValidationLevel.WARNING,
passed=has_views,
message=f"View type: {views or 'unspecified'}"
))
# Check 5: Completeness (has all expected keys)
expected_keys = ['archetype', 'features', 'interactions', 'data_types', 'views']
missing_keys = set(expected_keys) - set(requirements.keys())
report.add(ValidationResult(
check_name="completeness",
level=ValidationLevel.INFO,
passed=len(missing_keys) == 0,
message=f"Complete structure" if not missing_keys else
f"Missing keys: {missing_keys}"
))
return report
def suggest_clarifications(self, requirements: Dict) -> List[str]:
"""
Suggest clarifying questions based on incomplete requirements.
Args:
requirements: Extracted requirements
Returns:
List of clarifying questions to ask user
"""
questions = []
# Check if archetype is unclear
if not requirements.get('archetype') or requirements['archetype'] == 'general':
questions.append(
"What type of TUI is this? (file manager, installer, dashboard, "
"form, viewer, etc.)"
)
# Check if features are vague
features = requirements.get('features', [])
if len(features) < 2:
questions.append(
"What are the main features/capabilities needed? "
"(e.g., navigation, selection, editing, search, filtering)"
)
# Check if data type is unspecified
data_types = requirements.get('data_types', [])
if not data_types:
questions.append(
"What type of data will the TUI display? "
"(files, text, tabular data, logs, etc.)"
)
# Check if interaction is unspecified
interactions = requirements.get('interactions', {})
if not interactions.get('keyboard') and not interactions.get('mouse'):
questions.append(
"How should users interact? Keyboard only, or mouse support needed?"
)
# Check if view type is unspecified
if not requirements.get('views'):
questions.append(
"Should this be single-view or multi-view? Need tabs or navigation?"
)
return questions
def validate_description_clarity(description: str) -> Tuple[bool, str]:
"""
Quick validation of description clarity.
Args:
description: User description
Returns:
Tuple of (is_clear, message)
"""
validator = RequirementValidator()
report = validator.validate_description(description)
if report.has_critical_issues():
return False, "Description has critical issues: " + report.get_summary()
warnings = report.get_warnings()
if warnings:
return True, "Description OK with suggestions: " + "; ".join(warnings)
return True, "Description is clear"
def validate_requirements_completeness(requirements: Dict) -> Tuple[bool, str]:
"""
Quick validation of requirements completeness.
Args:
requirements: Extracted requirements dict
Returns:
Tuple of (is_complete, message)
"""
validator = RequirementValidator()
report = validator.validate_requirements(requirements)
if report.has_critical_issues():
return False, "Requirements incomplete: " + report.get_summary()
warnings = report.get_warnings()
if warnings:
return True, "Requirements OK with warnings: " + "; ".join(warnings)
return True, "Requirements complete"
def main():
"""Test requirement validator."""
print("Testing Requirement Validator\n" + "=" * 50)
validator = RequirementValidator()
# Test 1: Empty description
print("\n1. Testing empty description...")
report = validator.validate_description("")
print(f" {report.get_summary()}")
assert report.has_critical_issues(), "Should fail for empty description"
print(" ✓ Correctly detected empty description")
# Test 2: Good description
print("\n2. Testing good description...")
good_desc = "Create a file manager TUI with three-column view showing parent directory, current directory, and file preview"
report = validator.validate_description(good_desc)
print(f" {report.get_summary()}")
print(" ✓ Good description validated")
# Test 3: Vague description
print("\n3. Testing vague description...")
vague_desc = "Build a TUI"
report = validator.validate_description(vague_desc)
print(f" {report.get_summary()}")
warnings = report.get_warnings()
if warnings:
print(f" Warnings: {warnings}")
print(" ✓ Vague description detected")
# Test 4: Requirements validation
print("\n4. Testing requirements validation...")
requirements = {
'archetype': 'file-manager',
'features': ['navigation', 'selection', 'preview'],
'interactions': {
'keyboard': ['arrows', 'enter', 'backspace'],
'mouse': []
},
'data_types': ['files', 'directories'],
'views': 'multi'
}
report = validator.validate_requirements(requirements)
print(f" {report.get_summary()}")
assert report.all_passed(), "Should pass for complete requirements"
print(" ✓ Complete requirements validated")
# Test 5: Incomplete requirements
print("\n5. Testing incomplete requirements...")
incomplete = {
'archetype': '',
'features': []
}
report = validator.validate_requirements(incomplete)
print(f" {report.get_summary()}")
assert report.has_critical_issues(), "Should fail for incomplete requirements"
print(" ✓ Incomplete requirements detected")
# Test 6: Clarification suggestions
print("\n6. Testing clarification suggestions...")
questions = validator.suggest_clarifications(incomplete)
print(f" Generated {len(questions)} clarifying questions:")
for i, q in enumerate(questions, 1):
print(f" {i}. {q}")
print(" ✓ Clarifications generated")
print("\n✅ All tests passed!")
if __name__ == "__main__":
main()