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.
Files changed (331) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +173 -17
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +127 -0
  7. htmlgraph/agent_registry.py +45 -30
  8. htmlgraph/agents.py +160 -107
  9. htmlgraph/analytics/__init__.py +9 -2
  10. htmlgraph/analytics/cli.py +190 -51
  11. htmlgraph/analytics/cost_analyzer.py +391 -0
  12. htmlgraph/analytics/cost_monitor.py +664 -0
  13. htmlgraph/analytics/cost_reporter.py +675 -0
  14. htmlgraph/analytics/cross_session.py +617 -0
  15. htmlgraph/analytics/dependency.py +192 -100
  16. htmlgraph/analytics/pattern_learning.py +771 -0
  17. htmlgraph/analytics/session_graph.py +707 -0
  18. htmlgraph/analytics/strategic/__init__.py +80 -0
  19. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  20. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  21. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  22. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  23. htmlgraph/analytics/work_type.py +190 -14
  24. htmlgraph/analytics_index.py +135 -51
  25. htmlgraph/api/__init__.py +3 -0
  26. htmlgraph/api/cost_alerts_websocket.py +416 -0
  27. htmlgraph/api/main.py +2498 -0
  28. htmlgraph/api/static/htmx.min.js +1 -0
  29. htmlgraph/api/static/style-redesign.css +1344 -0
  30. htmlgraph/api/static/style.css +1079 -0
  31. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  32. htmlgraph/api/templates/dashboard.html +794 -0
  33. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  34. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  35. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  36. htmlgraph/api/templates/partials/agents.html +317 -0
  37. htmlgraph/api/templates/partials/event-traces.html +373 -0
  38. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  39. htmlgraph/api/templates/partials/features.html +578 -0
  40. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  41. htmlgraph/api/templates/partials/metrics.html +346 -0
  42. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  43. htmlgraph/api/templates/partials/orchestration.html +198 -0
  44. htmlgraph/api/templates/partials/spawners.html +375 -0
  45. htmlgraph/api/templates/partials/work-items.html +613 -0
  46. htmlgraph/api/websocket.py +538 -0
  47. htmlgraph/archive/__init__.py +24 -0
  48. htmlgraph/archive/bloom.py +234 -0
  49. htmlgraph/archive/fts.py +297 -0
  50. htmlgraph/archive/manager.py +583 -0
  51. htmlgraph/archive/search.py +244 -0
  52. htmlgraph/atomic_ops.py +560 -0
  53. htmlgraph/attribute_index.py +208 -0
  54. htmlgraph/bounded_paths.py +539 -0
  55. htmlgraph/builders/__init__.py +14 -0
  56. htmlgraph/builders/base.py +118 -29
  57. htmlgraph/builders/bug.py +150 -0
  58. htmlgraph/builders/chore.py +119 -0
  59. htmlgraph/builders/epic.py +150 -0
  60. htmlgraph/builders/feature.py +31 -6
  61. htmlgraph/builders/insight.py +195 -0
  62. htmlgraph/builders/metric.py +217 -0
  63. htmlgraph/builders/pattern.py +202 -0
  64. htmlgraph/builders/phase.py +162 -0
  65. htmlgraph/builders/spike.py +52 -19
  66. htmlgraph/builders/track.py +148 -72
  67. htmlgraph/cigs/__init__.py +81 -0
  68. htmlgraph/cigs/autonomy.py +385 -0
  69. htmlgraph/cigs/cost.py +475 -0
  70. htmlgraph/cigs/messages_basic.py +472 -0
  71. htmlgraph/cigs/messaging.py +365 -0
  72. htmlgraph/cigs/models.py +771 -0
  73. htmlgraph/cigs/pattern_storage.py +427 -0
  74. htmlgraph/cigs/patterns.py +503 -0
  75. htmlgraph/cigs/posttool_analyzer.py +234 -0
  76. htmlgraph/cigs/reporter.py +818 -0
  77. htmlgraph/cigs/tracker.py +317 -0
  78. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  79. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  80. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  81. htmlgraph/cli/__init__.py +42 -0
  82. htmlgraph/cli/__main__.py +6 -0
  83. htmlgraph/cli/analytics.py +1424 -0
  84. htmlgraph/cli/base.py +685 -0
  85. htmlgraph/cli/constants.py +206 -0
  86. htmlgraph/cli/core.py +954 -0
  87. htmlgraph/cli/main.py +147 -0
  88. htmlgraph/cli/models.py +475 -0
  89. htmlgraph/cli/templates/__init__.py +1 -0
  90. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  91. htmlgraph/cli/work/__init__.py +239 -0
  92. htmlgraph/cli/work/browse.py +115 -0
  93. htmlgraph/cli/work/features.py +568 -0
  94. htmlgraph/cli/work/orchestration.py +676 -0
  95. htmlgraph/cli/work/report.py +728 -0
  96. htmlgraph/cli/work/sessions.py +466 -0
  97. htmlgraph/cli/work/snapshot.py +559 -0
  98. htmlgraph/cli/work/tracks.py +486 -0
  99. htmlgraph/cli_commands/__init__.py +1 -0
  100. htmlgraph/cli_commands/feature.py +195 -0
  101. htmlgraph/cli_framework.py +115 -0
  102. htmlgraph/collections/__init__.py +18 -0
  103. htmlgraph/collections/base.py +415 -98
  104. htmlgraph/collections/bug.py +53 -0
  105. htmlgraph/collections/chore.py +53 -0
  106. htmlgraph/collections/epic.py +53 -0
  107. htmlgraph/collections/feature.py +12 -26
  108. htmlgraph/collections/insight.py +100 -0
  109. htmlgraph/collections/metric.py +92 -0
  110. htmlgraph/collections/pattern.py +97 -0
  111. htmlgraph/collections/phase.py +53 -0
  112. htmlgraph/collections/session.py +194 -0
  113. htmlgraph/collections/spike.py +56 -16
  114. htmlgraph/collections/task_delegation.py +241 -0
  115. htmlgraph/collections/todo.py +511 -0
  116. htmlgraph/collections/traces.py +487 -0
  117. htmlgraph/config/cost_models.json +56 -0
  118. htmlgraph/config.py +190 -0
  119. htmlgraph/context_analytics.py +344 -0
  120. htmlgraph/converter.py +216 -28
  121. htmlgraph/cost_analysis/__init__.py +5 -0
  122. htmlgraph/cost_analysis/analyzer.py +438 -0
  123. htmlgraph/dashboard.html +2406 -307
  124. htmlgraph/dashboard.html.backup +6592 -0
  125. htmlgraph/dashboard.html.bak +7181 -0
  126. htmlgraph/dashboard.html.bak2 +7231 -0
  127. htmlgraph/dashboard.html.bak3 +7232 -0
  128. htmlgraph/db/__init__.py +38 -0
  129. htmlgraph/db/queries.py +790 -0
  130. htmlgraph/db/schema.py +1788 -0
  131. htmlgraph/decorators.py +317 -0
  132. htmlgraph/dependency_models.py +19 -2
  133. htmlgraph/deploy.py +142 -125
  134. htmlgraph/deployment_models.py +474 -0
  135. htmlgraph/docs/API_REFERENCE.md +841 -0
  136. htmlgraph/docs/HTTP_API.md +750 -0
  137. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  138. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  139. htmlgraph/docs/README.md +532 -0
  140. htmlgraph/docs/__init__.py +77 -0
  141. htmlgraph/docs/docs_version.py +55 -0
  142. htmlgraph/docs/metadata.py +93 -0
  143. htmlgraph/docs/migrations.py +232 -0
  144. htmlgraph/docs/template_engine.py +143 -0
  145. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  146. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  147. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  148. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  149. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  150. htmlgraph/docs/version_check.py +163 -0
  151. htmlgraph/edge_index.py +182 -27
  152. htmlgraph/error_handler.py +544 -0
  153. htmlgraph/event_log.py +100 -52
  154. htmlgraph/event_migration.py +13 -4
  155. htmlgraph/exceptions.py +49 -0
  156. htmlgraph/file_watcher.py +101 -28
  157. htmlgraph/find_api.py +75 -63
  158. htmlgraph/git_events.py +145 -63
  159. htmlgraph/graph.py +1122 -106
  160. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  161. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  162. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  163. htmlgraph/hooks/__init__.py +45 -0
  164. htmlgraph/hooks/bootstrap.py +169 -0
  165. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  166. htmlgraph/hooks/concurrent_sessions.py +208 -0
  167. htmlgraph/hooks/context.py +350 -0
  168. htmlgraph/hooks/drift_handler.py +525 -0
  169. htmlgraph/hooks/event_tracker.py +1314 -0
  170. htmlgraph/hooks/git_commands.py +175 -0
  171. htmlgraph/hooks/hooks-config.example.json +12 -0
  172. htmlgraph/hooks/installer.py +343 -0
  173. htmlgraph/hooks/orchestrator.py +674 -0
  174. htmlgraph/hooks/orchestrator_reflector.py +223 -0
  175. htmlgraph/hooks/post-checkout.sh +28 -0
  176. htmlgraph/hooks/post-commit.sh +24 -0
  177. htmlgraph/hooks/post-merge.sh +26 -0
  178. htmlgraph/hooks/post_tool_use_failure.py +273 -0
  179. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  180. htmlgraph/hooks/posttooluse.py +408 -0
  181. htmlgraph/hooks/pre-commit.sh +94 -0
  182. htmlgraph/hooks/pre-push.sh +28 -0
  183. htmlgraph/hooks/pretooluse.py +819 -0
  184. htmlgraph/hooks/prompt_analyzer.py +637 -0
  185. htmlgraph/hooks/session_handler.py +668 -0
  186. htmlgraph/hooks/session_summary.py +395 -0
  187. htmlgraph/hooks/state_manager.py +504 -0
  188. htmlgraph/hooks/subagent_detection.py +202 -0
  189. htmlgraph/hooks/subagent_stop.py +369 -0
  190. htmlgraph/hooks/task_enforcer.py +255 -0
  191. htmlgraph/hooks/task_validator.py +177 -0
  192. htmlgraph/hooks/validator.py +628 -0
  193. htmlgraph/ids.py +41 -27
  194. htmlgraph/index.d.ts +286 -0
  195. htmlgraph/learning.py +767 -0
  196. htmlgraph/mcp_server.py +69 -23
  197. htmlgraph/models.py +1586 -87
  198. htmlgraph/operations/README.md +62 -0
  199. htmlgraph/operations/__init__.py +79 -0
  200. htmlgraph/operations/analytics.py +339 -0
  201. htmlgraph/operations/bootstrap.py +289 -0
  202. htmlgraph/operations/events.py +244 -0
  203. htmlgraph/operations/fastapi_server.py +231 -0
  204. htmlgraph/operations/hooks.py +350 -0
  205. htmlgraph/operations/initialization.py +597 -0
  206. htmlgraph/operations/initialization.py.backup +228 -0
  207. htmlgraph/operations/server.py +303 -0
  208. htmlgraph/orchestration/__init__.py +58 -0
  209. htmlgraph/orchestration/claude_launcher.py +179 -0
  210. htmlgraph/orchestration/command_builder.py +72 -0
  211. htmlgraph/orchestration/headless_spawner.py +281 -0
  212. htmlgraph/orchestration/live_events.py +377 -0
  213. htmlgraph/orchestration/model_selection.py +327 -0
  214. htmlgraph/orchestration/plugin_manager.py +140 -0
  215. htmlgraph/orchestration/prompts.py +137 -0
  216. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  217. htmlgraph/orchestration/spawners/__init__.py +16 -0
  218. htmlgraph/orchestration/spawners/base.py +194 -0
  219. htmlgraph/orchestration/spawners/claude.py +173 -0
  220. htmlgraph/orchestration/spawners/codex.py +435 -0
  221. htmlgraph/orchestration/spawners/copilot.py +294 -0
  222. htmlgraph/orchestration/spawners/gemini.py +471 -0
  223. htmlgraph/orchestration/subprocess_runner.py +36 -0
  224. htmlgraph/orchestration/task_coordination.py +343 -0
  225. htmlgraph/orchestration.md +563 -0
  226. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  227. htmlgraph/orchestrator.py +669 -0
  228. htmlgraph/orchestrator_config.py +357 -0
  229. htmlgraph/orchestrator_mode.py +328 -0
  230. htmlgraph/orchestrator_validator.py +133 -0
  231. htmlgraph/parallel.py +646 -0
  232. htmlgraph/parser.py +160 -35
  233. htmlgraph/path_query.py +608 -0
  234. htmlgraph/pattern_matcher.py +636 -0
  235. htmlgraph/planning.py +147 -52
  236. htmlgraph/pydantic_models.py +476 -0
  237. htmlgraph/quality_gates.py +350 -0
  238. htmlgraph/query_builder.py +109 -72
  239. htmlgraph/query_composer.py +509 -0
  240. htmlgraph/reflection.py +443 -0
  241. htmlgraph/refs.py +344 -0
  242. htmlgraph/repo_hash.py +512 -0
  243. htmlgraph/repositories/__init__.py +292 -0
  244. htmlgraph/repositories/analytics_repository.py +455 -0
  245. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  246. htmlgraph/repositories/feature_repository.py +581 -0
  247. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  248. htmlgraph/repositories/feature_repository_memory.py +607 -0
  249. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  250. htmlgraph/repositories/filter_service.py +620 -0
  251. htmlgraph/repositories/filter_service_standard.py +445 -0
  252. htmlgraph/repositories/shared_cache.py +621 -0
  253. htmlgraph/repositories/shared_cache_memory.py +395 -0
  254. htmlgraph/repositories/track_repository.py +552 -0
  255. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  256. htmlgraph/repositories/track_repository_memory.py +508 -0
  257. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  258. htmlgraph/routing.py +8 -19
  259. htmlgraph/scripts/deploy.py +1 -2
  260. htmlgraph/sdk/__init__.py +398 -0
  261. htmlgraph/sdk/__init__.pyi +14 -0
  262. htmlgraph/sdk/analytics/__init__.py +19 -0
  263. htmlgraph/sdk/analytics/engine.py +155 -0
  264. htmlgraph/sdk/analytics/helpers.py +178 -0
  265. htmlgraph/sdk/analytics/registry.py +109 -0
  266. htmlgraph/sdk/base.py +484 -0
  267. htmlgraph/sdk/constants.py +216 -0
  268. htmlgraph/sdk/core.pyi +308 -0
  269. htmlgraph/sdk/discovery.py +120 -0
  270. htmlgraph/sdk/help/__init__.py +12 -0
  271. htmlgraph/sdk/help/mixin.py +699 -0
  272. htmlgraph/sdk/mixins/__init__.py +15 -0
  273. htmlgraph/sdk/mixins/attribution.py +113 -0
  274. htmlgraph/sdk/mixins/mixin.py +410 -0
  275. htmlgraph/sdk/operations/__init__.py +12 -0
  276. htmlgraph/sdk/operations/mixin.py +427 -0
  277. htmlgraph/sdk/orchestration/__init__.py +17 -0
  278. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  279. htmlgraph/sdk/orchestration/spawner.py +204 -0
  280. htmlgraph/sdk/planning/__init__.py +19 -0
  281. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  282. htmlgraph/sdk/planning/mixin.py +211 -0
  283. htmlgraph/sdk/planning/parallel.py +186 -0
  284. htmlgraph/sdk/planning/queue.py +210 -0
  285. htmlgraph/sdk/planning/recommendations.py +87 -0
  286. htmlgraph/sdk/planning/smart_planning.py +319 -0
  287. htmlgraph/sdk/session/__init__.py +19 -0
  288. htmlgraph/sdk/session/continuity.py +57 -0
  289. htmlgraph/sdk/session/handoff.py +110 -0
  290. htmlgraph/sdk/session/info.py +309 -0
  291. htmlgraph/sdk/session/manager.py +103 -0
  292. htmlgraph/sdk/strategic/__init__.py +26 -0
  293. htmlgraph/sdk/strategic/mixin.py +563 -0
  294. htmlgraph/server.py +685 -180
  295. htmlgraph/services/__init__.py +10 -0
  296. htmlgraph/services/claiming.py +199 -0
  297. htmlgraph/session_hooks.py +300 -0
  298. htmlgraph/session_manager.py +1392 -175
  299. htmlgraph/session_registry.py +587 -0
  300. htmlgraph/session_state.py +436 -0
  301. htmlgraph/session_warning.py +201 -0
  302. htmlgraph/sessions/__init__.py +23 -0
  303. htmlgraph/sessions/handoff.py +756 -0
  304. htmlgraph/setup.py +34 -17
  305. htmlgraph/spike_index.py +143 -0
  306. htmlgraph/sync_docs.py +12 -15
  307. htmlgraph/system_prompts.py +450 -0
  308. htmlgraph/templates/AGENTS.md.template +366 -0
  309. htmlgraph/templates/CLAUDE.md.template +97 -0
  310. htmlgraph/templates/GEMINI.md.template +87 -0
  311. htmlgraph/templates/orchestration-view.html +350 -0
  312. htmlgraph/track_builder.py +146 -15
  313. htmlgraph/track_manager.py +69 -21
  314. htmlgraph/transcript.py +890 -0
  315. htmlgraph/transcript_analytics.py +699 -0
  316. htmlgraph/types.py +323 -0
  317. htmlgraph/validation.py +115 -0
  318. htmlgraph/watch.py +8 -5
  319. htmlgraph/work_type_utils.py +3 -2
  320. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
  321. htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
  322. htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
  323. htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
  324. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
  325. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  326. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  327. htmlgraph/cli.py +0 -2688
  328. htmlgraph/sdk.py +0 -709
  329. htmlgraph-0.9.3.dist-info/RECORD +0 -61
  330. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  331. {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 __future__ import annotations
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
- CriticalPathResult,
19
- CriticalPathNode,
23
+ ImpactAnalysis,
20
24
  ParallelizationReport,
21
25
  ParallelLevel,
22
26
  RiskAssessment,
23
- RiskNode,
24
27
  RiskFactor,
25
- HealthReport,
26
- HealthIndicator,
27
- TaskRecommendations,
28
+ RiskNode,
28
29
  TaskRecommendation,
29
- ImpactAnalysis,
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
- print(f"{bn.title} blocks {bn.transitive_blocking} features")
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
- print(f"Work on: {rec.title} (unlocks {len(rec.unlocks)} features)")
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 = {"critical": 3.0, "high": 2.0, "medium": 1.0, "low": 0.5}.get(node.priority, 1.0)
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(BottleneckNode(
132
- id=node.id,
133
- title=node.title,
134
- status=node.status,
135
- priority=node.priority,
136
- completion_pct=completion_pct,
137
- direct_blocking=direct,
138
- transitive_blocking=transitive,
139
- weighted_impact=weighted_impact,
140
- blocked_nodes=blocked_nodes
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
- direct = self._count_direct_dependents(node_id)
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(node.priority, 1.0)
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
- print(f"Can work on {report.max_parallelism} features in parallel")
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(ParallelLevel(
214
- level=level_idx,
215
- nodes=list(node_ids),
216
- max_parallel=max_parallel,
217
- independent_groups=independent_groups
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(self, status_filter: list[str] | None = None) -> list[set[str]]:
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 = [n for n in self.graph.nodes.values() if n.status in status_filter]
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 edge_ref.target_id in in_degree and edge_ref.target_id not in processed:
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(RiskNode(
353
- id=node_id,
354
- title=node.title,
355
- risk_score=risk_score,
356
- risk_factors=risk_factors
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 node in high_risk[:3]:
374
- recommendations.append(f"Break {node.title} into smaller features to reduce SPOF risk")
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(f"Resolve {len(cycles)} circular dependencies detected")
402
+ recommendations.append(
403
+ f"Resolve {len(cycles)} circular dependencies detected"
404
+ )
377
405
  if orphaned:
378
- recommendations.append(f"Review {len(orphaned)} orphaned nodes with no dependents")
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
- print(f"Work on: {rec.title}")
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((score, TaskRecommendation(
474
- id=node.id,
475
- title=node.title,
476
- priority=node.priority,
477
- score=score,
478
- reasons=reasons,
479
- estimated_effort=effort,
480
- unlocks=unlocks
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([recommendations[i].id, recommendations[i+1].id])
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(node_id, include_done=include_done)
589
- affected = self._get_all_transitive_dependents(node_id, include_done=include_done)
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([n for n in self.graph.nodes.values() if include_done or n.status != "done"])
593
- completion_impact = (transitive / total_nodes * 100.0) if total_nodes > 0 else 0.0
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 [edge_ref.source_id for edge_ref in self._edge_index.get_incoming(node_id)]
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(self, node_id: str, include_done: bool = False) -> int:
614
- """Count all downstream nodes that transitively depend on this node."""
615
- return len(self._get_all_transitive_dependents(node_id, include_done=include_done))
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(self, node_id: str, include_done: bool = False) -> list[str]:
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()