attune-ai 2.0.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.
- attune/__init__.py +358 -0
- attune/adaptive/__init__.py +13 -0
- attune/adaptive/task_complexity.py +127 -0
- attune/agent_monitoring.py +414 -0
- attune/cache/__init__.py +117 -0
- attune/cache/base.py +166 -0
- attune/cache/dependency_manager.py +256 -0
- attune/cache/hash_only.py +251 -0
- attune/cache/hybrid.py +457 -0
- attune/cache/storage.py +285 -0
- attune/cache_monitor.py +356 -0
- attune/cache_stats.py +298 -0
- attune/cli/__init__.py +152 -0
- attune/cli/__main__.py +12 -0
- attune/cli/commands/__init__.py +1 -0
- attune/cli/commands/batch.py +264 -0
- attune/cli/commands/cache.py +248 -0
- attune/cli/commands/help.py +331 -0
- attune/cli/commands/info.py +140 -0
- attune/cli/commands/inspect.py +436 -0
- attune/cli/commands/inspection.py +57 -0
- attune/cli/commands/memory.py +48 -0
- attune/cli/commands/metrics.py +92 -0
- attune/cli/commands/orchestrate.py +184 -0
- attune/cli/commands/patterns.py +207 -0
- attune/cli/commands/profiling.py +202 -0
- attune/cli/commands/provider.py +98 -0
- attune/cli/commands/routing.py +285 -0
- attune/cli/commands/setup.py +96 -0
- attune/cli/commands/status.py +235 -0
- attune/cli/commands/sync.py +166 -0
- attune/cli/commands/tier.py +121 -0
- attune/cli/commands/utilities.py +114 -0
- attune/cli/commands/workflow.py +579 -0
- attune/cli/core.py +32 -0
- attune/cli/parsers/__init__.py +68 -0
- attune/cli/parsers/batch.py +118 -0
- attune/cli/parsers/cache.py +65 -0
- attune/cli/parsers/help.py +41 -0
- attune/cli/parsers/info.py +26 -0
- attune/cli/parsers/inspect.py +66 -0
- attune/cli/parsers/metrics.py +42 -0
- attune/cli/parsers/orchestrate.py +61 -0
- attune/cli/parsers/patterns.py +54 -0
- attune/cli/parsers/provider.py +40 -0
- attune/cli/parsers/routing.py +110 -0
- attune/cli/parsers/setup.py +42 -0
- attune/cli/parsers/status.py +47 -0
- attune/cli/parsers/sync.py +31 -0
- attune/cli/parsers/tier.py +33 -0
- attune/cli/parsers/workflow.py +77 -0
- attune/cli/utils/__init__.py +1 -0
- attune/cli/utils/data.py +242 -0
- attune/cli/utils/helpers.py +68 -0
- attune/cli_legacy.py +3957 -0
- attune/cli_minimal.py +1159 -0
- attune/cli_router.py +437 -0
- attune/cli_unified.py +814 -0
- attune/config/__init__.py +66 -0
- attune/config/xml_config.py +286 -0
- attune/config.py +545 -0
- attune/coordination.py +870 -0
- attune/core.py +1511 -0
- attune/core_modules/__init__.py +15 -0
- attune/cost_tracker.py +626 -0
- attune/dashboard/__init__.py +41 -0
- attune/dashboard/app.py +512 -0
- attune/dashboard/simple_server.py +435 -0
- attune/dashboard/standalone_server.py +547 -0
- attune/discovery.py +306 -0
- attune/emergence.py +306 -0
- attune/exceptions.py +123 -0
- attune/feedback_loops.py +373 -0
- attune/hot_reload/README.md +473 -0
- attune/hot_reload/__init__.py +62 -0
- attune/hot_reload/config.py +83 -0
- attune/hot_reload/integration.py +229 -0
- attune/hot_reload/reloader.py +298 -0
- attune/hot_reload/watcher.py +183 -0
- attune/hot_reload/websocket.py +177 -0
- attune/levels.py +577 -0
- attune/leverage_points.py +441 -0
- attune/logging_config.py +261 -0
- attune/mcp/__init__.py +10 -0
- attune/mcp/server.py +506 -0
- attune/memory/__init__.py +237 -0
- attune/memory/claude_memory.py +469 -0
- attune/memory/config.py +224 -0
- attune/memory/control_panel.py +1290 -0
- attune/memory/control_panel_support.py +145 -0
- attune/memory/cross_session.py +845 -0
- attune/memory/edges.py +179 -0
- attune/memory/encryption.py +159 -0
- attune/memory/file_session.py +770 -0
- attune/memory/graph.py +570 -0
- attune/memory/long_term.py +913 -0
- attune/memory/long_term_types.py +99 -0
- attune/memory/mixins/__init__.py +25 -0
- attune/memory/mixins/backend_init_mixin.py +249 -0
- attune/memory/mixins/capabilities_mixin.py +208 -0
- attune/memory/mixins/handoff_mixin.py +208 -0
- attune/memory/mixins/lifecycle_mixin.py +49 -0
- attune/memory/mixins/long_term_mixin.py +352 -0
- attune/memory/mixins/promotion_mixin.py +109 -0
- attune/memory/mixins/short_term_mixin.py +182 -0
- attune/memory/nodes.py +179 -0
- attune/memory/redis_bootstrap.py +540 -0
- attune/memory/security/__init__.py +31 -0
- attune/memory/security/audit_logger.py +932 -0
- attune/memory/security/pii_scrubber.py +640 -0
- attune/memory/security/secrets_detector.py +678 -0
- attune/memory/short_term.py +2192 -0
- attune/memory/simple_storage.py +302 -0
- attune/memory/storage/__init__.py +15 -0
- attune/memory/storage_backend.py +167 -0
- attune/memory/summary_index.py +583 -0
- attune/memory/types.py +446 -0
- attune/memory/unified.py +182 -0
- attune/meta_workflows/__init__.py +74 -0
- attune/meta_workflows/agent_creator.py +248 -0
- attune/meta_workflows/builtin_templates.py +567 -0
- attune/meta_workflows/cli_commands/__init__.py +56 -0
- attune/meta_workflows/cli_commands/agent_commands.py +321 -0
- attune/meta_workflows/cli_commands/analytics_commands.py +442 -0
- attune/meta_workflows/cli_commands/config_commands.py +232 -0
- attune/meta_workflows/cli_commands/memory_commands.py +182 -0
- attune/meta_workflows/cli_commands/template_commands.py +354 -0
- attune/meta_workflows/cli_commands/workflow_commands.py +382 -0
- attune/meta_workflows/cli_meta_workflows.py +59 -0
- attune/meta_workflows/form_engine.py +292 -0
- attune/meta_workflows/intent_detector.py +409 -0
- attune/meta_workflows/models.py +569 -0
- attune/meta_workflows/pattern_learner.py +738 -0
- attune/meta_workflows/plan_generator.py +384 -0
- attune/meta_workflows/session_context.py +397 -0
- attune/meta_workflows/template_registry.py +229 -0
- attune/meta_workflows/workflow.py +984 -0
- attune/metrics/__init__.py +12 -0
- attune/metrics/collector.py +31 -0
- attune/metrics/prompt_metrics.py +194 -0
- attune/models/__init__.py +172 -0
- attune/models/__main__.py +13 -0
- attune/models/adaptive_routing.py +437 -0
- attune/models/auth_cli.py +444 -0
- attune/models/auth_strategy.py +450 -0
- attune/models/cli.py +655 -0
- attune/models/empathy_executor.py +354 -0
- attune/models/executor.py +257 -0
- attune/models/fallback.py +762 -0
- attune/models/provider_config.py +282 -0
- attune/models/registry.py +472 -0
- attune/models/tasks.py +359 -0
- attune/models/telemetry/__init__.py +71 -0
- attune/models/telemetry/analytics.py +594 -0
- attune/models/telemetry/backend.py +196 -0
- attune/models/telemetry/data_models.py +431 -0
- attune/models/telemetry/storage.py +489 -0
- attune/models/token_estimator.py +420 -0
- attune/models/validation.py +280 -0
- attune/monitoring/__init__.py +52 -0
- attune/monitoring/alerts.py +946 -0
- attune/monitoring/alerts_cli.py +448 -0
- attune/monitoring/multi_backend.py +271 -0
- attune/monitoring/otel_backend.py +362 -0
- attune/optimization/__init__.py +19 -0
- attune/optimization/context_optimizer.py +272 -0
- attune/orchestration/__init__.py +67 -0
- attune/orchestration/agent_templates.py +707 -0
- attune/orchestration/config_store.py +499 -0
- attune/orchestration/execution_strategies.py +2111 -0
- attune/orchestration/meta_orchestrator.py +1168 -0
- attune/orchestration/pattern_learner.py +696 -0
- attune/orchestration/real_tools.py +931 -0
- attune/pattern_cache.py +187 -0
- attune/pattern_library.py +542 -0
- attune/patterns/debugging/all_patterns.json +81 -0
- attune/patterns/debugging/workflow_20260107_1770825e.json +77 -0
- attune/patterns/refactoring_memory.json +89 -0
- attune/persistence.py +564 -0
- attune/platform_utils.py +265 -0
- attune/plugins/__init__.py +28 -0
- attune/plugins/base.py +361 -0
- attune/plugins/registry.py +268 -0
- attune/project_index/__init__.py +32 -0
- attune/project_index/cli.py +335 -0
- attune/project_index/index.py +667 -0
- attune/project_index/models.py +504 -0
- attune/project_index/reports.py +474 -0
- attune/project_index/scanner.py +777 -0
- attune/project_index/scanner_parallel.py +291 -0
- attune/prompts/__init__.py +61 -0
- attune/prompts/config.py +77 -0
- attune/prompts/context.py +177 -0
- attune/prompts/parser.py +285 -0
- attune/prompts/registry.py +313 -0
- attune/prompts/templates.py +208 -0
- attune/redis_config.py +302 -0
- attune/redis_memory.py +799 -0
- attune/resilience/__init__.py +56 -0
- attune/resilience/circuit_breaker.py +256 -0
- attune/resilience/fallback.py +179 -0
- attune/resilience/health.py +300 -0
- attune/resilience/retry.py +209 -0
- attune/resilience/timeout.py +135 -0
- attune/routing/__init__.py +43 -0
- attune/routing/chain_executor.py +433 -0
- attune/routing/classifier.py +217 -0
- attune/routing/smart_router.py +234 -0
- attune/routing/workflow_registry.py +343 -0
- attune/scaffolding/README.md +589 -0
- attune/scaffolding/__init__.py +35 -0
- attune/scaffolding/__main__.py +14 -0
- attune/scaffolding/cli.py +240 -0
- attune/scaffolding/templates/base_wizard.py.jinja2 +121 -0
- attune/scaffolding/templates/coach_wizard.py.jinja2 +321 -0
- attune/scaffolding/templates/domain_wizard.py.jinja2 +408 -0
- attune/scaffolding/templates/linear_flow_wizard.py.jinja2 +203 -0
- attune/socratic/__init__.py +256 -0
- attune/socratic/ab_testing.py +958 -0
- attune/socratic/blueprint.py +533 -0
- attune/socratic/cli.py +703 -0
- attune/socratic/collaboration.py +1114 -0
- attune/socratic/domain_templates.py +924 -0
- attune/socratic/embeddings.py +738 -0
- attune/socratic/engine.py +794 -0
- attune/socratic/explainer.py +682 -0
- attune/socratic/feedback.py +772 -0
- attune/socratic/forms.py +629 -0
- attune/socratic/generator.py +732 -0
- attune/socratic/llm_analyzer.py +637 -0
- attune/socratic/mcp_server.py +702 -0
- attune/socratic/session.py +312 -0
- attune/socratic/storage.py +667 -0
- attune/socratic/success.py +730 -0
- attune/socratic/visual_editor.py +860 -0
- attune/socratic/web_ui.py +958 -0
- attune/telemetry/__init__.py +39 -0
- attune/telemetry/agent_coordination.py +475 -0
- attune/telemetry/agent_tracking.py +367 -0
- attune/telemetry/approval_gates.py +545 -0
- attune/telemetry/cli.py +1231 -0
- attune/telemetry/commands/__init__.py +14 -0
- attune/telemetry/commands/dashboard_commands.py +696 -0
- attune/telemetry/event_streaming.py +409 -0
- attune/telemetry/feedback_loop.py +567 -0
- attune/telemetry/usage_tracker.py +591 -0
- attune/templates.py +754 -0
- attune/test_generator/__init__.py +38 -0
- attune/test_generator/__main__.py +14 -0
- attune/test_generator/cli.py +234 -0
- attune/test_generator/generator.py +355 -0
- attune/test_generator/risk_analyzer.py +216 -0
- attune/test_generator/templates/unit_test.py.jinja2 +272 -0
- attune/tier_recommender.py +384 -0
- attune/tools.py +183 -0
- attune/trust/__init__.py +28 -0
- attune/trust/circuit_breaker.py +579 -0
- attune/trust_building.py +527 -0
- attune/validation/__init__.py +19 -0
- attune/validation/xml_validator.py +281 -0
- attune/vscode_bridge.py +173 -0
- attune/workflow_commands.py +780 -0
- attune/workflow_patterns/__init__.py +33 -0
- attune/workflow_patterns/behavior.py +249 -0
- attune/workflow_patterns/core.py +76 -0
- attune/workflow_patterns/output.py +99 -0
- attune/workflow_patterns/registry.py +255 -0
- attune/workflow_patterns/structural.py +288 -0
- attune/workflows/__init__.py +539 -0
- attune/workflows/autonomous_test_gen.py +1268 -0
- attune/workflows/base.py +2667 -0
- attune/workflows/batch_processing.py +342 -0
- attune/workflows/bug_predict.py +1084 -0
- attune/workflows/builder.py +273 -0
- attune/workflows/caching.py +253 -0
- attune/workflows/code_review.py +1048 -0
- attune/workflows/code_review_adapters.py +312 -0
- attune/workflows/code_review_pipeline.py +722 -0
- attune/workflows/config.py +645 -0
- attune/workflows/dependency_check.py +644 -0
- attune/workflows/document_gen/__init__.py +25 -0
- attune/workflows/document_gen/config.py +30 -0
- attune/workflows/document_gen/report_formatter.py +162 -0
- attune/workflows/document_gen/workflow.py +1426 -0
- attune/workflows/document_manager.py +216 -0
- attune/workflows/document_manager_README.md +134 -0
- attune/workflows/documentation_orchestrator.py +1205 -0
- attune/workflows/history.py +510 -0
- attune/workflows/keyboard_shortcuts/__init__.py +39 -0
- attune/workflows/keyboard_shortcuts/generators.py +391 -0
- attune/workflows/keyboard_shortcuts/parsers.py +416 -0
- attune/workflows/keyboard_shortcuts/prompts.py +295 -0
- attune/workflows/keyboard_shortcuts/schema.py +193 -0
- attune/workflows/keyboard_shortcuts/workflow.py +509 -0
- attune/workflows/llm_base.py +363 -0
- attune/workflows/manage_docs.py +87 -0
- attune/workflows/manage_docs_README.md +134 -0
- attune/workflows/manage_documentation.py +821 -0
- attune/workflows/new_sample_workflow1.py +149 -0
- attune/workflows/new_sample_workflow1_README.md +150 -0
- attune/workflows/orchestrated_health_check.py +849 -0
- attune/workflows/orchestrated_release_prep.py +600 -0
- attune/workflows/output.py +413 -0
- attune/workflows/perf_audit.py +863 -0
- attune/workflows/pr_review.py +762 -0
- attune/workflows/progress.py +785 -0
- attune/workflows/progress_server.py +322 -0
- attune/workflows/progressive/README 2.md +454 -0
- attune/workflows/progressive/README.md +454 -0
- attune/workflows/progressive/__init__.py +82 -0
- attune/workflows/progressive/cli.py +219 -0
- attune/workflows/progressive/core.py +488 -0
- attune/workflows/progressive/orchestrator.py +723 -0
- attune/workflows/progressive/reports.py +520 -0
- attune/workflows/progressive/telemetry.py +274 -0
- attune/workflows/progressive/test_gen.py +495 -0
- attune/workflows/progressive/workflow.py +589 -0
- attune/workflows/refactor_plan.py +694 -0
- attune/workflows/release_prep.py +895 -0
- attune/workflows/release_prep_crew.py +969 -0
- attune/workflows/research_synthesis.py +404 -0
- attune/workflows/routing.py +168 -0
- attune/workflows/secure_release.py +593 -0
- attune/workflows/security_adapters.py +297 -0
- attune/workflows/security_audit.py +1329 -0
- attune/workflows/security_audit_phase3.py +355 -0
- attune/workflows/seo_optimization.py +633 -0
- attune/workflows/step_config.py +234 -0
- attune/workflows/telemetry_mixin.py +269 -0
- attune/workflows/test5.py +125 -0
- attune/workflows/test5_README.md +158 -0
- attune/workflows/test_coverage_boost_crew.py +849 -0
- attune/workflows/test_gen/__init__.py +52 -0
- attune/workflows/test_gen/ast_analyzer.py +249 -0
- attune/workflows/test_gen/config.py +88 -0
- attune/workflows/test_gen/data_models.py +38 -0
- attune/workflows/test_gen/report_formatter.py +289 -0
- attune/workflows/test_gen/test_templates.py +381 -0
- attune/workflows/test_gen/workflow.py +655 -0
- attune/workflows/test_gen.py +54 -0
- attune/workflows/test_gen_behavioral.py +477 -0
- attune/workflows/test_gen_parallel.py +341 -0
- attune/workflows/test_lifecycle.py +526 -0
- attune/workflows/test_maintenance.py +627 -0
- attune/workflows/test_maintenance_cli.py +590 -0
- attune/workflows/test_maintenance_crew.py +840 -0
- attune/workflows/test_runner.py +622 -0
- attune/workflows/tier_tracking.py +531 -0
- attune/workflows/xml_enhanced_crew.py +285 -0
- attune_ai-2.0.0.dist-info/METADATA +1026 -0
- attune_ai-2.0.0.dist-info/RECORD +457 -0
- attune_ai-2.0.0.dist-info/WHEEL +5 -0
- attune_ai-2.0.0.dist-info/entry_points.txt +26 -0
- attune_ai-2.0.0.dist-info/licenses/LICENSE +201 -0
- attune_ai-2.0.0.dist-info/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +101 -0
- attune_ai-2.0.0.dist-info/top_level.txt +5 -0
- attune_healthcare/__init__.py +13 -0
- attune_healthcare/monitors/__init__.py +9 -0
- attune_healthcare/monitors/clinical_protocol_monitor.py +315 -0
- attune_healthcare/monitors/monitoring/__init__.py +44 -0
- attune_healthcare/monitors/monitoring/protocol_checker.py +300 -0
- attune_healthcare/monitors/monitoring/protocol_loader.py +214 -0
- attune_healthcare/monitors/monitoring/sensor_parsers.py +306 -0
- attune_healthcare/monitors/monitoring/trajectory_analyzer.py +389 -0
- attune_llm/README.md +553 -0
- attune_llm/__init__.py +28 -0
- attune_llm/agent_factory/__init__.py +53 -0
- attune_llm/agent_factory/adapters/__init__.py +85 -0
- attune_llm/agent_factory/adapters/autogen_adapter.py +312 -0
- attune_llm/agent_factory/adapters/crewai_adapter.py +483 -0
- attune_llm/agent_factory/adapters/haystack_adapter.py +298 -0
- attune_llm/agent_factory/adapters/langchain_adapter.py +362 -0
- attune_llm/agent_factory/adapters/langgraph_adapter.py +333 -0
- attune_llm/agent_factory/adapters/native.py +228 -0
- attune_llm/agent_factory/adapters/wizard_adapter.py +423 -0
- attune_llm/agent_factory/base.py +305 -0
- attune_llm/agent_factory/crews/__init__.py +67 -0
- attune_llm/agent_factory/crews/code_review.py +1113 -0
- attune_llm/agent_factory/crews/health_check.py +1262 -0
- attune_llm/agent_factory/crews/refactoring.py +1128 -0
- attune_llm/agent_factory/crews/security_audit.py +1018 -0
- attune_llm/agent_factory/decorators.py +287 -0
- attune_llm/agent_factory/factory.py +558 -0
- attune_llm/agent_factory/framework.py +193 -0
- attune_llm/agent_factory/memory_integration.py +328 -0
- attune_llm/agent_factory/resilient.py +320 -0
- attune_llm/agents_md/__init__.py +22 -0
- attune_llm/agents_md/loader.py +218 -0
- attune_llm/agents_md/parser.py +271 -0
- attune_llm/agents_md/registry.py +307 -0
- attune_llm/claude_memory.py +466 -0
- attune_llm/cli/__init__.py +8 -0
- attune_llm/cli/sync_claude.py +487 -0
- attune_llm/code_health.py +1313 -0
- attune_llm/commands/__init__.py +51 -0
- attune_llm/commands/context.py +375 -0
- attune_llm/commands/loader.py +301 -0
- attune_llm/commands/models.py +231 -0
- attune_llm/commands/parser.py +371 -0
- attune_llm/commands/registry.py +429 -0
- attune_llm/config/__init__.py +29 -0
- attune_llm/config/unified.py +291 -0
- attune_llm/context/__init__.py +22 -0
- attune_llm/context/compaction.py +455 -0
- attune_llm/context/manager.py +434 -0
- attune_llm/contextual_patterns.py +361 -0
- attune_llm/core.py +907 -0
- attune_llm/git_pattern_extractor.py +435 -0
- attune_llm/hooks/__init__.py +24 -0
- attune_llm/hooks/config.py +306 -0
- attune_llm/hooks/executor.py +289 -0
- attune_llm/hooks/registry.py +302 -0
- attune_llm/hooks/scripts/__init__.py +39 -0
- attune_llm/hooks/scripts/evaluate_session.py +201 -0
- attune_llm/hooks/scripts/first_time_init.py +285 -0
- attune_llm/hooks/scripts/pre_compact.py +207 -0
- attune_llm/hooks/scripts/session_end.py +183 -0
- attune_llm/hooks/scripts/session_start.py +163 -0
- attune_llm/hooks/scripts/suggest_compact.py +225 -0
- attune_llm/learning/__init__.py +30 -0
- attune_llm/learning/evaluator.py +438 -0
- attune_llm/learning/extractor.py +514 -0
- attune_llm/learning/storage.py +560 -0
- attune_llm/levels.py +227 -0
- attune_llm/pattern_confidence.py +414 -0
- attune_llm/pattern_resolver.py +272 -0
- attune_llm/pattern_summary.py +350 -0
- attune_llm/providers.py +967 -0
- attune_llm/routing/__init__.py +32 -0
- attune_llm/routing/model_router.py +362 -0
- attune_llm/security/IMPLEMENTATION_SUMMARY.md +413 -0
- attune_llm/security/PHASE2_COMPLETE.md +384 -0
- attune_llm/security/PHASE2_SECRETS_DETECTOR_COMPLETE.md +271 -0
- attune_llm/security/QUICK_REFERENCE.md +316 -0
- attune_llm/security/README.md +262 -0
- attune_llm/security/__init__.py +62 -0
- attune_llm/security/audit_logger.py +929 -0
- attune_llm/security/audit_logger_example.py +152 -0
- attune_llm/security/pii_scrubber.py +640 -0
- attune_llm/security/secrets_detector.py +678 -0
- attune_llm/security/secrets_detector_example.py +304 -0
- attune_llm/security/secure_memdocs.py +1192 -0
- attune_llm/security/secure_memdocs_example.py +278 -0
- attune_llm/session_status.py +745 -0
- attune_llm/state.py +246 -0
- attune_llm/utils/__init__.py +5 -0
- attune_llm/utils/tokens.py +349 -0
- attune_software/SOFTWARE_PLUGIN_README.md +57 -0
- attune_software/__init__.py +13 -0
- attune_software/cli/__init__.py +120 -0
- attune_software/cli/inspect.py +362 -0
- attune_software/cli.py +574 -0
- attune_software/plugin.py +188 -0
- workflow_scaffolding/__init__.py +11 -0
- workflow_scaffolding/__main__.py +12 -0
- workflow_scaffolding/cli.py +206 -0
- workflow_scaffolding/generator.py +265 -0
attune/memory/graph.py
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
"""Memory Graph - Cross-Workflow Knowledge Base
|
|
2
|
+
|
|
3
|
+
A knowledge graph that connects findings across all workflows,
|
|
4
|
+
enabling intelligent correlation and learning.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Add findings from any workflow 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 attune.config import _validate_file_path
|
|
26
|
+
|
|
27
|
+
from .edges import REVERSE_EDGE_TYPES, Edge, EdgeType
|
|
28
|
+
from .nodes import Node, NodeType
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MemoryGraph:
|
|
32
|
+
"""Knowledge graph for cross-workflow intelligence.
|
|
33
|
+
|
|
34
|
+
Stores nodes (findings) and edges (relationships) discovered
|
|
35
|
+
by workflows, enabling pattern correlation across sessions.
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
graph = MemoryGraph()
|
|
39
|
+
|
|
40
|
+
# Add a bug finding
|
|
41
|
+
bug_id = graph.add_finding(
|
|
42
|
+
workflow="bug-predict",
|
|
43
|
+
finding={
|
|
44
|
+
"type": "bug",
|
|
45
|
+
"name": "Null reference in auth.py",
|
|
46
|
+
"description": "Missing null check on user object",
|
|
47
|
+
"file": "src/auth.py",
|
|
48
|
+
"line": 42,
|
|
49
|
+
"severity": "high"
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Connect to a fix
|
|
54
|
+
fix_id = graph.add_finding(
|
|
55
|
+
workflow="bug-predict",
|
|
56
|
+
finding={
|
|
57
|
+
"type": "fix",
|
|
58
|
+
"name": "Add null check",
|
|
59
|
+
"description": "Added guard clause for user object"
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
graph.add_edge(bug_id, fix_id, EdgeType.FIXED_BY)
|
|
63
|
+
|
|
64
|
+
# Find similar bugs
|
|
65
|
+
similar = graph.find_similar({"name": "Null reference"})
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, path: str | Path = "patterns/memory_graph.json"):
|
|
69
|
+
"""Initialize the memory graph.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
path: Path to JSON storage file
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
self.path = Path(path)
|
|
76
|
+
self.nodes: dict[str, Node] = {}
|
|
77
|
+
self.edges: list[Edge] = []
|
|
78
|
+
|
|
79
|
+
# Indexes for fast lookup
|
|
80
|
+
self._edges_by_source: dict[str, list[Edge]] = defaultdict(list)
|
|
81
|
+
self._edges_by_target: dict[str, list[Edge]] = defaultdict(list)
|
|
82
|
+
self._nodes_by_type: dict[NodeType, list[str]] = defaultdict(list)
|
|
83
|
+
self._nodes_by_workflow: dict[str, list[str]] = defaultdict(list)
|
|
84
|
+
self._nodes_by_file: dict[str, list[str]] = defaultdict(list)
|
|
85
|
+
|
|
86
|
+
self._load()
|
|
87
|
+
|
|
88
|
+
def _load(self) -> None:
|
|
89
|
+
"""Load graph from JSON file."""
|
|
90
|
+
if not self.path.exists():
|
|
91
|
+
# Ensure directory exists
|
|
92
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
self._save()
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
with open(self.path) as f:
|
|
98
|
+
data = json.load(f)
|
|
99
|
+
|
|
100
|
+
# Load nodes
|
|
101
|
+
for node_data in data.get("nodes", []):
|
|
102
|
+
node = Node.from_dict(node_data)
|
|
103
|
+
self.nodes[node.id] = node
|
|
104
|
+
self._index_node(node)
|
|
105
|
+
|
|
106
|
+
# Load edges
|
|
107
|
+
for edge_data in data.get("edges", []):
|
|
108
|
+
edge = Edge.from_dict(edge_data)
|
|
109
|
+
self.edges.append(edge)
|
|
110
|
+
self._index_edge(edge)
|
|
111
|
+
|
|
112
|
+
except (json.JSONDecodeError, KeyError) as e:
|
|
113
|
+
print(f"Warning: Could not load graph from {self.path}: {e}")
|
|
114
|
+
self.nodes = {}
|
|
115
|
+
self.edges = []
|
|
116
|
+
|
|
117
|
+
def _save(self) -> None:
|
|
118
|
+
"""Save graph to JSON file."""
|
|
119
|
+
data = {
|
|
120
|
+
"version": "1.0",
|
|
121
|
+
"updated_at": datetime.now().isoformat(),
|
|
122
|
+
"node_count": len(self.nodes),
|
|
123
|
+
"edge_count": len(self.edges),
|
|
124
|
+
"nodes": [node.to_dict() for node in self.nodes.values()],
|
|
125
|
+
"edges": [edge.to_dict() for edge in self.edges],
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
129
|
+
validated_path = _validate_file_path(str(self.path))
|
|
130
|
+
with open(validated_path, "w") as f:
|
|
131
|
+
json.dump(data, f, indent=2)
|
|
132
|
+
|
|
133
|
+
def _index_node(self, node: Node) -> None:
|
|
134
|
+
"""Add node to indexes."""
|
|
135
|
+
self._nodes_by_type[node.type].append(node.id)
|
|
136
|
+
if node.source_workflow:
|
|
137
|
+
self._nodes_by_workflow[node.source_workflow].append(node.id)
|
|
138
|
+
if node.source_file:
|
|
139
|
+
self._nodes_by_file[node.source_file].append(node.id)
|
|
140
|
+
|
|
141
|
+
def _index_edge(self, edge: Edge) -> None:
|
|
142
|
+
"""Add edge to indexes."""
|
|
143
|
+
self._edges_by_source[edge.source_id].append(edge)
|
|
144
|
+
self._edges_by_target[edge.target_id].append(edge)
|
|
145
|
+
|
|
146
|
+
def _generate_id(self, finding: dict[str, Any]) -> str:
|
|
147
|
+
"""Generate unique ID for a finding."""
|
|
148
|
+
# Create hash from content
|
|
149
|
+
content = json.dumps(finding, sort_keys=True)
|
|
150
|
+
hash_val = hashlib.sha256(content.encode()).hexdigest()[:12]
|
|
151
|
+
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
152
|
+
return f"{finding.get('type', 'node')}_{timestamp}_{hash_val}"
|
|
153
|
+
|
|
154
|
+
def add_finding(self, workflow: str, finding: dict[str, Any]) -> str:
|
|
155
|
+
"""Add a finding from any workflow, return node ID.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
workflow: Name of the workflow adding this finding
|
|
159
|
+
finding: Dict with at least 'type' and 'name' keys
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Node ID for the created node
|
|
163
|
+
|
|
164
|
+
Example:
|
|
165
|
+
node_id = graph.add_finding(
|
|
166
|
+
workflow="security-audit",
|
|
167
|
+
finding={
|
|
168
|
+
"type": "vulnerability",
|
|
169
|
+
"name": "SQL Injection in query builder",
|
|
170
|
+
"description": "User input not sanitized",
|
|
171
|
+
"file": "src/db/query.py",
|
|
172
|
+
"line": 156,
|
|
173
|
+
"severity": "critical"
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
"""
|
|
178
|
+
node_id = self._generate_id(finding)
|
|
179
|
+
|
|
180
|
+
# Map finding type to NodeType
|
|
181
|
+
type_str = finding.get("type", "pattern")
|
|
182
|
+
try:
|
|
183
|
+
node_type = NodeType(type_str)
|
|
184
|
+
except ValueError:
|
|
185
|
+
node_type = NodeType.PATTERN
|
|
186
|
+
|
|
187
|
+
node = Node(
|
|
188
|
+
id=node_id,
|
|
189
|
+
type=node_type,
|
|
190
|
+
name=finding.get("name", "Unnamed finding"),
|
|
191
|
+
description=finding.get("description", ""),
|
|
192
|
+
source_workflow=workflow,
|
|
193
|
+
source_file=finding.get("file", finding.get("source_file", "")),
|
|
194
|
+
source_line=finding.get("line", finding.get("source_line")),
|
|
195
|
+
severity=finding.get("severity", ""),
|
|
196
|
+
confidence=finding.get("confidence", 1.0),
|
|
197
|
+
metadata=finding.get("metadata", {}),
|
|
198
|
+
tags=finding.get("tags", []),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
self.nodes[node_id] = node
|
|
202
|
+
self._index_node(node)
|
|
203
|
+
self._save()
|
|
204
|
+
|
|
205
|
+
return node_id
|
|
206
|
+
|
|
207
|
+
def add_edge(
|
|
208
|
+
self,
|
|
209
|
+
source_id: str,
|
|
210
|
+
target_id: str,
|
|
211
|
+
edge_type: EdgeType,
|
|
212
|
+
description: str = "",
|
|
213
|
+
workflow: str = "",
|
|
214
|
+
weight: float = 1.0,
|
|
215
|
+
bidirectional: bool = False,
|
|
216
|
+
) -> str:
|
|
217
|
+
"""Add an edge between two nodes.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
source_id: Source node ID
|
|
221
|
+
target_id: Target node ID
|
|
222
|
+
edge_type: Type of relationship
|
|
223
|
+
description: Optional description of the relationship
|
|
224
|
+
workflow: Workflow that created this edge
|
|
225
|
+
weight: Strength of relationship (0.0 - 1.0)
|
|
226
|
+
bidirectional: If True, also create reverse edge
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Edge ID
|
|
230
|
+
|
|
231
|
+
"""
|
|
232
|
+
if source_id not in self.nodes:
|
|
233
|
+
raise ValueError(f"Source node not found: {source_id}")
|
|
234
|
+
if target_id not in self.nodes:
|
|
235
|
+
raise ValueError(f"Target node not found: {target_id}")
|
|
236
|
+
|
|
237
|
+
edge = Edge(
|
|
238
|
+
source_id=source_id,
|
|
239
|
+
target_id=target_id,
|
|
240
|
+
type=edge_type,
|
|
241
|
+
description=description,
|
|
242
|
+
source_workflow=workflow,
|
|
243
|
+
weight=weight,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
self.edges.append(edge)
|
|
247
|
+
self._index_edge(edge)
|
|
248
|
+
|
|
249
|
+
# Optionally create reverse edge
|
|
250
|
+
if bidirectional and edge_type in REVERSE_EDGE_TYPES:
|
|
251
|
+
reverse_type = REVERSE_EDGE_TYPES[edge_type]
|
|
252
|
+
reverse_edge = Edge(
|
|
253
|
+
source_id=target_id,
|
|
254
|
+
target_id=source_id,
|
|
255
|
+
type=reverse_type,
|
|
256
|
+
description=description,
|
|
257
|
+
source_workflow=workflow,
|
|
258
|
+
weight=weight,
|
|
259
|
+
)
|
|
260
|
+
self.edges.append(reverse_edge)
|
|
261
|
+
self._index_edge(reverse_edge)
|
|
262
|
+
|
|
263
|
+
self._save()
|
|
264
|
+
return edge.id
|
|
265
|
+
|
|
266
|
+
def get_node(self, node_id: str) -> Node | None:
|
|
267
|
+
"""Get a node by ID."""
|
|
268
|
+
return self.nodes.get(node_id)
|
|
269
|
+
|
|
270
|
+
def find_related(
|
|
271
|
+
self,
|
|
272
|
+
node_id: str,
|
|
273
|
+
edge_types: list[EdgeType] | None = None,
|
|
274
|
+
direction: str = "outgoing",
|
|
275
|
+
max_depth: int = 1,
|
|
276
|
+
) -> list[Node]:
|
|
277
|
+
"""Find related nodes via specified edge types.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
node_id: Starting node ID
|
|
281
|
+
edge_types: List of edge types to follow (None = all)
|
|
282
|
+
direction: "outgoing", "incoming", or "both"
|
|
283
|
+
max_depth: Maximum traversal depth
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
List of related nodes
|
|
287
|
+
|
|
288
|
+
"""
|
|
289
|
+
if node_id not in self.nodes:
|
|
290
|
+
return []
|
|
291
|
+
|
|
292
|
+
visited: set[str] = {node_id}
|
|
293
|
+
result: list[Node] = []
|
|
294
|
+
current_level: set[str] = {node_id}
|
|
295
|
+
|
|
296
|
+
for _ in range(max_depth):
|
|
297
|
+
next_level: set[str] = set()
|
|
298
|
+
|
|
299
|
+
for current_id in current_level:
|
|
300
|
+
edges_to_check: list[Edge] = []
|
|
301
|
+
|
|
302
|
+
if direction in ("outgoing", "both"):
|
|
303
|
+
edges_to_check.extend(self._edges_by_source.get(current_id, []))
|
|
304
|
+
if direction in ("incoming", "both"):
|
|
305
|
+
edges_to_check.extend(self._edges_by_target.get(current_id, []))
|
|
306
|
+
|
|
307
|
+
for edge in edges_to_check:
|
|
308
|
+
if edge_types and edge.type not in edge_types:
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
# Get the other node
|
|
312
|
+
other_id = edge.target_id if edge.source_id == current_id else edge.source_id
|
|
313
|
+
|
|
314
|
+
if other_id not in visited:
|
|
315
|
+
visited.add(other_id)
|
|
316
|
+
next_level.add(other_id)
|
|
317
|
+
if other_id in self.nodes:
|
|
318
|
+
result.append(self.nodes[other_id])
|
|
319
|
+
|
|
320
|
+
if not next_level:
|
|
321
|
+
break
|
|
322
|
+
current_level = next_level
|
|
323
|
+
|
|
324
|
+
return result
|
|
325
|
+
|
|
326
|
+
def find_similar(
|
|
327
|
+
self,
|
|
328
|
+
finding: dict[str, Any],
|
|
329
|
+
threshold: float = 0.5,
|
|
330
|
+
limit: int = 10,
|
|
331
|
+
) -> list[tuple[Node, float]]:
|
|
332
|
+
"""Find similar past findings.
|
|
333
|
+
|
|
334
|
+
Uses simple text similarity on name and description.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
finding: Dict with 'name' and/or 'description'
|
|
338
|
+
threshold: Minimum similarity score (0.0 - 1.0)
|
|
339
|
+
limit: Maximum results to return
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
List of (node, similarity_score) tuples
|
|
343
|
+
|
|
344
|
+
"""
|
|
345
|
+
query_name = finding.get("name", "").lower()
|
|
346
|
+
query_desc = finding.get("description", "").lower()
|
|
347
|
+
query_type = finding.get("type")
|
|
348
|
+
query_file = finding.get("file", "")
|
|
349
|
+
|
|
350
|
+
results: list[tuple[Node, float]] = []
|
|
351
|
+
|
|
352
|
+
for node in self.nodes.values():
|
|
353
|
+
score = 0.0
|
|
354
|
+
factors = 0.0
|
|
355
|
+
|
|
356
|
+
# Name similarity (word overlap)
|
|
357
|
+
if query_name and node.name:
|
|
358
|
+
name_words = set(query_name.split())
|
|
359
|
+
node_words = set(node.name.lower().split())
|
|
360
|
+
if name_words and node_words:
|
|
361
|
+
overlap = len(name_words & node_words)
|
|
362
|
+
union = len(name_words | node_words)
|
|
363
|
+
score += (overlap / union) * 0.5
|
|
364
|
+
factors += 0.5
|
|
365
|
+
|
|
366
|
+
# Description similarity
|
|
367
|
+
if query_desc and node.description:
|
|
368
|
+
desc_words = set(query_desc.split())
|
|
369
|
+
node_words = set(node.description.lower().split())
|
|
370
|
+
if desc_words and node_words:
|
|
371
|
+
overlap = len(desc_words & node_words)
|
|
372
|
+
union = len(desc_words | node_words)
|
|
373
|
+
score += (overlap / union) * 0.3
|
|
374
|
+
factors += 0.3
|
|
375
|
+
|
|
376
|
+
# Type match bonus
|
|
377
|
+
if query_type:
|
|
378
|
+
try:
|
|
379
|
+
if node.type == NodeType(query_type):
|
|
380
|
+
score += 0.15
|
|
381
|
+
factors += 0.15
|
|
382
|
+
except ValueError:
|
|
383
|
+
pass
|
|
384
|
+
|
|
385
|
+
# File match bonus
|
|
386
|
+
if query_file and node.source_file:
|
|
387
|
+
if query_file == node.source_file:
|
|
388
|
+
score += 0.05
|
|
389
|
+
factors += 0.05
|
|
390
|
+
|
|
391
|
+
# Normalize
|
|
392
|
+
if factors > 0:
|
|
393
|
+
score = score / factors
|
|
394
|
+
|
|
395
|
+
if score >= threshold:
|
|
396
|
+
results.append((node, score))
|
|
397
|
+
|
|
398
|
+
# Sort by score descending
|
|
399
|
+
results.sort(key=lambda x: x[1], reverse=True)
|
|
400
|
+
return results[:limit]
|
|
401
|
+
|
|
402
|
+
def find_by_type(self, node_type: NodeType) -> list[Node]:
|
|
403
|
+
"""Find all nodes of a specific type."""
|
|
404
|
+
node_ids = self._nodes_by_type.get(node_type, [])
|
|
405
|
+
return [self.nodes[nid] for nid in node_ids if nid in self.nodes]
|
|
406
|
+
|
|
407
|
+
def find_by_workflow(self, workflow: str) -> list[Node]:
|
|
408
|
+
"""Find all nodes created by a specific workflow."""
|
|
409
|
+
node_ids = self._nodes_by_workflow.get(workflow, [])
|
|
410
|
+
return [self.nodes[nid] for nid in node_ids if nid in self.nodes]
|
|
411
|
+
|
|
412
|
+
def find_by_file(self, file_path: str) -> list[Node]:
|
|
413
|
+
"""Find all nodes related to a specific file."""
|
|
414
|
+
node_ids = self._nodes_by_file.get(file_path, [])
|
|
415
|
+
return [self.nodes[nid] for nid in node_ids if nid in self.nodes]
|
|
416
|
+
|
|
417
|
+
def get_path(
|
|
418
|
+
self,
|
|
419
|
+
source_id: str,
|
|
420
|
+
target_id: str,
|
|
421
|
+
edge_types: list[EdgeType] | None = None,
|
|
422
|
+
max_depth: int = 5,
|
|
423
|
+
) -> list[tuple[Node, Edge | None]]:
|
|
424
|
+
"""Find a path between two nodes using BFS.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
source_id: Starting node ID
|
|
428
|
+
target_id: Target node ID
|
|
429
|
+
edge_types: Edge types to traverse (None = all)
|
|
430
|
+
max_depth: Maximum path length
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
List of (node, edge_to_node) tuples representing the path
|
|
434
|
+
|
|
435
|
+
"""
|
|
436
|
+
if source_id not in self.nodes or target_id not in self.nodes:
|
|
437
|
+
return []
|
|
438
|
+
|
|
439
|
+
# BFS with path tracking (deque for O(1) popleft)
|
|
440
|
+
visited: set[str] = {source_id}
|
|
441
|
+
queue: deque[list[tuple[str, Edge | None]]] = deque([[(source_id, None)]])
|
|
442
|
+
|
|
443
|
+
while queue:
|
|
444
|
+
path = queue.popleft()
|
|
445
|
+
current_id = path[-1][0]
|
|
446
|
+
|
|
447
|
+
if len(path) > max_depth:
|
|
448
|
+
continue
|
|
449
|
+
|
|
450
|
+
if current_id == target_id:
|
|
451
|
+
return [(self.nodes[nid], edge) for nid, edge in path]
|
|
452
|
+
|
|
453
|
+
for edge in self._edges_by_source.get(current_id, []):
|
|
454
|
+
if edge_types and edge.type not in edge_types:
|
|
455
|
+
continue
|
|
456
|
+
if edge.target_id not in visited:
|
|
457
|
+
visited.add(edge.target_id)
|
|
458
|
+
new_path = path + [(edge.target_id, edge)]
|
|
459
|
+
queue.append(new_path)
|
|
460
|
+
|
|
461
|
+
return []
|
|
462
|
+
|
|
463
|
+
def get_statistics(self) -> dict[str, Any]:
|
|
464
|
+
"""Get graph statistics."""
|
|
465
|
+
type_counts: dict[str, int] = defaultdict(int)
|
|
466
|
+
workflow_counts: dict[str, int] = defaultdict(int)
|
|
467
|
+
severity_counts: dict[str, int] = defaultdict(int)
|
|
468
|
+
|
|
469
|
+
for node in self.nodes.values():
|
|
470
|
+
type_counts[node.type.value] += 1
|
|
471
|
+
if node.source_workflow:
|
|
472
|
+
workflow_counts[node.source_workflow] += 1
|
|
473
|
+
if node.severity:
|
|
474
|
+
severity_counts[node.severity] += 1
|
|
475
|
+
|
|
476
|
+
edge_type_counts: dict[str, int] = defaultdict(int)
|
|
477
|
+
for edge in self.edges:
|
|
478
|
+
edge_type_counts[edge.type.value] += 1
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
"total_nodes": len(self.nodes),
|
|
482
|
+
"total_edges": len(self.edges),
|
|
483
|
+
"nodes_by_type": dict(type_counts),
|
|
484
|
+
"nodes_by_workflow": dict(workflow_counts),
|
|
485
|
+
"nodes_by_severity": dict(severity_counts),
|
|
486
|
+
"edges_by_type": dict(edge_type_counts),
|
|
487
|
+
"unique_files": len(self._nodes_by_file),
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
def update_node(self, node_id: str, updates: dict[str, Any]) -> bool:
|
|
491
|
+
"""Update a node's properties.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
node_id: Node to update
|
|
495
|
+
updates: Dict of properties to update
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
True if updated, False if node not found
|
|
499
|
+
|
|
500
|
+
"""
|
|
501
|
+
if node_id not in self.nodes:
|
|
502
|
+
return False
|
|
503
|
+
|
|
504
|
+
node = self.nodes[node_id]
|
|
505
|
+
|
|
506
|
+
if "status" in updates:
|
|
507
|
+
node.status = updates["status"]
|
|
508
|
+
if "description" in updates:
|
|
509
|
+
node.description = updates["description"]
|
|
510
|
+
if "severity" in updates:
|
|
511
|
+
node.severity = updates["severity"]
|
|
512
|
+
if "tags" in updates:
|
|
513
|
+
node.tags = updates["tags"]
|
|
514
|
+
if "metadata" in updates:
|
|
515
|
+
node.metadata.update(updates["metadata"])
|
|
516
|
+
|
|
517
|
+
node.updated_at = datetime.now()
|
|
518
|
+
self._save()
|
|
519
|
+
return True
|
|
520
|
+
|
|
521
|
+
def delete_node(self, node_id: str) -> bool:
|
|
522
|
+
"""Delete a node and its connected edges.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
node_id: Node to delete
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
True if deleted, False if not found
|
|
529
|
+
|
|
530
|
+
"""
|
|
531
|
+
if node_id not in self.nodes:
|
|
532
|
+
return False
|
|
533
|
+
|
|
534
|
+
# Remove from indexes
|
|
535
|
+
node = self.nodes[node_id]
|
|
536
|
+
if node.type in self._nodes_by_type:
|
|
537
|
+
self._nodes_by_type[node.type] = [
|
|
538
|
+
nid for nid in self._nodes_by_type[node.type] if nid != node_id
|
|
539
|
+
]
|
|
540
|
+
if node.source_workflow in self._nodes_by_workflow:
|
|
541
|
+
self._nodes_by_workflow[node.source_workflow] = [
|
|
542
|
+
nid for nid in self._nodes_by_workflow[node.source_workflow] if nid != node_id
|
|
543
|
+
]
|
|
544
|
+
if node.source_file in self._nodes_by_file:
|
|
545
|
+
self._nodes_by_file[node.source_file] = [
|
|
546
|
+
nid for nid in self._nodes_by_file[node.source_file] if nid != node_id
|
|
547
|
+
]
|
|
548
|
+
|
|
549
|
+
# Remove connected edges
|
|
550
|
+
self.edges = [e for e in self.edges if e.source_id != node_id and e.target_id != node_id]
|
|
551
|
+
if node_id in self._edges_by_source:
|
|
552
|
+
del self._edges_by_source[node_id]
|
|
553
|
+
if node_id in self._edges_by_target:
|
|
554
|
+
del self._edges_by_target[node_id]
|
|
555
|
+
|
|
556
|
+
# Remove node
|
|
557
|
+
del self.nodes[node_id]
|
|
558
|
+
self._save()
|
|
559
|
+
return True
|
|
560
|
+
|
|
561
|
+
def clear(self) -> None:
|
|
562
|
+
"""Clear all nodes and edges."""
|
|
563
|
+
self.nodes = {}
|
|
564
|
+
self.edges = []
|
|
565
|
+
self._edges_by_source = defaultdict(list)
|
|
566
|
+
self._edges_by_target = defaultdict(list)
|
|
567
|
+
self._nodes_by_type = defaultdict(list)
|
|
568
|
+
self._nodes_by_workflow = defaultdict(list)
|
|
569
|
+
self._nodes_by_file = defaultdict(list)
|
|
570
|
+
self._save()
|