Files
gh-human-frontier-labs-inc-…/scripts/suggest_architecture.py
2025-11-29 18:47:33 +08:00

737 lines
22 KiB
Python

#!/usr/bin/env python3
"""
Suggest architectural improvements for Bubble Tea applications.
Analyzes complexity and recommends patterns like model trees, composable views, etc.
"""
import os
import re
import json
from pathlib import Path
from typing import Dict, List, Any, Tuple, Optional
def suggest_architecture(code_path: str, complexity_level: str = "auto") -> Dict[str, Any]:
"""
Analyze code and suggest architectural improvements.
Args:
code_path: Path to Go file or directory
complexity_level: "auto" (detect), "simple", "medium", "complex"
Returns:
Dictionary containing:
- current_pattern: Detected architectural pattern
- complexity_score: 0-100 (higher = more complex)
- recommended_pattern: Suggested pattern for improvement
- refactoring_steps: List of steps to implement
- code_templates: Example code for new pattern
- validation: Validation report
"""
path = Path(code_path)
if not path.exists():
return {
"error": f"Path not found: {code_path}",
"validation": {"status": "error", "summary": "Invalid path"}
}
# Collect all .go files
go_files = []
if path.is_file():
if path.suffix == '.go':
go_files = [path]
else:
go_files = list(path.glob('**/*.go'))
if not go_files:
return {
"error": "No .go files found",
"validation": {"status": "error", "summary": "No Go files"}
}
# Read all code
all_content = ""
for go_file in go_files:
try:
all_content += go_file.read_text() + "\n"
except Exception:
pass
# Analyze current architecture
current_pattern = _detect_current_pattern(all_content)
complexity_score = _calculate_complexity(all_content, go_files)
# Auto-detect complexity level if needed
if complexity_level == "auto":
if complexity_score < 30:
complexity_level = "simple"
elif complexity_score < 70:
complexity_level = "medium"
else:
complexity_level = "complex"
# Generate recommendations
recommended_pattern = _recommend_pattern(current_pattern, complexity_score, complexity_level)
refactoring_steps = _generate_refactoring_steps(current_pattern, recommended_pattern, all_content)
code_templates = _generate_code_templates(recommended_pattern, all_content)
# Summary
if recommended_pattern == current_pattern:
summary = f"✅ Current architecture ({current_pattern}) is appropriate for complexity level"
else:
summary = f"💡 Recommend refactoring from {current_pattern} to {recommended_pattern}"
# Validation
validation = {
"status": "pass" if recommended_pattern == current_pattern else "info",
"summary": summary,
"checks": {
"complexity_analyzed": complexity_score >= 0,
"pattern_detected": current_pattern != "unknown",
"has_recommendations": len(refactoring_steps) > 0,
"has_templates": len(code_templates) > 0
}
}
return {
"current_pattern": current_pattern,
"complexity_score": complexity_score,
"complexity_level": complexity_level,
"recommended_pattern": recommended_pattern,
"refactoring_steps": refactoring_steps,
"code_templates": code_templates,
"summary": summary,
"analysis": {
"files_analyzed": len(go_files),
"model_count": _count_models(all_content),
"view_functions": _count_view_functions(all_content),
"state_fields": _count_state_fields(all_content)
},
"validation": validation
}
def _detect_current_pattern(content: str) -> str:
"""Detect the current architectural pattern."""
# Check for various patterns
patterns_detected = []
# Pattern 1: Flat Model (single model struct, no child models)
has_model = bool(re.search(r'type\s+\w*[Mm]odel\s+struct', content))
has_child_models = bool(re.search(r'\w+Model\s+\w+Model', content))
if has_model and not has_child_models:
patterns_detected.append("flat_model")
# Pattern 2: Model Tree (parent model with child models)
if has_child_models:
patterns_detected.append("model_tree")
# Pattern 3: Multi-view (multiple view rendering based on state)
has_view_switcher = bool(re.search(r'switch\s+m\.\w*(view|mode|screen|state)', content, re.IGNORECASE))
if has_view_switcher:
patterns_detected.append("multi_view")
# Pattern 4: Component-based (using Bubble Tea components like list, viewport, etc.)
bubbletea_components = [
'list.Model',
'viewport.Model',
'textinput.Model',
'textarea.Model',
'table.Model',
'progress.Model',
'spinner.Model'
]
component_count = sum(1 for comp in bubbletea_components if comp in content)
if component_count >= 3:
patterns_detected.append("component_based")
elif component_count >= 1:
patterns_detected.append("uses_components")
# Pattern 5: State Machine (explicit state enums/constants)
has_state_enum = bool(re.search(r'type\s+\w*State\s+(int|string)', content))
has_iota_states = bool(re.search(r'const\s+\(\s*\w+State\s+\w*State\s+=\s+iota', content))
if has_state_enum or has_iota_states:
patterns_detected.append("state_machine")
# Pattern 6: Event-driven (heavy use of custom messages)
custom_msg_count = len(re.findall(r'type\s+\w+Msg\s+struct', content))
if custom_msg_count >= 5:
patterns_detected.append("event_driven")
# Return the most dominant pattern
if "model_tree" in patterns_detected:
return "model_tree"
elif "state_machine" in patterns_detected and "multi_view" in patterns_detected:
return "state_machine_multi_view"
elif "component_based" in patterns_detected:
return "component_based"
elif "multi_view" in patterns_detected:
return "multi_view"
elif "flat_model" in patterns_detected:
return "flat_model"
elif has_model:
return "basic_model"
else:
return "unknown"
def _calculate_complexity(content: str, files: List[Path]) -> int:
"""Calculate complexity score (0-100)."""
score = 0
# Factor 1: Number of files (10 points max)
file_count = len(files)
score += min(10, file_count * 2)
# Factor 2: Model field count (20 points max)
model_match = re.search(r'type\s+(\w*[Mm]odel)\s+struct\s*\{([^}]+)\}', content, re.DOTALL)
if model_match:
model_body = model_match.group(2)
field_count = len([line for line in model_body.split('\n')
if line.strip() and not line.strip().startswith('//')])
score += min(20, field_count)
# Factor 3: Number of Update() branches (20 points max)
update_match = re.search(r'func\s+\([^)]+\)\s+Update\s*\([^)]+\)\s*\([^)]+\)\s*\{(.+?)^func\s',
content, re.DOTALL | re.MULTILINE)
if update_match:
update_body = update_match.group(1)
case_count = len(re.findall(r'case\s+', update_body))
score += min(20, case_count * 2)
# Factor 4: View() complexity (15 points max)
view_match = re.search(r'func\s+\([^)]+\)\s+View\s*\(\s*\)\s+string\s*\{(.+?)^func\s',
content, re.DOTALL | re.MULTILINE)
if view_match:
view_body = view_match.group(1)
view_lines = len(view_body.split('\n'))
score += min(15, view_lines // 2)
# Factor 5: Custom message types (10 points max)
custom_msg_count = len(re.findall(r'type\s+\w+Msg\s+struct', content))
score += min(10, custom_msg_count * 2)
# Factor 6: Number of views/screens (15 points max)
view_count = len(re.findall(r'func\s+\([^)]+\)\s+render\w+', content, re.IGNORECASE))
score += min(15, view_count * 3)
# Factor 7: Use of channels/goroutines (10 points max)
has_channels = len(re.findall(r'make\s*\(\s*chan\s+', content))
has_goroutines = len(re.findall(r'\bgo\s+func', content))
score += min(10, (has_channels + has_goroutines) * 2)
return min(100, score)
def _recommend_pattern(current: str, complexity: int, level: str) -> str:
"""Recommend architectural pattern based on current state and complexity."""
# Simple apps (< 30 complexity)
if complexity < 30:
if current in ["unknown", "basic_model"]:
return "flat_model" # Simple flat model is fine
return current # Keep current pattern
# Medium complexity (30-70)
elif complexity < 70:
if current == "flat_model":
return "multi_view" # Evolve to multi-view
elif current == "basic_model":
return "component_based" # Start using components
return current
# High complexity (70+)
else:
if current in ["flat_model", "multi_view"]:
return "model_tree" # Need hierarchy
elif current == "component_based":
return "model_tree_with_components" # Combine patterns
return current
def _count_models(content: str) -> int:
"""Count model structs."""
return len(re.findall(r'type\s+\w*[Mm]odel\s+struct', content))
def _count_view_functions(content: str) -> int:
"""Count view rendering functions."""
return len(re.findall(r'func\s+\([^)]+\)\s+(View|render\w+)', content, re.IGNORECASE))
def _count_state_fields(content: str) -> int:
"""Count state fields in model."""
model_match = re.search(r'type\s+(\w*[Mm]odel)\s+struct\s*\{([^}]+)\}', content, re.DOTALL)
if not model_match:
return 0
model_body = model_match.group(2)
return len([line for line in model_body.split('\n')
if line.strip() and not line.strip().startswith('//')])
def _generate_refactoring_steps(current: str, recommended: str, content: str) -> List[str]:
"""Generate step-by-step refactoring guide."""
if current == recommended:
return ["No refactoring needed - current architecture is appropriate"]
steps = []
# Flat Model → Multi-view
if current == "flat_model" and recommended == "multi_view":
steps = [
"1. Add view state enum to model",
"2. Create separate render functions for each view",
"3. Add view switching logic in Update()",
"4. Implement switch statement in View() to route to render functions",
"5. Add keyboard shortcuts for view navigation"
]
# Flat Model → Model Tree
elif current == "flat_model" and recommended == "model_tree":
steps = [
"1. Identify logical groupings of fields in current model",
"2. Create child model structs for each grouping",
"3. Add Init() methods to child models",
"4. Create parent model with child model fields",
"5. Implement message routing in parent's Update()",
"6. Delegate rendering to child models in View()",
"7. Test each child model independently"
]
# Multi-view → Model Tree
elif current == "multi_view" and recommended == "model_tree":
steps = [
"1. Convert each view into a separate child model",
"2. Extract view-specific state into child models",
"3. Create parent router model with activeView field",
"4. Implement message routing based on activeView",
"5. Move view rendering logic into child models",
"6. Add inter-model communication via custom messages"
]
# Component-based → Model Tree with Components
elif current == "component_based" and recommended == "model_tree_with_components":
steps = [
"1. Group related components into logical views",
"2. Create view models that own related components",
"3. Create parent model to manage view models",
"4. Implement message routing to active view",
"5. Keep component updates within their view models",
"6. Compose final view from view model renders"
]
# Basic Model → Component-based
elif current == "basic_model" and recommended == "component_based":
steps = [
"1. Identify UI patterns that match Bubble Tea components",
"2. Replace custom text input with textinput.Model",
"3. Replace custom list with list.Model",
"4. Replace custom scrolling with viewport.Model",
"5. Update Init() to initialize components",
"6. Route messages to components in Update()",
"7. Compose View() using component.View() calls"
]
# Generic fallback
else:
steps = [
f"1. Analyze current {current} pattern",
f"2. Study {recommended} pattern examples",
"3. Plan gradual migration strategy",
"4. Implement incrementally with tests",
"5. Validate each step before proceeding"
]
return steps
def _generate_code_templates(pattern: str, existing_code: str) -> Dict[str, str]:
"""Generate code templates for recommended pattern."""
templates = {}
if pattern == "model_tree":
templates["parent_model"] = '''// Parent model manages child models
type appModel struct {
activeView int
// Child models
listView listViewModel
detailView detailViewModel
searchView searchViewModel
}
func (m appModel) Init() tea.Cmd {
return tea.Batch(
m.listView.Init(),
m.detailView.Init(),
m.searchView.Init(),
)
}
func (m appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
// Global navigation
if key, ok := msg.(tea.KeyMsg); ok {
switch key.String() {
case "1":
m.activeView = 0
return m, nil
case "2":
m.activeView = 1
return m, nil
case "3":
m.activeView = 2
return m, nil
}
}
// Route to active child
switch m.activeView {
case 0:
m.listView, cmd = m.listView.Update(msg)
case 1:
m.detailView, cmd = m.detailView.Update(msg)
case 2:
m.searchView, cmd = m.searchView.Update(msg)
}
return m, cmd
}
func (m appModel) View() string {
switch m.activeView {
case 0:
return m.listView.View()
case 1:
return m.detailView.View()
case 2:
return m.searchView.View()
}
return ""
}'''
templates["child_model"] = '''// Child model handles its own state and rendering
type listViewModel struct {
items []string
cursor int
selected map[int]bool
}
func (m listViewModel) Init() tea.Cmd {
return nil
}
func (m listViewModel) Update(msg tea.Msg) (listViewModel, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "up", "k":
if m.cursor > 0 {
m.cursor--
}
case "down", "j":
if m.cursor < len(m.items)-1 {
m.cursor++
}
case " ":
m.selected[m.cursor] = !m.selected[m.cursor]
}
}
return m, nil
}
func (m listViewModel) View() string {
s := "Select items:\\n\\n"
for i, item := range m.items {
cursor := " "
if m.cursor == i {
cursor = ">"
}
checked := " "
if m.selected[i] {
checked = "x"
}
s += fmt.Sprintf("%s [%s] %s\\n", cursor, checked, item)
}
return s
}'''
templates["message_passing"] = '''// Custom message for inter-model communication
type itemSelectedMsg struct {
itemID string
}
// In listViewModel:
func (m listViewModel) Update(msg tea.Msg) (listViewModel, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
if msg.String() == "enter" {
// Send message to parent (who routes to detail view)
return m, func() tea.Msg {
return itemSelectedMsg{itemID: m.items[m.cursor]}
}
}
}
return m, nil
}
// In appModel:
func (m appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case itemSelectedMsg:
// List selected item, switch to detail view
m.detailView.LoadItem(msg.itemID)
m.activeView = 1 // Switch to detail
return m, nil
}
// Route to children...
return m, nil
}'''
elif pattern == "multi_view":
templates["view_state"] = '''type viewState int
const (
listView viewState = iota
detailView
searchView
)
type model struct {
currentView viewState
// View-specific state
listItems []string
listCursor int
detailItem string
searchQuery string
}'''
templates["view_switching"] = '''func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
// Global navigation
switch msg.String() {
case "1":
m.currentView = listView
return m, nil
case "2":
m.currentView = detailView
return m, nil
case "3":
m.currentView = searchView
return m, nil
}
// View-specific handling
switch m.currentView {
case listView:
return m.updateListView(msg)
case detailView:
return m.updateDetailView(msg)
case searchView:
return m.updateSearchView(msg)
}
}
return m, nil
}
func (m model) View() string {
switch m.currentView {
case listView:
return m.renderListView()
case detailView:
return m.renderDetailView()
case searchView:
return m.renderSearchView()
}
return ""
}'''
elif pattern == "component_based":
templates["using_components"] = '''import (
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
)
type model struct {
list list.Model
search textinput.Model
viewer viewport.Model
activeComponent int
}
func initialModel() model {
// Initialize components
items := []list.Item{
item{title: "Item 1", desc: "Description"},
item{title: "Item 2", desc: "Description"},
}
l := list.New(items, list.NewDefaultDelegate(), 20, 10)
l.Title = "Items"
ti := textinput.New()
ti.Placeholder = "Search..."
ti.Focus()
vp := viewport.New(80, 20)
return model{
list: l,
search: ti,
viewer: vp,
activeComponent: 0,
}
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
// Route to active component
switch m.activeComponent {
case 0:
m.list, cmd = m.list.Update(msg)
case 1:
m.search, cmd = m.search.Update(msg)
case 2:
m.viewer, cmd = m.viewer.Update(msg)
}
return m, cmd
}
func (m model) View() string {
return lipgloss.JoinVertical(
lipgloss.Left,
m.search.View(),
m.list.View(),
m.viewer.View(),
)
}'''
elif pattern == "state_machine_multi_view":
templates["state_machine"] = '''type appState int
const (
loadingState appState = iota
listState
detailState
errorState
)
type model struct {
state appState
prevState appState
// State data
items []string
selected string
error error
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case itemsLoadedMsg:
m.items = msg.items
m.state = listState
return m, nil
case itemSelectedMsg:
m.selected = msg.item
m.state = detailState
return m, loadItemDetails
case errorMsg:
m.prevState = m.state
m.state = errorState
m.error = msg.err
return m, nil
case tea.KeyMsg:
if msg.String() == "esc" && m.state == errorState {
m.state = m.prevState // Return to previous state
return m, nil
}
}
// State-specific update
switch m.state {
case listState:
return m.updateList(msg)
case detailState:
return m.updateDetail(msg)
}
return m, nil
}
func (m model) View() string {
switch m.state {
case loadingState:
return "Loading..."
case listState:
return m.renderList()
case detailState:
return m.renderDetail()
case errorState:
return fmt.Sprintf("Error: %v\\nPress ESC to continue", m.error)
}
return ""
}'''
return templates
def validate_architecture_suggestion(result: Dict[str, Any]) -> Dict[str, Any]:
"""Validate architecture suggestion result."""
if 'error' in result:
return {"status": "error", "summary": result['error']}
validation = result.get('validation', {})
status = validation.get('status', 'unknown')
summary = validation.get('summary', 'Architecture analysis complete')
checks = [
(result.get('current_pattern') is not None, "Pattern detected"),
(result.get('complexity_score') is not None, "Complexity calculated"),
(result.get('recommended_pattern') is not None, "Recommendation generated"),
(len(result.get('refactoring_steps', [])) > 0, "Has refactoring steps"),
]
all_pass = all(check[0] for check in checks)
return {
"status": status,
"summary": summary,
"checks": {check[1]: check[0] for check in checks},
"valid": all_pass
}
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage: suggest_architecture.py <code_path> [complexity_level]")
sys.exit(1)
code_path = sys.argv[1]
complexity_level = sys.argv[2] if len(sys.argv) > 2 else "auto"
result = suggest_architecture(code_path, complexity_level)
print(json.dumps(result, indent=2))