htmlgraph 0.9.3__py3-none-any.whl → 0.27.5__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.
- htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +173 -17
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +127 -0
- htmlgraph/agent_registry.py +45 -30
- htmlgraph/agents.py +160 -107
- htmlgraph/analytics/__init__.py +9 -2
- htmlgraph/analytics/cli.py +190 -51
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +192 -100
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +190 -14
- htmlgraph/analytics_index.py +135 -51
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +578 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +208 -0
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/__init__.py +14 -0
- htmlgraph/builders/base.py +118 -29
- htmlgraph/builders/bug.py +150 -0
- htmlgraph/builders/chore.py +119 -0
- htmlgraph/builders/epic.py +150 -0
- htmlgraph/builders/feature.py +31 -6
- htmlgraph/builders/insight.py +195 -0
- htmlgraph/builders/metric.py +217 -0
- htmlgraph/builders/pattern.py +202 -0
- htmlgraph/builders/phase.py +162 -0
- htmlgraph/builders/spike.py +52 -19
- htmlgraph/builders/track.py +148 -72
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +18 -0
- htmlgraph/collections/base.py +415 -98
- htmlgraph/collections/bug.py +53 -0
- htmlgraph/collections/chore.py +53 -0
- htmlgraph/collections/epic.py +53 -0
- htmlgraph/collections/feature.py +12 -26
- htmlgraph/collections/insight.py +100 -0
- htmlgraph/collections/metric.py +92 -0
- htmlgraph/collections/pattern.py +97 -0
- htmlgraph/collections/phase.py +53 -0
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +56 -16
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +511 -0
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +344 -0
- htmlgraph/converter.py +216 -28
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2406 -307
- htmlgraph/dashboard.html.backup +6592 -0
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +19 -2
- htmlgraph/deploy.py +142 -125
- htmlgraph/deployment_models.py +474 -0
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +182 -27
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +100 -52
- htmlgraph/event_migration.py +13 -4
- htmlgraph/exceptions.py +49 -0
- htmlgraph/file_watcher.py +101 -28
- htmlgraph/find_api.py +75 -63
- htmlgraph/git_events.py +145 -63
- htmlgraph/graph.py +1122 -106
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +45 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +1314 -0
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/hooks-config.example.json +12 -0
- htmlgraph/hooks/installer.py +343 -0
- htmlgraph/hooks/orchestrator.py +674 -0
- htmlgraph/hooks/orchestrator_reflector.py +223 -0
- htmlgraph/hooks/post-checkout.sh +28 -0
- htmlgraph/hooks/post-commit.sh +24 -0
- htmlgraph/hooks/post-merge.sh +26 -0
- htmlgraph/hooks/post_tool_use_failure.py +273 -0
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +408 -0
- htmlgraph/hooks/pre-commit.sh +94 -0
- htmlgraph/hooks/pre-push.sh +28 -0
- htmlgraph/hooks/pretooluse.py +819 -0
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +255 -0
- htmlgraph/hooks/task_validator.py +177 -0
- htmlgraph/hooks/validator.py +628 -0
- htmlgraph/ids.py +41 -27
- htmlgraph/index.d.ts +286 -0
- htmlgraph/learning.py +767 -0
- htmlgraph/mcp_server.py +69 -23
- htmlgraph/models.py +1586 -87
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/orchestration/task_coordination.py +343 -0
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +669 -0
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +328 -0
- htmlgraph/orchestrator_validator.py +133 -0
- htmlgraph/parallel.py +646 -0
- htmlgraph/parser.py +160 -35
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/planning.py +147 -52
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +109 -72
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/routing.py +8 -19
- htmlgraph/scripts/deploy.py +1 -2
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +685 -180
- htmlgraph/services/__init__.py +10 -0
- htmlgraph/services/claiming.py +199 -0
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +1392 -175
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +201 -0
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/setup.py +34 -17
- htmlgraph/spike_index.py +143 -0
- htmlgraph/sync_docs.py +12 -15
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph/templates/GEMINI.md.template +87 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +146 -15
- htmlgraph/track_manager.py +69 -21
- htmlgraph/transcript.py +890 -0
- htmlgraph/transcript_analytics.py +699 -0
- htmlgraph/types.py +323 -0
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +8 -5
- htmlgraph/work_type_utils.py +3 -2
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
- htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -2688
- htmlgraph/sdk.py +0 -709
- htmlgraph-0.9.3.dist-info/RECORD +0 -61
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
1
7
|
"""
|
|
2
8
|
Dependency-aware analytics for HtmlGraph.
|
|
3
9
|
|
|
@@ -9,25 +15,19 @@ Provides advanced graph analysis for project management:
|
|
|
9
15
|
- Work prioritization
|
|
10
16
|
"""
|
|
11
17
|
|
|
12
|
-
from
|
|
18
|
+
from collections import deque
|
|
13
19
|
from typing import TYPE_CHECKING
|
|
14
|
-
from collections import defaultdict, deque
|
|
15
20
|
|
|
16
21
|
from htmlgraph.dependency_models import (
|
|
17
22
|
BottleneckNode,
|
|
18
|
-
|
|
19
|
-
CriticalPathNode,
|
|
23
|
+
ImpactAnalysis,
|
|
20
24
|
ParallelizationReport,
|
|
21
25
|
ParallelLevel,
|
|
22
26
|
RiskAssessment,
|
|
23
|
-
RiskNode,
|
|
24
27
|
RiskFactor,
|
|
25
|
-
|
|
26
|
-
HealthIndicator,
|
|
27
|
-
TaskRecommendations,
|
|
28
|
+
RiskNode,
|
|
28
29
|
TaskRecommendation,
|
|
29
|
-
|
|
30
|
-
WhatIfResult,
|
|
30
|
+
TaskRecommendations,
|
|
31
31
|
)
|
|
32
32
|
|
|
33
33
|
if TYPE_CHECKING:
|
|
@@ -46,21 +46,29 @@ class DependencyAnalytics:
|
|
|
46
46
|
- What should we prioritize next?
|
|
47
47
|
- What are the high-risk dependencies?
|
|
48
48
|
|
|
49
|
+
Performance: Uses internal caching to optimize transitive dependency calculations.
|
|
50
|
+
Multiple calls to bottleneck detection or task recommendations reuse cached results.
|
|
51
|
+
Call invalidate_cache() after graph structure changes to refresh the cache.
|
|
52
|
+
|
|
49
53
|
Example:
|
|
50
54
|
from htmlgraph import SDK
|
|
51
55
|
|
|
52
56
|
sdk = SDK(agent="claude")
|
|
53
57
|
dep = sdk.dep_analytics
|
|
54
58
|
|
|
55
|
-
# Find bottlenecks
|
|
59
|
+
# Find bottlenecks (cached internally for performance)
|
|
56
60
|
bottlenecks = dep.find_bottlenecks(top_n=5)
|
|
57
61
|
for bn in bottlenecks:
|
|
58
|
-
|
|
62
|
+
logger.info(f"{bn.title} blocks {bn.transitive_blocking} features")
|
|
59
63
|
|
|
60
|
-
# Get work recommendations
|
|
64
|
+
# Get work recommendations (reuses cached data)
|
|
61
65
|
recs = dep.recommend_next_tasks(agent_count=3)
|
|
62
66
|
for rec in recs.recommendations:
|
|
63
|
-
|
|
67
|
+
logger.info(f"Work on: {rec.title} (unlocks {len(rec.unlocks)} features)")
|
|
68
|
+
|
|
69
|
+
# After making graph changes, invalidate cache
|
|
70
|
+
sdk.features.update(feature_id, status="done")
|
|
71
|
+
dep.invalidate_cache() # Refresh for accurate results
|
|
64
72
|
"""
|
|
65
73
|
|
|
66
74
|
def __init__(self, graph: HtmlGraph):
|
|
@@ -72,6 +80,7 @@ class DependencyAnalytics:
|
|
|
72
80
|
"""
|
|
73
81
|
self.graph = graph
|
|
74
82
|
self._edge_index = graph.edge_index
|
|
83
|
+
self._transitive_cache: dict[str, set[str]] = {}
|
|
75
84
|
|
|
76
85
|
# === Bottleneck Detection ===
|
|
77
86
|
|
|
@@ -79,7 +88,7 @@ class DependencyAnalytics:
|
|
|
79
88
|
self,
|
|
80
89
|
status_filter: list[str] | None = None,
|
|
81
90
|
top_n: int = 10,
|
|
82
|
-
min_impact: int = 1
|
|
91
|
+
min_impact: int = 1,
|
|
83
92
|
) -> list[BottleneckNode]:
|
|
84
93
|
"""
|
|
85
94
|
Identify nodes that are blocking the most work.
|
|
@@ -120,7 +129,12 @@ class DependencyAnalytics:
|
|
|
120
129
|
transitive = self._count_transitive_dependents(node.id)
|
|
121
130
|
|
|
122
131
|
# Calculate weighted impact
|
|
123
|
-
priority_weight = {
|
|
132
|
+
priority_weight = {
|
|
133
|
+
"critical": 3.0,
|
|
134
|
+
"high": 2.0,
|
|
135
|
+
"medium": 1.0,
|
|
136
|
+
"low": 0.5,
|
|
137
|
+
}.get(node.priority, 1.0)
|
|
124
138
|
completion_pct = self._calculate_completion(node)
|
|
125
139
|
incompletion_factor = (100.0 - completion_pct) / 100.0
|
|
126
140
|
weighted_impact = transitive * priority_weight * incompletion_factor
|
|
@@ -128,17 +142,19 @@ class DependencyAnalytics:
|
|
|
128
142
|
# Get list of blocked nodes
|
|
129
143
|
blocked_nodes = self._get_direct_dependents(node.id)
|
|
130
144
|
|
|
131
|
-
bottlenecks.append(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
bottlenecks.append(
|
|
146
|
+
BottleneckNode(
|
|
147
|
+
id=node.id,
|
|
148
|
+
title=node.title,
|
|
149
|
+
status=node.status,
|
|
150
|
+
priority=node.priority,
|
|
151
|
+
completion_pct=completion_pct,
|
|
152
|
+
direct_blocking=direct,
|
|
153
|
+
transitive_blocking=transitive,
|
|
154
|
+
weighted_impact=weighted_impact,
|
|
155
|
+
blocked_nodes=blocked_nodes,
|
|
156
|
+
)
|
|
157
|
+
)
|
|
142
158
|
|
|
143
159
|
# Sort by weighted impact descending
|
|
144
160
|
bottlenecks.sort(key=lambda x: x.weighted_impact, reverse=True)
|
|
@@ -159,9 +175,11 @@ class DependencyAnalytics:
|
|
|
159
175
|
if not node:
|
|
160
176
|
return 0.0
|
|
161
177
|
|
|
162
|
-
|
|
178
|
+
self._count_direct_dependents(node_id)
|
|
163
179
|
transitive = self._count_transitive_dependents(node_id)
|
|
164
|
-
priority_weight = {"critical": 3.0, "high": 2.0, "medium": 1.0, "low": 0.5}.get(
|
|
180
|
+
priority_weight = {"critical": 3.0, "high": 2.0, "medium": 1.0, "low": 0.5}.get(
|
|
181
|
+
node.priority, 1.0
|
|
182
|
+
)
|
|
165
183
|
completion_pct = self._calculate_completion(node)
|
|
166
184
|
incompletion_factor = (100.0 - completion_pct) / 100.0
|
|
167
185
|
|
|
@@ -170,9 +188,7 @@ class DependencyAnalytics:
|
|
|
170
188
|
# === Parallelization Analysis ===
|
|
171
189
|
|
|
172
190
|
def find_parallelizable_work(
|
|
173
|
-
self,
|
|
174
|
-
status: str = "todo",
|
|
175
|
-
max_levels: int | None = None
|
|
191
|
+
self, status: str = "todo", max_levels: int | None = None
|
|
176
192
|
) -> ParallelizationReport:
|
|
177
193
|
"""
|
|
178
194
|
Identify work that can be done in parallel.
|
|
@@ -189,7 +205,7 @@ class DependencyAnalytics:
|
|
|
189
205
|
|
|
190
206
|
Example:
|
|
191
207
|
report = dep.find_parallelizable_work(status="todo")
|
|
192
|
-
|
|
208
|
+
logger.info(f"Can work on {report.max_parallelism} features in parallel")
|
|
193
209
|
"""
|
|
194
210
|
# Get dependency levels (topological layers)
|
|
195
211
|
levels = self.dependency_levels(status_filter=[status])
|
|
@@ -210,27 +226,31 @@ class DependencyAnalytics:
|
|
|
210
226
|
max_parallel = len(node_ids)
|
|
211
227
|
max_parallelism = max(max_parallelism, max_parallel)
|
|
212
228
|
|
|
213
|
-
parallel_levels.append(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
229
|
+
parallel_levels.append(
|
|
230
|
+
ParallelLevel(
|
|
231
|
+
level=level_idx,
|
|
232
|
+
nodes=list(node_ids),
|
|
233
|
+
max_parallel=max_parallel,
|
|
234
|
+
independent_groups=independent_groups,
|
|
235
|
+
)
|
|
236
|
+
)
|
|
219
237
|
|
|
220
238
|
# Suggest assignments (round-robin for now)
|
|
221
239
|
suggestions = []
|
|
222
240
|
if parallel_levels and parallel_levels[0].nodes:
|
|
223
241
|
for i, node_id in enumerate(parallel_levels[0].nodes[:3]): # Top 3
|
|
224
|
-
agent_name = f"agent-{i+1}"
|
|
242
|
+
agent_name = f"agent-{i + 1}"
|
|
225
243
|
suggestions.append((agent_name, [node_id]))
|
|
226
244
|
|
|
227
245
|
return ParallelizationReport(
|
|
228
246
|
max_parallelism=max_parallelism,
|
|
229
247
|
dependency_levels=parallel_levels,
|
|
230
|
-
suggested_assignments=suggestions
|
|
248
|
+
suggested_assignments=suggestions,
|
|
231
249
|
)
|
|
232
250
|
|
|
233
|
-
def dependency_levels(
|
|
251
|
+
def dependency_levels(
|
|
252
|
+
self, status_filter: list[str] | None = None
|
|
253
|
+
) -> list[set[str]]:
|
|
234
254
|
"""
|
|
235
255
|
Group nodes by dependency level (topological layers).
|
|
236
256
|
|
|
@@ -247,7 +267,9 @@ class DependencyAnalytics:
|
|
|
247
267
|
"""
|
|
248
268
|
# Get all nodes matching filter
|
|
249
269
|
if status_filter:
|
|
250
|
-
nodes_to_process = [
|
|
270
|
+
nodes_to_process = [
|
|
271
|
+
n for n in self.graph.nodes.values() if n.status in status_filter
|
|
272
|
+
]
|
|
251
273
|
else:
|
|
252
274
|
nodes_to_process = list(self.graph.nodes.values())
|
|
253
275
|
|
|
@@ -263,7 +285,7 @@ class DependencyAnalytics:
|
|
|
263
285
|
in_degree[node.id] = count
|
|
264
286
|
|
|
265
287
|
levels = []
|
|
266
|
-
processed = set()
|
|
288
|
+
processed: set[str] = set()
|
|
267
289
|
|
|
268
290
|
while len(processed) < len(node_ids):
|
|
269
291
|
# Find all nodes with in-degree 0 (no unprocessed dependencies)
|
|
@@ -287,7 +309,10 @@ class DependencyAnalytics:
|
|
|
287
309
|
# Decrease in-degree for neighbors
|
|
288
310
|
for node_id in current_level:
|
|
289
311
|
for edge_ref in self._edge_index.get_outgoing(node_id):
|
|
290
|
-
if
|
|
312
|
+
if (
|
|
313
|
+
edge_ref.target_id in in_degree
|
|
314
|
+
and edge_ref.target_id not in processed
|
|
315
|
+
):
|
|
291
316
|
in_degree[edge_ref.target_id] -= 1
|
|
292
317
|
|
|
293
318
|
return levels
|
|
@@ -309,10 +334,7 @@ class DependencyAnalytics:
|
|
|
309
334
|
|
|
310
335
|
# === Risk Assessment ===
|
|
311
336
|
|
|
312
|
-
def assess_dependency_risk(
|
|
313
|
-
self,
|
|
314
|
-
spof_threshold: int = 3
|
|
315
|
-
) -> RiskAssessment:
|
|
337
|
+
def assess_dependency_risk(self, spof_threshold: int = 3) -> RiskAssessment:
|
|
316
338
|
"""
|
|
317
339
|
Assess risk based on dependency structure.
|
|
318
340
|
|
|
@@ -343,18 +365,20 @@ class DependencyAnalytics:
|
|
|
343
365
|
type="spof",
|
|
344
366
|
severity="high" if dependents_count > 10 else "medium",
|
|
345
367
|
description=f"Blocks {dependents_count} features with no alternative paths",
|
|
346
|
-
mitigation="Consider breaking into smaller independent features"
|
|
368
|
+
mitigation="Consider breaking into smaller independent features",
|
|
347
369
|
)
|
|
348
370
|
]
|
|
349
371
|
|
|
350
372
|
risk_score = min(dependents_count / 20.0, 1.0) # Cap at 1.0
|
|
351
373
|
|
|
352
|
-
high_risk.append(
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
374
|
+
high_risk.append(
|
|
375
|
+
RiskNode(
|
|
376
|
+
id=node_id,
|
|
377
|
+
title=node.title,
|
|
378
|
+
risk_score=risk_score,
|
|
379
|
+
risk_factors=risk_factors,
|
|
380
|
+
)
|
|
381
|
+
)
|
|
358
382
|
|
|
359
383
|
# Find circular dependencies
|
|
360
384
|
cycles = self.graph.find_cycles()
|
|
@@ -370,24 +394,27 @@ class DependencyAnalytics:
|
|
|
370
394
|
|
|
371
395
|
# Generate recommendations
|
|
372
396
|
recommendations = []
|
|
373
|
-
for
|
|
374
|
-
recommendations.append(
|
|
397
|
+
for risk_node in high_risk[:3]:
|
|
398
|
+
recommendations.append(
|
|
399
|
+
f"Break {risk_node.title} into smaller features to reduce SPOF risk"
|
|
400
|
+
)
|
|
375
401
|
if cycles:
|
|
376
|
-
recommendations.append(
|
|
402
|
+
recommendations.append(
|
|
403
|
+
f"Resolve {len(cycles)} circular dependencies detected"
|
|
404
|
+
)
|
|
377
405
|
if orphaned:
|
|
378
|
-
recommendations.append(
|
|
406
|
+
recommendations.append(
|
|
407
|
+
f"Review {len(orphaned)} orphaned nodes with no dependents"
|
|
408
|
+
)
|
|
379
409
|
|
|
380
410
|
return RiskAssessment(
|
|
381
411
|
high_risk=high_risk,
|
|
382
412
|
circular_dependencies=cycles,
|
|
383
413
|
orphaned_nodes=orphaned,
|
|
384
|
-
recommendations=recommendations
|
|
414
|
+
recommendations=recommendations,
|
|
385
415
|
)
|
|
386
416
|
|
|
387
|
-
def single_points_of_failure(
|
|
388
|
-
self,
|
|
389
|
-
min_dependents: int = 3
|
|
390
|
-
) -> list[str]:
|
|
417
|
+
def single_points_of_failure(self, min_dependents: int = 3) -> list[str]:
|
|
391
418
|
"""
|
|
392
419
|
Identify nodes with high fan-in (many dependents).
|
|
393
420
|
|
|
@@ -411,10 +438,7 @@ class DependencyAnalytics:
|
|
|
411
438
|
# === Work Prioritization ===
|
|
412
439
|
|
|
413
440
|
def recommend_next_tasks(
|
|
414
|
-
self,
|
|
415
|
-
agent_count: int = 1,
|
|
416
|
-
status: str = "todo",
|
|
417
|
-
lookahead: int = 3
|
|
441
|
+
self, agent_count: int = 1, status: str = "todo", lookahead: int = 3
|
|
418
442
|
) -> TaskRecommendations:
|
|
419
443
|
"""
|
|
420
444
|
Recommend which tasks to work on next.
|
|
@@ -436,7 +460,7 @@ class DependencyAnalytics:
|
|
|
436
460
|
Example:
|
|
437
461
|
recs = dep.recommend_next_tasks(agent_count=3)
|
|
438
462
|
for rec in recs.recommendations:
|
|
439
|
-
|
|
463
|
+
logger.info(f"Work on: {rec.title}")
|
|
440
464
|
"""
|
|
441
465
|
# Get all nodes with target status
|
|
442
466
|
candidates = [n for n in self.graph.nodes.values() if n.status == status]
|
|
@@ -470,21 +494,26 @@ class DependencyAnalytics:
|
|
|
470
494
|
if not reasons:
|
|
471
495
|
reasons.append("Ready to start (all dependencies complete)")
|
|
472
496
|
|
|
473
|
-
scored.append(
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
497
|
+
scored.append(
|
|
498
|
+
(
|
|
499
|
+
score,
|
|
500
|
+
TaskRecommendation(
|
|
501
|
+
id=node.id,
|
|
502
|
+
title=node.title,
|
|
503
|
+
priority=node.priority,
|
|
504
|
+
score=score,
|
|
505
|
+
reasons=reasons,
|
|
506
|
+
estimated_effort=effort,
|
|
507
|
+
unlocks=unlocks,
|
|
508
|
+
),
|
|
509
|
+
)
|
|
510
|
+
)
|
|
482
511
|
|
|
483
512
|
# Sort by score descending
|
|
484
513
|
scored.sort(key=lambda x: x[0], reverse=True)
|
|
485
514
|
|
|
486
515
|
# Take top recommendations
|
|
487
|
-
recommendations = [rec for _, rec in scored[:lookahead * agent_count]]
|
|
516
|
+
recommendations = [rec for _, rec in scored[: lookahead * agent_count]]
|
|
488
517
|
|
|
489
518
|
# Find parallel suggestions
|
|
490
519
|
parallel_suggestions = []
|
|
@@ -492,17 +521,16 @@ class DependencyAnalytics:
|
|
|
492
521
|
# Simple approach: suggest non-overlapping dependency chains
|
|
493
522
|
for i in range(0, min(len(recommendations), agent_count * 2), 2):
|
|
494
523
|
if i + 1 < len(recommendations):
|
|
495
|
-
parallel_suggestions.append(
|
|
524
|
+
parallel_suggestions.append(
|
|
525
|
+
[recommendations[i].id, recommendations[i + 1].id]
|
|
526
|
+
)
|
|
496
527
|
|
|
497
528
|
return TaskRecommendations(
|
|
498
|
-
recommendations=recommendations,
|
|
499
|
-
parallel_suggestions=parallel_suggestions
|
|
529
|
+
recommendations=recommendations, parallel_suggestions=parallel_suggestions
|
|
500
530
|
)
|
|
501
531
|
|
|
502
532
|
def prioritization_score(
|
|
503
|
-
self,
|
|
504
|
-
node_id: str,
|
|
505
|
-
weights: dict[str, float] | None = None
|
|
533
|
+
self, node_id: str, weights: dict[str, float] | None = None
|
|
506
534
|
) -> float:
|
|
507
535
|
"""
|
|
508
536
|
Calculate priority score for a node.
|
|
@@ -525,7 +553,7 @@ class DependencyAnalytics:
|
|
|
525
553
|
"transitive_blocking": 2.0,
|
|
526
554
|
"priority": 1.5,
|
|
527
555
|
"dependency_penalty": -0.5,
|
|
528
|
-
"critical_path": 3.0
|
|
556
|
+
"critical_path": 3.0,
|
|
529
557
|
}
|
|
530
558
|
|
|
531
559
|
node = self.graph.get(node_id)
|
|
@@ -570,9 +598,7 @@ class DependencyAnalytics:
|
|
|
570
598
|
return (fan_in, fan_out)
|
|
571
599
|
|
|
572
600
|
def impact_analysis(
|
|
573
|
-
self,
|
|
574
|
-
node_id: str,
|
|
575
|
-
include_done: bool = False
|
|
601
|
+
self, node_id: str, include_done: bool = False
|
|
576
602
|
) -> ImpactAnalysis:
|
|
577
603
|
"""
|
|
578
604
|
Analyze the downstream impact of a node.
|
|
@@ -585,19 +611,27 @@ class DependencyAnalytics:
|
|
|
585
611
|
ImpactAnalysis with dependency impact
|
|
586
612
|
"""
|
|
587
613
|
direct = self._count_direct_dependents(node_id)
|
|
588
|
-
transitive = self._count_transitive_dependents(
|
|
589
|
-
|
|
614
|
+
transitive = self._count_transitive_dependents(
|
|
615
|
+
node_id, include_done=include_done
|
|
616
|
+
)
|
|
617
|
+
affected = self._get_all_transitive_dependents(
|
|
618
|
+
node_id, include_done=include_done
|
|
619
|
+
)
|
|
590
620
|
|
|
591
621
|
# Calculate what % of total work this represents
|
|
592
|
-
total_nodes = len(
|
|
593
|
-
|
|
622
|
+
total_nodes = len(
|
|
623
|
+
[n for n in self.graph.nodes.values() if include_done or n.status != "done"]
|
|
624
|
+
)
|
|
625
|
+
completion_impact = (
|
|
626
|
+
(transitive / total_nodes * 100.0) if total_nodes > 0 else 0.0
|
|
627
|
+
)
|
|
594
628
|
|
|
595
629
|
return ImpactAnalysis(
|
|
596
630
|
node_id=node_id,
|
|
597
631
|
direct_dependents=direct,
|
|
598
632
|
transitive_dependents=transitive,
|
|
599
633
|
affected_nodes=affected,
|
|
600
|
-
completion_impact=completion_impact
|
|
634
|
+
completion_impact=completion_impact,
|
|
601
635
|
)
|
|
602
636
|
|
|
603
637
|
# === Private Helper Methods ===
|
|
@@ -608,13 +642,25 @@ class DependencyAnalytics:
|
|
|
608
642
|
|
|
609
643
|
def _get_direct_dependents(self, node_id: str) -> list[str]:
|
|
610
644
|
"""Get list of node IDs that directly depend on this node."""
|
|
611
|
-
return [
|
|
645
|
+
return [
|
|
646
|
+
edge_ref.source_id for edge_ref in self._edge_index.get_incoming(node_id)
|
|
647
|
+
]
|
|
612
648
|
|
|
613
|
-
def _count_transitive_dependents(
|
|
614
|
-
|
|
615
|
-
|
|
649
|
+
def _count_transitive_dependents(
|
|
650
|
+
self, node_id: str, include_done: bool = False
|
|
651
|
+
) -> int:
|
|
652
|
+
"""
|
|
653
|
+
Count all downstream nodes that transitively depend on this node.
|
|
654
|
+
|
|
655
|
+
Uses cached results when available to improve performance from O(V²+VE) to O(V+E)
|
|
656
|
+
for repeated calls.
|
|
657
|
+
"""
|
|
658
|
+
transitive_set = self._get_or_compute_transitive(node_id, include_done)
|
|
659
|
+
return len(transitive_set)
|
|
616
660
|
|
|
617
|
-
def _get_all_transitive_dependents(
|
|
661
|
+
def _get_all_transitive_dependents(
|
|
662
|
+
self, node_id: str, include_done: bool = False
|
|
663
|
+
) -> list[str]:
|
|
618
664
|
"""Get all downstream nodes (BFS traversal of dependents)."""
|
|
619
665
|
visited = set()
|
|
620
666
|
queue = deque([node_id])
|
|
@@ -676,3 +722,49 @@ class DependencyAnalytics:
|
|
|
676
722
|
# Simple implementation: return individual nodes for now
|
|
677
723
|
# A more sophisticated version would use graph coloring
|
|
678
724
|
return [[nid] for nid in node_ids]
|
|
725
|
+
|
|
726
|
+
def _get_or_compute_transitive(
|
|
727
|
+
self, node_id: str, include_done: bool = False
|
|
728
|
+
) -> set[str]:
|
|
729
|
+
"""
|
|
730
|
+
Get or compute transitive dependents with caching.
|
|
731
|
+
|
|
732
|
+
Uses a cache to avoid redundant BFS traversals. The cache key combines
|
|
733
|
+
node_id and include_done flag to ensure correct results for both cases.
|
|
734
|
+
|
|
735
|
+
Args:
|
|
736
|
+
node_id: Node to analyze
|
|
737
|
+
include_done: Whether to include completed nodes
|
|
738
|
+
|
|
739
|
+
Returns:
|
|
740
|
+
Set of node IDs that transitively depend on this node
|
|
741
|
+
"""
|
|
742
|
+
cache_key = f"{node_id}:{include_done}"
|
|
743
|
+
|
|
744
|
+
if cache_key in self._transitive_cache:
|
|
745
|
+
return self._transitive_cache[cache_key]
|
|
746
|
+
|
|
747
|
+
# Compute transitive dependents via BFS
|
|
748
|
+
dependents = self._get_all_transitive_dependents(
|
|
749
|
+
node_id, include_done=include_done
|
|
750
|
+
)
|
|
751
|
+
dependents_set = set(dependents)
|
|
752
|
+
|
|
753
|
+
# Cache the result
|
|
754
|
+
self._transitive_cache[cache_key] = dependents_set
|
|
755
|
+
|
|
756
|
+
return dependents_set
|
|
757
|
+
|
|
758
|
+
def invalidate_cache(self) -> None:
|
|
759
|
+
"""
|
|
760
|
+
Clear the transitive dependency cache.
|
|
761
|
+
|
|
762
|
+
Call this method after making structural changes to the graph
|
|
763
|
+
(adding/removing nodes or edges) to ensure cached results remain accurate.
|
|
764
|
+
|
|
765
|
+
Example:
|
|
766
|
+
analytics = sdk.dep_analytics
|
|
767
|
+
analytics.invalidate_cache() # After graph updates
|
|
768
|
+
bottlenecks = analytics.find_bottlenecks() # Fresh calculation
|
|
769
|
+
"""
|
|
770
|
+
self._transitive_cache.clear()
|