mcp-ticketer 2.0.1__py3-none-any.whl → 2.2.13__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.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/aitrackdown.py +122 -0
- mcp_ticketer/adapters/asana/adapter.py +121 -0
- mcp_ticketer/adapters/github/__init__.py +26 -0
- mcp_ticketer/adapters/{github.py → github/adapter.py} +1506 -365
- mcp_ticketer/adapters/github/client.py +335 -0
- mcp_ticketer/adapters/github/mappers.py +797 -0
- mcp_ticketer/adapters/github/queries.py +692 -0
- mcp_ticketer/adapters/github/types.py +460 -0
- mcp_ticketer/adapters/jira/__init__.py +35 -0
- mcp_ticketer/adapters/{jira.py → jira/adapter.py} +250 -678
- mcp_ticketer/adapters/jira/client.py +271 -0
- mcp_ticketer/adapters/jira/mappers.py +246 -0
- mcp_ticketer/adapters/jira/queries.py +216 -0
- mcp_ticketer/adapters/jira/types.py +304 -0
- mcp_ticketer/adapters/linear/adapter.py +1000 -92
- mcp_ticketer/adapters/linear/client.py +91 -1
- mcp_ticketer/adapters/linear/mappers.py +107 -0
- mcp_ticketer/adapters/linear/queries.py +112 -2
- mcp_ticketer/adapters/linear/types.py +50 -10
- mcp_ticketer/cli/configure.py +524 -89
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/main.py +10 -0
- mcp_ticketer/cli/mcp_configure.py +177 -49
- mcp_ticketer/cli/platform_installer.py +9 -0
- mcp_ticketer/cli/setup_command.py +157 -1
- mcp_ticketer/cli/ticket_commands.py +443 -81
- mcp_ticketer/cli/utils.py +113 -0
- mcp_ticketer/core/__init__.py +28 -0
- mcp_ticketer/core/adapter.py +367 -1
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +345 -0
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/session_state.py +6 -1
- mcp_ticketer/core/state_matcher.py +36 -3
- mcp_ticketer/mcp/server/__main__.py +2 -1
- mcp_ticketer/mcp/server/routing.py +68 -0
- mcp_ticketer/mcp/server/tools/__init__.py +7 -4
- mcp_ticketer/mcp/server/tools/attachment_tools.py +3 -1
- mcp_ticketer/mcp/server/tools/config_tools.py +233 -35
- mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +30 -1
- mcp_ticketer/mcp/server/tools/ticket_tools.py +37 -1
- mcp_ticketer/queue/queue.py +68 -0
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/METADATA +33 -3
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/RECORD +72 -36
- mcp_ticketer-2.2.13.dist-info/top_level.txt +2 -0
- py_mcp_installer/examples/phase3_demo.py +178 -0
- py_mcp_installer/scripts/manage_version.py +54 -0
- py_mcp_installer/setup.py +6 -0
- py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
- py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
- py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
- py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
- py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
- py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
- py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
- py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
- py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
- py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
- py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
- py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
- py_mcp_installer/src/py_mcp_installer/types.py +222 -0
- py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
- py_mcp_installer/tests/__init__.py +0 -0
- py_mcp_installer/tests/platforms/__init__.py +0 -0
- py_mcp_installer/tests/test_platform_detector.py +17 -0
- mcp_ticketer-2.0.1.dist-info/top_level.txt +0 -1
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
6
8
|
from typing import Any
|
|
7
9
|
|
|
8
10
|
try:
|
|
@@ -19,6 +21,8 @@ except ImportError:
|
|
|
19
21
|
|
|
20
22
|
from ...core.exceptions import AdapterError, AuthenticationError, RateLimitError
|
|
21
23
|
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
22
26
|
|
|
23
27
|
class LinearGraphQLClient:
|
|
24
28
|
"""GraphQL client for Linear API with error handling and retry logic."""
|
|
@@ -102,13 +106,42 @@ class LinearGraphQLClient:
|
|
|
102
106
|
"""
|
|
103
107
|
query = gql(query_string)
|
|
104
108
|
|
|
109
|
+
# Extract operation name from query for logging
|
|
110
|
+
operation_name = "unknown"
|
|
111
|
+
try:
|
|
112
|
+
# Simple extraction - look for 'query' or 'mutation' keyword
|
|
113
|
+
query_lower = query_string.strip().lower()
|
|
114
|
+
if query_lower.startswith("mutation"):
|
|
115
|
+
operation_name = (
|
|
116
|
+
query_string.split("{")[0].strip().replace("mutation", "").strip()
|
|
117
|
+
)
|
|
118
|
+
elif query_lower.startswith("query"):
|
|
119
|
+
operation_name = (
|
|
120
|
+
query_string.split("{")[0].strip().replace("query", "").strip()
|
|
121
|
+
)
|
|
122
|
+
except Exception:
|
|
123
|
+
pass # Use default 'unknown' if extraction fails
|
|
124
|
+
|
|
105
125
|
for attempt in range(retries + 1):
|
|
106
126
|
try:
|
|
127
|
+
# Log request details before execution
|
|
128
|
+
logger.debug(
|
|
129
|
+
f"[Linear GraphQL] Executing operation: {operation_name}\n"
|
|
130
|
+
f"Variables:\n{json.dumps(variables or {}, indent=2, default=str)}"
|
|
131
|
+
)
|
|
132
|
+
|
|
107
133
|
client = self.create_client()
|
|
108
134
|
async with client as session:
|
|
109
135
|
result = await session.execute(
|
|
110
136
|
query, variable_values=variables or {}
|
|
111
137
|
)
|
|
138
|
+
|
|
139
|
+
# Log successful response
|
|
140
|
+
logger.debug(
|
|
141
|
+
f"[Linear GraphQL] Operation successful: {operation_name}\n"
|
|
142
|
+
f"Response:\n{json.dumps(result, indent=2, default=str)}"
|
|
143
|
+
)
|
|
144
|
+
|
|
112
145
|
return result
|
|
113
146
|
|
|
114
147
|
except TransportQueryError as e:
|
|
@@ -118,8 +151,46 @@ class LinearGraphQLClient:
|
|
|
118
151
|
|
|
119
152
|
Related: 1M-398 - Label duplicate error handling
|
|
120
153
|
"""
|
|
154
|
+
# Log detailed error information
|
|
155
|
+
logger.error(
|
|
156
|
+
f"[Linear GraphQL] TransportQueryError occurred\n"
|
|
157
|
+
f"Operation: {operation_name}\n"
|
|
158
|
+
f"Variables:\n{json.dumps(variables or {}, indent=2, default=str)}\n"
|
|
159
|
+
f"Error: {e}\n"
|
|
160
|
+
f"Error details: {e.errors if hasattr(e, 'errors') else 'No error details'}"
|
|
161
|
+
)
|
|
162
|
+
|
|
121
163
|
if e.errors:
|
|
122
|
-
|
|
164
|
+
error = e.errors[0]
|
|
165
|
+
error_msg = error.get("message", "Unknown GraphQL error")
|
|
166
|
+
|
|
167
|
+
# Parse extensions for field-specific details (enhanced debugging)
|
|
168
|
+
extensions = error.get("extensions", {})
|
|
169
|
+
|
|
170
|
+
# Check for user-presentable message (clearer error for users)
|
|
171
|
+
user_message = extensions.get("userPresentableMessage")
|
|
172
|
+
if user_message:
|
|
173
|
+
error_msg = user_message
|
|
174
|
+
|
|
175
|
+
# Check for argument path (which field failed validation)
|
|
176
|
+
arg_path = extensions.get("argumentPath")
|
|
177
|
+
if arg_path:
|
|
178
|
+
field_path = ".".join(str(p) for p in arg_path)
|
|
179
|
+
error_msg = f"{error_msg} (field: {field_path})"
|
|
180
|
+
|
|
181
|
+
# Check for validation errors (additional context)
|
|
182
|
+
validation_errors = extensions.get("validationErrors")
|
|
183
|
+
if validation_errors:
|
|
184
|
+
error_msg = (
|
|
185
|
+
f"{error_msg}\nValidation errors: {validation_errors}"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Log full error context for debugging
|
|
189
|
+
logger.error(
|
|
190
|
+
"Linear GraphQL error: %s (extensions: %s)",
|
|
191
|
+
error_msg,
|
|
192
|
+
extensions,
|
|
193
|
+
)
|
|
123
194
|
|
|
124
195
|
# Check for duplicate label errors specifically
|
|
125
196
|
if (
|
|
@@ -139,6 +210,15 @@ class LinearGraphQLClient:
|
|
|
139
210
|
raise AdapterError(f"Linear GraphQL error: {e}", "linear") from e
|
|
140
211
|
|
|
141
212
|
except TransportError as e:
|
|
213
|
+
# Log transport error details
|
|
214
|
+
logger.error(
|
|
215
|
+
f"[Linear GraphQL] TransportError occurred\n"
|
|
216
|
+
f"Operation: {operation_name}\n"
|
|
217
|
+
f"Variables:\n{json.dumps(variables or {}, indent=2, default=str)}\n"
|
|
218
|
+
f"Error: {e}\n"
|
|
219
|
+
f"Status code: {e.response.status if hasattr(e, 'response') and e.response else 'N/A'}"
|
|
220
|
+
)
|
|
221
|
+
|
|
142
222
|
# Handle HTTP errors
|
|
143
223
|
if hasattr(e, "response") and e.response:
|
|
144
224
|
status_code = e.response.status
|
|
@@ -173,6 +253,16 @@ class LinearGraphQLClient:
|
|
|
173
253
|
raise AdapterError(f"Linear API transport error: {e}", "linear") from e
|
|
174
254
|
|
|
175
255
|
except Exception as e:
|
|
256
|
+
# Log generic error details
|
|
257
|
+
logger.error(
|
|
258
|
+
f"[Linear GraphQL] Unexpected error occurred\n"
|
|
259
|
+
f"Operation: {operation_name}\n"
|
|
260
|
+
f"Variables:\n{json.dumps(variables or {}, indent=2, default=str)}\n"
|
|
261
|
+
f"Error type: {type(e).__name__}\n"
|
|
262
|
+
f"Error: {e}",
|
|
263
|
+
exc_info=True,
|
|
264
|
+
)
|
|
265
|
+
|
|
176
266
|
# GraphQL or other errors
|
|
177
267
|
error_msg = str(e)
|
|
178
268
|
|
|
@@ -418,3 +418,110 @@ def map_linear_attachment_to_attachment(
|
|
|
418
418
|
description=subtitle,
|
|
419
419
|
metadata=metadata,
|
|
420
420
|
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def task_to_compact_format(task: Task) -> dict[str, Any]:
|
|
424
|
+
"""Convert Task to compact format for efficient token usage.
|
|
425
|
+
|
|
426
|
+
Compact format includes only essential fields, reducing token usage by ~70-80%
|
|
427
|
+
compared to full Task serialization.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
----
|
|
431
|
+
task: Universal Task model
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
-------
|
|
435
|
+
Compact dictionary with minimal essential fields
|
|
436
|
+
|
|
437
|
+
Design Decision: Compact Format Fields (1M-554)
|
|
438
|
+
-----------------------------------------------
|
|
439
|
+
Rationale: Selected fields based on token efficiency analysis and user needs.
|
|
440
|
+
Full Task serialization averages ~600 tokens per item. Compact format targets
|
|
441
|
+
~120 tokens per item (80% reduction).
|
|
442
|
+
|
|
443
|
+
Essential fields included:
|
|
444
|
+
- id: Required for all operations (identifier)
|
|
445
|
+
- title: Primary user-facing information
|
|
446
|
+
- state: Critical for workflow understanding
|
|
447
|
+
- priority: Important for triage and filtering
|
|
448
|
+
- assignee: Key for task assignment visibility
|
|
449
|
+
|
|
450
|
+
Fields excluded:
|
|
451
|
+
- description: Often large (100-500 tokens), available via get()
|
|
452
|
+
- creator: Less critical for list views
|
|
453
|
+
- tags: Available in full format, not essential for scanning
|
|
454
|
+
- children: Hierarchy details available via dedicated queries
|
|
455
|
+
- created_at/updated_at: Not essential for list scanning
|
|
456
|
+
- metadata: Platform-specific, not needed in compact view
|
|
457
|
+
|
|
458
|
+
Performance: Reduces typical 50-item list from ~30,000 to ~6,000 tokens.
|
|
459
|
+
|
|
460
|
+
"""
|
|
461
|
+
# Handle state - can be TicketState enum or string
|
|
462
|
+
state_value = None
|
|
463
|
+
if task.state:
|
|
464
|
+
state_value = task.state.value if hasattr(task.state, "value") else task.state
|
|
465
|
+
|
|
466
|
+
# Handle priority - can be Priority enum or string
|
|
467
|
+
priority_value = None
|
|
468
|
+
if task.priority:
|
|
469
|
+
priority_value = (
|
|
470
|
+
task.priority.value if hasattr(task.priority, "value") else task.priority
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
"id": task.id,
|
|
475
|
+
"title": task.title,
|
|
476
|
+
"state": state_value,
|
|
477
|
+
"priority": priority_value,
|
|
478
|
+
"assignee": task.assignee,
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def epic_to_compact_format(epic: Epic) -> dict[str, Any]:
|
|
483
|
+
"""Convert Epic to compact format for efficient token usage.
|
|
484
|
+
|
|
485
|
+
Compact format includes only essential fields, reducing token usage by ~70-80%
|
|
486
|
+
compared to full Epic serialization.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
----
|
|
490
|
+
epic: Universal Epic model
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
-------
|
|
494
|
+
Compact dictionary with minimal essential fields
|
|
495
|
+
|
|
496
|
+
Design Decision: Epic Compact Format (1M-554)
|
|
497
|
+
---------------------------------------------
|
|
498
|
+
Rationale: Epics typically have less metadata than tasks, but descriptions
|
|
499
|
+
can still be large. Compact format focuses on overview information.
|
|
500
|
+
|
|
501
|
+
Essential fields:
|
|
502
|
+
- id: Required for all operations
|
|
503
|
+
- title: Primary identifier
|
|
504
|
+
- state: Project status
|
|
505
|
+
- child_count: Useful for project overview (if available)
|
|
506
|
+
|
|
507
|
+
Performance: Similar token reduction to task compact format.
|
|
508
|
+
|
|
509
|
+
"""
|
|
510
|
+
# Handle state - can be TicketState enum or string
|
|
511
|
+
state_value = None
|
|
512
|
+
if epic.state:
|
|
513
|
+
state_value = epic.state.value if hasattr(epic.state, "value") else epic.state
|
|
514
|
+
|
|
515
|
+
compact = {
|
|
516
|
+
"id": epic.id,
|
|
517
|
+
"title": epic.title,
|
|
518
|
+
"state": state_value,
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
# Include child count if available in metadata
|
|
522
|
+
if epic.metadata and "linear" in epic.metadata:
|
|
523
|
+
linear_meta = epic.metadata["linear"]
|
|
524
|
+
if "issue_count" in linear_meta:
|
|
525
|
+
compact["child_count"] = linear_meta["issue_count"]
|
|
526
|
+
|
|
527
|
+
return compact
|
|
@@ -354,11 +354,15 @@ LIST_PROJECTS_QUERY = (
|
|
|
354
354
|
TEAM_FRAGMENT # Required by PROJECT_FRAGMENT which uses ...TeamFields
|
|
355
355
|
+ PROJECT_FRAGMENT
|
|
356
356
|
+ """
|
|
357
|
-
query ListProjects($filter: ProjectFilter, $first: Int
|
|
358
|
-
projects(filter: $filter, first: $first, orderBy: updatedAt) {
|
|
357
|
+
query ListProjects($filter: ProjectFilter, $first: Int!, $after: String) {
|
|
358
|
+
projects(filter: $filter, first: $first, after: $after, orderBy: updatedAt) {
|
|
359
359
|
nodes {
|
|
360
360
|
...ProjectFields
|
|
361
361
|
}
|
|
362
|
+
pageInfo {
|
|
363
|
+
hasNextPage
|
|
364
|
+
endCursor
|
|
365
|
+
}
|
|
362
366
|
}
|
|
363
367
|
}
|
|
364
368
|
"""
|
|
@@ -554,3 +558,109 @@ GET_PROJECT_UPDATE_QUERY = (
|
|
|
554
558
|
}
|
|
555
559
|
"""
|
|
556
560
|
)
|
|
561
|
+
|
|
562
|
+
# Milestone/Cycle Operations (1M-607 Phase 2)
|
|
563
|
+
|
|
564
|
+
CREATE_CYCLE_MUTATION = """
|
|
565
|
+
mutation CycleCreate($input: CycleCreateInput!) {
|
|
566
|
+
cycleCreate(input: $input) {
|
|
567
|
+
success
|
|
568
|
+
cycle {
|
|
569
|
+
id
|
|
570
|
+
name
|
|
571
|
+
description
|
|
572
|
+
startsAt
|
|
573
|
+
endsAt
|
|
574
|
+
completedAt
|
|
575
|
+
progress
|
|
576
|
+
completedIssueCount
|
|
577
|
+
issueCount
|
|
578
|
+
team {
|
|
579
|
+
id
|
|
580
|
+
name
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
"""
|
|
586
|
+
|
|
587
|
+
GET_CYCLE_QUERY = """
|
|
588
|
+
query Cycle($id: String!) {
|
|
589
|
+
cycle(id: $id) {
|
|
590
|
+
id
|
|
591
|
+
name
|
|
592
|
+
description
|
|
593
|
+
startsAt
|
|
594
|
+
endsAt
|
|
595
|
+
completedAt
|
|
596
|
+
progress
|
|
597
|
+
completedIssueCount
|
|
598
|
+
issueCount
|
|
599
|
+
team {
|
|
600
|
+
id
|
|
601
|
+
name
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
"""
|
|
606
|
+
|
|
607
|
+
UPDATE_CYCLE_MUTATION = """
|
|
608
|
+
mutation CycleUpdate($id: String!, $input: CycleUpdateInput!) {
|
|
609
|
+
cycleUpdate(id: $id, input: $input) {
|
|
610
|
+
success
|
|
611
|
+
cycle {
|
|
612
|
+
id
|
|
613
|
+
name
|
|
614
|
+
description
|
|
615
|
+
startsAt
|
|
616
|
+
endsAt
|
|
617
|
+
completedAt
|
|
618
|
+
progress
|
|
619
|
+
completedIssueCount
|
|
620
|
+
issueCount
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
"""
|
|
625
|
+
|
|
626
|
+
ARCHIVE_CYCLE_MUTATION = """
|
|
627
|
+
mutation CycleArchive($id: String!) {
|
|
628
|
+
cycleArchive(id: $id) {
|
|
629
|
+
success
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
"""
|
|
633
|
+
|
|
634
|
+
GET_CYCLE_ISSUES_QUERY = """
|
|
635
|
+
query CycleIssues($cycleId: String!, $first: Int!) {
|
|
636
|
+
cycle(id: $cycleId) {
|
|
637
|
+
issues(first: $first) {
|
|
638
|
+
nodes {
|
|
639
|
+
id
|
|
640
|
+
identifier
|
|
641
|
+
title
|
|
642
|
+
description
|
|
643
|
+
state {
|
|
644
|
+
id
|
|
645
|
+
name
|
|
646
|
+
type
|
|
647
|
+
}
|
|
648
|
+
priority
|
|
649
|
+
assignee {
|
|
650
|
+
id
|
|
651
|
+
email
|
|
652
|
+
name
|
|
653
|
+
}
|
|
654
|
+
labels {
|
|
655
|
+
nodes {
|
|
656
|
+
id
|
|
657
|
+
name
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
createdAt
|
|
661
|
+
updatedAt
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
"""
|
|
@@ -51,6 +51,34 @@ class LinearStateMapping:
|
|
|
51
51
|
"canceled": TicketState.CLOSED,
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
# Semantic state name mappings for flexible workflow matching (1M-552)
|
|
55
|
+
# Maps universal states to common Linear state names (case-insensitive)
|
|
56
|
+
SEMANTIC_NAMES: dict[TicketState, list[str]] = {
|
|
57
|
+
TicketState.OPEN: ["todo", "to do", "open", "new", "backlog"],
|
|
58
|
+
TicketState.READY: ["ready", "triage", "ready for dev", "ready to start"],
|
|
59
|
+
TicketState.TESTED: [
|
|
60
|
+
"tested",
|
|
61
|
+
"in review",
|
|
62
|
+
"review",
|
|
63
|
+
"qa",
|
|
64
|
+
"testing",
|
|
65
|
+
"ready for review",
|
|
66
|
+
],
|
|
67
|
+
TicketState.WAITING: ["waiting", "on hold", "paused"],
|
|
68
|
+
TicketState.BLOCKED: ["blocked"],
|
|
69
|
+
TicketState.IN_PROGRESS: [
|
|
70
|
+
"in progress",
|
|
71
|
+
"in-progress",
|
|
72
|
+
"started",
|
|
73
|
+
"doing",
|
|
74
|
+
"active",
|
|
75
|
+
"in development",
|
|
76
|
+
"in dev",
|
|
77
|
+
],
|
|
78
|
+
TicketState.DONE: ["done", "completed", "finished"],
|
|
79
|
+
TicketState.CLOSED: ["closed", "canceled", "cancelled", "won't do", "wont do"],
|
|
80
|
+
}
|
|
81
|
+
|
|
54
82
|
|
|
55
83
|
class LinearWorkflowStateType(Enum):
|
|
56
84
|
"""Linear workflow state types."""
|
|
@@ -138,8 +166,9 @@ def get_universal_state(
|
|
|
138
166
|
2. Try synonym matching on state name (ToDo, In Review, Testing, etc.)
|
|
139
167
|
3. Default to OPEN for unknown states
|
|
140
168
|
|
|
141
|
-
Synonym Matching Rules (ticket 1M-164):
|
|
142
|
-
- "
|
|
169
|
+
Synonym Matching Rules (ticket 1M-164, fixed in v2.0.4):
|
|
170
|
+
- "done", "completed", "finished", "resolved" → DONE
|
|
171
|
+
- "closed", "canceled", "cancelled", "won't do" → CLOSED
|
|
143
172
|
- Everything else → OPEN
|
|
144
173
|
|
|
145
174
|
Args:
|
|
@@ -158,24 +187,35 @@ def get_universal_state(
|
|
|
158
187
|
if state_name:
|
|
159
188
|
state_name_lower = state_name.lower().strip()
|
|
160
189
|
|
|
161
|
-
#
|
|
162
|
-
|
|
190
|
+
# DONE states: Work successfully completed
|
|
191
|
+
# - User finished the work
|
|
192
|
+
# - Requirements met
|
|
193
|
+
# - Quality verified
|
|
194
|
+
done_synonyms = [
|
|
163
195
|
"done",
|
|
196
|
+
"completed",
|
|
197
|
+
"finished",
|
|
198
|
+
"resolved",
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
if any(synonym in state_name_lower for synonym in done_synonyms):
|
|
202
|
+
return TicketState.DONE
|
|
203
|
+
|
|
204
|
+
# CLOSED states: Work terminated without completion
|
|
205
|
+
# - User decided not to do it
|
|
206
|
+
# - Requirements changed
|
|
207
|
+
# - Duplicate/invalid ticket
|
|
208
|
+
closed_synonyms = [
|
|
164
209
|
"closed",
|
|
165
210
|
"cancelled",
|
|
166
211
|
"canceled",
|
|
167
|
-
"completed",
|
|
168
212
|
"won't do",
|
|
169
213
|
"wont do",
|
|
170
214
|
"rejected",
|
|
171
|
-
"resolved",
|
|
172
|
-
"finished",
|
|
173
215
|
]
|
|
174
216
|
|
|
175
217
|
if any(synonym in state_name_lower for synonym in closed_synonyms):
|
|
176
|
-
return
|
|
177
|
-
TicketState.DONE if state_name_lower == "done" else TicketState.CLOSED
|
|
178
|
-
)
|
|
218
|
+
return TicketState.CLOSED
|
|
179
219
|
|
|
180
220
|
# Check for "in progress" synonyms
|
|
181
221
|
in_progress_synonyms = [
|