Initial commit
This commit is contained in:
244
scripts/analyze_requirements.py
Normal file
244
scripts/analyze_requirements.py
Normal 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()
|
||||
67
scripts/design_architecture.py
Normal file
67
scripts/design_architecture.py
Normal 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
224
scripts/design_tui.py
Normal 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()
|
||||
77
scripts/generate_workflow.py
Normal file
77
scripts/generate_workflow.py
Normal 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
161
scripts/map_components.py
Normal 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()
|
||||
40
scripts/select_patterns.py
Normal file
40
scripts/select_patterns.py
Normal 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"
|
||||
}
|
||||
59
scripts/utils/ascii_diagram.py
Normal file
59
scripts/utils/ascii_diagram.py
Normal 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)
|
||||
379
scripts/utils/component_matcher.py
Normal file
379
scripts/utils/component_matcher.py
Normal 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
40
scripts/utils/helpers.py
Normal 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)"
|
||||
334
scripts/utils/inventory_loader.py
Normal file
334
scripts/utils/inventory_loader.py
Normal 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())
|
||||
140
scripts/utils/template_generator.py
Normal file
140
scripts/utils/template_generator.py
Normal 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)
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
26
scripts/utils/validators/__init__.py
Normal file
26
scripts/utils/validators/__init__.py
Normal 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'
|
||||
]
|
||||
425
scripts/utils/validators/design_validator.py
Normal file
425
scripts/utils/validators/design_validator.py
Normal 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()
|
||||
393
scripts/utils/validators/requirement_validator.py
Normal file
393
scripts/utils/validators/requirement_validator.py
Normal 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()
|
||||
Reference in New Issue
Block a user