devsquad 3.6.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.
Files changed (95) hide show
  1. devsquad-3.6.0.dist-info/METADATA +944 -0
  2. devsquad-3.6.0.dist-info/RECORD +95 -0
  3. devsquad-3.6.0.dist-info/WHEEL +5 -0
  4. devsquad-3.6.0.dist-info/entry_points.txt +2 -0
  5. devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
  6. devsquad-3.6.0.dist-info/top_level.txt +2 -0
  7. scripts/__init__.py +0 -0
  8. scripts/ai_semantic_matcher.py +512 -0
  9. scripts/alert_manager.py +505 -0
  10. scripts/api/__init__.py +43 -0
  11. scripts/api/models.py +386 -0
  12. scripts/api/routes/__init__.py +20 -0
  13. scripts/api/routes/dispatch.py +348 -0
  14. scripts/api/routes/lifecycle.py +330 -0
  15. scripts/api/routes/metrics_gates.py +347 -0
  16. scripts/api_server.py +318 -0
  17. scripts/auth.py +451 -0
  18. scripts/cli/__init__.py +1 -0
  19. scripts/cli/cli_visual.py +642 -0
  20. scripts/cli.py +1094 -0
  21. scripts/collaboration/__init__.py +212 -0
  22. scripts/collaboration/_version.py +1 -0
  23. scripts/collaboration/agent_briefing.py +656 -0
  24. scripts/collaboration/ai_semantic_matcher.py +260 -0
  25. scripts/collaboration/anchor_checker.py +281 -0
  26. scripts/collaboration/anti_rationalization.py +470 -0
  27. scripts/collaboration/async_integration_example.py +255 -0
  28. scripts/collaboration/batch_scheduler.py +149 -0
  29. scripts/collaboration/checkpoint_manager.py +561 -0
  30. scripts/collaboration/ci_feedback_adapter.py +351 -0
  31. scripts/collaboration/code_map_generator.py +247 -0
  32. scripts/collaboration/concern_pack_loader.py +352 -0
  33. scripts/collaboration/confidence_score.py +496 -0
  34. scripts/collaboration/config_loader.py +188 -0
  35. scripts/collaboration/consensus.py +244 -0
  36. scripts/collaboration/context_compressor.py +533 -0
  37. scripts/collaboration/coordinator.py +668 -0
  38. scripts/collaboration/dispatcher.py +1636 -0
  39. scripts/collaboration/dual_layer_context.py +128 -0
  40. scripts/collaboration/enhanced_worker.py +539 -0
  41. scripts/collaboration/feature_usage_tracker.py +206 -0
  42. scripts/collaboration/five_axis_consensus.py +334 -0
  43. scripts/collaboration/input_validator.py +401 -0
  44. scripts/collaboration/integration_example.py +287 -0
  45. scripts/collaboration/intent_workflow_mapper.py +350 -0
  46. scripts/collaboration/language_parsers.py +269 -0
  47. scripts/collaboration/lifecycle_protocol.py +1446 -0
  48. scripts/collaboration/llm_backend.py +453 -0
  49. scripts/collaboration/llm_cache.py +448 -0
  50. scripts/collaboration/llm_cache_async.py +347 -0
  51. scripts/collaboration/llm_retry.py +387 -0
  52. scripts/collaboration/llm_retry_async.py +389 -0
  53. scripts/collaboration/mce_adapter.py +597 -0
  54. scripts/collaboration/memory_bridge.py +1607 -0
  55. scripts/collaboration/models.py +537 -0
  56. scripts/collaboration/null_providers.py +297 -0
  57. scripts/collaboration/operation_classifier.py +289 -0
  58. scripts/collaboration/output_slicer.py +225 -0
  59. scripts/collaboration/performance_monitor.py +462 -0
  60. scripts/collaboration/permission_guard.py +865 -0
  61. scripts/collaboration/prompt_assembler.py +756 -0
  62. scripts/collaboration/prompt_variant_generator.py +483 -0
  63. scripts/collaboration/protocols.py +267 -0
  64. scripts/collaboration/report_formatter.py +352 -0
  65. scripts/collaboration/retrospective.py +279 -0
  66. scripts/collaboration/role_matcher.py +92 -0
  67. scripts/collaboration/role_template_market.py +352 -0
  68. scripts/collaboration/rule_collector.py +678 -0
  69. scripts/collaboration/scratchpad.py +346 -0
  70. scripts/collaboration/skill_registry.py +151 -0
  71. scripts/collaboration/skillifier.py +878 -0
  72. scripts/collaboration/standardized_role_template.py +317 -0
  73. scripts/collaboration/task_completion_checker.py +237 -0
  74. scripts/collaboration/test_quality_guard.py +695 -0
  75. scripts/collaboration/unified_gate_engine.py +598 -0
  76. scripts/collaboration/usage_tracker.py +309 -0
  77. scripts/collaboration/user_friendly_error.py +176 -0
  78. scripts/collaboration/verification_gate.py +312 -0
  79. scripts/collaboration/warmup_manager.py +635 -0
  80. scripts/collaboration/worker.py +513 -0
  81. scripts/collaboration/workflow_engine.py +684 -0
  82. scripts/dashboard.py +1088 -0
  83. scripts/generate_benchmark_report.py +786 -0
  84. scripts/history_manager.py +604 -0
  85. scripts/mcp_server.py +289 -0
  86. skills/__init__.py +32 -0
  87. skills/dispatch/handler.py +52 -0
  88. skills/intent/handler.py +59 -0
  89. skills/registry.py +67 -0
  90. skills/retrospective/__init__.py +0 -0
  91. skills/retrospective/handler.py +125 -0
  92. skills/review/handler.py +356 -0
  93. skills/security/handler.py +454 -0
  94. skills/test/__init__.py +0 -0
  95. skills/test/handler.py +78 -0
