mcp-ticketer 0.12.0__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.

Files changed (129) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/aitrackdown.py +507 -6
  5. mcp_ticketer/adapters/asana/adapter.py +229 -0
  6. mcp_ticketer/adapters/asana/mappers.py +14 -0
  7. mcp_ticketer/adapters/github/__init__.py +26 -0
  8. mcp_ticketer/adapters/github/adapter.py +3229 -0
  9. mcp_ticketer/adapters/github/client.py +335 -0
  10. mcp_ticketer/adapters/github/mappers.py +797 -0
  11. mcp_ticketer/adapters/github/queries.py +692 -0
  12. mcp_ticketer/adapters/github/types.py +460 -0
  13. mcp_ticketer/adapters/hybrid.py +47 -5
  14. mcp_ticketer/adapters/jira/__init__.py +35 -0
  15. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  16. mcp_ticketer/adapters/jira/client.py +271 -0
  17. mcp_ticketer/adapters/jira/mappers.py +246 -0
  18. mcp_ticketer/adapters/jira/queries.py +216 -0
  19. mcp_ticketer/adapters/jira/types.py +304 -0
  20. mcp_ticketer/adapters/linear/adapter.py +2730 -139
  21. mcp_ticketer/adapters/linear/client.py +175 -3
  22. mcp_ticketer/adapters/linear/mappers.py +203 -8
  23. mcp_ticketer/adapters/linear/queries.py +280 -3
  24. mcp_ticketer/adapters/linear/types.py +120 -4
  25. mcp_ticketer/analysis/__init__.py +56 -0
  26. mcp_ticketer/analysis/dependency_graph.py +255 -0
  27. mcp_ticketer/analysis/health_assessment.py +304 -0
  28. mcp_ticketer/analysis/orphaned.py +218 -0
  29. mcp_ticketer/analysis/project_status.py +594 -0
  30. mcp_ticketer/analysis/similarity.py +224 -0
  31. mcp_ticketer/analysis/staleness.py +266 -0
  32. mcp_ticketer/automation/__init__.py +11 -0
  33. mcp_ticketer/automation/project_updates.py +378 -0
  34. mcp_ticketer/cli/adapter_diagnostics.py +3 -1
  35. mcp_ticketer/cli/auggie_configure.py +17 -5
  36. mcp_ticketer/cli/codex_configure.py +97 -61
  37. mcp_ticketer/cli/configure.py +1288 -105
  38. mcp_ticketer/cli/cursor_configure.py +314 -0
  39. mcp_ticketer/cli/diagnostics.py +13 -12
  40. mcp_ticketer/cli/discover.py +5 -0
  41. mcp_ticketer/cli/gemini_configure.py +17 -5
  42. mcp_ticketer/cli/init_command.py +880 -0
  43. mcp_ticketer/cli/install_mcp_server.py +418 -0
  44. mcp_ticketer/cli/instruction_commands.py +6 -0
  45. mcp_ticketer/cli/main.py +267 -3175
  46. mcp_ticketer/cli/mcp_configure.py +821 -119
  47. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  48. mcp_ticketer/cli/platform_detection.py +77 -12
  49. mcp_ticketer/cli/platform_installer.py +545 -0
  50. mcp_ticketer/cli/project_update_commands.py +350 -0
  51. mcp_ticketer/cli/setup_command.py +795 -0
  52. mcp_ticketer/cli/simple_health.py +12 -10
  53. mcp_ticketer/cli/ticket_commands.py +705 -103
  54. mcp_ticketer/cli/utils.py +113 -0
  55. mcp_ticketer/core/__init__.py +56 -6
  56. mcp_ticketer/core/adapter.py +533 -2
  57. mcp_ticketer/core/config.py +21 -21
  58. mcp_ticketer/core/exceptions.py +7 -1
  59. mcp_ticketer/core/label_manager.py +732 -0
  60. mcp_ticketer/core/mappers.py +31 -19
  61. mcp_ticketer/core/milestone_manager.py +252 -0
  62. mcp_ticketer/core/models.py +480 -0
  63. mcp_ticketer/core/onepassword_secrets.py +1 -1
  64. mcp_ticketer/core/priority_matcher.py +463 -0
  65. mcp_ticketer/core/project_config.py +132 -14
  66. mcp_ticketer/core/project_utils.py +281 -0
  67. mcp_ticketer/core/project_validator.py +376 -0
  68. mcp_ticketer/core/session_state.py +176 -0
  69. mcp_ticketer/core/state_matcher.py +625 -0
  70. mcp_ticketer/core/url_parser.py +425 -0
  71. mcp_ticketer/core/validators.py +69 -0
  72. mcp_ticketer/mcp/server/__main__.py +2 -1
  73. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  74. mcp_ticketer/mcp/server/main.py +106 -25
  75. mcp_ticketer/mcp/server/routing.py +723 -0
  76. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  77. mcp_ticketer/mcp/server/tools/__init__.py +33 -11
  78. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  79. mcp_ticketer/mcp/server/tools/attachment_tools.py +5 -5
  80. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  81. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  82. mcp_ticketer/mcp/server/tools/config_tools.py +1391 -145
  83. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  84. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
  85. mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
  86. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  87. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  88. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  89. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  90. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  91. mcp_ticketer/mcp/server/tools/search_tools.py +209 -97
  92. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  93. mcp_ticketer/mcp/server/tools/ticket_tools.py +1107 -124
  94. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
  95. mcp_ticketer/queue/queue.py +68 -0
  96. mcp_ticketer/queue/worker.py +1 -1
  97. mcp_ticketer/utils/__init__.py +5 -0
  98. mcp_ticketer/utils/token_utils.py +246 -0
  99. mcp_ticketer-2.2.13.dist-info/METADATA +1396 -0
  100. mcp_ticketer-2.2.13.dist-info/RECORD +158 -0
  101. mcp_ticketer-2.2.13.dist-info/top_level.txt +2 -0
  102. py_mcp_installer/examples/phase3_demo.py +178 -0
  103. py_mcp_installer/scripts/manage_version.py +54 -0
  104. py_mcp_installer/setup.py +6 -0
  105. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  106. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  107. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  108. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  109. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  110. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  111. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  112. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  113. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  114. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  115. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  116. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  117. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  118. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  119. py_mcp_installer/tests/__init__.py +0 -0
  120. py_mcp_installer/tests/platforms/__init__.py +0 -0
  121. py_mcp_installer/tests/test_platform_detector.py +17 -0
  122. mcp_ticketer/adapters/github.py +0 -1574
  123. mcp_ticketer/adapters/jira.py +0 -1258
  124. mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
  125. mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
  126. mcp_ticketer-0.12.0.dist-info/top_level.txt +0 -1
  127. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
  128. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
  129. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,625 @@
