🎯 MAJOR CLI ARCHITECTURE CONSOLIDATION: ✅ Added Missing CLI Entry Points: • tddai = "tddai_cli:main" - TDD workflow management • issue = "cli.issue_cli:main" - Pure issue management • All three CLIs now properly installed: markitect, tddai, issue 🧹 Eliminated Functionality Redundancy: • Removed issue commands from markitect/cli.py (clean separation) • MarkiTect now focuses purely on document processing • TDD workflow in tddai CLI, issue management in issue CLI 🏗️ Clean Architecture Implementation: • Created cli/issue_cli.py - Dedicated pure issue management • Enhanced cli/commands/export.py with export_issues_csv/json • Updated cli/core.py with proper export method delegation • Fixed pyproject.toml to include all required packages 🧪 Comprehensive Testing: • Added tests/test_cli_consolidation.py - Prevents CLI regression • Tests ensure all CLIs are installed and functional • Tests verify no functionality duplication • Regression protection against missing CLI commands 📋 Clear Separation of Concerns: • markitect CLI - Document processing, templates, performance • tddai CLI - TDD workflow, workspace management, coverage • issue CLI - Pure issue operations, project management, export 🔧 Package Configuration: • Updated pyproject.toml to include cli*, tddai*, services*, etc. • Added py-modules for tddai_cli standalone module • Fixed import paths and dependencies This consolidation resolves the major redundancy identified in issues functionality and ensures proper CLI interfaces are available and tested. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
178 lines
7.0 KiB
Python
178 lines
7.0 KiB
Python
"""
|
|
Issue service - business logic for issue operations.
|
|
"""
|
|
|
|
from typing import List, Dict, Any, Optional
|
|
from datetime import datetime
|
|
|
|
from tddai import IssueFetcher, TddaiError
|
|
from tddai.issue_creator import IssueCreator
|
|
from gitea.models import Issue, Priority
|
|
|
|
|
|
class IssueService:
|
|
"""Service for issue operations."""
|
|
|
|
def __init__(self):
|
|
self.issue_fetcher = IssueFetcher()
|
|
self.issue_creator = IssueCreator()
|
|
|
|
def get_issue(self, issue_number: int) -> Issue:
|
|
"""Get a specific issue by number."""
|
|
return self.issue_fetcher.fetch_issue(issue_number)
|
|
|
|
def list_issues(self, state: str = "all") -> List[Issue]:
|
|
"""List issues with optional state filter."""
|
|
return self.issue_fetcher.fetch_issues(state)
|
|
|
|
def list_open_issues(self) -> List[Issue]:
|
|
"""List only open issues."""
|
|
return self.issue_fetcher.fetch_open_issues()
|
|
|
|
def create_issue(self, title: str, body: str, **kwargs) -> Dict[str, Any]:
|
|
"""Create a new issue."""
|
|
return self.issue_creator.create_issue(title, body, **kwargs)
|
|
|
|
def create_enhancement_issue(self, title: str, use_case: str,
|
|
technical_requirements: str = "",
|
|
acceptance_criteria: Optional[List[str]] = None,
|
|
dependencies: Optional[List[str]] = None,
|
|
priority: str = "Medium") -> Dict[str, Any]:
|
|
"""Create a structured enhancement issue."""
|
|
return self.issue_creator.create_enhancement_issue(
|
|
title, use_case, technical_requirements,
|
|
acceptance_criteria, dependencies, priority
|
|
)
|
|
|
|
def create_from_template(self, template_file: str, **kwargs) -> Dict[str, Any]:
|
|
"""Create issue from template file."""
|
|
return self.issue_creator.create_from_template(template_file, **kwargs)
|
|
|
|
def close_issue(self, issue_number: int, comment: str = "") -> Dict[str, Any]:
|
|
"""Close an issue with optional comment."""
|
|
from gitea import GiteaClient, GiteaConfig
|
|
from tddai.config import get_config
|
|
import os
|
|
|
|
# Get config and create Gitea client
|
|
config = get_config()
|
|
gitea_config = GiteaConfig.from_tddai_config(config)
|
|
auth_token = os.getenv('GITEA_API_TOKEN')
|
|
if auth_token:
|
|
gitea_config.auth_token = auth_token
|
|
|
|
gitea_client = GiteaClient(gitea_config)
|
|
|
|
# Close the issue
|
|
issue = gitea_client.issues.close(issue_number)
|
|
|
|
# If comment provided, add it (this would need API support for comments)
|
|
# For now, we'll just close the issue
|
|
|
|
# Convert to dict format for consistency
|
|
return gitea_client.issues.to_dict(issue)
|
|
|
|
def get_issue_details(self, issue_number: int) -> Dict[str, Any]:
|
|
"""Get comprehensive issue details for display purposes."""
|
|
issue = self.get_issue(issue_number)
|
|
|
|
# Get additional project management information
|
|
from tddai.project_manager import ProjectManager
|
|
project_mgr = ProjectManager()
|
|
|
|
# Get detailed issue data via API for milestone and project information
|
|
from tddai.config import get_config
|
|
config = get_config()
|
|
issue_url = f"{config.issues_api_url}/{issue_number}"
|
|
detailed_issue = project_mgr._make_api_call('GET', issue_url)
|
|
|
|
# Process labels
|
|
labels = detailed_issue.get('labels', [])
|
|
state_labels = [l['name'] for l in labels if l['name'].startswith('status:')]
|
|
priority_labels = [l['name'] for l in labels if l['name'].startswith('priority:')]
|
|
type_labels = [l['name'] for l in labels if l['name'].startswith('type:')]
|
|
other_labels = [l['name'] for l in labels if not any(l['name'].startswith(p) for p in ['status:', 'priority:', 'type:'])]
|
|
|
|
# Determine project column/state
|
|
if detailed_issue.get('state') == 'closed':
|
|
if any(l['name'] == 'status:done' for l in labels):
|
|
column = "Done"
|
|
else:
|
|
column = "Closed"
|
|
else:
|
|
state_labels = [l['name'] for l in labels if l['name'].startswith('status:')]
|
|
if state_labels:
|
|
state = state_labels[0].replace('status:', '')
|
|
column_map = {
|
|
'todo': 'Todo',
|
|
'active': 'Active',
|
|
'review': 'Review',
|
|
'blocked': 'Blocked'
|
|
}
|
|
column = column_map.get(state, 'Todo')
|
|
else:
|
|
column = "Todo"
|
|
|
|
return {
|
|
'number': issue.number,
|
|
'title': issue.title,
|
|
'body': issue.body,
|
|
'state': issue.state,
|
|
'created_at': issue.created_at,
|
|
'updated_at': issue.updated_at,
|
|
'html_url': issue.html_url,
|
|
'assignee': issue.assignee.login if issue.assignee else None,
|
|
'milestone': detailed_issue.get('milestone'),
|
|
'state_label': state_labels[0].replace('status:', '').title() if state_labels else "No state label",
|
|
'priority_label': priority_labels[0].replace('priority:', '').title() if priority_labels else "No priority set",
|
|
'type_labels': [l.replace('type:', '').title() for l in type_labels],
|
|
'other_labels': other_labels,
|
|
'kanban_column': column
|
|
}
|
|
|
|
def get_issue_summary(self, issue_number: int) -> Dict[str, Any]:
|
|
"""Get issue summary for display purposes."""
|
|
issue = self.get_issue(issue_number)
|
|
|
|
return {
|
|
'number': issue.number,
|
|
'title': issue.title,
|
|
'body': issue.body,
|
|
'state': issue.state,
|
|
'priority': issue.priority,
|
|
'status': issue.status,
|
|
'created_at': issue.created_at,
|
|
'updated_at': issue.updated_at,
|
|
'html_url': issue.html_url,
|
|
'assignee': issue.assignee.login if issue.assignee else None,
|
|
'labels': [label.name for label in issue.labels],
|
|
'has_milestone': issue.milestone is not None,
|
|
'milestone_title': issue.milestone.title if issue.milestone else None
|
|
}
|
|
|
|
def analyze_coverage(self, issue_number: int) -> Dict[str, Any]:
|
|
"""Analyze test coverage for a specific issue.
|
|
|
|
Args:
|
|
issue_number: The issue number to analyze
|
|
|
|
Returns:
|
|
Coverage analysis data for the issue
|
|
|
|
Raises:
|
|
IssueError: When coverage analysis fails
|
|
"""
|
|
from tddai.coverage_analyzer import CoverageAnalyzer
|
|
|
|
analyzer = CoverageAnalyzer()
|
|
assessment = analyzer.analyze_issue_coverage(issue_number)
|
|
|
|
return {
|
|
'issue_number': assessment.issue_number,
|
|
'issue_title': assessment.issue_title,
|
|
'coverage_percentage': assessment.coverage_percentage,
|
|
'requirements': assessment.requirements,
|
|
'existing_tests': assessment.existing_tests,
|
|
'coverage_gaps': assessment.coverage_gaps,
|
|
'recommendations': assessment.recommendations
|
|
} |