htmlgraph 0.20.1__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 (304) 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 +51 -1
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +26 -10
  7. htmlgraph/agent_registry.py +2 -1
  8. htmlgraph/analytics/__init__.py +8 -1
  9. htmlgraph/analytics/cli.py +86 -20
  10. htmlgraph/analytics/cost_analyzer.py +391 -0
  11. htmlgraph/analytics/cost_monitor.py +664 -0
  12. htmlgraph/analytics/cost_reporter.py +675 -0
  13. htmlgraph/analytics/cross_session.py +617 -0
  14. htmlgraph/analytics/dependency.py +10 -6
  15. htmlgraph/analytics/pattern_learning.py +771 -0
  16. htmlgraph/analytics/session_graph.py +707 -0
  17. htmlgraph/analytics/strategic/__init__.py +80 -0
  18. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  19. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  20. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  21. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  22. htmlgraph/analytics/work_type.py +67 -27
  23. htmlgraph/analytics_index.py +53 -20
  24. htmlgraph/api/__init__.py +3 -0
  25. htmlgraph/api/cost_alerts_websocket.py +416 -0
  26. htmlgraph/api/main.py +2498 -0
  27. htmlgraph/api/static/htmx.min.js +1 -0
  28. htmlgraph/api/static/style-redesign.css +1344 -0
  29. htmlgraph/api/static/style.css +1079 -0
  30. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  31. htmlgraph/api/templates/dashboard.html +794 -0
  32. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  33. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  34. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  35. htmlgraph/api/templates/partials/agents.html +317 -0
  36. htmlgraph/api/templates/partials/event-traces.html +373 -0
  37. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  38. htmlgraph/api/templates/partials/features.html +578 -0
  39. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  40. htmlgraph/api/templates/partials/metrics.html +346 -0
  41. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  42. htmlgraph/api/templates/partials/orchestration.html +198 -0
  43. htmlgraph/api/templates/partials/spawners.html +375 -0
  44. htmlgraph/api/templates/partials/work-items.html +613 -0
  45. htmlgraph/api/websocket.py +538 -0
  46. htmlgraph/archive/__init__.py +24 -0
  47. htmlgraph/archive/bloom.py +234 -0
  48. htmlgraph/archive/fts.py +297 -0
  49. htmlgraph/archive/manager.py +583 -0
  50. htmlgraph/archive/search.py +244 -0
  51. htmlgraph/atomic_ops.py +560 -0
  52. htmlgraph/attribute_index.py +2 -1
  53. htmlgraph/bounded_paths.py +539 -0
  54. htmlgraph/builders/base.py +57 -2
  55. htmlgraph/builders/bug.py +19 -3
  56. htmlgraph/builders/chore.py +19 -3
  57. htmlgraph/builders/epic.py +19 -3
  58. htmlgraph/builders/feature.py +27 -3
  59. htmlgraph/builders/insight.py +2 -1
  60. htmlgraph/builders/metric.py +2 -1
  61. htmlgraph/builders/pattern.py +2 -1
  62. htmlgraph/builders/phase.py +19 -3
  63. htmlgraph/builders/spike.py +29 -3
  64. htmlgraph/builders/track.py +42 -1
  65. htmlgraph/cigs/__init__.py +81 -0
  66. htmlgraph/cigs/autonomy.py +385 -0
  67. htmlgraph/cigs/cost.py +475 -0
  68. htmlgraph/cigs/messages_basic.py +472 -0
  69. htmlgraph/cigs/messaging.py +365 -0
  70. htmlgraph/cigs/models.py +771 -0
  71. htmlgraph/cigs/pattern_storage.py +427 -0
  72. htmlgraph/cigs/patterns.py +503 -0
  73. htmlgraph/cigs/posttool_analyzer.py +234 -0
  74. htmlgraph/cigs/reporter.py +818 -0
  75. htmlgraph/cigs/tracker.py +317 -0
  76. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  77. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  78. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  79. htmlgraph/cli/__init__.py +42 -0
  80. htmlgraph/cli/__main__.py +6 -0
  81. htmlgraph/cli/analytics.py +1424 -0
  82. htmlgraph/cli/base.py +685 -0
  83. htmlgraph/cli/constants.py +206 -0
  84. htmlgraph/cli/core.py +954 -0
  85. htmlgraph/cli/main.py +147 -0
  86. htmlgraph/cli/models.py +475 -0
  87. htmlgraph/cli/templates/__init__.py +1 -0
  88. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  89. htmlgraph/cli/work/__init__.py +239 -0
  90. htmlgraph/cli/work/browse.py +115 -0
  91. htmlgraph/cli/work/features.py +568 -0
  92. htmlgraph/cli/work/orchestration.py +676 -0
  93. htmlgraph/cli/work/report.py +728 -0
  94. htmlgraph/cli/work/sessions.py +466 -0
  95. htmlgraph/cli/work/snapshot.py +559 -0
  96. htmlgraph/cli/work/tracks.py +486 -0
  97. htmlgraph/cli_commands/__init__.py +1 -0
  98. htmlgraph/cli_commands/feature.py +195 -0
  99. htmlgraph/cli_framework.py +115 -0
  100. htmlgraph/collections/__init__.py +2 -0
  101. htmlgraph/collections/base.py +197 -14
  102. htmlgraph/collections/bug.py +2 -1
  103. htmlgraph/collections/chore.py +2 -1
  104. htmlgraph/collections/epic.py +2 -1
  105. htmlgraph/collections/feature.py +2 -1
  106. htmlgraph/collections/insight.py +2 -1
  107. htmlgraph/collections/metric.py +2 -1
  108. htmlgraph/collections/pattern.py +2 -1
  109. htmlgraph/collections/phase.py +2 -1
  110. htmlgraph/collections/session.py +194 -0
  111. htmlgraph/collections/spike.py +13 -2
  112. htmlgraph/collections/task_delegation.py +241 -0
  113. htmlgraph/collections/todo.py +14 -1
  114. htmlgraph/collections/traces.py +487 -0
  115. htmlgraph/config/cost_models.json +56 -0
  116. htmlgraph/config.py +190 -0
  117. htmlgraph/context_analytics.py +2 -1
  118. htmlgraph/converter.py +116 -7
  119. htmlgraph/cost_analysis/__init__.py +5 -0
  120. htmlgraph/cost_analysis/analyzer.py +438 -0
  121. htmlgraph/dashboard.html +2246 -248
  122. htmlgraph/dashboard.html.backup +6592 -0
  123. htmlgraph/dashboard.html.bak +7181 -0
  124. htmlgraph/dashboard.html.bak2 +7231 -0
  125. htmlgraph/dashboard.html.bak3 +7232 -0
  126. htmlgraph/db/__init__.py +38 -0
  127. htmlgraph/db/queries.py +790 -0
  128. htmlgraph/db/schema.py +1788 -0
  129. htmlgraph/decorators.py +317 -0
  130. htmlgraph/dependency_models.py +2 -1
  131. htmlgraph/deploy.py +26 -27
  132. htmlgraph/docs/API_REFERENCE.md +841 -0
  133. htmlgraph/docs/HTTP_API.md +750 -0
  134. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  135. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  136. htmlgraph/docs/README.md +532 -0
  137. htmlgraph/docs/__init__.py +77 -0
  138. htmlgraph/docs/docs_version.py +55 -0
  139. htmlgraph/docs/metadata.py +93 -0
  140. htmlgraph/docs/migrations.py +232 -0
  141. htmlgraph/docs/template_engine.py +143 -0
  142. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  143. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  144. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  145. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  146. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  147. htmlgraph/docs/version_check.py +163 -0
  148. htmlgraph/edge_index.py +2 -1
  149. htmlgraph/error_handler.py +544 -0
  150. htmlgraph/event_log.py +86 -37
  151. htmlgraph/event_migration.py +2 -1
  152. htmlgraph/file_watcher.py +12 -8
  153. htmlgraph/find_api.py +2 -1
  154. htmlgraph/git_events.py +67 -9
  155. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  156. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  157. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  158. htmlgraph/hooks/__init__.py +8 -0
  159. htmlgraph/hooks/bootstrap.py +169 -0
  160. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  161. htmlgraph/hooks/concurrent_sessions.py +208 -0
  162. htmlgraph/hooks/context.py +350 -0
  163. htmlgraph/hooks/drift_handler.py +525 -0
  164. htmlgraph/hooks/event_tracker.py +790 -99
  165. htmlgraph/hooks/git_commands.py +175 -0
  166. htmlgraph/hooks/installer.py +5 -1
  167. htmlgraph/hooks/orchestrator.py +327 -76
  168. htmlgraph/hooks/orchestrator_reflector.py +31 -4
  169. htmlgraph/hooks/post_tool_use_failure.py +32 -7
  170. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  171. htmlgraph/hooks/posttooluse.py +92 -19
  172. htmlgraph/hooks/pretooluse.py +527 -7
  173. htmlgraph/hooks/prompt_analyzer.py +637 -0
  174. htmlgraph/hooks/session_handler.py +668 -0
  175. htmlgraph/hooks/session_summary.py +395 -0
  176. htmlgraph/hooks/state_manager.py +504 -0
  177. htmlgraph/hooks/subagent_detection.py +202 -0
  178. htmlgraph/hooks/subagent_stop.py +369 -0
  179. htmlgraph/hooks/task_enforcer.py +99 -4
  180. htmlgraph/hooks/validator.py +212 -91
  181. htmlgraph/ids.py +2 -1
  182. htmlgraph/learning.py +125 -100
  183. htmlgraph/mcp_server.py +2 -1
  184. htmlgraph/models.py +217 -18
  185. htmlgraph/operations/README.md +62 -0
  186. htmlgraph/operations/__init__.py +79 -0
  187. htmlgraph/operations/analytics.py +339 -0
  188. htmlgraph/operations/bootstrap.py +289 -0
  189. htmlgraph/operations/events.py +244 -0
  190. htmlgraph/operations/fastapi_server.py +231 -0
  191. htmlgraph/operations/hooks.py +350 -0
  192. htmlgraph/operations/initialization.py +597 -0
  193. htmlgraph/operations/initialization.py.backup +228 -0
  194. htmlgraph/operations/server.py +303 -0
  195. htmlgraph/orchestration/__init__.py +58 -0
  196. htmlgraph/orchestration/claude_launcher.py +179 -0
  197. htmlgraph/orchestration/command_builder.py +72 -0
  198. htmlgraph/orchestration/headless_spawner.py +281 -0
  199. htmlgraph/orchestration/live_events.py +377 -0
  200. htmlgraph/orchestration/model_selection.py +327 -0
  201. htmlgraph/orchestration/plugin_manager.py +140 -0
  202. htmlgraph/orchestration/prompts.py +137 -0
  203. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  204. htmlgraph/orchestration/spawners/__init__.py +16 -0
  205. htmlgraph/orchestration/spawners/base.py +194 -0
  206. htmlgraph/orchestration/spawners/claude.py +173 -0
  207. htmlgraph/orchestration/spawners/codex.py +435 -0
  208. htmlgraph/orchestration/spawners/copilot.py +294 -0
  209. htmlgraph/orchestration/spawners/gemini.py +471 -0
  210. htmlgraph/orchestration/subprocess_runner.py +36 -0
  211. htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
  212. htmlgraph/orchestration.md +563 -0
  213. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  214. htmlgraph/orchestrator.py +2 -1
  215. htmlgraph/orchestrator_config.py +357 -0
  216. htmlgraph/orchestrator_mode.py +115 -4
  217. htmlgraph/parallel.py +2 -1
  218. htmlgraph/parser.py +86 -6
  219. htmlgraph/path_query.py +608 -0
  220. htmlgraph/pattern_matcher.py +636 -0
  221. htmlgraph/pydantic_models.py +476 -0
  222. htmlgraph/quality_gates.py +350 -0
  223. htmlgraph/query_builder.py +2 -1
  224. htmlgraph/query_composer.py +509 -0
  225. htmlgraph/reflection.py +443 -0
  226. htmlgraph/refs.py +344 -0
  227. htmlgraph/repo_hash.py +512 -0
  228. htmlgraph/repositories/__init__.py +292 -0
  229. htmlgraph/repositories/analytics_repository.py +455 -0
  230. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  231. htmlgraph/repositories/feature_repository.py +581 -0
  232. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  233. htmlgraph/repositories/feature_repository_memory.py +607 -0
  234. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  235. htmlgraph/repositories/filter_service.py +620 -0
  236. htmlgraph/repositories/filter_service_standard.py +445 -0
  237. htmlgraph/repositories/shared_cache.py +621 -0
  238. htmlgraph/repositories/shared_cache_memory.py +395 -0
  239. htmlgraph/repositories/track_repository.py +552 -0
  240. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  241. htmlgraph/repositories/track_repository_memory.py +508 -0
  242. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  243. htmlgraph/sdk/__init__.py +398 -0
  244. htmlgraph/sdk/__init__.pyi +14 -0
  245. htmlgraph/sdk/analytics/__init__.py +19 -0
  246. htmlgraph/sdk/analytics/engine.py +155 -0
  247. htmlgraph/sdk/analytics/helpers.py +178 -0
  248. htmlgraph/sdk/analytics/registry.py +109 -0
  249. htmlgraph/sdk/base.py +484 -0
  250. htmlgraph/sdk/constants.py +216 -0
  251. htmlgraph/sdk/core.pyi +308 -0
  252. htmlgraph/sdk/discovery.py +120 -0
  253. htmlgraph/sdk/help/__init__.py +12 -0
  254. htmlgraph/sdk/help/mixin.py +699 -0
  255. htmlgraph/sdk/mixins/__init__.py +15 -0
  256. htmlgraph/sdk/mixins/attribution.py +113 -0
  257. htmlgraph/sdk/mixins/mixin.py +410 -0
  258. htmlgraph/sdk/operations/__init__.py +12 -0
  259. htmlgraph/sdk/operations/mixin.py +427 -0
  260. htmlgraph/sdk/orchestration/__init__.py +17 -0
  261. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  262. htmlgraph/sdk/orchestration/spawner.py +204 -0
  263. htmlgraph/sdk/planning/__init__.py +19 -0
  264. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  265. htmlgraph/sdk/planning/mixin.py +211 -0
  266. htmlgraph/sdk/planning/parallel.py +186 -0
  267. htmlgraph/sdk/planning/queue.py +210 -0
  268. htmlgraph/sdk/planning/recommendations.py +87 -0
  269. htmlgraph/sdk/planning/smart_planning.py +319 -0
  270. htmlgraph/sdk/session/__init__.py +19 -0
  271. htmlgraph/sdk/session/continuity.py +57 -0
  272. htmlgraph/sdk/session/handoff.py +110 -0
  273. htmlgraph/sdk/session/info.py +309 -0
  274. htmlgraph/sdk/session/manager.py +103 -0
  275. htmlgraph/sdk/strategic/__init__.py +26 -0
  276. htmlgraph/sdk/strategic/mixin.py +563 -0
  277. htmlgraph/server.py +295 -107
  278. htmlgraph/session_hooks.py +300 -0
  279. htmlgraph/session_manager.py +285 -3
  280. htmlgraph/session_registry.py +587 -0
  281. htmlgraph/session_state.py +436 -0
  282. htmlgraph/session_warning.py +2 -1
  283. htmlgraph/sessions/__init__.py +23 -0
  284. htmlgraph/sessions/handoff.py +756 -0
  285. htmlgraph/system_prompts.py +450 -0
  286. htmlgraph/templates/orchestration-view.html +350 -0
  287. htmlgraph/track_builder.py +33 -1
  288. htmlgraph/track_manager.py +38 -0
  289. htmlgraph/transcript.py +18 -5
  290. htmlgraph/validation.py +115 -0
  291. htmlgraph/watch.py +2 -1
  292. htmlgraph/work_type_utils.py +2 -1
  293. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
  294. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
  295. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  296. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  297. htmlgraph/cli.py +0 -4839
  298. htmlgraph/sdk.py +0 -2359
  299. htmlgraph-0.20.1.dist-info/RECORD +0 -118
  300. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  301. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  302. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  303. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  304. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
