mcp-ticketer 0.3.0__py3-none-any.whl → 2.2.9__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.
Files changed (160) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/__init__.py +2 -0
  5. mcp_ticketer/adapters/aitrackdown.py +930 -52
  6. mcp_ticketer/adapters/asana/__init__.py +15 -0
  7. mcp_ticketer/adapters/asana/adapter.py +1537 -0
  8. mcp_ticketer/adapters/asana/client.py +292 -0
  9. mcp_ticketer/adapters/asana/mappers.py +348 -0
  10. mcp_ticketer/adapters/asana/types.py +146 -0
  11. mcp_ticketer/adapters/github/__init__.py +26 -0
  12. mcp_ticketer/adapters/github/adapter.py +3229 -0
  13. mcp_ticketer/adapters/github/client.py +335 -0
  14. mcp_ticketer/adapters/github/mappers.py +797 -0
  15. mcp_ticketer/adapters/github/queries.py +692 -0
  16. mcp_ticketer/adapters/github/types.py +460 -0
  17. mcp_ticketer/adapters/hybrid.py +58 -16
  18. mcp_ticketer/adapters/jira/__init__.py +35 -0
  19. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  20. mcp_ticketer/adapters/jira/client.py +271 -0
  21. mcp_ticketer/adapters/jira/mappers.py +246 -0
  22. mcp_ticketer/adapters/jira/queries.py +216 -0
  23. mcp_ticketer/adapters/jira/types.py +304 -0
  24. mcp_ticketer/adapters/linear/__init__.py +1 -1
  25. mcp_ticketer/adapters/linear/adapter.py +3810 -462
  26. mcp_ticketer/adapters/linear/client.py +312 -69
  27. mcp_ticketer/adapters/linear/mappers.py +305 -85
  28. mcp_ticketer/adapters/linear/queries.py +317 -17
  29. mcp_ticketer/adapters/linear/types.py +187 -64
  30. mcp_ticketer/adapters/linear.py +2 -2
  31. mcp_ticketer/analysis/__init__.py +56 -0
  32. mcp_ticketer/analysis/dependency_graph.py +255 -0
  33. mcp_ticketer/analysis/health_assessment.py +304 -0
  34. mcp_ticketer/analysis/orphaned.py +218 -0
  35. mcp_ticketer/analysis/project_status.py +594 -0
  36. mcp_ticketer/analysis/similarity.py +224 -0
  37. mcp_ticketer/analysis/staleness.py +266 -0
  38. mcp_ticketer/automation/__init__.py +11 -0
  39. mcp_ticketer/automation/project_updates.py +378 -0
  40. mcp_ticketer/cache/memory.py +9 -8
  41. mcp_ticketer/cli/adapter_diagnostics.py +91 -54
  42. mcp_ticketer/cli/auggie_configure.py +116 -15
  43. mcp_ticketer/cli/codex_configure.py +274 -82
  44. mcp_ticketer/cli/configure.py +1323 -151
  45. mcp_ticketer/cli/cursor_configure.py +314 -0
  46. mcp_ticketer/cli/diagnostics.py +209 -114
  47. mcp_ticketer/cli/discover.py +297 -26
  48. mcp_ticketer/cli/gemini_configure.py +119 -26
  49. mcp_ticketer/cli/init_command.py +880 -0
  50. mcp_ticketer/cli/install_mcp_server.py +418 -0
  51. mcp_ticketer/cli/instruction_commands.py +435 -0
  52. mcp_ticketer/cli/linear_commands.py +256 -130
  53. mcp_ticketer/cli/main.py +140 -1544
  54. mcp_ticketer/cli/mcp_configure.py +1013 -100
  55. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  56. mcp_ticketer/cli/migrate_config.py +12 -8
  57. mcp_ticketer/cli/platform_commands.py +123 -0
  58. mcp_ticketer/cli/platform_detection.py +477 -0
  59. mcp_ticketer/cli/platform_installer.py +545 -0
  60. mcp_ticketer/cli/project_update_commands.py +350 -0
  61. mcp_ticketer/cli/python_detection.py +126 -0
  62. mcp_ticketer/cli/queue_commands.py +15 -15
  63. mcp_ticketer/cli/setup_command.py +794 -0
  64. mcp_ticketer/cli/simple_health.py +84 -59
  65. mcp_ticketer/cli/ticket_commands.py +1375 -0
  66. mcp_ticketer/cli/update_checker.py +313 -0
  67. mcp_ticketer/cli/utils.py +195 -72
  68. mcp_ticketer/core/__init__.py +64 -1
  69. mcp_ticketer/core/adapter.py +618 -18
  70. mcp_ticketer/core/config.py +77 -68
  71. mcp_ticketer/core/env_discovery.py +75 -16
  72. mcp_ticketer/core/env_loader.py +121 -97
  73. mcp_ticketer/core/exceptions.py +32 -24
  74. mcp_ticketer/core/http_client.py +26 -26
  75. mcp_ticketer/core/instructions.py +405 -0
  76. mcp_ticketer/core/label_manager.py +732 -0
  77. mcp_ticketer/core/mappers.py +42 -30
  78. mcp_ticketer/core/milestone_manager.py +252 -0
  79. mcp_ticketer/core/models.py +566 -19
  80. mcp_ticketer/core/onepassword_secrets.py +379 -0
  81. mcp_ticketer/core/priority_matcher.py +463 -0
  82. mcp_ticketer/core/project_config.py +189 -49
  83. mcp_ticketer/core/project_utils.py +281 -0
  84. mcp_ticketer/core/project_validator.py +376 -0
  85. mcp_ticketer/core/registry.py +3 -3
  86. mcp_ticketer/core/session_state.py +176 -0
  87. mcp_ticketer/core/state_matcher.py +592 -0
  88. mcp_ticketer/core/url_parser.py +425 -0
  89. mcp_ticketer/core/validators.py +69 -0
  90. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  91. mcp_ticketer/mcp/__init__.py +29 -1
  92. mcp_ticketer/mcp/__main__.py +60 -0
  93. mcp_ticketer/mcp/server/__init__.py +25 -0
  94. mcp_ticketer/mcp/server/__main__.py +60 -0
  95. mcp_ticketer/mcp/server/constants.py +58 -0
  96. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  97. mcp_ticketer/mcp/server/dto.py +195 -0
  98. mcp_ticketer/mcp/server/main.py +1343 -0
  99. mcp_ticketer/mcp/server/response_builder.py +206 -0
  100. mcp_ticketer/mcp/server/routing.py +723 -0
  101. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  102. mcp_ticketer/mcp/server/tools/__init__.py +69 -0
  103. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  104. mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
  105. mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
  106. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  107. mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
  108. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  109. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
  110. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  111. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  112. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  113. mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
  114. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  115. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  116. mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
  117. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  118. mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
  119. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  120. mcp_ticketer/queue/__init__.py +1 -0
  121. mcp_ticketer/queue/health_monitor.py +168 -136
  122. mcp_ticketer/queue/manager.py +78 -63
  123. mcp_ticketer/queue/queue.py +108 -21
  124. mcp_ticketer/queue/run_worker.py +2 -2
  125. mcp_ticketer/queue/ticket_registry.py +213 -155
  126. mcp_ticketer/queue/worker.py +96 -58
  127. mcp_ticketer/utils/__init__.py +5 -0
  128. mcp_ticketer/utils/token_utils.py +246 -0
  129. mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
  130. mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
  131. mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
  132. py_mcp_installer/examples/phase3_demo.py +178 -0
  133. py_mcp_installer/scripts/manage_version.py +54 -0
  134. py_mcp_installer/setup.py +6 -0
  135. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  136. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  137. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  138. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  139. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  140. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  141. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  142. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  143. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  144. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  145. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  146. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  147. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  148. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  149. py_mcp_installer/tests/__init__.py +0 -0
  150. py_mcp_installer/tests/platforms/__init__.py +0 -0
  151. py_mcp_installer/tests/test_platform_detector.py +17 -0
  152. mcp_ticketer/adapters/github.py +0 -1354
  153. mcp_ticketer/adapters/jira.py +0 -1011
  154. mcp_ticketer/mcp/server.py +0 -2030
  155. mcp_ticketer-0.3.0.dist-info/METADATA +0 -414
  156. mcp_ticketer-0.3.0.dist-info/RECORD +0 -59
  157. mcp_ticketer-0.3.0.dist-info/top_level.txt +0 -1
  158. {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
  159. {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
  160. {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
@@ -3,47 +3,47 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from enum import Enum
6
- from typing import Any, Dict
6
+ from typing import Any
7
7
 
8
8
  from mcp_ticketer.core.models import Priority, TicketState
9
9
 
10
10
 
11
11
  class LinearPriorityMapping:
12
12
  """Mapping between universal Priority and Linear priority values."""
13
-
13
+
14
14
  # Linear uses numeric priorities: 0=No priority, 1=Urgent, 2=High, 3=Medium, 4=Low
15
- TO_LINEAR: Dict[Priority, int] = {
15
+ TO_LINEAR: dict[Priority, int] = {
16
16
  Priority.CRITICAL: 1, # Urgent
17
- Priority.HIGH: 2, # High
18
- Priority.MEDIUM: 3, # Medium
19
- Priority.LOW: 4, # Low
17
+ Priority.HIGH: 2, # High
18
+ Priority.MEDIUM: 3, # Medium
19
+ Priority.LOW: 4, # Low
20
20
  }
21
-
22
- FROM_LINEAR: Dict[int, Priority] = {
23
- 0: Priority.LOW, # No priority -> Low
21
+
22
+ FROM_LINEAR: dict[int, Priority] = {
23
+ 0: Priority.LOW, # No priority -> Low
24
24
  1: Priority.CRITICAL, # Urgent -> Critical
25
- 2: Priority.HIGH, # High -> High
26
- 3: Priority.MEDIUM, # Medium -> Medium
27
- 4: Priority.LOW, # Low -> Low
25
+ 2: Priority.HIGH, # High -> High
26
+ 3: Priority.MEDIUM, # Medium -> Medium
27
+ 4: Priority.LOW, # Low -> Low
28
28
  }
29
29
 
30
30
 
31
31
  class LinearStateMapping:
32
32
  """Mapping between universal TicketState and Linear workflow state types."""
33
-
33
+
34
34
  # Linear workflow state types
35
- TO_LINEAR: Dict[TicketState, str] = {
35
+ TO_LINEAR: dict[TicketState, str] = {
36
36
  TicketState.OPEN: "unstarted",
37
37
  TicketState.IN_PROGRESS: "started",
38
38
  TicketState.READY: "unstarted", # No direct equivalent, use unstarted
39
- TicketState.TESTED: "started", # No direct equivalent, use started
39
+ TicketState.TESTED: "started", # No direct equivalent, use started
40
40
  TicketState.DONE: "completed",
41
41
  TicketState.CLOSED: "canceled",
42
42
  TicketState.WAITING: "unstarted",
43
43
  TicketState.BLOCKED: "unstarted",
44
44
  }
45
-
46
- FROM_LINEAR: Dict[str, TicketState] = {
45
+
46
+ FROM_LINEAR: dict[str, TicketState] = {
47
47
  "backlog": TicketState.OPEN,
48
48
  "unstarted": TicketState.OPEN,
49
49
  "started": TicketState.IN_PROGRESS,
@@ -51,10 +51,38 @@ 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."""
57
-
85
+
58
86
  BACKLOG = "backlog"
59
87
  UNSTARTED = "unstarted"
60
88
  STARTED = "started"
@@ -64,7 +92,7 @@ class LinearWorkflowStateType(Enum):
64
92
 
65
93
  class LinearProjectState(Enum):
66
94
  """Linear project states."""
67
-
95
+
68
96
  PLANNED = "planned"
69
97
  STARTED = "started"
70
98
  COMPLETED = "completed"
@@ -74,7 +102,7 @@ class LinearProjectState(Enum):
74
102
 
75
103
  class LinearIssueRelationType(Enum):
76
104
  """Linear issue relation types."""
77
-
105
+
78
106
  BLOCKS = "blocks"
79
107
  BLOCKED_BY = "blockedBy"
80
108
  DUPLICATE = "duplicate"
@@ -84,57 +112,143 @@ class LinearIssueRelationType(Enum):
84
112
 
85
113
  class LinearCommentType(Enum):
86
114
  """Linear comment types."""
87
-
115
+
88
116
  COMMENT = "comment"
89
117
  SYSTEM = "system"
90
118
 
91
119
 
92
120
  def get_linear_priority(priority: Priority) -> int:
93
121
  """Convert universal Priority to Linear priority value.
94
-
122
+
95
123
  Args:
96
124
  priority: Universal priority enum
97
-
125
+
98
126
  Returns:
99
127
  Linear priority integer (0-4)
128
+
100
129
  """
101
130
  return LinearPriorityMapping.TO_LINEAR.get(priority, 3) # Default to Medium
102
131
 
103
132
 
104
133
  def get_universal_priority(linear_priority: int) -> Priority:
105
134
  """Convert Linear priority value to universal Priority.
106
-
135
+
107
136
  Args:
108
137
  linear_priority: Linear priority integer (0-4)
109
-
138
+
110
139
  Returns:
111
140
  Universal priority enum
141
+
112
142
  """
113
143
  return LinearPriorityMapping.FROM_LINEAR.get(linear_priority, Priority.MEDIUM)
114
144
 
115
145
 
116
146
  def get_linear_state_type(state: TicketState) -> str:
117
147
  """Convert universal TicketState to Linear workflow state type.
118
-
148
+
119
149
  Args:
120
150
  state: Universal ticket state enum
121
-
151
+
122
152
  Returns:
123
153
  Linear workflow state type string
154
+
124
155
  """
125
156
  return LinearStateMapping.TO_LINEAR.get(state, "unstarted")
126
157
 
127
158
 
128
- def get_universal_state(linear_state_type: str) -> TicketState:
129
- """Convert Linear workflow state type to universal TicketState.
130
-
159
+ def get_universal_state(
160
+ linear_state_type: str, state_name: str | None = None
161
+ ) -> TicketState:
162
+ """Convert Linear workflow state type to universal TicketState with synonym matching.
163
+
164
+ This function implements intelligent state mapping with fallback strategies:
165
+ 1. Try exact match on state type (backlog, unstarted, started, completed, canceled)
166
+ 2. Try synonym matching on state name (ToDo, In Review, Testing, etc.)
167
+ 3. Default to OPEN for unknown states
168
+
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
172
+ - Everything else → OPEN
173
+
131
174
  Args:
132
- linear_state_type: Linear workflow state type string
133
-
175
+ linear_state_type: Linear workflow state type string (from state.type field)
176
+ state_name: Linear workflow state name (from state.name field, optional)
177
+
134
178
  Returns:
135
179
  Universal ticket state enum
180
+
136
181
  """
137
- return LinearStateMapping.FROM_LINEAR.get(linear_state_type, TicketState.OPEN)
182
+ # First try exact type match
183
+ if linear_state_type in LinearStateMapping.FROM_LINEAR:
184
+ return LinearStateMapping.FROM_LINEAR[linear_state_type]
185
+
186
+ # If no exact match and state_name provided, try synonym matching
187
+ if state_name:
188
+ state_name_lower = state_name.lower().strip()
189
+
190
+ # DONE states: Work successfully completed
191
+ # - User finished the work
192
+ # - Requirements met
193
+ # - Quality verified
194
+ done_synonyms = [
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 = [
209
+ "closed",
210
+ "cancelled",
211
+ "canceled",
212
+ "won't do",
213
+ "wont do",
214
+ "rejected",
215
+ ]
216
+
217
+ if any(synonym in state_name_lower for synonym in closed_synonyms):
218
+ return TicketState.CLOSED
219
+
220
+ # Check for "in progress" synonyms
221
+ in_progress_synonyms = [
222
+ "in progress",
223
+ "in-progress",
224
+ "working",
225
+ "active",
226
+ "started",
227
+ "doing",
228
+ "in development",
229
+ "in dev",
230
+ ]
231
+
232
+ if any(synonym in state_name_lower for synonym in in_progress_synonyms):
233
+ return TicketState.IN_PROGRESS
234
+
235
+ # Check for "review/testing" synonyms
236
+ review_synonyms = [
237
+ "review",
238
+ "in review",
239
+ "in-review",
240
+ "testing",
241
+ "in test",
242
+ "in-test",
243
+ "qa",
244
+ "ready for review",
245
+ ]
246
+
247
+ if any(synonym in state_name_lower for synonym in review_synonyms):
248
+ return TicketState.READY
249
+
250
+ # Default: everything else is OPEN (including "ToDo", "Backlog", "To Do", etc.)
251
+ return TicketState.OPEN
138
252
 
139
253
 
140
254
  def build_issue_filter(
@@ -143,57 +257,64 @@ def build_issue_filter(
143
257
  priority: Priority | None = None,
144
258
  team_id: str | None = None,
145
259
  project_id: str | None = None,
260
+ parent_id: str | None = None,
146
261
  labels: list[str] | None = None,
147
262
  created_after: str | None = None,
148
263
  updated_after: str | None = None,
149
264
  due_before: str | None = None,
150
265
  include_archived: bool = False,
151
- ) -> Dict[str, Any]:
266
+ ) -> dict[str, Any]:
152
267
  """Build a Linear issue filter from parameters.
153
-
268
+
154
269
  Args:
155
270
  state: Filter by ticket state
156
271
  assignee_id: Filter by assignee Linear user ID
157
272
  priority: Filter by priority
158
273
  team_id: Filter by team ID
159
274
  project_id: Filter by project ID
275
+ parent_id: Filter by parent issue ID (for listing sub-issues)
160
276
  labels: Filter by label names
161
277
  created_after: Filter by creation date (ISO string)
162
278
  updated_after: Filter by update date (ISO string)
163
279
  due_before: Filter by due date (ISO string)
164
280
  include_archived: Whether to include archived issues
165
-
281
+
166
282
  Returns:
167
283
  Linear GraphQL filter object
284
+
168
285
  """
169
- issue_filter: Dict[str, Any] = {}
170
-
286
+ issue_filter: dict[str, Any] = {}
287
+
171
288
  # Team filter (required for most operations)
172
289
  if team_id:
173
290
  issue_filter["team"] = {"id": {"eq": team_id}}
174
-
291
+
175
292
  # State filter
176
293
  if state:
177
294
  state_type = get_linear_state_type(state)
178
295
  issue_filter["state"] = {"type": {"eq": state_type}}
179
-
296
+
180
297
  # Assignee filter
181
298
  if assignee_id:
182
299
  issue_filter["assignee"] = {"id": {"eq": assignee_id}}
183
-
300
+
184
301
  # Priority filter
185
302
  if priority:
186
303
  linear_priority = get_linear_priority(priority)
187
304
  issue_filter["priority"] = {"eq": linear_priority}
188
-
305
+
189
306
  # Project filter
190
307
  if project_id:
191
308
  issue_filter["project"] = {"id": {"eq": project_id}}
192
-
309
+
310
+ # Parent filter (for listing children/sub-issues)
311
+ if parent_id:
312
+ issue_filter["parent"] = {"id": {"eq": parent_id}}
313
+
193
314
  # Labels filter
194
315
  if labels:
195
316
  issue_filter["labels"] = {"some": {"name": {"in": labels}}}
196
-
317
+
197
318
  # Date filters
198
319
  if created_after:
199
320
  issue_filter["createdAt"] = {"gte": created_after}
@@ -201,11 +322,11 @@ def build_issue_filter(
201
322
  issue_filter["updatedAt"] = {"gte": updated_after}
202
323
  if due_before:
203
324
  issue_filter["dueDate"] = {"lte": due_before}
204
-
325
+
205
326
  # Archived filter
206
327
  if not include_archived:
207
328
  issue_filter["archivedAt"] = {"null": True}
208
-
329
+
209
330
  return issue_filter
210
331
 
211
332
 
@@ -213,65 +334,67 @@ def build_project_filter(
213
334
  state: str | None = None,
214
335
  team_id: str | None = None,
215
336
  include_completed: bool = True,
216
- ) -> Dict[str, Any]:
337
+ ) -> dict[str, Any]:
217
338
  """Build a Linear project filter from parameters.
218
-
339
+
219
340
  Args:
220
341
  state: Filter by project state
221
342
  team_id: Filter by team ID
222
343
  include_completed: Whether to include completed projects
223
-
344
+
224
345
  Returns:
225
346
  Linear GraphQL filter object
347
+
226
348
  """
227
- project_filter: Dict[str, Any] = {}
228
-
349
+ project_filter: dict[str, Any] = {}
350
+
229
351
  # Team filter
230
352
  if team_id:
231
353
  project_filter["teams"] = {"some": {"id": {"eq": team_id}}}
232
-
354
+
233
355
  # State filter
234
356
  if state:
235
357
  project_filter["state"] = {"eq": state}
236
358
  elif not include_completed:
237
359
  # Exclude completed projects by default
238
360
  project_filter["state"] = {"neq": "completed"}
239
-
361
+
240
362
  return project_filter
241
363
 
242
364
 
243
- def extract_linear_metadata(issue_data: Dict[str, Any]) -> Dict[str, Any]:
365
+ def extract_linear_metadata(issue_data: dict[str, Any]) -> dict[str, Any]:
244
366
  """Extract Linear-specific metadata from issue data.
245
-
367
+
246
368
  Args:
247
369
  issue_data: Raw Linear issue data from GraphQL
248
-
370
+
249
371
  Returns:
250
372
  Dictionary of Linear-specific metadata
373
+
251
374
  """
252
375
  metadata = {}
253
-
376
+
254
377
  # Extract Linear-specific fields
255
378
  if "dueDate" in issue_data and issue_data["dueDate"]:
256
379
  metadata["due_date"] = issue_data["dueDate"]
257
-
380
+
258
381
  if "cycle" in issue_data and issue_data["cycle"]:
259
382
  metadata["cycle_id"] = issue_data["cycle"]["id"]
260
383
  metadata["cycle_name"] = issue_data["cycle"]["name"]
261
-
384
+
262
385
  if "estimate" in issue_data and issue_data["estimate"]:
263
386
  metadata["estimate"] = issue_data["estimate"]
264
-
387
+
265
388
  if "branchName" in issue_data and issue_data["branchName"]:
266
389
  metadata["branch_name"] = issue_data["branchName"]
267
-
390
+
268
391
  if "url" in issue_data:
269
392
  metadata["linear_url"] = issue_data["url"]
270
-
393
+
271
394
  if "slaBreachesAt" in issue_data and issue_data["slaBreachesAt"]:
272
395
  metadata["sla_breaches_at"] = issue_data["slaBreachesAt"]
273
-
396
+
274
397
  if "customerTicketCount" in issue_data:
275
398
  metadata["customer_ticket_count"] = issue_data["customerTicketCount"]
276
-
399
+
277
400
  return metadata
@@ -8,8 +8,8 @@ For new code, import directly from the linear package:
8
8
  from mcp_ticketer.adapters.linear import LinearAdapter
9
9
  """
10
10
 
11
- # Import the refactored adapter
12
- from .linear import LinearAdapter
11
+ # Import the refactored adapter from the modular structure
12
+ from .linear.adapter import LinearAdapter
13
13
 
14
14
  # Re-export for backward compatibility
15
15
  __all__ = ["LinearAdapter"]
@@ -0,0 +1,56 @@
1
+ """Ticket analysis and cleanup tools for PM monitoring.
2
+
3
+ This module provides comprehensive analysis capabilities for ticket health:
4
+ - Similarity detection: Find duplicate or related tickets using TF-IDF
5
+ - Staleness detection: Identify old, inactive tickets
6
+ - Orphaned detection: Find tickets missing hierarchy (epic/project)
7
+ - Cleanup reports: Comprehensive analysis with recommendations
8
+ - Dependency graph: Build and analyze ticket dependency graphs
9
+ - Health assessment: Assess project health based on ticket metrics
10
+ - Project status: Comprehensive project status analysis and work planning
11
+
12
+ These tools help product managers maintain ticket health and development practices.
13
+
14
+ Note: Some analysis features require optional dependencies (scikit-learn, rapidfuzz).
15
+ Install with: pip install mcp-ticketer[analysis]
16
+ """
17
+
18
+ # Import dependency graph and health assessment (no optional deps required)
19
+ from .dependency_graph import DependencyGraph, DependencyNode
20
+ from .health_assessment import HealthAssessor, HealthMetrics, ProjectHealth
21
+ from .project_status import ProjectStatusResult, StatusAnalyzer, TicketRecommendation
22
+
23
+ # Import optional analysis modules (may fail if dependencies not installed)
24
+ try:
25
+ from .orphaned import OrphanedResult, OrphanedTicketDetector
26
+ from .similarity import SimilarityResult, TicketSimilarityAnalyzer
27
+ from .staleness import StalenessResult, StaleTicketDetector
28
+
29
+ ANALYSIS_AVAILABLE = True
30
+ except ImportError:
31
+ # Set placeholder values when optional deps not available
32
+ OrphanedResult = None # type: ignore
33
+ OrphanedTicketDetector = None # type: ignore
34
+ SimilarityResult = None # type: ignore
35
+ TicketSimilarityAnalyzer = None # type: ignore
36
+ StalenessResult = None # type: ignore
37
+ StaleTicketDetector = None # type: ignore
38
+ ANALYSIS_AVAILABLE = False
39
+
40
+ __all__ = [
41
+ "DependencyGraph",
42
+ "DependencyNode",
43
+ "HealthAssessor",
44
+ "HealthMetrics",
45
+ "ProjectHealth",
46
+ "ProjectStatusResult",
47
+ "StatusAnalyzer",
48
+ "TicketRecommendation",
49
+ "SimilarityResult",
50
+ "TicketSimilarityAnalyzer",
51
+ "StalenessResult",
52
+ "StaleTicketDetector",
53
+ "OrphanedResult",
54
+ "OrphanedTicketDetector",
55
+ "ANALYSIS_AVAILABLE",
56
+ ]