htmlgraph 0.26.25__py3-none-any.whl → 0.27.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. htmlgraph/__init__.py +23 -1
  2. htmlgraph/__init__.pyi +123 -0
  3. htmlgraph/agent_registry.py +2 -1
  4. htmlgraph/analytics/cli.py +3 -3
  5. htmlgraph/analytics/cost_analyzer.py +5 -1
  6. htmlgraph/analytics/cross_session.py +13 -9
  7. htmlgraph/analytics/dependency.py +10 -6
  8. htmlgraph/analytics/work_type.py +15 -11
  9. htmlgraph/analytics_index.py +2 -1
  10. htmlgraph/api/main.py +58 -28
  11. htmlgraph/attribute_index.py +2 -1
  12. htmlgraph/builders/base.py +2 -1
  13. htmlgraph/builders/bug.py +2 -1
  14. htmlgraph/builders/chore.py +2 -1
  15. htmlgraph/builders/epic.py +2 -1
  16. htmlgraph/builders/feature.py +2 -1
  17. htmlgraph/builders/insight.py +2 -1
  18. htmlgraph/builders/metric.py +2 -1
  19. htmlgraph/builders/pattern.py +2 -1
  20. htmlgraph/builders/phase.py +2 -1
  21. htmlgraph/builders/spike.py +2 -1
  22. htmlgraph/builders/track.py +2 -1
  23. htmlgraph/cli/analytics.py +2 -1
  24. htmlgraph/cli/base.py +2 -1
  25. htmlgraph/cli/core.py +2 -1
  26. htmlgraph/cli/main.py +2 -1
  27. htmlgraph/cli/models.py +2 -1
  28. htmlgraph/cli/templates/cost_dashboard.py +2 -1
  29. htmlgraph/cli/work/__init__.py +2 -1
  30. htmlgraph/cli/work/browse.py +2 -1
  31. htmlgraph/cli/work/features.py +2 -1
  32. htmlgraph/cli/work/orchestration.py +2 -1
  33. htmlgraph/cli/work/report.py +2 -1
  34. htmlgraph/cli/work/sessions.py +2 -1
  35. htmlgraph/cli/work/snapshot.py +2 -1
  36. htmlgraph/cli/work/tracks.py +2 -1
  37. htmlgraph/collections/base.py +10 -5
  38. htmlgraph/collections/bug.py +2 -1
  39. htmlgraph/collections/chore.py +2 -1
  40. htmlgraph/collections/epic.py +2 -1
  41. htmlgraph/collections/feature.py +2 -1
  42. htmlgraph/collections/insight.py +2 -1
  43. htmlgraph/collections/metric.py +2 -1
  44. htmlgraph/collections/pattern.py +2 -1
  45. htmlgraph/collections/phase.py +2 -1
  46. htmlgraph/collections/session.py +12 -7
  47. htmlgraph/collections/spike.py +6 -1
  48. htmlgraph/collections/task_delegation.py +7 -2
  49. htmlgraph/collections/todo.py +2 -1
  50. htmlgraph/collections/traces.py +15 -10
  51. htmlgraph/context_analytics.py +2 -1
  52. htmlgraph/dependency_models.py +2 -1
  53. htmlgraph/edge_index.py +2 -1
  54. htmlgraph/event_log.py +81 -66
  55. htmlgraph/event_migration.py +2 -1
  56. htmlgraph/file_watcher.py +12 -8
  57. htmlgraph/find_api.py +2 -1
  58. htmlgraph/git_events.py +6 -2
  59. htmlgraph/hooks/cigs_pretool_enforcer.py +5 -1
  60. htmlgraph/hooks/drift_handler.py +3 -3
  61. htmlgraph/hooks/event_tracker.py +40 -61
  62. htmlgraph/hooks/installer.py +5 -1
  63. htmlgraph/hooks/orchestrator.py +4 -0
  64. htmlgraph/hooks/orchestrator_reflector.py +4 -0
  65. htmlgraph/hooks/post_tool_use_failure.py +7 -3
  66. htmlgraph/hooks/posttooluse.py +4 -0
  67. htmlgraph/hooks/prompt_analyzer.py +5 -5
  68. htmlgraph/hooks/session_handler.py +2 -1
  69. htmlgraph/hooks/session_summary.py +6 -2
  70. htmlgraph/hooks/validator.py +8 -4
  71. htmlgraph/ids.py +2 -1
  72. htmlgraph/learning.py +2 -1
  73. htmlgraph/mcp_server.py +2 -1
  74. htmlgraph/operations/analytics.py +2 -1
  75. htmlgraph/operations/bootstrap.py +2 -1
  76. htmlgraph/operations/events.py +2 -1
  77. htmlgraph/operations/fastapi_server.py +2 -1
  78. htmlgraph/operations/hooks.py +2 -1
  79. htmlgraph/operations/initialization.py +2 -1
  80. htmlgraph/operations/server.py +2 -1
  81. htmlgraph/orchestration/claude_launcher.py +23 -20
  82. htmlgraph/orchestration/command_builder.py +2 -1
  83. htmlgraph/orchestration/headless_spawner.py +6 -2
  84. htmlgraph/orchestration/model_selection.py +7 -3
  85. htmlgraph/orchestration/plugin_manager.py +24 -19
  86. htmlgraph/orchestration/spawners/claude.py +5 -2
  87. htmlgraph/orchestration/spawners/codex.py +12 -19
  88. htmlgraph/orchestration/spawners/copilot.py +13 -18
  89. htmlgraph/orchestration/spawners/gemini.py +12 -19
  90. htmlgraph/orchestration/subprocess_runner.py +6 -3
  91. htmlgraph/orchestration/task_coordination.py +16 -8
  92. htmlgraph/orchestrator.py +2 -1
  93. htmlgraph/parallel.py +2 -1
  94. htmlgraph/query_builder.py +2 -1
  95. htmlgraph/reflection.py +2 -1
  96. htmlgraph/refs.py +2 -1
  97. htmlgraph/repo_hash.py +2 -1
  98. htmlgraph/sdk/__init__.py +398 -0
  99. htmlgraph/sdk/__init__.pyi +14 -0
  100. htmlgraph/sdk/analytics/__init__.py +19 -0
  101. htmlgraph/sdk/analytics/engine.py +155 -0
  102. htmlgraph/sdk/analytics/helpers.py +178 -0
  103. htmlgraph/sdk/analytics/registry.py +109 -0
  104. htmlgraph/sdk/base.py +484 -0
  105. htmlgraph/sdk/constants.py +216 -0
  106. htmlgraph/sdk/core.pyi +308 -0
  107. htmlgraph/sdk/discovery.py +120 -0
  108. htmlgraph/sdk/help/__init__.py +12 -0
  109. htmlgraph/sdk/help/mixin.py +699 -0
  110. htmlgraph/sdk/mixins/__init__.py +15 -0
  111. htmlgraph/sdk/mixins/attribution.py +113 -0
  112. htmlgraph/sdk/mixins/mixin.py +410 -0
  113. htmlgraph/sdk/operations/__init__.py +12 -0
  114. htmlgraph/sdk/operations/mixin.py +427 -0
  115. htmlgraph/sdk/orchestration/__init__.py +17 -0
  116. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  117. htmlgraph/sdk/orchestration/spawner.py +204 -0
  118. htmlgraph/sdk/planning/__init__.py +19 -0
  119. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  120. htmlgraph/sdk/planning/mixin.py +211 -0
  121. htmlgraph/sdk/planning/parallel.py +186 -0
  122. htmlgraph/sdk/planning/queue.py +210 -0
  123. htmlgraph/sdk/planning/recommendations.py +87 -0
  124. htmlgraph/sdk/planning/smart_planning.py +319 -0
  125. htmlgraph/sdk/session/__init__.py +19 -0
  126. htmlgraph/sdk/session/continuity.py +57 -0
  127. htmlgraph/sdk/session/handoff.py +110 -0
  128. htmlgraph/sdk/session/info.py +309 -0
  129. htmlgraph/sdk/session/manager.py +103 -0
  130. htmlgraph/server.py +21 -17
  131. htmlgraph/session_warning.py +2 -1
  132. htmlgraph/sessions/handoff.py +4 -3
  133. htmlgraph/system_prompts.py +2 -1
  134. htmlgraph/track_builder.py +2 -1
  135. htmlgraph/transcript.py +2 -1
  136. htmlgraph/watch.py +2 -1
  137. htmlgraph/work_type_utils.py +2 -1
  138. {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.0.dist-info}/METADATA +15 -1
  139. {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.0.dist-info}/RECORD +146 -114
  140. htmlgraph/sdk.py +0 -3500
  141. {htmlgraph-0.26.25.data → htmlgraph-0.27.0.data}/data/htmlgraph/dashboard.html +0 -0
  142. {htmlgraph-0.26.25.data → htmlgraph-0.27.0.data}/data/htmlgraph/styles.css +0 -0
  143. {htmlgraph-0.26.25.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  144. {htmlgraph-0.26.25.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  145. {htmlgraph-0.26.25.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  146. {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.0.dist-info}/WHEEL +0 -0
  147. {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.0.dist-info}/entry_points.txt +0 -0
htmlgraph/__init__.py CHANGED
@@ -5,6 +5,28 @@ A lightweight graph database framework using HTML files as nodes,
5
5
  hyperlinks as edges, and CSS selectors as the query language.
6
6
  """
7
7
 
8
+ import logging
9
+
10
+ from rich.console import Console
11
+ from rich.logging import RichHandler
12
+
13
+ # Configure Rich logging for entire SDK
14
+ # CRITICAL: Use stderr=True to prevent pollution of stdout (hooks output JSON to stdout)
15
+ logging.basicConfig(
16
+ handlers=[
17
+ RichHandler(
18
+ console=Console(stderr=True),
19
+ show_time=True,
20
+ show_level=True,
21
+ rich_tracebacks=True,
22
+ )
23
+ ],
24
+ level=logging.INFO,
25
+ format="%(message)s",
26
+ datefmt="[%X]",
27
+ )
28
+
29
+ # SDK now lives in sdk/core.py - clean import from sdk package
8
30
  from htmlgraph.agent_detection import detect_agent_name, get_agent_display_name
9
31
  from htmlgraph.agents import AgentInterface
10
32
  from htmlgraph.analytics import Analytics, DependencyAnalytics
@@ -96,7 +118,7 @@ from htmlgraph.types import (
96
118
  )
97
119
  from htmlgraph.work_type_utils import infer_work_type, infer_work_type_from_id
98
120
 
99
- __version__ = "0.26.25"
121
+ __version__ = "0.27.0"
100
122
  __all__ = [
101
123
  # Exceptions
102
124
  "HtmlGraphError",
htmlgraph/__init__.pyi ADDED
@@ -0,0 +1,123 @@
1
+ """
2
+ Type stub for htmlgraph package.
3
+
4
+ This stub provides type information for mypy when the SDK class
5
+ is loaded dynamically via importlib at runtime.
6
+ """
7
+
8
+ from htmlgraph.agent_detection import detect_agent_name as detect_agent_name
9
+ from htmlgraph.agent_detection import get_agent_display_name as get_agent_display_name
10
+ from htmlgraph.agents import AgentInterface as AgentInterface
11
+ from htmlgraph.analytics import Analytics as Analytics
12
+
13
+ # Analytics
14
+ from htmlgraph.analytics import DependencyAnalytics as DependencyAnalytics
15
+ from htmlgraph.atomic_ops import AtomicFileWriter as AtomicFileWriter
16
+ from htmlgraph.atomic_ops import DirectoryLocker as DirectoryLocker
17
+ from htmlgraph.atomic_ops import atomic_rename as atomic_rename
18
+ from htmlgraph.atomic_ops import (
19
+ cleanup_orphaned_temp_files as cleanup_orphaned_temp_files,
20
+ )
21
+ from htmlgraph.atomic_ops import safe_temp_file as safe_temp_file
22
+ from htmlgraph.atomic_ops import validate_atomic_write as validate_atomic_write
23
+ from htmlgraph.builders import BaseBuilder as BaseBuilder
24
+ from htmlgraph.builders import FeatureBuilder as FeatureBuilder
25
+ from htmlgraph.builders import SpikeBuilder as SpikeBuilder
26
+ from htmlgraph.collections import BaseCollection as BaseCollection
27
+
28
+ # Collections
29
+ from htmlgraph.collections import FeatureCollection as FeatureCollection
30
+ from htmlgraph.collections import SpikeCollection as SpikeCollection
31
+ from htmlgraph.context_analytics import ContextAnalytics as ContextAnalytics
32
+ from htmlgraph.context_analytics import ContextUsage as ContextUsage
33
+ from htmlgraph.decorators import RetryError as RetryError
34
+ from htmlgraph.decorators import retry as retry
35
+ from htmlgraph.decorators import retry_async as retry_async
36
+ from htmlgraph.edge_index import EdgeIndex as EdgeIndex
37
+ from htmlgraph.edge_index import EdgeRef as EdgeRef
38
+ from htmlgraph.exceptions import ClaimConflictError as ClaimConflictError
39
+ from htmlgraph.exceptions import HtmlGraphError as HtmlGraphError
40
+ from htmlgraph.exceptions import NodeNotFoundError as NodeNotFoundError
41
+ from htmlgraph.exceptions import SessionNotFoundError as SessionNotFoundError
42
+ from htmlgraph.exceptions import ValidationError as ValidationError
43
+ from htmlgraph.find_api import FindAPI as FindAPI
44
+ from htmlgraph.find_api import find as find
45
+ from htmlgraph.find_api import find_all as find_all
46
+ from htmlgraph.graph import CompiledQuery as CompiledQuery
47
+ from htmlgraph.graph import HtmlGraph as HtmlGraph
48
+ from htmlgraph.ids import generate_hierarchical_id as generate_hierarchical_id
49
+ from htmlgraph.ids import generate_id as generate_id
50
+ from htmlgraph.ids import is_legacy_id as is_legacy_id
51
+ from htmlgraph.ids import is_valid_id as is_valid_id
52
+ from htmlgraph.ids import parse_id as parse_id
53
+ from htmlgraph.learning import LearningPersistence as LearningPersistence
54
+ from htmlgraph.learning import (
55
+ auto_persist_on_session_end as auto_persist_on_session_end,
56
+ )
57
+ from htmlgraph.models import ActivityEntry as ActivityEntry
58
+ from htmlgraph.models import AggregatedMetric as AggregatedMetric
59
+ from htmlgraph.models import Chore as Chore
60
+ from htmlgraph.models import ContextSnapshot as ContextSnapshot
61
+ from htmlgraph.models import Edge as Edge
62
+ from htmlgraph.models import Graph as Graph
63
+ from htmlgraph.models import MaintenanceType as MaintenanceType
64
+ from htmlgraph.models import Node as Node
65
+ from htmlgraph.models import Pattern as Pattern
66
+ from htmlgraph.models import Session as Session
67
+ from htmlgraph.models import SessionInsight as SessionInsight
68
+ from htmlgraph.models import Spike as Spike
69
+ from htmlgraph.models import SpikeType as SpikeType
70
+ from htmlgraph.models import Step as Step
71
+ from htmlgraph.models import WorkType as WorkType
72
+ from htmlgraph.orchestration import delegate_with_id as delegate_with_id
73
+ from htmlgraph.orchestration import generate_task_id as generate_task_id
74
+ from htmlgraph.orchestration import get_results_by_task_id as get_results_by_task_id
75
+ from htmlgraph.orchestration import parallel_delegate as parallel_delegate
76
+ from htmlgraph.orchestrator_mode import OrchestratorMode as OrchestratorMode
77
+ from htmlgraph.orchestrator_mode import (
78
+ OrchestratorModeManager as OrchestratorModeManager,
79
+ )
80
+ from htmlgraph.parallel import AggregateResult as AggregateResult
81
+ from htmlgraph.parallel import ParallelAnalysis as ParallelAnalysis
82
+ from htmlgraph.parallel import ParallelWorkflow as ParallelWorkflow
83
+ from htmlgraph.query_builder import Condition as Condition
84
+ from htmlgraph.query_builder import Operator as Operator
85
+ from htmlgraph.query_builder import QueryBuilder as QueryBuilder
86
+ from htmlgraph.reflection import ComputationalReflection as ComputationalReflection
87
+ from htmlgraph.reflection import get_reflection_context as get_reflection_context
88
+ from htmlgraph.repo_hash import RepoHash as RepoHash
89
+ from htmlgraph.sdk.core import SDK as SDK
90
+ from htmlgraph.server import serve as serve
91
+ from htmlgraph.session_manager import SessionManager as SessionManager
92
+ from htmlgraph.session_registry import SessionRegistry as SessionRegistry
93
+ from htmlgraph.types import ActiveWorkItem as ActiveWorkItem
94
+ from htmlgraph.types import AggregateResultsDict as AggregateResultsDict
95
+ from htmlgraph.types import BottleneckDict as BottleneckDict
96
+ from htmlgraph.types import FeatureSummary as FeatureSummary
97
+ from htmlgraph.types import HighRiskTask as HighRiskTask
98
+ from htmlgraph.types import ImpactAnalysisDict as ImpactAnalysisDict
99
+ from htmlgraph.types import OrchestrationResult as OrchestrationResult
100
+ from htmlgraph.types import ParallelGuidelines as ParallelGuidelines
101
+ from htmlgraph.types import ParallelPlanResult as ParallelPlanResult
102
+ from htmlgraph.types import ParallelWorkInfo as ParallelWorkInfo
103
+ from htmlgraph.types import PlanningContext as PlanningContext
104
+ from htmlgraph.types import ProjectStatus as ProjectStatus
105
+ from htmlgraph.types import RiskAssessmentDict as RiskAssessmentDict
106
+ from htmlgraph.types import SessionAnalytics as SessionAnalytics
107
+ from htmlgraph.types import SessionStartInfo as SessionStartInfo
108
+ from htmlgraph.types import SessionSummary as SessionSummary
109
+ from htmlgraph.types import SmartPlanResult as SmartPlanResult
110
+ from htmlgraph.types import SubagentPrompt as SubagentPrompt
111
+ from htmlgraph.types import TaskPrompt as TaskPrompt
112
+ from htmlgraph.types import TrackCreationResult as TrackCreationResult
113
+ from htmlgraph.types import WorkQueueItem as WorkQueueItem
114
+ from htmlgraph.types import WorkRecommendation as WorkRecommendation
115
+ from htmlgraph.work_type_utils import infer_work_type as infer_work_type
116
+ from htmlgraph.work_type_utils import infer_work_type_from_id as infer_work_type_from_id
117
+
118
+ __version__: str
119
+
120
+ # SDK is imported from htmlgraph.sdk.core (see import above)
121
+ # Re-exported here for backward compatibility
122
+
123
+ __all__: list[str]
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  Agent Registry - Manages agent capabilities and routing.
3
5
 
@@ -7,7 +9,6 @@ Provides:
7
9
  - Capability matching for task routing
8
10
  """
9
11
 
10
- from __future__ import annotations
11
12
 
12
13
  import json
13
14
  from dataclasses import dataclass, field
@@ -109,7 +109,7 @@ def _iter_with_progress(
109
109
 
110
110
 
111
111
  def _display_session_analytics(
112
- console: Console, sdk: SDK, session_id: str, graph_dir: str, quiet: bool
112
+ console: Console, sdk: "SDK", session_id: str, graph_dir: str, quiet: bool
113
113
  ) -> None:
114
114
  """Display analytics for a single session."""
115
115
  from htmlgraph.converter import html_to_session
@@ -221,7 +221,7 @@ def _display_session_analytics(
221
221
 
222
222
  def _display_recent_sessions(
223
223
  console: Console,
224
- sdk: SDK,
224
+ sdk: "SDK",
225
225
  session_files: list[Path],
226
226
  graph_dir: str,
227
227
  quiet: bool,
@@ -286,7 +286,7 @@ def _display_recent_sessions(
286
286
 
287
287
  def _display_project_analytics(
288
288
  console: Console,
289
- sdk: SDK,
289
+ sdk: "SDK",
290
290
  session_files: list[Path],
291
291
  graph_dir: str,
292
292
  quiet: bool,
@@ -1,3 +1,7 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
4
+
1
5
  """
2
6
  CostAnalyzer for OTEL ROI Analysis - Phase 1.
3
7
 
@@ -15,7 +19,7 @@ Usage:
15
19
  from htmlgraph.analytics.cost_analyzer import CostAnalyzer
16
20
  analyzer = CostAnalyzer()
17
21
  delegations = analyzer.get_task_delegations_with_costs()
18
- print(f"Total delegation cost: ${delegations['total_cost_usd']:.2f}")
22
+ logger.info(f"Total delegation cost: ${delegations['total_cost_usd']:.2f}")
19
23
  """
20
24
 
21
25
  import sqlite3
@@ -1,3 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ logger = logging.getLogger(__name__)
6
+
1
7
  """
2
8
  Cross-session analytics using Git commits as the continuity spine.
3
9
 
@@ -38,8 +44,6 @@ Example:
38
44
  authors = cross.work_by_author(since_commit="abc123")
39
45
  """
40
46
 
41
- from __future__ import annotations
42
-
43
47
  import subprocess
44
48
  from collections import defaultdict
45
49
  from dataclasses import dataclass
@@ -147,8 +151,8 @@ class CrossSessionAnalytics:
147
151
  ... from_commit="HEAD~10",
148
152
  ... to_commit="HEAD"
149
153
  ... )
150
- >>> print(f"Total events: {report.total_events}")
151
- >>> print(f"Features: {', '.join(report.features)}")
154
+ >>> logger.info(f"Total events: {report.total_events}")
155
+ >>> logger.info(f"Features: {', '.join(report.features)}")
152
156
  """
153
157
  # Get commit list from Git
154
158
  commits = self._get_commits_in_range(from_commit, to_commit)
@@ -273,7 +277,7 @@ class CrossSessionAnalytics:
273
277
 
274
278
  Example:
275
279
  >>> sessions = cross.sessions_for_feature("feature-auth")
276
- >>> print(f"Feature worked on in {len(sessions)} sessions")
280
+ >>> logger.info(f"Feature worked on in {len(sessions)} sessions")
277
281
  """
278
282
  sessions = set()
279
283
 
@@ -320,8 +324,8 @@ class CrossSessionAnalytics:
320
324
 
321
325
  Example:
322
326
  >>> report = cross.feature_cross_session_report("feature-auth")
323
- >>> print(f"Implemented across {len(report.sessions)} sessions")
324
- >>> print(f"Duration: {report.duration_hours:.1f} hours")
327
+ >>> logger.info(f"Implemented across {len(report.sessions)} sessions")
328
+ >>> logger.info(f"Duration: {report.duration_hours:.1f} hours")
325
329
  """
326
330
  sessions = set()
327
331
  commits = set()
@@ -395,7 +399,7 @@ class CrossSessionAnalytics:
395
399
  Example:
396
400
  >>> authors = cross.work_by_author(since_commit="v1.0.0")
397
401
  >>> for email, stats in authors.items():
398
- ... print(f"{email}: {stats['event_count']} events")
402
+ ... logger.info(f"{email}: {stats['event_count']} events")
399
403
  """
400
404
  authors: dict[str, dict[str, Any]] = defaultdict(
401
405
  lambda: {
@@ -476,7 +480,7 @@ class CrossSessionAnalytics:
476
480
 
477
481
  Example:
478
482
  >>> commits = cross.commits_for_session("session-abc")
479
- >>> print(f"Session produced {len(commits)} commits")
483
+ >>> logger.info(f"Session produced {len(commits)} commits")
480
484
  """
481
485
  commits = set()
482
486
 
@@ -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,8 +15,6 @@ Provides advanced graph analysis for project management:
9
15
  - Work prioritization
10
16
  """
11
17
 
12
- from __future__ import annotations
13
-
14
18
  from collections import deque
15
19
  from typing import TYPE_CHECKING
16
20
 
@@ -55,12 +59,12 @@ class DependencyAnalytics:
55
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
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)")
64
68
 
65
69
  # After making graph changes, invalidate cache
66
70
  sdk.features.update(feature_id, status="done")
@@ -201,7 +205,7 @@ class DependencyAnalytics:
201
205
 
202
206
  Example:
203
207
  report = dep.find_parallelizable_work(status="todo")
204
- print(f"Can work on {report.max_parallelism} features in parallel")
208
+ logger.info(f"Can work on {report.max_parallelism} features in parallel")
205
209
  """
206
210
  # Get dependency levels (topological layers)
207
211
  levels = self.dependency_levels(status_filter=[status])
@@ -456,7 +460,7 @@ class DependencyAnalytics:
456
460
  Example:
457
461
  recs = dep.recommend_next_tasks(agent_count=3)
458
462
  for rec in recs.recommendations:
459
- print(f"Work on: {rec.title}")
463
+ logger.info(f"Work on: {rec.title}")
460
464
  """
461
465
  # Get all nodes with target status
462
466
  candidates = [n for n in self.graph.nodes.values() if n.status == status]
@@ -1,3 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ logger = logging.getLogger(__name__)
6
+
1
7
  """
2
8
  Analytics API for HtmlGraph work type analysis.
3
9
 
@@ -25,8 +31,6 @@ Example:
25
31
  # Returns: 25.5 (% of work spent on maintenance)
26
32
  """
27
33
 
28
- from __future__ import annotations
29
-
30
34
  from datetime import datetime, timezone
31
35
  from typing import TYPE_CHECKING
32
36
 
@@ -97,7 +101,7 @@ class Analytics:
97
101
  Example:
98
102
  >>> analytics = sdk.analytics
99
103
  >>> dist = analytics.work_type_distribution(session_id="session-123")
100
- >>> print(dist)
104
+ >>> logger.info("%s", dist)
101
105
  {
102
106
  "feature-implementation": 45.2,
103
107
  "spike-investigation": 28.3,
@@ -163,11 +167,11 @@ class Analytics:
163
167
 
164
168
  Example:
165
169
  >>> ratio = sdk.analytics.spike_to_feature_ratio(session_id="session-123")
166
- >>> print(f"Spike-to-feature ratio: {ratio:.2f}")
170
+ >>> logger.info(f"Spike-to-feature ratio: {ratio:.2f}")
167
171
  Spike-to-feature ratio: 0.63
168
172
 
169
173
  >>> if ratio > 0.5:
170
- ... print("This was a research-heavy session")
174
+ ... logger.info("This was a research-heavy session")
171
175
  """
172
176
  events = self._get_events(session_id, start_date, end_date)
173
177
 
@@ -221,11 +225,11 @@ class Analytics:
221
225
 
222
226
  Example:
223
227
  >>> burden = sdk.analytics.maintenance_burden(session_id="session-123")
224
- >>> print(f"Maintenance burden: {burden:.1f}%")
228
+ >>> logger.info(f"Maintenance burden: {burden:.1f}%")
225
229
  Maintenance burden: 32.5%
226
230
 
227
231
  >>> if burden > 40:
228
- ... print("⚠️ High maintenance burden - consider addressing technical debt")
232
+ ... logger.info("⚠️ High maintenance burden - consider addressing technical debt")
229
233
  """
230
234
  events = self._get_events(session_id, start_date, end_date)
231
235
 
@@ -276,7 +280,7 @@ class Analytics:
276
280
  >>> spike_sessions = sdk.analytics.get_sessions_by_work_type(
277
281
  ... "spike-investigation"
278
282
  ... )
279
- >>> print(f"Found {len(spike_sessions)} exploratory sessions")
283
+ >>> logger.info(f"Found {len(spike_sessions)} exploratory sessions")
280
284
  """
281
285
  session_nodes = self.sdk.sessions.all()
282
286
  matching_sessions = []
@@ -316,7 +320,7 @@ class Analytics:
316
320
 
317
321
  Example:
318
322
  >>> breakdown = sdk.analytics.calculate_session_work_breakdown("session-123")
319
- >>> print(breakdown)
323
+ >>> logger.info("%s", breakdown)
320
324
  {
321
325
  "feature-implementation": 45,
322
326
  "spike-investigation": 28,
@@ -344,7 +348,7 @@ class Analytics:
344
348
 
345
349
  Example:
346
350
  >>> primary = sdk.analytics.calculate_session_primary_work_type("session-123")
347
- >>> print(f"Primary work type: {primary}")
351
+ >>> logger.info(f"Primary work type: {primary}")
348
352
  Primary work type: spike-investigation
349
353
  """
350
354
  session = self._get_session(session_id)
@@ -399,7 +403,7 @@ class Analytics:
399
403
 
400
404
  Example:
401
405
  >>> metrics = sdk.analytics.transition_time_metrics(session_id="session-123")
402
- >>> print(f"Transition time: {metrics['transition_percent']:.1f}%")
406
+ >>> logger.info(f"Transition time: {metrics['transition_percent']:.1f}%")
403
407
  Transition time: 15.3%
404
408
  """
405
409
  from pathlib import Path
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  SQLite analytics index for HtmlGraph event logs.
3
5
 
@@ -5,7 +7,6 @@ This is a rebuildable cache/index for fast dashboard queries.
5
7
  The canonical source of truth is the JSONL event log under `.htmlgraph/events/`.
6
8
  """
7
9
 
8
- from __future__ import annotations
9
10
 
10
11
  import json
11
12
  import sqlite3
htmlgraph/api/main.py CHANGED
@@ -204,7 +204,37 @@ def get_app(db_path: str) -> FastAPI:
204
204
  return "0"
205
205
  return f"{value:,}"
206
206
 
207
+ def format_duration(seconds: float | int | None) -> str:
208
+ """Format duration in seconds to human-readable string."""
209
+ if seconds is None:
210
+ return "0.00s"
211
+ return f"{float(seconds):.2f}s"
212
+
213
+ def format_bytes(bytes_size: int | float | None) -> str:
214
+ """Format bytes to MB with 2 decimal places."""
215
+ if bytes_size is None:
216
+ return "0.00MB"
217
+ return f"{int(bytes_size) / (1024 * 1024):.2f}MB"
218
+
219
+ def truncate_text(text: str | None, length: int = 50) -> str:
220
+ """Truncate text to specified length with ellipsis."""
221
+ if text is None:
222
+ return ""
223
+ return text[:length] + "..." if len(text) > length else text
224
+
225
+ def format_timestamp(ts: Any) -> str:
226
+ """Format timestamp to readable string."""
227
+ if ts is None:
228
+ return ""
229
+ if hasattr(ts, "strftime"):
230
+ return str(ts.strftime("%Y-%m-%d %H:%M:%S"))
231
+ return str(ts)
232
+
207
233
  templates.env.filters["format_number"] = format_number
234
+ templates.env.filters["format_duration"] = format_duration
235
+ templates.env.filters["format_bytes"] = format_bytes
236
+ templates.env.filters["truncate"] = truncate_text
237
+ templates.env.filters["format_timestamp"] = format_timestamp
208
238
 
209
239
  # Setup static files
210
240
  static_dir = Path(__file__).parent / "static"
@@ -287,8 +317,8 @@ def get_app(db_path: str) -> FastAPI:
287
317
 
288
318
  # Execute query with timing
289
319
  exec_start = time.time()
290
- cursor = await db.execute(query)
291
- rows = await cursor.fetchall()
320
+ async with db.execute(query) as cursor:
321
+ rows = await cursor.fetchall()
292
322
  exec_time_ms = (time.time() - exec_start) * 1000
293
323
 
294
324
  agents = []
@@ -434,8 +464,8 @@ def get_app(db_path: str) -> FastAPI:
434
464
 
435
465
  # Execute query with timing
436
466
  exec_start = time.time()
437
- cursor = await db.execute(query, params)
438
- rows = await cursor.fetchall()
467
+ async with db.execute(query, params) as cursor:
468
+ rows = await cursor.fetchall()
439
469
  exec_time_ms = (time.time() - exec_start) * 1000
440
470
 
441
471
  # Build result models
@@ -485,15 +515,15 @@ def get_app(db_path: str) -> FastAPI:
485
515
  (SELECT COUNT(DISTINCT agent_id) FROM agent_events) as total_agents,
486
516
  (SELECT COUNT(*) FROM sessions) as total_sessions
487
517
  """
488
- cursor = await db.execute(stats_query)
489
- row = await cursor.fetchone()
518
+ async with db.execute(stats_query) as cursor:
519
+ row = await cursor.fetchone()
490
520
 
491
521
  # Query distinct agent IDs for the agent set
492
522
  agents_query = (
493
523
  "SELECT DISTINCT agent_id FROM agent_events WHERE agent_id IS NOT NULL"
494
524
  )
495
- agents_cursor = await db.execute(agents_query)
496
- agents_rows = await agents_cursor.fetchall()
525
+ async with db.execute(agents_query) as agents_cursor:
526
+ agents_rows = await agents_cursor.fetchall()
497
527
  agents = [row[0] for row in agents_rows]
498
528
 
499
529
  if row is None:
@@ -616,8 +646,8 @@ def get_app(db_path: str) -> FastAPI:
616
646
  parent_query += " ORDER BY timestamp DESC LIMIT ?"
617
647
  parent_params.append(limit)
618
648
 
619
- cursor = await db.execute(parent_query, parent_params)
620
- parent_rows = await cursor.fetchall()
649
+ async with db.execute(parent_query, parent_params) as cursor:
650
+ parent_rows = await cursor.fetchall()
621
651
 
622
652
  traces: list[dict[str, Any]] = []
623
653
 
@@ -655,8 +685,8 @@ def get_app(db_path: str) -> FastAPI:
655
685
  WHERE parent_event_id = ?
656
686
  ORDER BY timestamp ASC
657
687
  """
658
- child_cursor = await db.execute(child_query, (parent_event_id,))
659
- child_rows = await child_cursor.fetchall()
688
+ async with db.execute(child_query, (parent_event_id,)) as child_cursor:
689
+ child_rows = await child_cursor.fetchall()
660
690
 
661
691
  child_events = []
662
692
  for child_row in child_rows:
@@ -806,8 +836,8 @@ def get_app(db_path: str) -> FastAPI:
806
836
  params.append(limit)
807
837
 
808
838
  exec_start = time.time()
809
- cursor = await db.execute(query, params)
810
- rows = await cursor.fetchall()
839
+ async with db.execute(query, params) as cursor:
840
+ rows = await cursor.fetchall()
811
841
 
812
842
  for row in rows:
813
843
  events.append(
@@ -851,8 +881,8 @@ def get_app(db_path: str) -> FastAPI:
851
881
  spike_query += " ORDER BY created_at DESC LIMIT ?"
852
882
  spike_params.append(limit)
853
883
 
854
- spike_cursor = await db.execute(spike_query, spike_params)
855
- spike_rows = await spike_cursor.fetchall()
884
+ async with db.execute(spike_query, spike_params) as spike_cursor:
885
+ spike_rows = await spike_cursor.fetchall()
856
886
 
857
887
  for row in spike_rows:
858
888
  events.append(
@@ -902,8 +932,8 @@ def get_app(db_path: str) -> FastAPI:
902
932
  collab_query += " ORDER BY timestamp DESC LIMIT ?"
903
933
  collab_params.append(limit)
904
934
 
905
- collab_cursor = await db.execute(collab_query, collab_params)
906
- collab_rows = await collab_cursor.fetchall()
935
+ async with db.execute(collab_query, collab_params) as collab_cursor:
936
+ collab_rows = await collab_cursor.fetchall()
907
937
 
908
938
  for row in collab_rows:
909
939
  events.append(
@@ -1016,8 +1046,8 @@ def get_app(db_path: str) -> FastAPI:
1016
1046
  LIMIT ?
1017
1047
  """
1018
1048
 
1019
- cursor = await db.execute(user_query_query, [limit])
1020
- user_query_rows = await cursor.fetchall()
1049
+ async with db.execute(user_query_query, [limit]) as cursor:
1050
+ user_query_rows = await cursor.fetchall()
1021
1051
 
1022
1052
  conversation_turns: list[dict[str, Any]] = []
1023
1053
 
@@ -1061,8 +1091,8 @@ def get_app(db_path: str) -> FastAPI:
1061
1091
  if depth >= max_depth:
1062
1092
  return [], 0.0, 0, 0
1063
1093
 
1064
- cursor = await db.execute(children_query, [parent_id])
1065
- rows = await cursor.fetchall()
1094
+ async with db.execute(children_query, [parent_id]) as cursor:
1095
+ rows = await cursor.fetchall()
1066
1096
 
1067
1097
  children_list: list[dict[str, Any]] = []
1068
1098
  total_dur = 0.0
@@ -1338,8 +1368,8 @@ def get_app(db_path: str) -> FastAPI:
1338
1368
  query += " ORDER BY started_at DESC LIMIT ? OFFSET ?"
1339
1369
  params.extend([limit, offset])
1340
1370
 
1341
- cursor = await db.execute(query, params)
1342
- rows = await cursor.fetchall()
1371
+ async with db.execute(query, params) as cursor:
1372
+ rows = await cursor.fetchall()
1343
1373
 
1344
1374
  # Get total count for pagination
1345
1375
  count_query = "SELECT COUNT(*) FROM sessions WHERE 1=1"
@@ -1348,8 +1378,8 @@ def get_app(db_path: str) -> FastAPI:
1348
1378
  count_query += " AND status = ?"
1349
1379
  count_params.append(status)
1350
1380
 
1351
- count_cursor = await db.execute(count_query, count_params)
1352
- count_row = await count_cursor.fetchone()
1381
+ async with db.execute(count_query, count_params) as count_cursor:
1382
+ count_row = await count_cursor.fetchone()
1353
1383
  total = int(count_row[0]) if count_row else 0
1354
1384
 
1355
1385
  # Build session objects
@@ -1416,8 +1446,8 @@ def get_app(db_path: str) -> FastAPI:
1416
1446
  LIMIT 50
1417
1447
  """
1418
1448
 
1419
- cursor = await db.execute(query)
1420
- rows = list(await cursor.fetchall())
1449
+ async with db.execute(query) as cursor:
1450
+ rows = list(await cursor.fetchall())
1421
1451
  logger.debug(f"orchestration_view: Query executed, got {len(rows)} rows")
1422
1452
 
1423
1453
  delegations = []
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  Attribute Index for O(1) attribute-based lookups.
3
5
 
@@ -11,7 +13,6 @@ Without this index, finding nodes by attribute requires scanning
11
13
  all nodes in the graph - O(n) complexity.
12
14
  """
13
15
 
14
- from __future__ import annotations
15
16
 
16
17
  from collections import defaultdict
17
18
  from dataclasses import dataclass, field
@@ -1,10 +1,11 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  Base builder class for fluent node creation.
3
5
 
4
6
  Provides common builder patterns shared across all node types.
5
7
  """
6
8
 
7
- from __future__ import annotations
8
9
 
9
10
  from datetime import datetime
10
11
  from typing import TYPE_CHECKING, Any, Generic, TypeVar
htmlgraph/builders/bug.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  Bug builder for creating bug report nodes.
3
5
 
@@ -5,7 +7,6 @@ Extends BaseBuilder with bug-specific methods like
5
7
  severity and reproduction steps.
6
8
  """
7
9
 
8
- from __future__ import annotations
9
10
 
10
11
  from typing import TYPE_CHECKING, Any
11
12