@@ -0,0 +1,617 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ """
8
+ Cross-session analytics using Git commits as the continuity spine.
9
+
10
+ This module provides analytics that track work across multiple sessions
11
+ using Git commit history as the linking mechanism. Unlike session-based
12
+ analytics that only look within a single session, these analytics span
13
+ the entire commit graph to provide comprehensive insights.
14
+
15
+ Key Features:
16
+ - Query work in commit ranges (e.g., show all work between two commits)
17
+ - Track feature implementation across multiple sessions
18
+ - Analyze work by author across the project history
19
+ - Find sessions that contributed to specific commits
20
+ - Build work timelines using commit timestamps
21
+
22
+ Design:
23
+ - Uses Git commit hashes from EventRecord.payload['commit_hash']
24
+ - Leverages event logs (JSONL) as primary data source
25
+ - Falls back to Git commands when needed
26
+ - Works with both active sessions and historical work
27
+
28
+ Example:
29
+ from htmlgraph import SDK
30
+
31
+ sdk = SDK(agent="claude")
32
+ cross = sdk.cross_session_analytics
33
+
34
+ # Get all work between two commits
35
+ work = cross.work_in_commit_range(
36
+ from_commit="abc123",
37
+ to_commit="def456"
38
+ )
39
+
40
+ # Find sessions that contributed to a feature
41
+ sessions = cross.sessions_for_feature("feature-auth")
42
+
43
+ # Analyze work by author
44
+ authors = cross.work_by_author(since_commit="abc123")
45
+ """
46
+
47
+ import subprocess
48
+ from collections import defaultdict
49
+ from dataclasses import dataclass
50
+ from datetime import datetime
51
+ from pathlib import Path
52
+ from typing import TYPE_CHECKING, Any
53
+
54
+ if TYPE_CHECKING:
55
+ from htmlgraph import SDK
56
+
57
+ from htmlgraph.event_log import JsonlEventLog
58
+ from htmlgraph.models import utc_now
59
+
60
+
61
+ @dataclass
62
+ class CommitWorkSummary:
63
+ """Summary of work done in a single commit."""
64
+
65
+ commit_hash: str
66
+ commit_hash_short: str
67
+ branch: str
68
+ author_name: str
69
+ author_email: str
70
+ commit_message: str
71
+ timestamp: datetime
72
+ features: list[str]
73
+ sessions: list[str]
74
+ event_count: int
75
+ files_changed: list[str]
76
+ insertions: int
77
+ deletions: int
78
+
79
+
80
+ @dataclass
81
+ class CommitRangeReport:
82
+ """Report of all work done in a commit range."""
83
+
84
+ from_commit: str
85
+ to_commit: str
86
+ commits: list[CommitWorkSummary]
87
+ total_events: int
88
+ features: list[str]
89
+ sessions: list[str]
90
+ authors: dict[str, int] # author_email -> event_count
91
+ work_types: dict[str, int] # work_type -> event_count
92
+
93
+
94
+ @dataclass
95
+ class FeatureCrossSessionReport:
96
+ """Report of a feature's implementation across multiple sessions."""
97
+
98
+ feature_id: str
99
+ sessions: list[str]
100
+ commits: list[str]
101
+ authors: list[str]
102
+ start_time: datetime | None
103
+ end_time: datetime | None
104
+ duration_hours: float | None
105
+ event_count: int
106
+ work_type_distribution: dict[str, int]
107
+
108
+
109
+ class CrossSessionAnalytics:
110
+ """
111
+ Analytics that track work across sessions using Git commits.
112
+
113
+ This class provides methods to query and analyze work that spans
114
+ multiple sessions, using Git commit history as the continuity spine.
115
+ """
116
+
117
+ def __init__(self, sdk: SDK):
118
+ """
119
+ Initialize CrossSessionAnalytics with SDK instance.
120
+
121
+ Args:
122
+ sdk: Parent SDK instance for accessing events and sessions
123
+ """
124
+ self.sdk = sdk
125
+ self._event_log = JsonlEventLog(sdk._directory / "events")
126
+ self._repo_root = self._find_repo_root(sdk._directory)
127
+
128
+ def work_in_commit_range(
129
+ self,
130
+ from_commit: str | None = None,
131
+ to_commit: str = "HEAD",
132
+ include_uncommitted: bool = False,
133
+ ) -> CommitRangeReport:
134
+ """
135
+ Get all work done in a commit range.
136
+
137
+ This method queries all events associated with commits in the
138
+ specified range and builds a comprehensive report.
139
+
140
+ Args:
141
+ from_commit: Starting commit (None = from beginning)
142
+ to_commit: Ending commit (default: HEAD)
143
+ include_uncommitted: Include events not yet committed
144
+
145
+ Returns:
146
+ CommitRangeReport with all work in the range
147
+
148
+ Example:
149
+ >>> # Get all work in last 10 commits
150
+ >>> report = cross.work_in_commit_range(
151
+ ... from_commit="HEAD~10",
152
+ ... to_commit="HEAD"
153
+ ... )
154
+ >>> logger.info(f"Total events: {report.total_events}")
155
+ >>> logger.info(f"Features: {', '.join(report.features)}")
156
+ """
157
+ # Get commit list from Git
158
+ commits = self._get_commits_in_range(from_commit, to_commit)
159
+
160
+ # Build commit hash set for fast lookup
161
+ commit_hashes = {c["hash"] for c in commits}
162
+
163
+ # Query events for these commits
164
+ commit_summaries: dict[str, CommitWorkSummary] = {}
165
+ features_set = set()
166
+ sessions_set = set()
167
+ authors_count: dict[str, int] = defaultdict(int)
168
+ work_types_count: dict[str, int] = defaultdict(int)
169
+ total_events = 0
170
+
171
+ for _, event in self._event_log.iter_events():
172
+ # Check if event is associated with a commit in our range
173
+ payload = event.get("payload", {})
174
+ commit_hash = payload.get("commit_hash")
175
+
176
+ if not commit_hash or commit_hash not in commit_hashes:
177
+ continue
178
+
179
+ # Extract event details
180
+ feature_id = event.get("feature_id")
181
+ session_id = event.get("session_id")
182
+ work_type = event.get("work_type")
183
+ author_email = payload.get("author_email", "")
184
+
185
+ # Track summary data
186
+ if feature_id:
187
+ features_set.add(feature_id)
188
+ if session_id:
189
+ sessions_set.add(session_id)
190
+ if work_type:
191
+ work_types_count[work_type] += 1
192
+ if author_email:
193
+ authors_count[author_email] += 1
194
+
195
+ total_events += 1
196
+
197
+ # Build commit summary (or update existing)
198
+ if commit_hash not in commit_summaries:
199
+ # Find commit details
200
+ commit_info = next(
201
+ (c for c in commits if c["hash"] == commit_hash), None
202
+ )
203
+ if not commit_info:
204
+ continue
205
+
206
+ commit_summaries[commit_hash] = CommitWorkSummary(
207
+ commit_hash=commit_hash,
208
+ commit_hash_short=commit_hash[:8],
209
+ branch=payload.get("branch", ""),
210
+ author_name=payload.get("author_name", ""),
211
+ author_email=author_email,
212
+ commit_message=payload.get("commit_message", ""),
213
+ timestamp=self._parse_timestamp(event.get("timestamp")),
214
+ features=[],
215
+ sessions=[],
216
+ event_count=0,
217
+ files_changed=payload.get("files_changed", []),
218
+ insertions=payload.get("insertions", 0),
219
+ deletions=payload.get("deletions", 0),
220
+ )
221
+
222
+ # Update commit summary
223
+ summary = commit_summaries[commit_hash]
224
+ if feature_id and feature_id not in summary.features:
225
+ summary.features.append(feature_id)
226
+ if session_id and session_id not in summary.sessions:
227
+ summary.sessions.append(session_id)
228
+ summary.event_count += 1
229
+
230
+ # Handle uncommitted work
231
+ if include_uncommitted:
232
+ # Find events without commit hashes
233
+ for _, event in self._event_log.iter_events():
234
+ payload = event.get("payload", {})
235
+ if payload.get("commit_hash"):
236
+ continue # Already processed
237
+
238
+ # Track uncommitted work
239
+ feature_id = event.get("feature_id")
240
+ session_id = event.get("session_id")
241
+ work_type = event.get("work_type")
242
+
243
+ if feature_id:
244
+ features_set.add(feature_id)
245
+ if session_id:
246
+ sessions_set.add(session_id)
247
+ if work_type:
248
+ work_types_count[work_type] += 1
249
+
250
+ total_events += 1
251
+
252
+ return CommitRangeReport(
253
+ from_commit=from_commit or "beginning",
254
+ to_commit=to_commit,
255
+ commits=sorted(
256
+ commit_summaries.values(), key=lambda c: c.timestamp, reverse=True
257
+ ),
258
+ total_events=total_events,
259
+ features=sorted(features_set),
260
+ sessions=sorted(sessions_set),
261
+ authors=dict(authors_count),
262
+ work_types=dict(work_types_count),
263
+ )
264
+
265
+ def sessions_for_feature(
266
+ self, feature_id: str, include_cross_session: bool = True
267
+ ) -> list[str]:
268
+ """
269
+ Find all sessions that contributed to a feature.
270
+
271
+ Args:
272
+ feature_id: Feature ID to query
273
+ include_cross_session: Include sessions linked via commit graph
274
+
275
+ Returns:
276
+ List of session IDs that worked on this feature
277
+
278
+ Example:
279
+ >>> sessions = cross.sessions_for_feature("feature-auth")
280
+ >>> logger.info(f"Feature worked on in {len(sessions)} sessions")
281
+ """
282
+ sessions = set()
283
+
284
+ # Direct attribution from events
285
+ for _, event in self._event_log.iter_events():
286
+ if event.get("feature_id") == feature_id:
287
+ session_id = event.get("session_id")
288
+ if session_id:
289
+ sessions.add(session_id)
290
+
291
+ # Cross-session via commits (if enabled)
292
+ if include_cross_session:
293
+ # Find commits that mention this feature
294
+ commits_for_feature = set()
295
+ for _, event in self._event_log.iter_events():
296
+ if event.get("feature_id") == feature_id:
297
+ payload = event.get("payload", {})
298
+ commit_hash = payload.get("commit_hash")
299
+ if commit_hash:
300
+ commits_for_feature.add(commit_hash)
301
+
302
+ # Find all sessions that touched these commits
303
+ for _, event in self._event_log.iter_events():
304
+ payload = event.get("payload", {})
305
+ commit_hash = payload.get("commit_hash")
306
+ if commit_hash and commit_hash in commits_for_feature:
307
+ session_id = event.get("session_id")
308
+ if session_id:
309
+ sessions.add(session_id)
310
+
311
+ return sorted(sessions)
312
+
313
+ def feature_cross_session_report(
314
+ self, feature_id: str
315
+ ) -> FeatureCrossSessionReport:
316
+ """
317
+ Generate comprehensive cross-session report for a feature.
318
+
319
+ Args:
320
+ feature_id: Feature ID to analyze
321
+
322
+ Returns:
323
+ FeatureCrossSessionReport with complete implementation history
324
+
325
+ Example:
326
+ >>> report = cross.feature_cross_session_report("feature-auth")
327
+ >>> logger.info(f"Implemented across {len(report.sessions)} sessions")
328
+ >>> logger.info(f"Duration: {report.duration_hours:.1f} hours")
329
+ """
330
+ sessions = set()
331
+ commits = set()
332
+ authors = set()
333
+ work_types: dict[str, int] = defaultdict(int)
334
+ timestamps: list[datetime] = []
335
+ event_count = 0
336
+
337
+ # Scan all events for this feature
338
+ for _, event in self._event_log.iter_events():
339
+ if event.get("feature_id") != feature_id:
340
+ continue
341
+
342
+ event_count += 1
343
+
344
+ # Track metadata
345
+ session_id = event.get("session_id")
346
+ if session_id:
347
+ sessions.add(session_id)
348
+
349
+ payload = event.get("payload", {})
350
+ commit_hash = payload.get("commit_hash")
351
+ if commit_hash:
352
+ commits.add(commit_hash)
353
+
354
+ author_email = payload.get("author_email")
355
+ if author_email:
356
+ authors.add(author_email)
357
+
358
+ work_type = event.get("work_type")
359
+ if work_type:
360
+ work_types[work_type] += 1
361
+
362
+ # Track timing
363
+ timestamp_str = event.get("timestamp")
364
+ if timestamp_str:
365
+ timestamps.append(self._parse_timestamp(timestamp_str))
366
+
367
+ # Calculate duration
368
+ start_time = min(timestamps) if timestamps else None
369
+ end_time = max(timestamps) if timestamps else None
370
+ duration_hours = None
371
+ if start_time and end_time:
372
+ duration_hours = (end_time - start_time).total_seconds() / 3600
373
+
374
+ return FeatureCrossSessionReport(
375
+ feature_id=feature_id,
376
+ sessions=sorted(sessions),
377
+ commits=sorted(commits),
378
+ authors=sorted(authors),
379
+ start_time=start_time,
380
+ end_time=end_time,
381
+ duration_hours=duration_hours,
382
+ event_count=event_count,
383
+ work_type_distribution=dict(work_types),
384
+ )
385
+
386
+ def work_by_author(
387
+ self, since_commit: str | None = None, author_email: str | None = None
388
+ ) -> dict[str, dict[str, Any]]:
389
+ """
390
+ Analyze work by author across the project.
391
+
392
+ Args:
393
+ since_commit: Only analyze work since this commit
394
+ author_email: Filter to specific author (None = all authors)
395
+
396
+ Returns:
397
+ Dictionary mapping author_email to work statistics
398
+
399
+ Example:
400
+ >>> authors = cross.work_by_author(since_commit="v1.0.0")
401
+ >>> for email, stats in authors.items():
402
+ ... logger.info(f"{email}: {stats['event_count']} events")
403
+ """
404
+ authors: dict[str, dict[str, Any]] = defaultdict(
405
+ lambda: {
406
+ "event_count": 0,
407
+ "features": set(),
408
+ "sessions": set(),
409
+ "commits": set(),
410
+ "work_types": defaultdict(int),
411
+ }
412
+ )
413
+
414
+ # Get commit range if specified
415
+ commit_hashes = None
416
+ if since_commit:
417
+ commits = self._get_commits_in_range(since_commit, "HEAD")
418
+ commit_hashes = {c["hash"] for c in commits}
419
+
420
+ # Scan events
421
+ for _, event in self._event_log.iter_events():
422
+ payload = event.get("payload", {})
423
+ event_author = payload.get("author_email")
424
+
425
+ # Skip if filtering by author
426
+ if author_email and event_author != author_email:
427
+ continue
428
+
429
+ # Skip if outside commit range
430
+ if commit_hashes:
431
+ commit_hash = payload.get("commit_hash")
432
+ if not commit_hash or commit_hash not in commit_hashes:
433
+ continue
434
+
435
+ if not event_author:
436
+ continue
437
+
438
+ # Track statistics
439
+ author_stats = authors[event_author]
440
+ author_stats["event_count"] += 1
441
+
442
+ feature_id = event.get("feature_id")
443
+ if feature_id:
444
+ author_stats["features"].add(feature_id)
445
+
446
+ session_id = event.get("session_id")
447
+ if session_id:
448
+ author_stats["sessions"].add(session_id)
449
+
450
+ commit_hash = payload.get("commit_hash")
451
+ if commit_hash:
452
+ author_stats["commits"].add(commit_hash)
453
+
454
+ work_type = event.get("work_type")
455
+ if work_type:
456
+ author_stats["work_types"][work_type] += 1
457
+
458
+ # Convert sets to lists for JSON serialization
459
+ result = {}
460
+ for email, stats in authors.items():
461
+ result[email] = {
462
+ "event_count": stats["event_count"],
463
+ "features": sorted(stats["features"]),
464
+ "sessions": sorted(stats["sessions"]),
465
+ "commits": sorted(stats["commits"]),
466
+ "work_types": dict(stats["work_types"]),
467
+ }
468
+
469
+ return result
470
+
471
+ def commits_for_session(self, session_id: str) -> list[str]:
472
+ """
473
+ Get all commits associated with a session.
474
+
475
+ Args:
476
+ session_id: Session ID to query
477
+
478
+ Returns:
479
+ List of commit hashes (in chronological order)
480
+
481
+ Example:
482
+ >>> commits = cross.commits_for_session("session-abc")
483
+ >>> logger.info(f"Session produced {len(commits)} commits")
484
+ """
485
+ commits = set()
486
+
487
+ for _, event in self._event_log.iter_events():
488
+ if event.get("session_id") != session_id:
489
+ continue
490
+
491
+ payload = event.get("payload", {})
492
+ commit_hash = payload.get("commit_hash")
493
+ if commit_hash:
494
+ commits.add(commit_hash)
495
+
496
+ # Get commit timestamps from Git for chronological ordering
497
+ commit_list = []
498
+ for commit_hash in commits:
499
+ try:
500
+ timestamp = self._get_commit_timestamp(commit_hash)
501
+ commit_list.append((timestamp, commit_hash))
502
+ except Exception:
503
+ commit_list.append((datetime.min, commit_hash))
504
+
505
+ commit_list.sort(key=lambda x: x[0])
506
+ return [commit_hash for _, commit_hash in commit_list]
507
+
508
+ # === Private Helper Methods ===
509
+
510
+ def _find_repo_root(self, start_path: Path) -> Path | None:
511
+ """Find the Git repository root directory."""
512
+ try:
513
+ result = subprocess.run(
514
+ ["git", "rev-parse", "--show-toplevel"],
515
+ cwd=str(start_path),
516
+ capture_output=True,
517
+ text=True,
518
+ check=True,
519
+ )
520
+ return Path(result.stdout.strip())
521
+ except (subprocess.CalledProcessError, FileNotFoundError):
522
+ return None
523
+
524
+ def _get_commits_in_range(
525
+ self, from_commit: str | None, to_commit: str
526
+ ) -> list[dict[str, Any]]:
527
+ """
528
+ Get list of commits in a range using Git.
529
+
530
+ Args:
531
+ from_commit: Starting commit (None = from beginning)
532
+ to_commit: Ending commit
533
+
534
+ Returns:
535
+ List of commit dictionaries with hash, author, date, message
536
+ """
537
+ if not self._repo_root:
538
+ return []
539
+
540
+ try:
541
+ # Build Git log command
542
+ if from_commit:
543
+ rev_range = f"{from_commit}..{to_commit}"
544
+ else:
545
+ rev_range = to_commit
546
+
547
+ # Get commit info in JSON-like format
548
+ result = subprocess.run(
549
+ [
550
+ "git",
551
+ "log",
552
+ rev_range,
553
+ "--pretty=format:%H|%h|%an|%ae|%aI|%s",
554
+ ],
555
+ cwd=str(self._repo_root),
556
+ capture_output=True,
557
+ text=True,
558
+ check=True,
559
+ )
560
+
561
+ commits = []
562
+ for line in result.stdout.strip().split("\n"):
563
+ if not line:
564
+ continue
565
+
566
+ parts = line.split("|")
567
+ if len(parts) < 6:
568
+ continue
569
+
570
+ commits.append(
571
+ {
572
+ "hash": parts[0],
573
+ "hash_short": parts[1],
574
+ "author_name": parts[2],
575
+ "author_email": parts[3],
576
+ "date": parts[4],
577
+ "subject": parts[5],
578
+ }
579
+ )
580
+
581
+ return commits
582
+
583
+ except (subprocess.CalledProcessError, FileNotFoundError):
584
+ return []
585
+
586
+ def _get_commit_timestamp(self, commit_hash: str) -> datetime:
587
+ """Get timestamp for a commit."""
588
+ if not self._repo_root:
589
+ raise ValueError("Not in a Git repository")
590
+
591
+ try:
592
+ result = subprocess.run(
593
+ ["git", "log", "-1", "--format=%aI", commit_hash],
594
+ cwd=str(self._repo_root),
595
+ capture_output=True,
596
+ text=True,
597
+ check=True,
598
+ )
599
+ return datetime.fromisoformat(result.stdout.strip())
600
+ except subprocess.CalledProcessError:
601
+ raise ValueError(f"Commit not found: {commit_hash}")
602
+
603
+ def _parse_timestamp(self, timestamp: str | datetime | None) -> datetime:
604
+ """Parse timestamp from various formats."""
605
+ if timestamp is None:
606
+ return utc_now()
607
+
608
+ if isinstance(timestamp, datetime):
609
+ return timestamp
610
+
611
+ if isinstance(timestamp, str):
612
+ try:
613
+ return datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
614
+ except (ValueError, AttributeError):
615
+ return utc_now()
616
+
617
+ return utc_now()
@@ -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]