1
+ """Semantic state matcher for natural language ticket state transitions.
2
+
3
+ This module provides intelligent state matching that accepts natural language inputs
4
+ and resolves them to universal TicketState values with confidence scoring.
5
+
6
+ Features:
7
+ - Comprehensive synonym dictionary (50+ synonyms per state)
8
+ - Multi-stage matching pipeline (exact → synonym → fuzzy → adapter)
9
+ - Confidence scoring with thresholds
10
+ - Support for all 8 universal states
11
+ - Adapter-specific state resolution
12
+
13
+ Design Decision: Multi-Stage Matching Pipeline
14
+ ----------------------------------------------
15
+ The matcher uses a cascading approach to maximize accuracy while maintaining
16
+ flexibility:
17
+
18
+ 1. Exact Match: Direct state name match (confidence: 1.0)
19
+ 2. Synonym Match: Pre-defined synonym lookup (confidence: 0.95)
20
+ 3. Fuzzy Match: Levenshtein distance with thresholds (confidence: 0.70-0.95)
21
+ 4. Adapter Match: Optional adapter-specific state names (confidence: 0.90)
22
+
23
+ This approach ensures high confidence for common inputs while gracefully handling
24
+ typos and variations.
25
+
26
+ Performance Considerations:
27
+ - Average match time: <5ms (target: <10ms)
28
+ - Synonym lookup: O(1) with dict hashing
29
+ - Fuzzy matching: O(n) where n = number of states (8)
30
+ - Memory footprint: <1MB for matcher instance
31
+
32
+ Example:
33
+ >>> matcher = SemanticStateMatcher()
34
+ >>> result = matcher.match_state("working on it")
35
+ >>> print(f"{result.state.value} (confidence: {result.confidence})")
36
+ in_progress (confidence: 0.95)
37
+
38
+ >>> suggestions = matcher.suggest_states("review", top_n=3)
39
+ >>> for s in suggestions:
40
+ ... print(f"{s.state.value}: {s.confidence}")
41
+ ready: 0.95
42
+ tested: 0.75
43
+
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ from dataclasses import dataclass
49
+
50
+ try:
51
+ from rapidfuzz import fuzz
52
+
53
+ FUZZY_AVAILABLE = True
54
+ except ImportError:
55
+ FUZZY_AVAILABLE = False
56
+
57
+ from .models import TicketState
58
+
59
+
60
+ @dataclass
61
+ class StateMatchResult:
62
+ """Result of a state matching operation.
63
+
64
+ Attributes:
65
+ state: Matched TicketState
66
+ confidence: Confidence score (0.0-1.0)
67
+ match_type: Type of match used (exact, synonym, fuzzy, adapter)
68
+ original_input: Original user input string
69
+ suggestions: Alternative matches for ambiguous inputs
70
+
71
+ """
72
+
73
+ state: TicketState
74
+ confidence: float
75
+ match_type: str
76
+ original_input: str
77
+ suggestions: list[StateMatchResult] | None = None
78
+
79
+ def is_high_confidence(self) -> bool:
80
+ """Check if confidence is high enough for auto-apply."""
81
+ return self.confidence >= 0.90
82
+
83
+ def is_medium_confidence(self) -> bool:
84
+ """Check if confidence is medium (needs confirmation)."""
85
+ return 0.70 <= self.confidence < 0.90
86
+
87
+ def is_low_confidence(self) -> bool:
88
+ """Check if confidence is too low (ambiguous)."""
89
+ return self.confidence < 0.70
90
+
91
+
92
+ @dataclass
93
+ class ValidationResult:
94
+ """Result of a state transition validation.
95
+
96
+ Attributes:
97
+ is_valid: Whether the transition is allowed
98
+ match_result: State matching result for target state
99
+ current_state: Current ticket state
100
+ error_message: Error message if invalid
101
+ valid_transitions: List of valid target states
102
+
103
+ """
104
+
105
+ is_valid: bool
106
+ match_result: StateMatchResult | None
107
+ current_state: TicketState
108
+ error_message: str | None = None
109
+ valid_transitions: list[TicketState] | None = None
110
+
111
+
112
+ class SemanticStateMatcher:
113
+ """Intelligent state matcher with natural language support.
114
+
115
+ Provides comprehensive synonym matching, fuzzy matching, and confidence
116
+ scoring for ticket state transitions.
117
+
118
+ The synonym dictionary includes 50+ synonyms across all 8 universal states,
119
+ covering common variations, typos, and platform-specific terminology.
120
+ """
121
+
122
+ # Comprehensive synonym dictionary for all universal states
123
+ STATE_SYNONYMS: dict[TicketState, list[str]] = {
124
+ TicketState.OPEN: [
125
+ "open",
126
+ "todo",
127
+ "to do",
128
+ "to-do",
129
+ "backlog",
130
+ "new",
131
+ "pending",
132
+ "queued",
133
+ "unstarted",
134
+ "not started",
135
+ "not-started",
136
+ "planned",
137
+ "triage",
138
+ "inbox",
139
+ ],
140
+ TicketState.IN_PROGRESS: [
141
+ "in_progress",
142
+ "in progress",
143
+ "in-progress",
144
+ "working",
145
+ "started",
146
+ "active",
147
+ "doing",
148
+ "in development",
149
+ "in-development",
150
+ "in dev",
151
+ "wip",
152
+ "work in progress",
153
+ "working on it",
154
+ "in flight",
155
+ "in-flight",
156
+ "ongoing",
157
+ ],
158
+ TicketState.READY: [
159
+ "ready",
160
+ "review",
161
+ "needs review",
162
+ "needs-review",
163
+ "pr ready",
164
+ "pr-ready",
165
+ "code review",
166
+ "code-review",
167
+ "done dev",
168
+ "done-dev",
169
+ "dev done",
170
+ "dev-done",
171
+ "qa ready",
172
+ "qa-ready",
173
+ "ready for review",
174
+ "ready for testing",
175
+ "ready-for-review",
176
+ "awaiting review",
177
+ ],
178
+ TicketState.TESTED: [
179
+ "tested",
180
+ "qa done",
181
+ "qa-done",
182
+ "qa complete",
183
+ "qa-complete",
184
+ "qa approved",
185
+ "verified",
186
+ "passed qa",
187
+ "passed-qa",
188
+ "qa passed",
189
+ "qa-passed",
190
+ "approved",
191
+ "validation complete",
192
+ "validation-complete",
193
+ "testing complete",
194
+ "testing-complete",
195
+ ],
196
+ TicketState.DONE: [
197
+ "done",
198
+ "completed",
199
+ "complete",
200
+ "finished",
201
+ "resolved",
202
+ "done done",
203
+ "done-done",
204
+ "delivered",
205
+ "shipped",
206
+ "merged",
207
+ "deployed",
208
+ "released",
209
+ "accepted",
210
+ ],
211
+ TicketState.WAITING: [
212
+ "waiting",
213
+ "on hold",
214
+ "on-hold",
215
+ "paused",
216
+ "waiting for",
217
+ "waiting-for",
218
+ "pending external",
219
+ "pending-external",
220
+ "deferred",
221
+ "stalled",
222
+ "awaiting",
223
+ "awaiting response",
224
+ "awaiting-response",
225
+ "external dependency",
226
+ "external-dependency",
227
+ ],
228
+ TicketState.BLOCKED: [
229
+ "blocked",
230
+ "stuck",
231
+ "can't proceed",
232
+ "cannot proceed",
233
+ "cant proceed",
234
+ "impediment",
235
+ "blocked by",
236
+ "blocked-by",
237
+ "stopped",
238
+ "obstructed",
239
+ "blocker",
240
+ "blocked on",
241
+ "blocked-on",
242
+ "needs unblocking",
243
+ ],
244
+ TicketState.CLOSED: [
245
+ "closed",
246
+ "archived",
247
+ "cancelled",
248
+ "canceled",
249
+ "won't do",
250
+ "wont do",
251
+ "won't-do",
252
+ "wont-do",
253
+ "abandoned",
254
+ "invalidated",
255
+ "rejected",
256
+ "obsolete",
257
+ "duplicate",
258
+ "wontfix",
259
+ "won't fix",
260
+ ],
261
+ }
262
+
263
+ # Confidence thresholds
264
+ CONFIDENCE_HIGH = 0.90
265
+ CONFIDENCE_MEDIUM = 0.70
266
+ FUZZY_THRESHOLD_HIGH = 90
267
+ FUZZY_THRESHOLD_MEDIUM = 70
268
+
269
+ def __init__(self) -> None:
270
+ """Initialize the semantic state matcher.
271
+
272
+ Creates reverse lookup dictionary for O(1) synonym matching.
273
+ Detects and logs duplicate synonyms across states.
274
+ """
275
+ import logging
276
+
277
+ logger = logging.getLogger(__name__)
278
+
279
+ # Build reverse lookup: synonym -> (state, is_exact)
280
+ self._synonym_to_state: dict[str, tuple[TicketState, bool]] = {}
281
+
282
+ # Track duplicates for validation
283
+ duplicate_check: dict[str, list[TicketState]] = {}
284
+
285
+ for state in TicketState:
286
+ # Add exact state value
287
+ normalized_value = state.value.lower()
288
+ self._synonym_to_state[normalized_value] = (state, True)
289
+
290
+ if normalized_value not in duplicate_check:
291
+ duplicate_check[normalized_value] = []
292
+ duplicate_check[normalized_value].append(state)
293
+
294
+ # Add all synonyms
295
+ for synonym in self.STATE_SYNONYMS.get(state, []):
296
+ normalized_synonym = synonym.lower()
297
+
298
+ # Check for duplicates
299
+ if normalized_synonym in duplicate_check:
300
+ duplicate_check[normalized_synonym].append(state)
301
+ else:
302
+ duplicate_check[normalized_synonym] = [state]
303
+
304
+ self._synonym_to_state[normalized_synonym] = (state, False)
305
+
306
+ # Log warnings for any duplicates found (excluding expected state value duplicates)
307
+ for synonym, states in duplicate_check.items():
308
+ if len(states) > 1:
309
+ # Filter out duplicate state values (they're expected - exact match + synonym)
310
+ unique_states = list(set(states))
311
+ if len(unique_states) > 1:
312
+ logger.warning(
313
+ "Duplicate synonym '%s' found in multiple states: %s. "
314
+ "This may cause non-deterministic behavior.",
315
+ synonym,
316
+ ", ".join(s.value for s in unique_states),
317
+ )
318
+
319
+ def match_state(
320
+ self,
321
+ user_input: str,
322
+ adapter_states: list[str] | None = None,
323
+ ) -> StateMatchResult:
324
+ """Match user input to universal state with confidence score.
325
+
326
+ Uses multi-stage matching pipeline:
327
+ 1. Exact match against state values
328
+ 2. Synonym lookup
329
+ 3. Fuzzy matching with Levenshtein distance
330
+ 4. Optional adapter-specific state matching
331
+
332
+ Args:
333
+ user_input: Natural language state input from user
334
+ adapter_states: Optional list of adapter-specific state names
335
+
336
+ Returns:
337
+ StateMatchResult with matched state and confidence score
338
+
339
+ Example:
340
+ >>> matcher = SemanticStateMatcher()
341
+ >>> result = matcher.match_state("working on it")
342
+ >>> print(f"{result.state.value}: {result.confidence}")
343
+ in_progress: 0.95
344
+
345
+ >>> result = matcher.match_state("reviw") # typo
346
+ >>> print(f"{result.state.value}: {result.confidence}")
347
+ ready: 0.85
348
+
349
+ """
350
+ if not user_input:
351
+ # Default to OPEN for empty input
352
+ return StateMatchResult(
353
+ state=TicketState.OPEN,
354
+ confidence=0.5,
355
+ match_type="default",
356
+ original_input=user_input,
357
+ )
358
+
359
+ # Normalize input
360
+ normalized = user_input.strip().lower()
361
+
362
+ # Stage 1: Exact match
363
+ exact_result = self._exact_match(normalized)
364
+ if exact_result:
365
+ return exact_result
366
+
367
+ # Stage 2: Synonym match
368
+ synonym_result = self._synonym_match(normalized)
369
+ if synonym_result:
370
+ return synonym_result
371
+
372
+ # Stage 3: Adapter state match (if provided)
373
+ if adapter_states:
374
+ adapter_result = self._adapter_match(normalized, adapter_states)
375
+ if adapter_result:
376
+ return adapter_result
377
+
378
+ # Stage 4: Fuzzy match
379
+ fuzzy_result = self._fuzzy_match(normalized)
380
+ if fuzzy_result:
381
+ return fuzzy_result
382
+
383
+ # No good match found - return suggestions
384
+ suggestions = self.suggest_states(user_input, top_n=3)
385
+ return StateMatchResult(
386
+ state=suggestions[0].state if suggestions else TicketState.OPEN,
387
+ confidence=suggestions[0].confidence if suggestions else 0.5,
388
+ match_type="fallback",
389
+ original_input=user_input,
390
+ suggestions=suggestions,
391
+ )
392
+
393
+ def suggest_states(
394
+ self,
395
+ user_input: str,
396
+ top_n: int = 3,
397
+ ) -> list[StateMatchResult]:
398
+ """Return top N state suggestions for ambiguous inputs.
399
+
400
+ Uses fuzzy matching to rank all possible states by similarity.
401
+ Useful for providing user with multiple options when confidence is low.
402
+
403
+ Args:
404
+ user_input: Natural language state input
405
+ top_n: Number of suggestions to return (default: 3)
406
+
407
+ Returns:
408
+ List of StateMatchResult sorted by confidence (highest first)
409
+
410
+ Example:
411
+ >>> matcher = SemanticStateMatcher()
412
+ >>> suggestions = matcher.suggest_states("dne", top_n=3)
413
+ >>> for s in suggestions:
414
+ ... print(f"{s.state.value}: {s.confidence:.2f}")
415
+ done: 0.75
416
+ open: 0.45
417
+ closed: 0.42
418
+
419
+ """
420
+ if not FUZZY_AVAILABLE:
421
+ # Without fuzzy matching, return all states with low confidence
422
+ return [
423
+ StateMatchResult(
424
+ state=state,
425
+ confidence=0.5,
426
+ match_type="suggestion",
427
+ original_input=user_input,
428
+ )
429
+ for state in TicketState
430
+ ][:top_n]
431
+
432
+ normalized = user_input.strip().lower()
433
+ suggestions: list[tuple[TicketState, float, str]] = []
434
+
435
+ # Calculate similarity for each state and its synonyms
436
+ for state in TicketState:
437
+ # Check state value
438
+ state_similarity = fuzz.ratio(normalized, state.value.lower())
439
+ max_similarity = state_similarity
440
+ match_text = state.value
441
+
442
+ # Check synonyms
443
+ for synonym in self.STATE_SYNONYMS.get(state, []):
444
+ similarity = fuzz.ratio(normalized, synonym.lower())
445
+ if similarity > max_similarity:
446
+ max_similarity = similarity
447
+ match_text = synonym
448
+
449
+ # Convert similarity to confidence (0-100 → 0.0-1.0)
450
+ confidence = max_similarity / 100.0
451
+ suggestions.append((state, confidence, match_text))
452
+
453
+ # Sort by confidence descending
454
+ suggestions.sort(key=lambda x: x[1], reverse=True)
455
+
456
+ # Convert to StateMatchResult
457
+ return [
458
+ StateMatchResult(
459
+ state=state,
460
+ confidence=conf,
461
+ match_type="suggestion",
462
+ original_input=user_input,
463
+ )
464
+ for state, conf, _ in suggestions[:top_n]
465
+ ]
466
+
467
+ def validate_transition(
468
+ self,
469
+ current_state: TicketState,
470
+ target_input: str,
471
+ ) -> ValidationResult:
472
+ """Validate if transition is allowed and resolve target state.
473
+
474
+ Combines state matching with workflow validation to ensure the
475
+ transition is both semantically valid and allowed by workflow rules.
476
+
477
+ Args:
478
+ current_state: Current ticket state
479
+ target_input: Natural language target state input
480
+
481
+ Returns:
482
+ ValidationResult with validation status and match result
483
+
484
+ Example:
485
+ >>> matcher = SemanticStateMatcher()
486
+ >>> result = matcher.validate_transition(
487
+ ... TicketState.OPEN,
488
+ ... "working on it"
489
+ ... )
490
+ >>> print(f"Valid: {result.is_valid}")
491
+ Valid: True
492
+
493
+ """
494
+ # Match the target state
495
+ match_result = self.match_state(target_input)
496
+
497
+ # Check if transition is allowed
498
+ if not current_state.can_transition_to(match_result.state):
499
+ valid_transitions_dict = TicketState.valid_transitions()
500
+ # The return type annotation is dict[str, list[str]] but actually returns
501
+ # dict[TicketState, list[TicketState]], so we need to cast properly
502
+ valid_transitions_raw = valid_transitions_dict.get(current_state, [])
503
+ valid_transitions: list[TicketState] = [
504
+ s for s in valid_transitions_raw if isinstance(s, TicketState)
505
+ ]
506
+
507
+ return ValidationResult(
508
+ is_valid=False,
509
+ match_result=match_result,
510
+ current_state=current_state,
511
+ error_message=(
512
+ f"Cannot transition from {current_state.value} to "
513
+ f"{match_result.state.value}. Valid transitions: "
514
+ f"{', '.join(s.value for s in valid_transitions) if valid_transitions else 'none (terminal state)'}"
515
+ ),
516
+ valid_transitions=valid_transitions,
517
+ )
518
+
519
+ return ValidationResult(
520
+ is_valid=True,
521
+ match_result=match_result,
522
+ current_state=current_state,
523
+ )
524
+
525
+ def _exact_match(self, normalized_input: str) -> StateMatchResult | None:
526
+ """Match exact state value."""
527
+ for state in TicketState:
528
+ if normalized_input == state.value.lower():
529
+ return StateMatchResult(
530
+ state=state,
531
+ confidence=1.0,
532
+ match_type="exact",
533
+ original_input=normalized_input,
534
+ )
535
+ return None
536
+
537
+ def _synonym_match(self, normalized_input: str) -> StateMatchResult | None:
538
+ """Match using synonym dictionary."""
539
+ if normalized_input in self._synonym_to_state:
540
+ state, is_exact = self._synonym_to_state[normalized_input]
541
+ return StateMatchResult(
542
+ state=state,
543
+ confidence=1.0 if is_exact else 0.95,
544
+ match_type="exact" if is_exact else "synonym",
545
+ original_input=normalized_input,
546
+ )
547
+ return None
548
+
549
+ def _adapter_match(
550
+ self,
551
+ normalized_input: str,
552
+ adapter_states: list[str],
553
+ ) -> StateMatchResult | None:
554
+ """Match using adapter-specific state names."""
555
+ # This is a simplified version - adapters should provide their own mapping
556
+ # For now, just do fuzzy matching against adapter states
557
+ if not FUZZY_AVAILABLE:
558
+ return None
559
+
560
+ for adapter_state in adapter_states:
561
+ similarity = fuzz.ratio(normalized_input, adapter_state.lower())
562
+ if similarity >= self.FUZZY_THRESHOLD_HIGH:
563
+ # Try to find which universal state this maps to
564
+ # This requires adapter to implement get_state_mapping
565
+ # For now, return None to fall through to fuzzy match
566
+ pass
567
+
568
+ return None
569
+
570
+ def _fuzzy_match(self, normalized_input: str) -> StateMatchResult | None:
571
+ """Match using fuzzy string matching."""
572
+ if not FUZZY_AVAILABLE:
573
+ return None
574
+
575
+ best_match: tuple[TicketState, float, str] | None = None
576
+
577
+ for state in TicketState:
578
+ # Check state value
579
+ state_similarity = fuzz.ratio(normalized_input, state.value.lower())
580
+
581
+ if state_similarity >= self.FUZZY_THRESHOLD_MEDIUM:
582
+ if best_match is None or state_similarity > best_match[1]:
583
+ best_match = (state, state_similarity, "state_value")
584
+
585
+ # Check synonyms
586
+ for synonym in self.STATE_SYNONYMS.get(state, []):
587
+ similarity = fuzz.ratio(normalized_input, synonym.lower())
588
+ if similarity >= self.FUZZY_THRESHOLD_MEDIUM:
589
+ if best_match is None or similarity > best_match[1]:
590
+ best_match = (state, similarity, "synonym")
591
+
592
+ if best_match:
593
+ state, similarity, match_source = best_match
594
+
595
+ # Calculate confidence based on similarity
596
+ if similarity >= self.FUZZY_THRESHOLD_HIGH:
597
+ confidence = 0.85 + (similarity - self.FUZZY_THRESHOLD_HIGH) / 100.0
598
+ else:
599
+ confidence = 0.70 + (similarity - self.FUZZY_THRESHOLD_MEDIUM) / 200.0
600
+
601
+ return StateMatchResult(
602
+ state=state,
603
+ confidence=min(confidence, 0.95), # Cap at 0.95
604
+ match_type="fuzzy",
605
+ original_input=normalized_input,
606
+ )
607
+
608
+ return None
609
+
610
+
611
+ # Singleton instance for convenience
612
+ _default_matcher: SemanticStateMatcher | None = None
613
+
614
+
615
+ def get_state_matcher() -> SemanticStateMatcher:
616
+ """Get the default state matcher instance.
617
+
618
+ Returns:
619
+ Singleton SemanticStateMatcher instance
620
+
621
+ """
622
+ global _default_matcher
623
+ if _default_matcher is None:
624
+ _default_matcher = SemanticStateMatcher()
625
+ return _default_matcher