empathy-framework 3.7.0__py3-none-any.whl → 3.8.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 (274) hide show
  1. coach_wizards/code_reviewer_README.md +60 -0
  2. coach_wizards/code_reviewer_wizard.py +180 -0
  3. {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/METADATA +148 -11
  4. empathy_framework-3.8.0.dist-info/RECORD +333 -0
  5. {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/top_level.txt +5 -1
  6. empathy_healthcare_plugin/monitors/__init__.py +9 -0
  7. empathy_healthcare_plugin/monitors/clinical_protocol_monitor.py +315 -0
  8. empathy_healthcare_plugin/monitors/monitoring/__init__.py +44 -0
  9. empathy_healthcare_plugin/monitors/monitoring/protocol_checker.py +300 -0
  10. empathy_healthcare_plugin/monitors/monitoring/protocol_loader.py +214 -0
  11. empathy_healthcare_plugin/monitors/monitoring/sensor_parsers.py +306 -0
  12. empathy_healthcare_plugin/monitors/monitoring/trajectory_analyzer.py +389 -0
  13. empathy_llm_toolkit/agent_factory/__init__.py +53 -0
  14. empathy_llm_toolkit/agent_factory/adapters/__init__.py +85 -0
  15. empathy_llm_toolkit/agent_factory/adapters/autogen_adapter.py +312 -0
  16. empathy_llm_toolkit/agent_factory/adapters/crewai_adapter.py +454 -0
  17. empathy_llm_toolkit/agent_factory/adapters/haystack_adapter.py +298 -0
  18. empathy_llm_toolkit/agent_factory/adapters/langchain_adapter.py +362 -0
  19. empathy_llm_toolkit/agent_factory/adapters/langgraph_adapter.py +333 -0
  20. empathy_llm_toolkit/agent_factory/adapters/native.py +228 -0
  21. empathy_llm_toolkit/agent_factory/adapters/wizard_adapter.py +426 -0
  22. empathy_llm_toolkit/agent_factory/base.py +305 -0
  23. empathy_llm_toolkit/agent_factory/crews/__init__.py +67 -0
  24. empathy_llm_toolkit/agent_factory/crews/code_review.py +1113 -0
  25. empathy_llm_toolkit/agent_factory/crews/health_check.py +1246 -0
  26. empathy_llm_toolkit/agent_factory/crews/refactoring.py +1128 -0
  27. empathy_llm_toolkit/agent_factory/crews/security_audit.py +1018 -0
  28. empathy_llm_toolkit/agent_factory/decorators.py +286 -0
  29. empathy_llm_toolkit/agent_factory/factory.py +558 -0
  30. empathy_llm_toolkit/agent_factory/framework.py +192 -0
  31. empathy_llm_toolkit/agent_factory/memory_integration.py +324 -0
  32. empathy_llm_toolkit/agent_factory/resilient.py +320 -0
  33. empathy_llm_toolkit/cli/__init__.py +8 -0
  34. empathy_llm_toolkit/cli/sync_claude.py +487 -0
  35. empathy_llm_toolkit/code_health.py +150 -3
  36. empathy_llm_toolkit/config/__init__.py +29 -0
  37. empathy_llm_toolkit/config/unified.py +295 -0
  38. empathy_llm_toolkit/routing/__init__.py +32 -0
  39. empathy_llm_toolkit/routing/model_router.py +362 -0
  40. empathy_llm_toolkit/security/IMPLEMENTATION_SUMMARY.md +413 -0
  41. empathy_llm_toolkit/security/PHASE2_COMPLETE.md +384 -0
  42. empathy_llm_toolkit/security/PHASE2_SECRETS_DETECTOR_COMPLETE.md +271 -0
  43. empathy_llm_toolkit/security/QUICK_REFERENCE.md +316 -0
  44. empathy_llm_toolkit/security/README.md +262 -0
  45. empathy_llm_toolkit/security/__init__.py +62 -0
  46. empathy_llm_toolkit/security/audit_logger.py +929 -0
  47. empathy_llm_toolkit/security/audit_logger_example.py +152 -0
  48. empathy_llm_toolkit/security/pii_scrubber.py +640 -0
  49. empathy_llm_toolkit/security/secrets_detector.py +678 -0
  50. empathy_llm_toolkit/security/secrets_detector_example.py +304 -0
  51. empathy_llm_toolkit/security/secure_memdocs.py +1192 -0
  52. empathy_llm_toolkit/security/secure_memdocs_example.py +278 -0
  53. empathy_llm_toolkit/wizards/__init__.py +38 -0
  54. empathy_llm_toolkit/wizards/base_wizard.py +364 -0
  55. empathy_llm_toolkit/wizards/customer_support_wizard.py +190 -0
  56. empathy_llm_toolkit/wizards/healthcare_wizard.py +362 -0
  57. empathy_llm_toolkit/wizards/patient_assessment_README.md +64 -0
  58. empathy_llm_toolkit/wizards/patient_assessment_wizard.py +193 -0
  59. empathy_llm_toolkit/wizards/technology_wizard.py +194 -0
  60. empathy_os/__init__.py +52 -52
  61. empathy_os/adaptive/__init__.py +13 -0
  62. empathy_os/adaptive/task_complexity.py +127 -0
  63. empathy_os/cache/__init__.py +117 -0
  64. empathy_os/cache/base.py +166 -0
  65. empathy_os/cache/dependency_manager.py +253 -0
  66. empathy_os/cache/hash_only.py +248 -0
  67. empathy_os/cache/hybrid.py +390 -0
  68. empathy_os/cache/storage.py +282 -0
  69. empathy_os/cli.py +118 -8
  70. empathy_os/cli_unified.py +121 -1
  71. empathy_os/config/__init__.py +63 -0
  72. empathy_os/config/xml_config.py +239 -0
  73. empathy_os/config.py +2 -1
  74. empathy_os/dashboard/__init__.py +15 -0
  75. empathy_os/dashboard/server.py +743 -0
  76. empathy_os/memory/__init__.py +195 -0
  77. empathy_os/memory/claude_memory.py +466 -0
  78. empathy_os/memory/config.py +224 -0
  79. empathy_os/memory/control_panel.py +1298 -0
  80. empathy_os/memory/edges.py +179 -0
  81. empathy_os/memory/graph.py +567 -0
  82. empathy_os/memory/long_term.py +1194 -0
  83. empathy_os/memory/nodes.py +179 -0
  84. empathy_os/memory/redis_bootstrap.py +540 -0
  85. empathy_os/memory/security/__init__.py +31 -0
  86. empathy_os/memory/security/audit_logger.py +930 -0
  87. empathy_os/memory/security/pii_scrubber.py +640 -0
  88. empathy_os/memory/security/secrets_detector.py +678 -0
  89. empathy_os/memory/short_term.py +2119 -0
  90. empathy_os/memory/storage/__init__.py +15 -0
  91. empathy_os/memory/summary_index.py +583 -0
  92. empathy_os/memory/unified.py +619 -0
  93. empathy_os/metrics/__init__.py +12 -0
  94. empathy_os/metrics/prompt_metrics.py +190 -0
  95. empathy_os/models/__init__.py +136 -0
  96. empathy_os/models/__main__.py +13 -0
  97. empathy_os/models/cli.py +655 -0
  98. empathy_os/models/empathy_executor.py +354 -0
  99. empathy_os/models/executor.py +252 -0
  100. empathy_os/models/fallback.py +671 -0
  101. empathy_os/models/provider_config.py +563 -0
  102. empathy_os/models/registry.py +382 -0
  103. empathy_os/models/tasks.py +302 -0
  104. empathy_os/models/telemetry.py +548 -0
  105. empathy_os/models/token_estimator.py +378 -0
  106. empathy_os/models/validation.py +274 -0
  107. empathy_os/monitoring/__init__.py +52 -0
  108. empathy_os/monitoring/alerts.py +23 -0
  109. empathy_os/monitoring/alerts_cli.py +268 -0
  110. empathy_os/monitoring/multi_backend.py +271 -0
  111. empathy_os/monitoring/otel_backend.py +363 -0
  112. empathy_os/optimization/__init__.py +19 -0
  113. empathy_os/optimization/context_optimizer.py +272 -0
  114. empathy_os/plugins/__init__.py +28 -0
  115. empathy_os/plugins/base.py +361 -0
  116. empathy_os/plugins/registry.py +268 -0
  117. empathy_os/project_index/__init__.py +30 -0
  118. empathy_os/project_index/cli.py +335 -0
  119. empathy_os/project_index/crew_integration.py +430 -0
  120. empathy_os/project_index/index.py +425 -0
  121. empathy_os/project_index/models.py +501 -0
  122. empathy_os/project_index/reports.py +473 -0
  123. empathy_os/project_index/scanner.py +538 -0
  124. empathy_os/prompts/__init__.py +61 -0
  125. empathy_os/prompts/config.py +77 -0
  126. empathy_os/prompts/context.py +177 -0
  127. empathy_os/prompts/parser.py +285 -0
  128. empathy_os/prompts/registry.py +313 -0
  129. empathy_os/prompts/templates.py +208 -0
  130. empathy_os/resilience/__init__.py +56 -0
  131. empathy_os/resilience/circuit_breaker.py +256 -0
  132. empathy_os/resilience/fallback.py +179 -0
  133. empathy_os/resilience/health.py +300 -0
  134. empathy_os/resilience/retry.py +209 -0
  135. empathy_os/resilience/timeout.py +135 -0
  136. empathy_os/routing/__init__.py +43 -0
  137. empathy_os/routing/chain_executor.py +433 -0
  138. empathy_os/routing/classifier.py +217 -0
  139. empathy_os/routing/smart_router.py +234 -0
  140. empathy_os/routing/wizard_registry.py +307 -0
  141. empathy_os/trust/__init__.py +28 -0
  142. empathy_os/trust/circuit_breaker.py +579 -0
  143. empathy_os/validation/__init__.py +19 -0
  144. empathy_os/validation/xml_validator.py +281 -0
  145. empathy_os/wizard_factory_cli.py +170 -0
  146. empathy_os/workflows/__init__.py +360 -0
  147. empathy_os/workflows/base.py +1660 -0
  148. empathy_os/workflows/bug_predict.py +962 -0
  149. empathy_os/workflows/code_review.py +960 -0
  150. empathy_os/workflows/code_review_adapters.py +310 -0
  151. empathy_os/workflows/code_review_pipeline.py +720 -0
  152. empathy_os/workflows/config.py +600 -0
  153. empathy_os/workflows/dependency_check.py +648 -0
  154. empathy_os/workflows/document_gen.py +1069 -0
  155. empathy_os/workflows/documentation_orchestrator.py +1205 -0
  156. empathy_os/workflows/health_check.py +679 -0
  157. empathy_os/workflows/keyboard_shortcuts/__init__.py +39 -0
  158. empathy_os/workflows/keyboard_shortcuts/generators.py +386 -0
  159. empathy_os/workflows/keyboard_shortcuts/parsers.py +414 -0
  160. empathy_os/workflows/keyboard_shortcuts/prompts.py +295 -0
  161. empathy_os/workflows/keyboard_shortcuts/schema.py +193 -0
  162. empathy_os/workflows/keyboard_shortcuts/workflow.py +505 -0
  163. empathy_os/workflows/manage_documentation.py +804 -0
  164. empathy_os/workflows/new_sample_workflow1.py +146 -0
  165. empathy_os/workflows/new_sample_workflow1_README.md +150 -0
  166. empathy_os/workflows/perf_audit.py +687 -0
  167. empathy_os/workflows/pr_review.py +748 -0
  168. empathy_os/workflows/progress.py +445 -0
  169. empathy_os/workflows/progress_server.py +322 -0
  170. empathy_os/workflows/refactor_plan.py +693 -0
  171. empathy_os/workflows/release_prep.py +808 -0
  172. empathy_os/workflows/research_synthesis.py +404 -0
  173. empathy_os/workflows/secure_release.py +585 -0
  174. empathy_os/workflows/security_adapters.py +297 -0
  175. empathy_os/workflows/security_audit.py +1046 -0
  176. empathy_os/workflows/step_config.py +234 -0
  177. empathy_os/workflows/test5.py +125 -0
  178. empathy_os/workflows/test5_README.md +158 -0
  179. empathy_os/workflows/test_gen.py +1855 -0
  180. empathy_os/workflows/test_lifecycle.py +526 -0
  181. empathy_os/workflows/test_maintenance.py +626 -0
  182. empathy_os/workflows/test_maintenance_cli.py +590 -0
  183. empathy_os/workflows/test_maintenance_crew.py +821 -0
  184. empathy_os/workflows/xml_enhanced_crew.py +285 -0
  185. empathy_software_plugin/cli/__init__.py +120 -0
  186. empathy_software_plugin/cli/inspect.py +362 -0
  187. empathy_software_plugin/cli.py +3 -1
  188. empathy_software_plugin/wizards/__init__.py +42 -0
  189. empathy_software_plugin/wizards/advanced_debugging_wizard.py +392 -0
  190. empathy_software_plugin/wizards/agent_orchestration_wizard.py +511 -0
  191. empathy_software_plugin/wizards/ai_collaboration_wizard.py +503 -0
  192. empathy_software_plugin/wizards/ai_context_wizard.py +441 -0
  193. empathy_software_plugin/wizards/ai_documentation_wizard.py +503 -0
  194. empathy_software_plugin/wizards/base_wizard.py +288 -0
  195. empathy_software_plugin/wizards/book_chapter_wizard.py +519 -0
  196. empathy_software_plugin/wizards/code_review_wizard.py +606 -0
  197. empathy_software_plugin/wizards/debugging/__init__.py +50 -0
  198. empathy_software_plugin/wizards/debugging/bug_risk_analyzer.py +414 -0
  199. empathy_software_plugin/wizards/debugging/config_loaders.py +442 -0
  200. empathy_software_plugin/wizards/debugging/fix_applier.py +469 -0
  201. empathy_software_plugin/wizards/debugging/language_patterns.py +383 -0
  202. empathy_software_plugin/wizards/debugging/linter_parsers.py +470 -0
  203. empathy_software_plugin/wizards/debugging/verification.py +369 -0
  204. empathy_software_plugin/wizards/enhanced_testing_wizard.py +537 -0
  205. empathy_software_plugin/wizards/memory_enhanced_debugging_wizard.py +816 -0
  206. empathy_software_plugin/wizards/multi_model_wizard.py +501 -0
  207. empathy_software_plugin/wizards/pattern_extraction_wizard.py +422 -0
  208. empathy_software_plugin/wizards/pattern_retriever_wizard.py +400 -0
  209. empathy_software_plugin/wizards/performance/__init__.py +9 -0
  210. empathy_software_plugin/wizards/performance/bottleneck_detector.py +221 -0
  211. empathy_software_plugin/wizards/performance/profiler_parsers.py +278 -0
  212. empathy_software_plugin/wizards/performance/trajectory_analyzer.py +429 -0
  213. empathy_software_plugin/wizards/performance_profiling_wizard.py +305 -0
  214. empathy_software_plugin/wizards/prompt_engineering_wizard.py +425 -0
  215. empathy_software_plugin/wizards/rag_pattern_wizard.py +461 -0
  216. empathy_software_plugin/wizards/security/__init__.py +32 -0
  217. empathy_software_plugin/wizards/security/exploit_analyzer.py +290 -0
  218. empathy_software_plugin/wizards/security/owasp_patterns.py +241 -0
  219. empathy_software_plugin/wizards/security/vulnerability_scanner.py +604 -0
  220. empathy_software_plugin/wizards/security_analysis_wizard.py +322 -0
  221. empathy_software_plugin/wizards/security_learning_wizard.py +740 -0
  222. empathy_software_plugin/wizards/tech_debt_wizard.py +726 -0
  223. empathy_software_plugin/wizards/testing/__init__.py +27 -0
  224. empathy_software_plugin/wizards/testing/coverage_analyzer.py +459 -0
  225. empathy_software_plugin/wizards/testing/quality_analyzer.py +531 -0
  226. empathy_software_plugin/wizards/testing/test_suggester.py +533 -0
  227. empathy_software_plugin/wizards/testing_wizard.py +274 -0
  228. hot_reload/README.md +473 -0
  229. hot_reload/__init__.py +62 -0
  230. hot_reload/config.py +84 -0
  231. hot_reload/integration.py +228 -0
  232. hot_reload/reloader.py +298 -0
  233. hot_reload/watcher.py +179 -0
  234. hot_reload/websocket.py +176 -0
  235. scaffolding/README.md +589 -0
  236. scaffolding/__init__.py +35 -0
  237. scaffolding/__main__.py +14 -0
  238. scaffolding/cli.py +240 -0
  239. test_generator/__init__.py +38 -0
  240. test_generator/__main__.py +14 -0
  241. test_generator/cli.py +226 -0
  242. test_generator/generator.py +325 -0
  243. test_generator/risk_analyzer.py +216 -0
  244. workflow_patterns/__init__.py +33 -0
  245. workflow_patterns/behavior.py +249 -0
  246. workflow_patterns/core.py +76 -0
  247. workflow_patterns/output.py +99 -0
  248. workflow_patterns/registry.py +255 -0
  249. workflow_patterns/structural.py +288 -0
  250. workflow_scaffolding/__init__.py +11 -0
  251. workflow_scaffolding/__main__.py +12 -0
  252. workflow_scaffolding/cli.py +206 -0
  253. workflow_scaffolding/generator.py +265 -0
  254. agents/code_inspection/patterns/inspection/recurring_B112.json +0 -18
  255. agents/code_inspection/patterns/inspection/recurring_F541.json +0 -16
  256. agents/code_inspection/patterns/inspection/recurring_FORMAT.json +0 -25
  257. agents/code_inspection/patterns/inspection/recurring_bug_20250822_def456.json +0 -16
  258. agents/code_inspection/patterns/inspection/recurring_bug_20250915_abc123.json +0 -16
  259. agents/code_inspection/patterns/inspection/recurring_bug_20251212_3c5b9951.json +0 -16
  260. agents/code_inspection/patterns/inspection/recurring_bug_20251212_97c0f72f.json +0 -16
  261. agents/code_inspection/patterns/inspection/recurring_bug_20251212_a0871d53.json +0 -16
  262. agents/code_inspection/patterns/inspection/recurring_bug_20251212_a9b6ec41.json +0 -16
  263. agents/code_inspection/patterns/inspection/recurring_bug_null_001.json +0 -16
  264. agents/code_inspection/patterns/inspection/recurring_builtin.json +0 -16
  265. agents/compliance_anticipation_agent.py +0 -1422
  266. agents/compliance_db.py +0 -339
  267. agents/epic_integration_wizard.py +0 -530
  268. agents/notifications.py +0 -291
  269. agents/trust_building_behaviors.py +0 -872
  270. empathy_framework-3.7.0.dist-info/RECORD +0 -105
  271. {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/WHEEL +0 -0
  272. {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/entry_points.txt +0 -0
  273. {empathy_framework-3.7.0.dist-info → empathy_framework-3.8.0.dist-info}/licenses/LICENSE +0 -0
  274. /empathy_os/{monitoring.py → agent_monitoring.py} +0 -0
@@ -0,0 +1,567 @@
1
+ """Memory Graph - Cross-Wizard Knowledge Base
2
+
3
+ A knowledge graph that connects findings across all wizards,
4
+ enabling intelligent correlation and learning.
5
+
6
+ Features:
7
+ - Add findings from any wizard as nodes
8
+ - Connect related findings with typed edges
9
+ - Query for similar past findings
10
+ - Traverse relationships to find root causes
11
+
12
+ Storage: JSON file in patterns/ directory
13
+
14
+ Copyright 2025 Smart AI Memory, LLC
15
+ Licensed under Fair Source 0.9
16
+ """
17
+
18
+ import hashlib
19
+ import json
20
+ from collections import defaultdict, deque
21
+ from datetime import datetime
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ from .edges import REVERSE_EDGE_TYPES, Edge, EdgeType
26
+ from .nodes import Node, NodeType
27
+
28
+
29
+ class MemoryGraph:
30
+ """Knowledge graph for cross-wizard intelligence.
31
+
32
+ Stores nodes (findings) and edges (relationships) discovered
33
+ by wizards, enabling pattern correlation across sessions.
34
+
35
+ Usage:
36
+ graph = MemoryGraph()
37
+
38
+ # Add a bug finding
39
+ bug_id = graph.add_finding(
40
+ wizard="bug-predict",
41
+ finding={
42
+ "type": "bug",
43
+ "name": "Null reference in auth.py",
44
+ "description": "Missing null check on user object",
45
+ "file": "src/auth.py",
46
+ "line": 42,
47
+ "severity": "high"
48
+ }
49
+ )
50
+
51
+ # Connect to a fix
52
+ fix_id = graph.add_finding(
53
+ wizard="bug-predict",
54
+ finding={
55
+ "type": "fix",
56
+ "name": "Add null check",
57
+ "description": "Added guard clause for user object"
58
+ }
59
+ )
60
+ graph.add_edge(bug_id, fix_id, EdgeType.FIXED_BY)
61
+
62
+ # Find similar bugs
63
+ similar = graph.find_similar({"name": "Null reference"})
64
+ """
65
+
66
+ def __init__(self, path: str | Path = "patterns/memory_graph.json"):
67
+ """Initialize the memory graph.
68
+
69
+ Args:
70
+ path: Path to JSON storage file
71
+
72
+ """
73
+ self.path = Path(path)
74
+ self.nodes: dict[str, Node] = {}
75
+ self.edges: list[Edge] = []
76
+
77
+ # Indexes for fast lookup
78
+ self._edges_by_source: dict[str, list[Edge]] = defaultdict(list)
79
+ self._edges_by_target: dict[str, list[Edge]] = defaultdict(list)
80
+ self._nodes_by_type: dict[NodeType, list[str]] = defaultdict(list)
81
+ self._nodes_by_wizard: dict[str, list[str]] = defaultdict(list)
82
+ self._nodes_by_file: dict[str, list[str]] = defaultdict(list)
83
+
84
+ self._load()
85
+
86
+ def _load(self) -> None:
87
+ """Load graph from JSON file."""
88
+ if not self.path.exists():
89
+ # Ensure directory exists
90
+ self.path.parent.mkdir(parents=True, exist_ok=True)
91
+ self._save()
92
+ return
93
+
94
+ try:
95
+ with open(self.path) as f:
96
+ data = json.load(f)
97
+
98
+ # Load nodes
99
+ for node_data in data.get("nodes", []):
100
+ node = Node.from_dict(node_data)
101
+ self.nodes[node.id] = node
102
+ self._index_node(node)
103
+
104
+ # Load edges
105
+ for edge_data in data.get("edges", []):
106
+ edge = Edge.from_dict(edge_data)
107
+ self.edges.append(edge)
108
+ self._index_edge(edge)
109
+
110
+ except (json.JSONDecodeError, KeyError) as e:
111
+ print(f"Warning: Could not load graph from {self.path}: {e}")
112
+ self.nodes = {}
113
+ self.edges = []
114
+
115
+ def _save(self) -> None:
116
+ """Save graph to JSON file."""
117
+ data = {
118
+ "version": "1.0",
119
+ "updated_at": datetime.now().isoformat(),
120
+ "node_count": len(self.nodes),
121
+ "edge_count": len(self.edges),
122
+ "nodes": [node.to_dict() for node in self.nodes.values()],
123
+ "edges": [edge.to_dict() for edge in self.edges],
124
+ }
125
+
126
+ self.path.parent.mkdir(parents=True, exist_ok=True)
127
+ with open(self.path, "w") as f:
128
+ json.dump(data, f, indent=2)
129
+
130
+ def _index_node(self, node: Node) -> None:
131
+ """Add node to indexes."""
132
+ self._nodes_by_type[node.type].append(node.id)
133
+ if node.source_wizard:
134
+ self._nodes_by_wizard[node.source_wizard].append(node.id)
135
+ if node.source_file:
136
+ self._nodes_by_file[node.source_file].append(node.id)
137
+
138
+ def _index_edge(self, edge: Edge) -> None:
139
+ """Add edge to indexes."""
140
+ self._edges_by_source[edge.source_id].append(edge)
141
+ self._edges_by_target[edge.target_id].append(edge)
142
+
143
+ def _generate_id(self, finding: dict[str, Any]) -> str:
144
+ """Generate unique ID for a finding."""
145
+ # Create hash from content
146
+ content = json.dumps(finding, sort_keys=True)
147
+ hash_val = hashlib.sha256(content.encode()).hexdigest()[:12]
148
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
149
+ return f"{finding.get('type', 'node')}_{timestamp}_{hash_val}"
150
+
151
+ def add_finding(self, wizard: str, finding: dict[str, Any]) -> str:
152
+ """Add a finding from any wizard, return node ID.
153
+
154
+ Args:
155
+ wizard: Name of the wizard adding this finding
156
+ finding: Dict with at least 'type' and 'name' keys
157
+
158
+ Returns:
159
+ Node ID for the created node
160
+
161
+ Example:
162
+ node_id = graph.add_finding(
163
+ wizard="security-audit",
164
+ finding={
165
+ "type": "vulnerability",
166
+ "name": "SQL Injection in query builder",
167
+ "description": "User input not sanitized",
168
+ "file": "src/db/query.py",
169
+ "line": 156,
170
+ "severity": "critical"
171
+ }
172
+ )
173
+
174
+ """
175
+ node_id = self._generate_id(finding)
176
+
177
+ # Map finding type to NodeType
178
+ type_str = finding.get("type", "pattern")
179
+ try:
180
+ node_type = NodeType(type_str)
181
+ except ValueError:
182
+ node_type = NodeType.PATTERN
183
+
184
+ node = Node(
185
+ id=node_id,
186
+ type=node_type,
187
+ name=finding.get("name", "Unnamed finding"),
188
+ description=finding.get("description", ""),
189
+ source_wizard=wizard,
190
+ source_file=finding.get("file", finding.get("source_file", "")),
191
+ source_line=finding.get("line", finding.get("source_line")),
192
+ severity=finding.get("severity", ""),
193
+ confidence=finding.get("confidence", 1.0),
194
+ metadata=finding.get("metadata", {}),
195
+ tags=finding.get("tags", []),
196
+ )
197
+
198
+ self.nodes[node_id] = node
199
+ self._index_node(node)
200
+ self._save()
201
+
202
+ return node_id
203
+
204
+ def add_edge(
205
+ self,
206
+ source_id: str,
207
+ target_id: str,
208
+ edge_type: EdgeType,
209
+ description: str = "",
210
+ wizard: str = "",
211
+ weight: float = 1.0,
212
+ bidirectional: bool = False,
213
+ ) -> str:
214
+ """Add an edge between two nodes.
215
+
216
+ Args:
217
+ source_id: Source node ID
218
+ target_id: Target node ID
219
+ edge_type: Type of relationship
220
+ description: Optional description of the relationship
221
+ wizard: Wizard that created this edge
222
+ weight: Strength of relationship (0.0 - 1.0)
223
+ bidirectional: If True, also create reverse edge
224
+
225
+ Returns:
226
+ Edge ID
227
+
228
+ """
229
+ if source_id not in self.nodes:
230
+ raise ValueError(f"Source node not found: {source_id}")
231
+ if target_id not in self.nodes:
232
+ raise ValueError(f"Target node not found: {target_id}")
233
+
234
+ edge = Edge(
235
+ source_id=source_id,
236
+ target_id=target_id,
237
+ type=edge_type,
238
+ description=description,
239
+ source_wizard=wizard,
240
+ weight=weight,
241
+ )
242
+
243
+ self.edges.append(edge)
244
+ self._index_edge(edge)
245
+
246
+ # Optionally create reverse edge
247
+ if bidirectional and edge_type in REVERSE_EDGE_TYPES:
248
+ reverse_type = REVERSE_EDGE_TYPES[edge_type]
249
+ reverse_edge = Edge(
250
+ source_id=target_id,
251
+ target_id=source_id,
252
+ type=reverse_type,
253
+ description=description,
254
+ source_wizard=wizard,
255
+ weight=weight,
256
+ )
257
+ self.edges.append(reverse_edge)
258
+ self._index_edge(reverse_edge)
259
+
260
+ self._save()
261
+ return edge.id
262
+
263
+ def get_node(self, node_id: str) -> Node | None:
264
+ """Get a node by ID."""
265
+ return self.nodes.get(node_id)
266
+
267
+ def find_related(
268
+ self,
269
+ node_id: str,
270
+ edge_types: list[EdgeType] | None = None,
271
+ direction: str = "outgoing",
272
+ max_depth: int = 1,
273
+ ) -> list[Node]:
274
+ """Find related nodes via specified edge types.
275
+
276
+ Args:
277
+ node_id: Starting node ID
278
+ edge_types: List of edge types to follow (None = all)
279
+ direction: "outgoing", "incoming", or "both"
280
+ max_depth: Maximum traversal depth
281
+
282
+ Returns:
283
+ List of related nodes
284
+
285
+ """
286
+ if node_id not in self.nodes:
287
+ return []
288
+
289
+ visited: set[str] = {node_id}
290
+ result: list[Node] = []
291
+ current_level: set[str] = {node_id}
292
+
293
+ for _ in range(max_depth):
294
+ next_level: set[str] = set()
295
+
296
+ for current_id in current_level:
297
+ edges_to_check: list[Edge] = []
298
+
299
+ if direction in ("outgoing", "both"):
300
+ edges_to_check.extend(self._edges_by_source.get(current_id, []))
301
+ if direction in ("incoming", "both"):
302
+ edges_to_check.extend(self._edges_by_target.get(current_id, []))
303
+
304
+ for edge in edges_to_check:
305
+ if edge_types and edge.type not in edge_types:
306
+ continue
307
+
308
+ # Get the other node
309
+ other_id = edge.target_id if edge.source_id == current_id else edge.source_id
310
+
311
+ if other_id not in visited:
312
+ visited.add(other_id)
313
+ next_level.add(other_id)
314
+ if other_id in self.nodes:
315
+ result.append(self.nodes[other_id])
316
+
317
+ if not next_level:
318
+ break
319
+ current_level = next_level
320
+
321
+ return result
322
+
323
+ def find_similar(
324
+ self,
325
+ finding: dict[str, Any],
326
+ threshold: float = 0.5,
327
+ limit: int = 10,
328
+ ) -> list[tuple[Node, float]]:
329
+ """Find similar past findings.
330
+
331
+ Uses simple text similarity on name and description.
332
+
333
+ Args:
334
+ finding: Dict with 'name' and/or 'description'
335
+ threshold: Minimum similarity score (0.0 - 1.0)
336
+ limit: Maximum results to return
337
+
338
+ Returns:
339
+ List of (node, similarity_score) tuples
340
+
341
+ """
342
+ query_name = finding.get("name", "").lower()
343
+ query_desc = finding.get("description", "").lower()
344
+ query_type = finding.get("type")
345
+ query_file = finding.get("file", "")
346
+
347
+ results: list[tuple[Node, float]] = []
348
+
349
+ for node in self.nodes.values():
350
+ score = 0.0
351
+ factors = 0.0
352
+
353
+ # Name similarity (word overlap)
354
+ if query_name and node.name:
355
+ name_words = set(query_name.split())
356
+ node_words = set(node.name.lower().split())
357
+ if name_words and node_words:
358
+ overlap = len(name_words & node_words)
359
+ union = len(name_words | node_words)
360
+ score += (overlap / union) * 0.5
361
+ factors += 0.5
362
+
363
+ # Description similarity
364
+ if query_desc and node.description:
365
+ desc_words = set(query_desc.split())
366
+ node_words = set(node.description.lower().split())
367
+ if desc_words and node_words:
368
+ overlap = len(desc_words & node_words)
369
+ union = len(desc_words | node_words)
370
+ score += (overlap / union) * 0.3
371
+ factors += 0.3
372
+
373
+ # Type match bonus
374
+ if query_type:
375
+ try:
376
+ if node.type == NodeType(query_type):
377
+ score += 0.15
378
+ factors += 0.15
379
+ except ValueError:
380
+ pass
381
+
382
+ # File match bonus
383
+ if query_file and node.source_file:
384
+ if query_file == node.source_file:
385
+ score += 0.05
386
+ factors += 0.05
387
+
388
+ # Normalize
389
+ if factors > 0:
390
+ score = score / factors
391
+
392
+ if score >= threshold:
393
+ results.append((node, score))
394
+
395
+ # Sort by score descending
396
+ results.sort(key=lambda x: x[1], reverse=True)
397
+ return results[:limit]
398
+
399
+ def find_by_type(self, node_type: NodeType) -> list[Node]:
400
+ """Find all nodes of a specific type."""
401
+ node_ids = self._nodes_by_type.get(node_type, [])
402
+ return [self.nodes[nid] for nid in node_ids if nid in self.nodes]
403
+
404
+ def find_by_wizard(self, wizard: str) -> list[Node]:
405
+ """Find all nodes created by a specific wizard."""
406
+ node_ids = self._nodes_by_wizard.get(wizard, [])
407
+ return [self.nodes[nid] for nid in node_ids if nid in self.nodes]
408
+
409
+ def find_by_file(self, file_path: str) -> list[Node]:
410
+ """Find all nodes related to a specific file."""
411
+ node_ids = self._nodes_by_file.get(file_path, [])
412
+ return [self.nodes[nid] for nid in node_ids if nid in self.nodes]
413
+
414
+ def get_path(
415
+ self,
416
+ source_id: str,
417
+ target_id: str,
418
+ edge_types: list[EdgeType] | None = None,
419
+ max_depth: int = 5,
420
+ ) -> list[tuple[Node, Edge | None]]:
421
+ """Find a path between two nodes using BFS.
422
+
423
+ Args:
424
+ source_id: Starting node ID
425
+ target_id: Target node ID
426
+ edge_types: Edge types to traverse (None = all)
427
+ max_depth: Maximum path length
428
+
429
+ Returns:
430
+ List of (node, edge_to_node) tuples representing the path
431
+
432
+ """
433
+ if source_id not in self.nodes or target_id not in self.nodes:
434
+ return []
435
+
436
+ # BFS with path tracking (deque for O(1) popleft)
437
+ visited: set[str] = {source_id}
438
+ queue: deque[list[tuple[str, Edge | None]]] = deque([[(source_id, None)]])
439
+
440
+ while queue:
441
+ path = queue.popleft()
442
+ current_id = path[-1][0]
443
+
444
+ if len(path) > max_depth:
445
+ continue
446
+
447
+ if current_id == target_id:
448
+ return [(self.nodes[nid], edge) for nid, edge in path]
449
+
450
+ for edge in self._edges_by_source.get(current_id, []):
451
+ if edge_types and edge.type not in edge_types:
452
+ continue
453
+ if edge.target_id not in visited:
454
+ visited.add(edge.target_id)
455
+ new_path = path + [(edge.target_id, edge)]
456
+ queue.append(new_path)
457
+
458
+ return []
459
+
460
+ def get_statistics(self) -> dict[str, Any]:
461
+ """Get graph statistics."""
462
+ type_counts: dict[str, int] = defaultdict(int)
463
+ wizard_counts: dict[str, int] = defaultdict(int)
464
+ severity_counts: dict[str, int] = defaultdict(int)
465
+
466
+ for node in self.nodes.values():
467
+ type_counts[node.type.value] += 1
468
+ if node.source_wizard:
469
+ wizard_counts[node.source_wizard] += 1
470
+ if node.severity:
471
+ severity_counts[node.severity] += 1
472
+
473
+ edge_type_counts: dict[str, int] = defaultdict(int)
474
+ for edge in self.edges:
475
+ edge_type_counts[edge.type.value] += 1
476
+
477
+ return {
478
+ "total_nodes": len(self.nodes),
479
+ "total_edges": len(self.edges),
480
+ "nodes_by_type": dict(type_counts),
481
+ "nodes_by_wizard": dict(wizard_counts),
482
+ "nodes_by_severity": dict(severity_counts),
483
+ "edges_by_type": dict(edge_type_counts),
484
+ "unique_files": len(self._nodes_by_file),
485
+ }
486
+
487
+ def update_node(self, node_id: str, updates: dict[str, Any]) -> bool:
488
+ """Update a node's properties.
489
+
490
+ Args:
491
+ node_id: Node to update
492
+ updates: Dict of properties to update
493
+
494
+ Returns:
495
+ True if updated, False if node not found
496
+
497
+ """
498
+ if node_id not in self.nodes:
499
+ return False
500
+
501
+ node = self.nodes[node_id]
502
+
503
+ if "status" in updates:
504
+ node.status = updates["status"]
505
+ if "description" in updates:
506
+ node.description = updates["description"]
507
+ if "severity" in updates:
508
+ node.severity = updates["severity"]
509
+ if "tags" in updates:
510
+ node.tags = updates["tags"]
511
+ if "metadata" in updates:
512
+ node.metadata.update(updates["metadata"])
513
+
514
+ node.updated_at = datetime.now()
515
+ self._save()
516
+ return True
517
+
518
+ def delete_node(self, node_id: str) -> bool:
519
+ """Delete a node and its connected edges.
520
+
521
+ Args:
522
+ node_id: Node to delete
523
+
524
+ Returns:
525
+ True if deleted, False if not found
526
+
527
+ """
528
+ if node_id not in self.nodes:
529
+ return False
530
+
531
+ # Remove from indexes
532
+ node = self.nodes[node_id]
533
+ if node.type in self._nodes_by_type:
534
+ self._nodes_by_type[node.type] = [
535
+ nid for nid in self._nodes_by_type[node.type] if nid != node_id
536
+ ]
537
+ if node.source_wizard in self._nodes_by_wizard:
538
+ self._nodes_by_wizard[node.source_wizard] = [
539
+ nid for nid in self._nodes_by_wizard[node.source_wizard] if nid != node_id
540
+ ]
541
+ if node.source_file in self._nodes_by_file:
542
+ self._nodes_by_file[node.source_file] = [
543
+ nid for nid in self._nodes_by_file[node.source_file] if nid != node_id
544
+ ]
545
+
546
+ # Remove connected edges
547
+ self.edges = [e for e in self.edges if e.source_id != node_id and e.target_id != node_id]
548
+ if node_id in self._edges_by_source:
549
+ del self._edges_by_source[node_id]
550
+ if node_id in self._edges_by_target:
551
+ del self._edges_by_target[node_id]
552
+
553
+ # Remove node
554
+ del self.nodes[node_id]
555
+ self._save()
556
+ return True
557
+
558
+ def clear(self) -> None:
559
+ """Clear all nodes and edges."""
560
+ self.nodes = {}
561
+ self.edges = []
562
+ self._edges_by_source = defaultdict(list)
563
+ self._edges_by_target = defaultdict(list)
564
+ self._nodes_by_type = defaultdict(list)
565
+ self._nodes_by_wizard = defaultdict(list)
566
+ self._nodes_by_file = defaultdict(list)
567
+ self._save()