@@ -0,0 +1,597 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ MCEAdapter — CarryMem (formerly MCE) Integration Adapter
5
+
6
+ Bridges DevSquad's MemoryBridge with CarryMem, the portable AI memory layer.
7
+ CarryMem is an EXTERNAL project — never modify its source code.
8
+
9
+ Design Principles:
10
+ - CarryMem as optional dependency — auto-degrade on import failure
11
+ - All calls wrapped in try/except — zero intrusion, no impact on main flow
12
+ - Lazy initialization — no impact on cold start speed
13
+ - Type mapping between DevSquad MemoryType and CarryMem memory types
14
+
15
+ CarryMem API Reference (v0.8+):
16
+ - CarryMem() — main entry point (replaces old MemoryClassificationEngineFacade)
17
+ - classify_message(text, context) → dict with entries
18
+ - classify_and_remember(text, context) → dict with entries + stored keys
19
+ - recall_memories(query, filters, limit) → list of dicts
20
+ - forget_memory(memory_id) → bool
21
+ - get_stats() → dict
22
+ - whoami() → dict (user identity profile)
23
+ - check_conflicts() → list of conflicts
24
+ - check_quality(min_score) → list of low-quality memories
25
+
26
+ Memory Type Mapping (DevSquad ↔ CarryMem):
27
+ DevSquad MemoryType | CarryMem Type
28
+ --------------------+------------------
29
+ KNOWLEDGE | fact_declaration
30
+ EPISODIC | task_pattern
31
+ SEMANTIC | sentiment_marker
32
+ FEEDBACK | correction
33
+ PATTERN | task_pattern
34
+ ANALYSIS | decision
35
+ CORRECTION | correction
36
+ (no mapping) | user_preference
37
+ (no mapping) | relationship
38
+
39
+ Example Usage:
40
+ adapter = MCEAdapter(enable=True)
41
+ if adapter.is_available:
42
+ result = adapter.classify("User successfully logged in")
43
+ print(result) # MCEResult(memory_type='decision', confidence=0.92, ...)
44
+ else:
45
+ print("CarryMem unavailable, using default classification")
46
+ """
47
+
48
+ import uuid
49
+ from dataclasses import dataclass, field
50
+ from typing import Any, Dict, List, Optional
51
+ import re as _re
52
+ import unicodedata
53
+ import logging
54
+ import threading
55
+
56
+ logger = logging.getLogger(__name__)
57
+
58
+ CARRYMEM_TO_DEVOPSQUAD = {
59
+ "user_preference": "knowledge",
60
+ "correction": "correction",
61
+ "fact_declaration": "knowledge",
62
+ "decision": "analysis",
63
+ "relationship": "knowledge",
64
+ "task_pattern": "pattern",
65
+ "sentiment_marker": "semantic",
66
+ }
67
+
68
+ DEVSQUAD_TO_CARRYMEM = {
69
+ "knowledge": "fact_declaration",
70
+ "episodic": "task_pattern",
71
+ "semantic": "sentiment_marker",
72
+ "feedback": "correction",
73
+ "pattern": "task_pattern",
74
+ "analysis": "decision",
75
+ "correction": "correction",
76
+ }
77
+
78
+ RULE_TYPES = frozenset({"forbid", "avoid", "always", "prefer"})
79
+
80
+ _MAX_RULE_TEXT_LENGTH = 500
81
+ _USER_ID_BLOCKED_CHARS = _re.compile(r'[<>\'";&|`$\\]')
82
+
83
+ @dataclass
84
+ class MCEResult:
85
+ memory_type: str = ""
86
+ confidence: float = 0.0
87
+ tier: str = "tier2"
88
+ metadata: Dict[str, Any] = field(default_factory=dict)
89
+
90
+ def to_dict(self) -> Dict:
91
+ return {
92
+ "type": self.memory_type,
93
+ "confidence": round(self.confidence, 4),
94
+ "tier": self.tier,
95
+ "metadata": self.metadata,
96
+ }
97
+
98
+
99
+ @dataclass
100
+ class MCEStatus:
101
+ available: bool = False
102
+ version: str = ""
103
+ init_error: Optional[str] = None
104
+ classify_count: int = 0
105
+ classify_fail_count: int = 0
106
+
107
+
108
+ class MCEAdapter:
109
+ """
110
+ CarryMem Integration Adapter
111
+
112
+ Wraps CarryMem (memory_classification_engine.CarryMem) providing
113
+ unified classify / store / retrieve interfaces.
114
+ Gracefully degrades when CarryMem is not installed.
115
+
116
+ Thread Safety: All public methods are thread-safe (internal RLock protection)
117
+ """
118
+
119
+ _instance = None
120
+ _singleton_lock = threading.Lock()
121
+
122
+ def __init__(self, enable: bool = False):
123
+ self._lock = threading.RLock()
124
+ self._status = MCEStatus()
125
+ self._carrymem = None
126
+ self._adapter_type = "none"
127
+
128
+ if enable:
129
+ self._try_init()
130
+
131
+ def _try_init(self):
132
+ with self._lock:
133
+ try:
134
+ from memory_classification_engine.integration.devsquad import DevSquadAdapter
135
+ self._carrymem = DevSquadAdapter()
136
+ self._status.available = True
137
+ self._adapter_type = "devsquad_adapter"
138
+
139
+ version = getattr(
140
+ __import__('memory_classification_engine'),
141
+ '__version__',
142
+ 'unknown'
143
+ )
144
+ self._status.version = str(version)
145
+
146
+ except ImportError:
147
+ try:
148
+ from memory_classification_engine import CarryMem
149
+ self._carrymem = CarryMem()
150
+ self._status.available = True
151
+ self._adapter_type = "carrymem_legacy"
152
+
153
+ version = getattr(
154
+ __import__('memory_classification_engine'),
155
+ '__version__',
156
+ 'unknown'
157
+ )
158
+ self._status.version = str(version)
159
+
160
+ except ImportError as e:
161
+ self._status.available = False
162
+ self._status.init_error = f"CarryMem not installed: {e}"
163
+ self._adapter_type = "none"
164
+ except (AttributeError, ModuleNotFoundError) as e:
165
+ self._status.available = False
166
+ self._status.init_error = f"CarryMem module error: {e}"
167
+ self._adapter_type = "none"
168
+ except Exception as e:
169
+ import logging
170
+ logging.getLogger(__name__).warning(
171
+ f"Unexpected error initializing CarryMem: {type(e).__name__}: {e}"
172
+ )
173
+ self._status.available = False
174
+ self._status.init_error = f"{type(e).__name__}: {e}"
175
+ self._adapter_type = "none"
176
+ except Exception as e:
177
+ import logging
178
+ logging.getLogger(__name__).warning(
179
+ f"Unexpected error in MCEAdapter init: {type(e).__name__}: {e}"
180
+ )
181
+ self._status.available = False
182
+ self._status.init_error = f"{type(e).__name__}: {e}"
183
+ self._adapter_type = "none"
184
+
185
+ @property
186
+ def is_available(self) -> bool:
187
+ return self._status.available
188
+
189
+ @property
190
+ def status(self) -> MCEStatus:
191
+ with self._lock:
192
+ return MCEStatus(
193
+ available=self._status.available,
194
+ version=self._status.version,
195
+ init_error=self._status.init_error,
196
+ classify_count=self._status.classify_count,
197
+ classify_fail_count=self._status.classify_fail_count,
198
+ )
199
+
200
+ def classify(self, text: str,
201
+ context: Optional[Dict] = None,
202
+ timeout_ms: int = 500) -> Optional[MCEResult]:
203
+ with self._lock:
204
+ if not self.is_available or not self._carrymem:
205
+ self._status.classify_fail_count += 1
206
+ return None
207
+
208
+ try:
209
+ import time as _time
210
+ start = _time.time()
211
+ raw_result = self._carrymem.classify_message(text, context)
212
+ elapsed_ms = (_time.time() - start) * 1000
213
+
214
+ if elapsed_ms > timeout_ms:
215
+ self._status.classify_fail_count += 1
216
+ return None
217
+
218
+ result = self._normalize_result(raw_result)
219
+ self._status.classify_count += 1
220
+ return result
221
+
222
+ except Exception as e:
223
+ logger.debug("MCE classify failed: %s", e)
224
+ self._status.classify_fail_count += 1
225
+ return None
226
+
227
+ def classify_batch(self,
228
+ texts: List[str],
229
+ context: Optional[Dict] = None) -> List[Optional[MCEResult]]:
230
+ return [self.classify(t, context) for t in texts]
231
+
232
+ def store_memory(self, memory_data: Dict) -> bool:
233
+ with self._lock:
234
+ if not self.is_available or not self._carrymem:
235
+ return False
236
+ try:
237
+ message = memory_data.get("content", memory_data.get("message", ""))
238
+ context = memory_data.get("context")
239
+ if not message:
240
+ return False
241
+ result = self._carrymem.classify_and_remember(message, context=context)
242
+ return result.get("stored", False)
243
+ except Exception as e:
244
+ logger.debug("MCE store_memory failed: %s", e)
245
+ return False
246
+
247
+ def retrieve_memories(self,
248
+ query: str,
249
+ tier: str = "tier2",
250
+ limit: int = 20,
251
+ memory_type: Optional[str] = None) -> List[Dict]:
252
+ with self._lock:
253
+ if not self.is_available or not self._carrymem:
254
+ return []
255
+ try:
256
+ filters = {}
257
+ if memory_type:
258
+ carrymem_type = DEVSQUAD_TO_CARRYMEM.get(memory_type, memory_type)
259
+ filters["type"] = carrymem_type
260
+ results = self._carrymem.recall_memories(
261
+ query=query, filters=filters or None, limit=limit,
262
+ )
263
+ return results if isinstance(results, list) else []
264
+ except Exception as e:
265
+ logger.debug("MCE retrieve_memories failed: %s", e)
266
+ return []
267
+
268
+ def whoami(self) -> Optional[Dict]:
269
+ with self._lock:
270
+ if not self.is_available or not self._carrymem:
271
+ return None
272
+ try:
273
+ return self._carrymem.whoami()
274
+ except Exception:
275
+ return None
276
+
277
+ def check_conflicts(self) -> List[Dict]:
278
+ with self._lock:
279
+ if not self.is_available or not self._carrymem:
280
+ return []
281
+ try:
282
+ return self._carrymem.check_conflicts()
283
+ except Exception:
284
+ return []
285
+
286
+ def shutdown(self):
287
+ with self._lock:
288
+ if self._carrymem and hasattr(self._carrymem, 'close'):
289
+ try:
290
+ self._carrymem.close()
291
+ except Exception:
292
+ pass
293
+ self._carrymem = None
294
+ self._status.available = False
295
+
296
+ def force_reinit(self):
297
+ self.shutdown()
298
+ self._try_init()
299
+
300
+ def get_stats(self) -> Dict[str, Any]:
301
+ """Return memory statistics from CarryMem."""
302
+ if not self.is_available or not self._carrymem:
303
+ return {
304
+ "total_users": 0,
305
+ "total_rules": 0,
306
+ "available": False,
307
+ "adapter_type": self._adapter_type,
308
+ }
309
+ with self._lock:
310
+ try:
311
+ if hasattr(self._carrymem, 'get_stats'):
312
+ return self._carrymem.get_stats()
313
+ except Exception:
314
+ pass
315
+ return {
316
+ "total_users": 0,
317
+ "total_rules": 0,
318
+ "available": self.is_available,
319
+ "adapter_type": self._adapter_type,
320
+ }
321
+
322
+ @staticmethod
323
+ def _sanitize_user_id(user_id: str) -> str:
324
+ """Sanitize user_id to prevent injection attacks.
325
+
326
+ CarryMem trusts caller-provided user_id without authentication.
327
+ DevSquad must ensure user_id is safe before passing it.
328
+ """
329
+ if not user_id:
330
+ return "default"
331
+ normalized = unicodedata.normalize('NFKC', str(user_id))
332
+ sanitized = _USER_ID_BLOCKED_CHARS.sub('_', normalized)
333
+ while '../' in sanitized or '..\\' in sanitized:
334
+ sanitized = sanitized.replace('../', '_').replace('..\\', '_')
335
+ sanitized = sanitized.replace('/', '_').replace('\\', '_')
336
+ sanitized = sanitized.strip()[:128]
337
+ return sanitized or "default"
338
+
339
+ def match_rules(self, task_description: str, user_id: str,
340
+ role: Optional[str] = None, max_rules: int = 5) -> List[Dict[str, Any]]:
341
+ """Match rules based on task description and role.
342
+
343
+ Calls DevSquadAdapter.match_rules() when available (CarryMem v0.2.8+).
344
+ Falls back to CarryMem legacy API, then keyword-based matching.
345
+ """
346
+ safe_user_id = self._sanitize_user_id(user_id)
347
+
348
+ if not self.is_available or not self._carrymem:
349
+ return self._keyword_fallback_match(task_description, safe_user_id, role, max_rules)
350
+
351
+ with self._lock:
352
+ try:
353
+ if hasattr(self._carrymem, 'match_rules'):
354
+ result = self._carrymem.match_rules(
355
+ task_description=task_description,
356
+ user_id=safe_user_id,
357
+ role=role,
358
+ max_rules=max_rules
359
+ )
360
+ else:
361
+ return self._keyword_fallback_match(task_description, safe_user_id, role, max_rules)
362
+ if isinstance(result, list):
363
+ return self._normalize_matched_rules(result)
364
+ return []
365
+ except Exception as e:
366
+ logger.warning("CarryMem match_rules failed: %s", e)
367
+ return self._keyword_fallback_match(task_description, safe_user_id, role, max_rules)
368
+
369
+ def format_rules_as_prompt(self, rules: List[Dict[str, Any]]) -> str:
370
+ """Format matched rules as injectable prompt text.
371
+
372
+ Calls DevSquadAdapter.format_rules_as_prompt() when available (CarryMem v0.2.8+).
373
+ Falls back to simple formatting when CarryMem is unavailable.
374
+ """
375
+ if not rules:
376
+ return ""
377
+
378
+ for rule in rules:
379
+ for key in ("action", "trigger"):
380
+ text = rule.get(key, "")
381
+ if isinstance(text, str) and len(text) > _MAX_RULE_TEXT_LENGTH:
382
+ rule[key] = text[:_MAX_RULE_TEXT_LENGTH]
383
+
384
+ if not self.is_available or not self._carrymem:
385
+ return self._format_rules_fallback(rules)
386
+
387
+ with self._lock:
388
+ try:
389
+ if hasattr(self._carrymem, 'format_rules_as_prompt'):
390
+ return self._carrymem.format_rules_as_prompt(rules)
391
+ return self._format_rules_fallback(rules)
392
+ except Exception as e:
393
+ logger.warning("CarryMem format_rules_as_prompt failed: %s", e)
394
+ return self._format_rules_fallback(rules)
395
+
396
+ def add_rule(self, trigger: str, action: str, rule_type: str = "always",
397
+ confidence: float = 0.8) -> Optional[Dict[str, Any]]:
398
+ """Add a rule to CarryMem or local storage.
399
+
400
+ Uses DevSquadAdapter.add_rule() when available (CarryMem v0.2.8+).
401
+ Falls back to local storage when CarryMem is unavailable.
402
+ """
403
+ if rule_type not in ("always", "avoid", "prefer", "forbid"):
404
+ rule_type = "always"
405
+
406
+ if self.is_available and self._carrymem:
407
+ with self._lock:
408
+ try:
409
+ if hasattr(self._carrymem, 'add_rule'):
410
+ return self._carrymem.add_rule(
411
+ trigger=trigger, action=action,
412
+ rule_type=rule_type, confidence=confidence,
413
+ )
414
+ except Exception as e:
415
+ logger.warning("CarryMem add_rule failed: %s", e)
416
+
417
+ logger.info("CarryMem add_rule unavailable, storing locally")
418
+ try:
419
+ from scripts.collaboration.rule_collector import RuleData, LocalRuleStorage
420
+ storage = LocalRuleStorage()
421
+ rule = RuleData(
422
+ trigger=trigger, action=action, type=rule_type,
423
+ confidence=confidence, source="mce_adapter_fallback",
424
+ )
425
+ result = storage.store(rule)
426
+ if result.success:
427
+ return {"rule_id": result.rule_id, "storage": "local_fallback",
428
+ "type": rule_type, "success": True}
429
+ except Exception as e:
430
+ logger.error("Local fallback store failed: %s", e)
431
+ return {"rule_id": f"RULE-LOCAL-{uuid.uuid4().hex[:12]}",
432
+ "storage": "local_fallback", "type": rule_type, "success": False}
433
+
434
+ def _keyword_fallback_match(self, task_description: str, user_id: str,
435
+ role: Optional[str] = None,
436
+ max_rules: int = 5) -> List[Dict[str, Any]]:
437
+ """Keyword-based rule matching fallback when CarryMem is unavailable."""
438
+ try:
439
+ all_rules = self._get_all_rules_raw(user_id, role)
440
+ except Exception:
441
+ return []
442
+
443
+ if not all_rules:
444
+ return []
445
+
446
+ desc_lower = task_description.lower()
447
+ desc_words = set(desc_lower.split())
448
+
449
+ scored = []
450
+ for rule in all_rules:
451
+ trigger = rule.get("trigger", "").lower()
452
+ action = rule.get("action", "").lower()
453
+ trigger_words = set(trigger.split())
454
+ overlap = len(desc_words & trigger_words)
455
+ if trigger and trigger in desc_lower:
456
+ overlap += 2
457
+ if overlap > 0:
458
+ rule_copy = dict(rule)
459
+ rule_copy["relevance_score"] = min(1.0, overlap / max(len(trigger_words), 1))
460
+ scored.append(rule_copy)
461
+
462
+ scored.sort(key=lambda r: r.get("relevance_score", 0.0), reverse=True)
463
+ return scored[:max_rules]
464
+
465
+ def _get_all_rules_raw(self, user_id: str, role: Optional[str] = None) -> List[Dict[str, Any]]:
466
+ """Retrieve all rules as raw dicts for keyword fallback matching."""
467
+ if not self.is_available or not self._carrymem:
468
+ return []
469
+
470
+ with self._lock:
471
+ try:
472
+ context = {}
473
+ if role:
474
+ context["role"] = role
475
+ rules_str = self._carrymem.get_rules(user_id=user_id, context=context)
476
+ if not isinstance(rules_str, list):
477
+ return []
478
+ return [self._parse_rule_string(r) for r in rules_str if isinstance(r, str)]
479
+ except Exception:
480
+ return []
481
+
482
+ @staticmethod
483
+ def _parse_rule_string(rule_str: str) -> Dict[str, Any]:
484
+ """Parse a prefixed rule string into a structured dict.
485
+
486
+ Format: "[TYPE] Description text (override)"
487
+ """
488
+ result: Dict[str, Any] = {
489
+ "rule_type": "always",
490
+ "trigger": "",
491
+ "action": rule_str,
492
+ "relevance_score": 0.0,
493
+ "rule_id": "",
494
+ "override": False,
495
+ }
496
+
497
+ stripped = rule_str.strip()
498
+ type_match = _re.match(r'^\[(FORBID|AVOID|ALWAYS)\]\s*', stripped, _re.IGNORECASE)
499
+ if type_match:
500
+ type_str = type_match.group(1).lower()
501
+ if type_str in RULE_TYPES:
502
+ result["rule_type"] = type_str
503
+ stripped = stripped[type_match.end():]
504
+
505
+ if stripped.endswith("(override)"):
506
+ result["override"] = True
507
+ stripped = stripped[:-len("(override)")].strip()
508
+
509
+ result["action"] = stripped
510
+ result["trigger"] = stripped.lower()
511
+ return result
512
+
513
+ @staticmethod
514
+ def _format_rules_fallback(rules: List[Dict[str, Any]]) -> str:
515
+ """Simple rule formatting fallback when CarryMem is unavailable."""
516
+ if not rules:
517
+ return ""
518
+
519
+ parts = ["=== Applicable Rules ==="]
520
+ for rule in rules:
521
+ rule_type = rule.get("rule_type", "always").upper()
522
+ action = rule.get("action", "")
523
+ override = " (non-overridable)" if rule.get("override") else ""
524
+ parts.append(f"[{rule_type}] {action}{override}")
525
+ parts.append("")
526
+ return "\n".join(parts)
527
+
528
+ @staticmethod
529
+ def _normalize_matched_rules(rules: List[Any]) -> List[Dict[str, Any]]:
530
+ """Normalize CarryMem rule objects into standard dict format."""
531
+ normalized = []
532
+ for rule in rules:
533
+ if isinstance(rule, dict):
534
+ rule_type = rule.get("rule_type", rule.get("type", "always"))
535
+ if rule_type not in RULE_TYPES:
536
+ rule_type = "always"
537
+ normalized.append({
538
+ "rule_type": rule_type,
539
+ "trigger": rule.get("trigger", ""),
540
+ "action": rule.get("action", rule.get("description", "")),
541
+ "relevance_score": float(rule.get("relevance_score", rule.get("score", 0.0))),
542
+ "rule_id": str(rule.get("rule_id", rule.get("id", ""))),
543
+ "override": bool(rule.get("override", False)),
544
+ })
545
+ return normalized
546
+
547
+ @staticmethod
548
+ def _normalize_result(raw: Any) -> MCEResult:
549
+ if raw is None:
550
+ return MCEResult()
551
+
552
+ if isinstance(raw, dict):
553
+ entries = raw.get("entries", [])
554
+ if entries:
555
+ first = entries[0] if isinstance(entries[0], dict) else {}
556
+ mt = first.get("type", first.get("memory_type", "general"))
557
+ carrymem_type = str(mt)
558
+ devsqu_type = CARRYMEM_TO_DEVOPSQUAD.get(carrymem_type, carrymem_type)
559
+ conf = first.get("confidence", 0.0)
560
+ if isinstance(conf, (int, float)):
561
+ conf = min(max(float(conf), 0.0), 1.0)
562
+ else:
563
+ try:
564
+ conf = float(str(conf))
565
+ except (ValueError, TypeError):
566
+ conf = 0.5
567
+ tier = first.get("tier", 2)
568
+ tier_str = f"tier{tier}" if isinstance(tier, int) else str(tier)
569
+ meta = {k: v for k, v in first.items()
570
+ if k not in ('type', 'memory_type', 'confidence', 'tier')}
571
+ meta["carrymem_type"] = carrymem_type
572
+ return MCEResult(
573
+ memory_type=devsqu_type,
574
+ confidence=conf,
575
+ tier=tier_str,
576
+ metadata=meta,
577
+ )
578
+ should_remember = raw.get("should_remember", False)
579
+ return MCEResult(
580
+ memory_type="general",
581
+ confidence=0.5,
582
+ metadata={"should_remember": should_remember},
583
+ )
584
+
585
+ if isinstance(raw, str):
586
+ devsqu_type = CARRYMEM_TO_DEVOPSQUAD.get(raw, raw)
587
+ return MCEResult(memory_type=devsqu_type)
588
+
589
+ return MCEResult(metadata={"raw": str(raw)[:200]})
590
+
591
+
592
+ def get_global_mce_adapter(enable: bool = False) -> MCEAdapter:
593
+ if MCEAdapter._instance is None:
594
+ with MCEAdapter._singleton_lock:
595
+ if MCEAdapter._instance is None:
596
+ MCEAdapter._instance = MCEAdapter(enable=enable)
597
+ return MCEAdapter._instance