titan-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- titan_cli/__init__.py +3 -0
- titan_cli/__main__.py +4 -0
- titan_cli/ai/__init__.py +0 -0
- titan_cli/ai/agents/__init__.py +15 -0
- titan_cli/ai/agents/base.py +152 -0
- titan_cli/ai/client.py +170 -0
- titan_cli/ai/constants.py +56 -0
- titan_cli/ai/exceptions.py +48 -0
- titan_cli/ai/models.py +34 -0
- titan_cli/ai/oauth_helper.py +120 -0
- titan_cli/ai/providers/__init__.py +9 -0
- titan_cli/ai/providers/anthropic.py +117 -0
- titan_cli/ai/providers/base.py +75 -0
- titan_cli/ai/providers/gemini.py +278 -0
- titan_cli/cli.py +59 -0
- titan_cli/clients/__init__.py +1 -0
- titan_cli/clients/gcloud_client.py +52 -0
- titan_cli/core/__init__.py +3 -0
- titan_cli/core/config.py +274 -0
- titan_cli/core/discovery.py +51 -0
- titan_cli/core/errors.py +81 -0
- titan_cli/core/models.py +52 -0
- titan_cli/core/plugins/available.py +36 -0
- titan_cli/core/plugins/models.py +67 -0
- titan_cli/core/plugins/plugin_base.py +108 -0
- titan_cli/core/plugins/plugin_registry.py +163 -0
- titan_cli/core/secrets.py +141 -0
- titan_cli/core/workflows/__init__.py +22 -0
- titan_cli/core/workflows/models.py +88 -0
- titan_cli/core/workflows/project_step_source.py +86 -0
- titan_cli/core/workflows/workflow_exceptions.py +17 -0
- titan_cli/core/workflows/workflow_filter_service.py +137 -0
- titan_cli/core/workflows/workflow_registry.py +419 -0
- titan_cli/core/workflows/workflow_sources.py +307 -0
- titan_cli/engine/__init__.py +39 -0
- titan_cli/engine/builder.py +159 -0
- titan_cli/engine/context.py +82 -0
- titan_cli/engine/mock_context.py +176 -0
- titan_cli/engine/results.py +91 -0
- titan_cli/engine/steps/ai_assistant_step.py +185 -0
- titan_cli/engine/steps/command_step.py +93 -0
- titan_cli/engine/utils/__init__.py +3 -0
- titan_cli/engine/utils/venv.py +31 -0
- titan_cli/engine/workflow_executor.py +187 -0
- titan_cli/external_cli/__init__.py +0 -0
- titan_cli/external_cli/configs.py +17 -0
- titan_cli/external_cli/launcher.py +65 -0
- titan_cli/messages.py +121 -0
- titan_cli/ui/tui/__init__.py +205 -0
- titan_cli/ui/tui/__previews__/statusbar_preview.py +88 -0
- titan_cli/ui/tui/app.py +113 -0
- titan_cli/ui/tui/icons.py +70 -0
- titan_cli/ui/tui/screens/__init__.py +24 -0
- titan_cli/ui/tui/screens/ai_config.py +498 -0
- titan_cli/ui/tui/screens/ai_config_wizard.py +882 -0
- titan_cli/ui/tui/screens/base.py +110 -0
- titan_cli/ui/tui/screens/cli_launcher.py +151 -0
- titan_cli/ui/tui/screens/global_setup_wizard.py +363 -0
- titan_cli/ui/tui/screens/main_menu.py +162 -0
- titan_cli/ui/tui/screens/plugin_config_wizard.py +550 -0
- titan_cli/ui/tui/screens/plugin_management.py +377 -0
- titan_cli/ui/tui/screens/project_setup_wizard.py +686 -0
- titan_cli/ui/tui/screens/workflow_execution.py +592 -0
- titan_cli/ui/tui/screens/workflows.py +249 -0
- titan_cli/ui/tui/textual_components.py +537 -0
- titan_cli/ui/tui/textual_workflow_executor.py +405 -0
- titan_cli/ui/tui/theme.py +102 -0
- titan_cli/ui/tui/widgets/__init__.py +40 -0
- titan_cli/ui/tui/widgets/button.py +108 -0
- titan_cli/ui/tui/widgets/header.py +116 -0
- titan_cli/ui/tui/widgets/panel.py +81 -0
- titan_cli/ui/tui/widgets/status_bar.py +115 -0
- titan_cli/ui/tui/widgets/table.py +77 -0
- titan_cli/ui/tui/widgets/text.py +177 -0
- titan_cli/utils/__init__.py +0 -0
- titan_cli/utils/autoupdate.py +155 -0
- titan_cli-0.1.0.dist-info/METADATA +149 -0
- titan_cli-0.1.0.dist-info/RECORD +146 -0
- titan_cli-0.1.0.dist-info/WHEEL +4 -0
- titan_cli-0.1.0.dist-info/entry_points.txt +9 -0
- titan_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
- titan_plugin_git/__init__.py +1 -0
- titan_plugin_git/clients/__init__.py +8 -0
- titan_plugin_git/clients/git_client.py +772 -0
- titan_plugin_git/exceptions.py +40 -0
- titan_plugin_git/messages.py +112 -0
- titan_plugin_git/models.py +39 -0
- titan_plugin_git/plugin.py +118 -0
- titan_plugin_git/steps/__init__.py +1 -0
- titan_plugin_git/steps/ai_commit_message_step.py +171 -0
- titan_plugin_git/steps/branch_steps.py +104 -0
- titan_plugin_git/steps/commit_step.py +80 -0
- titan_plugin_git/steps/push_step.py +63 -0
- titan_plugin_git/steps/status_step.py +59 -0
- titan_plugin_git/workflows/__previews__/__init__.py +1 -0
- titan_plugin_git/workflows/__previews__/commit_ai_preview.py +124 -0
- titan_plugin_git/workflows/commit-ai.yaml +28 -0
- titan_plugin_github/__init__.py +11 -0
- titan_plugin_github/agents/__init__.py +6 -0
- titan_plugin_github/agents/config_loader.py +130 -0
- titan_plugin_github/agents/issue_generator.py +353 -0
- titan_plugin_github/agents/pr_agent.py +528 -0
- titan_plugin_github/clients/__init__.py +8 -0
- titan_plugin_github/clients/github_client.py +1105 -0
- titan_plugin_github/config/__init__.py +0 -0
- titan_plugin_github/config/pr_agent.toml +85 -0
- titan_plugin_github/exceptions.py +28 -0
- titan_plugin_github/messages.py +88 -0
- titan_plugin_github/models.py +330 -0
- titan_plugin_github/plugin.py +131 -0
- titan_plugin_github/steps/__init__.py +12 -0
- titan_plugin_github/steps/ai_pr_step.py +172 -0
- titan_plugin_github/steps/create_pr_step.py +86 -0
- titan_plugin_github/steps/github_prompt_steps.py +171 -0
- titan_plugin_github/steps/issue_steps.py +143 -0
- titan_plugin_github/steps/preview_step.py +40 -0
- titan_plugin_github/utils.py +82 -0
- titan_plugin_github/workflows/__previews__/__init__.py +1 -0
- titan_plugin_github/workflows/__previews__/create_pr_ai_preview.py +140 -0
- titan_plugin_github/workflows/create-issue-ai.yaml +32 -0
- titan_plugin_github/workflows/create-pr-ai.yaml +49 -0
- titan_plugin_jira/__init__.py +8 -0
- titan_plugin_jira/agents/__init__.py +6 -0
- titan_plugin_jira/agents/config_loader.py +154 -0
- titan_plugin_jira/agents/jira_agent.py +553 -0
- titan_plugin_jira/agents/prompts.py +364 -0
- titan_plugin_jira/agents/response_parser.py +435 -0
- titan_plugin_jira/agents/token_tracker.py +223 -0
- titan_plugin_jira/agents/validators.py +246 -0
- titan_plugin_jira/clients/jira_client.py +745 -0
- titan_plugin_jira/config/jira_agent.toml +92 -0
- titan_plugin_jira/config/templates/issue_analysis.md.j2 +78 -0
- titan_plugin_jira/exceptions.py +37 -0
- titan_plugin_jira/formatters/__init__.py +6 -0
- titan_plugin_jira/formatters/markdown_formatter.py +245 -0
- titan_plugin_jira/messages.py +115 -0
- titan_plugin_jira/models.py +89 -0
- titan_plugin_jira/plugin.py +264 -0
- titan_plugin_jira/steps/ai_analyze_issue_step.py +105 -0
- titan_plugin_jira/steps/get_issue_step.py +82 -0
- titan_plugin_jira/steps/prompt_select_issue_step.py +80 -0
- titan_plugin_jira/steps/search_saved_query_step.py +238 -0
- titan_plugin_jira/utils/__init__.py +13 -0
- titan_plugin_jira/utils/issue_sorter.py +140 -0
- titan_plugin_jira/utils/saved_queries.py +150 -0
- titan_plugin_jira/workflows/analyze-jira-issues.yaml +34 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# plugins/titan-plugin-jira/titan_plugin_jira/agents/validators.py
|
|
2
|
+
"""
|
|
3
|
+
Input validation for JiraAgent.
|
|
4
|
+
|
|
5
|
+
Provides comprehensive validation for issue data before processing.
|
|
6
|
+
Addresses PR #74 comment: "Falta Validación de Issue Data"
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Optional, List, Tuple
|
|
10
|
+
from ..models import JiraTicket
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class IssueValidationError(ValueError):
|
|
14
|
+
"""Raised when issue data validation fails."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class IssueValidator:
|
|
19
|
+
"""
|
|
20
|
+
Validates JIRA issue data before AI processing.
|
|
21
|
+
|
|
22
|
+
Ensures that issues have sufficient data for meaningful analysis:
|
|
23
|
+
- Non-empty description
|
|
24
|
+
- Valid issue type
|
|
25
|
+
- No malformed data
|
|
26
|
+
- Reasonable length constraints
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
# Valid JIRA issue types
|
|
30
|
+
VALID_ISSUE_TYPES = {
|
|
31
|
+
"Story", "Task", "Bug", "Epic", "Sub-task",
|
|
32
|
+
"Improvement", "New Feature", "Technical Debt",
|
|
33
|
+
"Spike", "User Story"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Length constraints
|
|
37
|
+
MIN_DESCRIPTION_LENGTH = 10 # Characters
|
|
38
|
+
MAX_DESCRIPTION_LENGTH = 100000 # 100k characters (safety limit)
|
|
39
|
+
MAX_SUMMARY_LENGTH = 255 # Standard JIRA limit
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
strict: bool = False,
|
|
44
|
+
min_description_length: int = MIN_DESCRIPTION_LENGTH
|
|
45
|
+
):
|
|
46
|
+
"""
|
|
47
|
+
Initialize validator.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
strict: If True, raise errors on validation failures.
|
|
51
|
+
If False, return validation results with warnings.
|
|
52
|
+
min_description_length: Minimum required description length
|
|
53
|
+
"""
|
|
54
|
+
self.strict = strict
|
|
55
|
+
self.min_description_length = min_description_length
|
|
56
|
+
|
|
57
|
+
def validate_issue(self, issue: JiraTicket) -> Tuple[bool, List[str]]:
|
|
58
|
+
"""
|
|
59
|
+
Validate issue data for AI processing.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
issue: The JIRA issue to validate
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Tuple of (is_valid, errors)
|
|
66
|
+
- is_valid: True if issue passes all validations
|
|
67
|
+
- errors: List of validation error messages
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
IssueValidationError: If strict=True and validation fails
|
|
71
|
+
"""
|
|
72
|
+
errors = []
|
|
73
|
+
|
|
74
|
+
# 1. Validate issue key
|
|
75
|
+
if not issue.key or not issue.key.strip():
|
|
76
|
+
errors.append("Issue key is empty")
|
|
77
|
+
elif not self._is_valid_key_format(issue.key):
|
|
78
|
+
errors.append(f"Issue key '{issue.key}' has invalid format (expected: PROJECT-123)")
|
|
79
|
+
|
|
80
|
+
# 2. Validate summary
|
|
81
|
+
if not issue.summary or not issue.summary.strip():
|
|
82
|
+
errors.append("Issue summary is empty")
|
|
83
|
+
elif len(issue.summary) > self.MAX_SUMMARY_LENGTH:
|
|
84
|
+
errors.append(
|
|
85
|
+
f"Issue summary is too long ({len(issue.summary)} chars, "
|
|
86
|
+
f"max {self.MAX_SUMMARY_LENGTH})"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# 3. Validate description (critical for AI analysis)
|
|
90
|
+
desc_errors = self._validate_description(issue.description)
|
|
91
|
+
errors.extend(desc_errors)
|
|
92
|
+
|
|
93
|
+
# 4. Validate issue type
|
|
94
|
+
if issue.issue_type:
|
|
95
|
+
if issue.issue_type not in self.VALID_ISSUE_TYPES:
|
|
96
|
+
errors.append(
|
|
97
|
+
f"Unknown issue type '{issue.issue_type}'. "
|
|
98
|
+
f"Expected one of: {', '.join(sorted(self.VALID_ISSUE_TYPES))}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# 5. Check for suspicious/malformed data
|
|
102
|
+
suspicious_errors = self._check_suspicious_data(issue)
|
|
103
|
+
errors.extend(suspicious_errors)
|
|
104
|
+
|
|
105
|
+
is_valid = len(errors) == 0
|
|
106
|
+
|
|
107
|
+
if self.strict and not is_valid:
|
|
108
|
+
raise IssueValidationError(
|
|
109
|
+
f"Issue validation failed for {issue.key}:\n" +
|
|
110
|
+
"\n".join(f" - {error}" for error in errors)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return is_valid, errors
|
|
114
|
+
|
|
115
|
+
def _validate_description(self, description: Optional[str]) -> List[str]:
|
|
116
|
+
"""Validate issue description."""
|
|
117
|
+
errors = []
|
|
118
|
+
|
|
119
|
+
if not description:
|
|
120
|
+
errors.append("Issue description is empty or None")
|
|
121
|
+
return errors
|
|
122
|
+
|
|
123
|
+
description = description.strip()
|
|
124
|
+
|
|
125
|
+
if len(description) == 0:
|
|
126
|
+
errors.append("Issue description is empty (whitespace only)")
|
|
127
|
+
elif len(description) < self.min_description_length:
|
|
128
|
+
errors.append(
|
|
129
|
+
f"Issue description is too short ({len(description)} chars, "
|
|
130
|
+
f"minimum {self.min_description_length})"
|
|
131
|
+
)
|
|
132
|
+
elif len(description) > self.MAX_DESCRIPTION_LENGTH:
|
|
133
|
+
errors.append(
|
|
134
|
+
f"Issue description is too long ({len(description)} chars, "
|
|
135
|
+
f"max {self.MAX_DESCRIPTION_LENGTH})"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return errors
|
|
139
|
+
|
|
140
|
+
def _is_valid_key_format(self, key: str) -> bool:
|
|
141
|
+
"""
|
|
142
|
+
Validate JIRA issue key format.
|
|
143
|
+
|
|
144
|
+
Expected format: PROJECT-123 (letters, hyphen, numbers)
|
|
145
|
+
"""
|
|
146
|
+
import re
|
|
147
|
+
pattern = r'^[A-Z][A-Z0-9]+-\d+$'
|
|
148
|
+
return bool(re.match(pattern, key))
|
|
149
|
+
|
|
150
|
+
def _check_suspicious_data(self, issue: JiraTicket) -> List[str]:
|
|
151
|
+
"""
|
|
152
|
+
Check for suspicious or malformed data.
|
|
153
|
+
|
|
154
|
+
Common issues:
|
|
155
|
+
- HTML/XML tags in plain text fields (indicates parsing error)
|
|
156
|
+
- Null bytes or control characters
|
|
157
|
+
- Excessive whitespace
|
|
158
|
+
"""
|
|
159
|
+
errors = []
|
|
160
|
+
|
|
161
|
+
# Check for HTML tags (suggests improper parsing)
|
|
162
|
+
if issue.description and ('<html>' in issue.description.lower() or
|
|
163
|
+
'<!doctype' in issue.description.lower()):
|
|
164
|
+
errors.append(
|
|
165
|
+
"Description contains HTML tags (possible parsing error)"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Check for null bytes
|
|
169
|
+
if issue.description and '\x00' in issue.description:
|
|
170
|
+
errors.append("Description contains null bytes")
|
|
171
|
+
|
|
172
|
+
# Check for excessive consecutive whitespace (> 10 newlines)
|
|
173
|
+
if issue.description:
|
|
174
|
+
max_consecutive_newlines = 10
|
|
175
|
+
if '\n' * max_consecutive_newlines in issue.description:
|
|
176
|
+
errors.append(
|
|
177
|
+
f"Description has excessive whitespace "
|
|
178
|
+
f"({max_consecutive_newlines}+ consecutive newlines)"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return errors
|
|
182
|
+
|
|
183
|
+
def validate_for_requirements_extraction(
|
|
184
|
+
self,
|
|
185
|
+
issue: JiraTicket
|
|
186
|
+
) -> Tuple[bool, List[str]]:
|
|
187
|
+
"""
|
|
188
|
+
Validate issue specifically for requirements extraction.
|
|
189
|
+
|
|
190
|
+
More strict than general validation - requires meaningful description.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Tuple of (is_valid, errors)
|
|
194
|
+
"""
|
|
195
|
+
is_valid, errors = self.validate_issue(issue)
|
|
196
|
+
|
|
197
|
+
# Additional requirement: description must have substance
|
|
198
|
+
if issue.description:
|
|
199
|
+
# Check for placeholder text
|
|
200
|
+
placeholder_phrases = [
|
|
201
|
+
"to be defined", "tbd", "todo", "placeholder",
|
|
202
|
+
"add description here", "fill this in"
|
|
203
|
+
]
|
|
204
|
+
desc_lower = issue.description.lower()
|
|
205
|
+
|
|
206
|
+
for phrase in placeholder_phrases:
|
|
207
|
+
if phrase in desc_lower and len(issue.description.strip()) < 100:
|
|
208
|
+
errors.append(
|
|
209
|
+
f"Description appears to be a placeholder (contains '{phrase}')"
|
|
210
|
+
)
|
|
211
|
+
is_valid = False
|
|
212
|
+
break
|
|
213
|
+
|
|
214
|
+
return is_valid, errors
|
|
215
|
+
|
|
216
|
+
def sanitize_description(self, description: str, max_length: int = 5000) -> str:
|
|
217
|
+
"""
|
|
218
|
+
Sanitize and truncate description for AI processing.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
description: Raw description text
|
|
222
|
+
max_length: Maximum length to truncate to
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Sanitized description
|
|
226
|
+
"""
|
|
227
|
+
if not description:
|
|
228
|
+
return ""
|
|
229
|
+
|
|
230
|
+
# Remove null bytes
|
|
231
|
+
sanitized = description.replace('\x00', '')
|
|
232
|
+
|
|
233
|
+
# Normalize excessive whitespace
|
|
234
|
+
import re
|
|
235
|
+
# Replace 5+ consecutive newlines with 3
|
|
236
|
+
sanitized = re.sub(r'\n{5,}', '\n\n\n', sanitized)
|
|
237
|
+
|
|
238
|
+
# Replace 10+ consecutive spaces with single space
|
|
239
|
+
sanitized = re.sub(r' {10,}', ' ', sanitized)
|
|
240
|
+
|
|
241
|
+
# Truncate if needed
|
|
242
|
+
if len(sanitized) > max_length:
|
|
243
|
+
sanitized = sanitized[:max_length]
|
|
244
|
+
sanitized += "\n\n... (description truncated for processing)"
|
|
245
|
+
|
|
246
|
+
return sanitized.strip()
|