htmlgraph 0.9.3__py3-none-any.whl → 0.27.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +173 -17
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +127 -0
- htmlgraph/agent_registry.py +45 -30
- htmlgraph/agents.py +160 -107
- htmlgraph/analytics/__init__.py +9 -2
- htmlgraph/analytics/cli.py +190 -51
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +192 -100
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +190 -14
- htmlgraph/analytics_index.py +135 -51
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +578 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +208 -0
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/__init__.py +14 -0
- htmlgraph/builders/base.py +118 -29
- htmlgraph/builders/bug.py +150 -0
- htmlgraph/builders/chore.py +119 -0
- htmlgraph/builders/epic.py +150 -0
- htmlgraph/builders/feature.py +31 -6
- htmlgraph/builders/insight.py +195 -0
- htmlgraph/builders/metric.py +217 -0
- htmlgraph/builders/pattern.py +202 -0
- htmlgraph/builders/phase.py +162 -0
- htmlgraph/builders/spike.py +52 -19
- htmlgraph/builders/track.py +148 -72
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +18 -0
- htmlgraph/collections/base.py +415 -98
- htmlgraph/collections/bug.py +53 -0
- htmlgraph/collections/chore.py +53 -0
- htmlgraph/collections/epic.py +53 -0
- htmlgraph/collections/feature.py +12 -26
- htmlgraph/collections/insight.py +100 -0
- htmlgraph/collections/metric.py +92 -0
- htmlgraph/collections/pattern.py +97 -0
- htmlgraph/collections/phase.py +53 -0
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +56 -16
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +511 -0
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +344 -0
- htmlgraph/converter.py +216 -28
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2406 -307
- htmlgraph/dashboard.html.backup +6592 -0
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +19 -2
- htmlgraph/deploy.py +142 -125
- htmlgraph/deployment_models.py +474 -0
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +182 -27
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +100 -52
- htmlgraph/event_migration.py +13 -4
- htmlgraph/exceptions.py +49 -0
- htmlgraph/file_watcher.py +101 -28
- htmlgraph/find_api.py +75 -63
- htmlgraph/git_events.py +145 -63
- htmlgraph/graph.py +1122 -106
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +45 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +1314 -0
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/hooks-config.example.json +12 -0
- htmlgraph/hooks/installer.py +343 -0
- htmlgraph/hooks/orchestrator.py +674 -0
- htmlgraph/hooks/orchestrator_reflector.py +223 -0
- htmlgraph/hooks/post-checkout.sh +28 -0
- htmlgraph/hooks/post-commit.sh +24 -0
- htmlgraph/hooks/post-merge.sh +26 -0
- htmlgraph/hooks/post_tool_use_failure.py +273 -0
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +408 -0
- htmlgraph/hooks/pre-commit.sh +94 -0
- htmlgraph/hooks/pre-push.sh +28 -0
- htmlgraph/hooks/pretooluse.py +819 -0
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +255 -0
- htmlgraph/hooks/task_validator.py +177 -0
- htmlgraph/hooks/validator.py +628 -0
- htmlgraph/ids.py +41 -27
- htmlgraph/index.d.ts +286 -0
- htmlgraph/learning.py +767 -0
- htmlgraph/mcp_server.py +69 -23
- htmlgraph/models.py +1586 -87
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/orchestration/task_coordination.py +343 -0
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +669 -0
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +328 -0
- htmlgraph/orchestrator_validator.py +133 -0
- htmlgraph/parallel.py +646 -0
- htmlgraph/parser.py +160 -35
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/planning.py +147 -52
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +109 -72
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/routing.py +8 -19
- htmlgraph/scripts/deploy.py +1 -2
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +685 -180
- htmlgraph/services/__init__.py +10 -0
- htmlgraph/services/claiming.py +199 -0
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +1392 -175
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +201 -0
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/setup.py +34 -17
- htmlgraph/spike_index.py +143 -0
- htmlgraph/sync_docs.py +12 -15
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph/templates/GEMINI.md.template +87 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +146 -15
- htmlgraph/track_manager.py +69 -21
- htmlgraph/transcript.py +890 -0
- htmlgraph/transcript_analytics.py +699 -0
- htmlgraph/types.py +323 -0
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +8 -5
- htmlgraph/work_type_utils.py +3 -2
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
- htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -2688
- htmlgraph/sdk.py +0 -709
- htmlgraph-0.9.3.dist-info/RECORD +0 -61
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
htmlgraph/analytics/work_type.py
CHANGED
|
@@ -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,16 +31,33 @@ Example:
|
|
|
25
31
|
# Returns: 25.5 (% of work spent on maintenance)
|
|
26
32
|
"""
|
|
27
33
|
|
|
28
|
-
from
|
|
29
|
-
from datetime import datetime
|
|
34
|
+
from datetime import datetime, timezone
|
|
30
35
|
from typing import TYPE_CHECKING
|
|
31
36
|
|
|
32
37
|
if TYPE_CHECKING:
|
|
33
38
|
from htmlgraph import SDK
|
|
34
39
|
|
|
35
|
-
from htmlgraph.models import WorkType, Session
|
|
36
|
-
from htmlgraph.session_manager import SessionManager
|
|
37
40
|
from htmlgraph.converter import html_to_session
|
|
41
|
+
from htmlgraph.models import Session, WorkType, utc_now
|
|
42
|
+
from htmlgraph.session_manager import SessionManager
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def normalize_datetime(dt: datetime | None) -> datetime | None:
|
|
46
|
+
"""
|
|
47
|
+
Normalize datetime to UTC-aware format for safe comparisons.
|
|
48
|
+
|
|
49
|
+
Handles three cases:
|
|
50
|
+
- None: returns None
|
|
51
|
+
- Naive (no timezone): assumes UTC and adds timezone
|
|
52
|
+
- Aware (has timezone): converts to UTC
|
|
53
|
+
"""
|
|
54
|
+
if dt is None:
|
|
55
|
+
return None
|
|
56
|
+
if dt.tzinfo is None:
|
|
57
|
+
# Naive datetime - assume UTC
|
|
58
|
+
return dt.replace(tzinfo=timezone.utc)
|
|
59
|
+
# Already aware - convert to UTC
|
|
60
|
+
return dt.astimezone(timezone.utc)
|
|
38
61
|
|
|
39
62
|
|
|
40
63
|
class Analytics:
|
|
@@ -78,7 +101,7 @@ class Analytics:
|
|
|
78
101
|
Example:
|
|
79
102
|
>>> analytics = sdk.analytics
|
|
80
103
|
>>> dist = analytics.work_type_distribution(session_id="session-123")
|
|
81
|
-
>>>
|
|
104
|
+
>>> logger.info("%s", dist)
|
|
82
105
|
{
|
|
83
106
|
"feature-implementation": 45.2,
|
|
84
107
|
"spike-investigation": 28.3,
|
|
@@ -144,11 +167,11 @@ class Analytics:
|
|
|
144
167
|
|
|
145
168
|
Example:
|
|
146
169
|
>>> ratio = sdk.analytics.spike_to_feature_ratio(session_id="session-123")
|
|
147
|
-
>>>
|
|
170
|
+
>>> logger.info(f"Spike-to-feature ratio: {ratio:.2f}")
|
|
148
171
|
Spike-to-feature ratio: 0.63
|
|
149
172
|
|
|
150
173
|
>>> if ratio > 0.5:
|
|
151
|
-
...
|
|
174
|
+
... logger.info("This was a research-heavy session")
|
|
152
175
|
"""
|
|
153
176
|
events = self._get_events(session_id, start_date, end_date)
|
|
154
177
|
|
|
@@ -202,11 +225,11 @@ class Analytics:
|
|
|
202
225
|
|
|
203
226
|
Example:
|
|
204
227
|
>>> burden = sdk.analytics.maintenance_burden(session_id="session-123")
|
|
205
|
-
>>>
|
|
228
|
+
>>> logger.info(f"Maintenance burden: {burden:.1f}%")
|
|
206
229
|
Maintenance burden: 32.5%
|
|
207
230
|
|
|
208
231
|
>>> if burden > 40:
|
|
209
|
-
...
|
|
232
|
+
... logger.info("⚠️ High maintenance burden - consider addressing technical debt")
|
|
210
233
|
"""
|
|
211
234
|
events = self._get_events(session_id, start_date, end_date)
|
|
212
235
|
|
|
@@ -257,7 +280,7 @@ class Analytics:
|
|
|
257
280
|
>>> spike_sessions = sdk.analytics.get_sessions_by_work_type(
|
|
258
281
|
... "spike-investigation"
|
|
259
282
|
... )
|
|
260
|
-
>>>
|
|
283
|
+
>>> logger.info(f"Found {len(spike_sessions)} exploratory sessions")
|
|
261
284
|
"""
|
|
262
285
|
session_nodes = self.sdk.sessions.all()
|
|
263
286
|
matching_sessions = []
|
|
@@ -269,9 +292,11 @@ class Analytics:
|
|
|
269
292
|
continue
|
|
270
293
|
|
|
271
294
|
# Check date range
|
|
272
|
-
|
|
295
|
+
start_normalized = normalize_datetime(start_date)
|
|
296
|
+
end_normalized = normalize_datetime(end_date)
|
|
297
|
+
if start_normalized and session.started_at < start_normalized:
|
|
273
298
|
continue
|
|
274
|
-
if
|
|
299
|
+
if end_normalized and session.started_at > end_normalized:
|
|
275
300
|
continue
|
|
276
301
|
|
|
277
302
|
# Check primary work type
|
|
@@ -295,7 +320,7 @@ class Analytics:
|
|
|
295
320
|
|
|
296
321
|
Example:
|
|
297
322
|
>>> breakdown = sdk.analytics.calculate_session_work_breakdown("session-123")
|
|
298
|
-
>>>
|
|
323
|
+
>>> logger.info("%s", breakdown)
|
|
299
324
|
{
|
|
300
325
|
"feature-implementation": 45,
|
|
301
326
|
"spike-investigation": 28,
|
|
@@ -323,7 +348,7 @@ class Analytics:
|
|
|
323
348
|
|
|
324
349
|
Example:
|
|
325
350
|
>>> primary = sdk.analytics.calculate_session_primary_work_type("session-123")
|
|
326
|
-
>>>
|
|
351
|
+
>>> logger.info(f"Primary work type: {primary}")
|
|
327
352
|
Primary work type: spike-investigation
|
|
328
353
|
"""
|
|
329
354
|
session = self._get_session(session_id)
|
|
@@ -352,6 +377,157 @@ class Analytics:
|
|
|
352
377
|
except Exception:
|
|
353
378
|
return None
|
|
354
379
|
|
|
380
|
+
def transition_time_metrics(
|
|
381
|
+
self,
|
|
382
|
+
session_id: str | None = None,
|
|
383
|
+
start_date: datetime | None = None,
|
|
384
|
+
end_date: datetime | None = None,
|
|
385
|
+
) -> dict[str, float]:
|
|
386
|
+
"""
|
|
387
|
+
Calculate time spent in transitions vs feature work.
|
|
388
|
+
|
|
389
|
+
Analyzes time spent in transition spikes (session-init, transition,
|
|
390
|
+
conversation-init) versus regular feature implementation.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
session_id: Optional session ID to analyze (analyzes single session)
|
|
394
|
+
start_date: Optional start date for date range query
|
|
395
|
+
end_date: Optional end date for date range query
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Dictionary with transition metrics:
|
|
399
|
+
- transition_minutes: Total time in transition spikes
|
|
400
|
+
- feature_minutes: Total time in regular features
|
|
401
|
+
- total_minutes: Combined time
|
|
402
|
+
- transition_percent: % time spent in transitions (0-100)
|
|
403
|
+
|
|
404
|
+
Example:
|
|
405
|
+
>>> metrics = sdk.analytics.transition_time_metrics(session_id="session-123")
|
|
406
|
+
>>> logger.info(f"Transition time: {metrics['transition_percent']:.1f}%")
|
|
407
|
+
Transition time: 15.3%
|
|
408
|
+
"""
|
|
409
|
+
from pathlib import Path
|
|
410
|
+
|
|
411
|
+
from htmlgraph.converter import NodeConverter
|
|
412
|
+
|
|
413
|
+
transition_minutes = 0.0
|
|
414
|
+
feature_minutes = 0.0
|
|
415
|
+
|
|
416
|
+
# Get all spikes
|
|
417
|
+
spikes_dir = Path(self.sdk._directory) / "spikes"
|
|
418
|
+
if not spikes_dir.exists():
|
|
419
|
+
return {
|
|
420
|
+
"transition_minutes": 0.0,
|
|
421
|
+
"feature_minutes": 0.0,
|
|
422
|
+
"total_minutes": 0.0,
|
|
423
|
+
"transition_percent": 0.0,
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
spike_converter = NodeConverter(spikes_dir)
|
|
427
|
+
all_spikes = spike_converter.load_all()
|
|
428
|
+
|
|
429
|
+
# Filter spikes by session if specified
|
|
430
|
+
if session_id:
|
|
431
|
+
session = self._get_session(session_id)
|
|
432
|
+
if session:
|
|
433
|
+
# Only include spikes linked to this session
|
|
434
|
+
spike_ids = set(session.worked_on)
|
|
435
|
+
all_spikes = [s for s in all_spikes if s.id in spike_ids]
|
|
436
|
+
|
|
437
|
+
# Calculate time for each spike
|
|
438
|
+
for spike in all_spikes:
|
|
439
|
+
# Apply date filters
|
|
440
|
+
start_normalized = normalize_datetime(start_date)
|
|
441
|
+
end_normalized = normalize_datetime(end_date)
|
|
442
|
+
if start_normalized and spike.created < start_normalized:
|
|
443
|
+
continue
|
|
444
|
+
if end_normalized and spike.created > end_normalized:
|
|
445
|
+
continue
|
|
446
|
+
|
|
447
|
+
# Calculate duration (normalize datetimes for safe comparison)
|
|
448
|
+
start_time = normalize_datetime(spike.created)
|
|
449
|
+
if not start_time:
|
|
450
|
+
continue # Skip if spike creation date is missing
|
|
451
|
+
if spike.status == "done" and spike.updated:
|
|
452
|
+
end_time = normalize_datetime(spike.updated)
|
|
453
|
+
else:
|
|
454
|
+
# If still in progress, use last updated time
|
|
455
|
+
end_time = normalize_datetime(
|
|
456
|
+
spike.updated if spike.updated else utc_now()
|
|
457
|
+
)
|
|
458
|
+
if not end_time:
|
|
459
|
+
end_time = start_time # Fallback to start time if end time missing
|
|
460
|
+
|
|
461
|
+
duration = (
|
|
462
|
+
end_time - start_time
|
|
463
|
+
).total_seconds() / 60 # Convert to minutes
|
|
464
|
+
|
|
465
|
+
# Categorize as transition or feature work
|
|
466
|
+
is_transition = spike.type == "spike" and spike.spike_subtype in (
|
|
467
|
+
"session-init",
|
|
468
|
+
"transition",
|
|
469
|
+
"conversation-init",
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
if is_transition:
|
|
473
|
+
transition_minutes += duration
|
|
474
|
+
else:
|
|
475
|
+
feature_minutes += duration
|
|
476
|
+
|
|
477
|
+
# Also get regular features, bugs, etc.
|
|
478
|
+
for collection in ["features", "bugs"]:
|
|
479
|
+
collection_dir = Path(self.sdk._directory) / collection
|
|
480
|
+
if not collection_dir.exists():
|
|
481
|
+
continue
|
|
482
|
+
|
|
483
|
+
converter = NodeConverter(collection_dir)
|
|
484
|
+
nodes = converter.load_all()
|
|
485
|
+
|
|
486
|
+
# Filter by session if specified
|
|
487
|
+
if session_id:
|
|
488
|
+
session = self._get_session(session_id)
|
|
489
|
+
if session:
|
|
490
|
+
node_ids = set(session.worked_on)
|
|
491
|
+
nodes = [n for n in nodes if n.id in node_ids]
|
|
492
|
+
|
|
493
|
+
for node in nodes:
|
|
494
|
+
# Apply date filters
|
|
495
|
+
start_normalized = normalize_datetime(start_date)
|
|
496
|
+
end_normalized = normalize_datetime(end_date)
|
|
497
|
+
if start_normalized and node.created < start_normalized:
|
|
498
|
+
continue
|
|
499
|
+
if end_normalized and node.created > end_normalized:
|
|
500
|
+
continue
|
|
501
|
+
|
|
502
|
+
# Calculate duration (normalize datetimes for safe comparison)
|
|
503
|
+
start_time = normalize_datetime(node.created)
|
|
504
|
+
if not start_time:
|
|
505
|
+
continue # Skip if node creation date is missing
|
|
506
|
+
if node.status == "done" and node.updated:
|
|
507
|
+
end_time = normalize_datetime(node.updated)
|
|
508
|
+
else:
|
|
509
|
+
end_time = normalize_datetime(
|
|
510
|
+
node.updated if node.updated else utc_now()
|
|
511
|
+
)
|
|
512
|
+
if not end_time:
|
|
513
|
+
end_time = start_time # Fallback to start time if end time missing
|
|
514
|
+
|
|
515
|
+
duration = (end_time - start_time).total_seconds() / 60
|
|
516
|
+
feature_minutes += duration
|
|
517
|
+
|
|
518
|
+
# Calculate metrics
|
|
519
|
+
total_minutes = transition_minutes + feature_minutes
|
|
520
|
+
transition_percent = (
|
|
521
|
+
(transition_minutes / total_minutes * 100) if total_minutes > 0 else 0.0
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
"transition_minutes": round(transition_minutes, 2),
|
|
526
|
+
"feature_minutes": round(feature_minutes, 2),
|
|
527
|
+
"total_minutes": round(total_minutes, 2),
|
|
528
|
+
"transition_percent": round(transition_percent, 2),
|
|
529
|
+
}
|
|
530
|
+
|
|
355
531
|
def _get_events(
|
|
356
532
|
self,
|
|
357
533
|
session_id: str | None = None,
|
htmlgraph/analytics_index.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
"""
|
|
2
4
|
SQLite analytics index for HtmlGraph event logs.
|
|
3
5
|
|
|
@@ -5,17 +7,15 @@ 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
|
|
13
|
+
from collections.abc import Iterable
|
|
12
14
|
from dataclasses import dataclass
|
|
13
|
-
from datetime import datetime
|
|
14
15
|
from pathlib import Path
|
|
15
|
-
from typing import Any
|
|
16
|
-
|
|
16
|
+
from typing import Any
|
|
17
17
|
|
|
18
|
-
SCHEMA_VERSION =
|
|
18
|
+
SCHEMA_VERSION = 4 # Bumped: renamed 'agent' column to 'agent_assigned'
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
@dataclass(frozen=True)
|
|
@@ -83,12 +83,14 @@ class AnalyticsIndex:
|
|
|
83
83
|
"""
|
|
84
84
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
85
85
|
session_id TEXT PRIMARY KEY,
|
|
86
|
-
|
|
86
|
+
agent_assigned TEXT,
|
|
87
87
|
start_commit TEXT,
|
|
88
88
|
continued_from TEXT,
|
|
89
89
|
status TEXT,
|
|
90
90
|
started_at TEXT,
|
|
91
|
-
ended_at TEXT
|
|
91
|
+
ended_at TEXT,
|
|
92
|
+
parent_session_id TEXT,
|
|
93
|
+
parent_event_id TEXT
|
|
92
94
|
)
|
|
93
95
|
"""
|
|
94
96
|
)
|
|
@@ -104,6 +106,9 @@ class AnalyticsIndex:
|
|
|
104
106
|
feature_id TEXT,
|
|
105
107
|
drift_score REAL,
|
|
106
108
|
payload_json TEXT,
|
|
109
|
+
parent_event_id TEXT,
|
|
110
|
+
cost_tokens INTEGER,
|
|
111
|
+
execution_duration_seconds REAL,
|
|
107
112
|
FOREIGN KEY(session_id) REFERENCES sessions(session_id)
|
|
108
113
|
)
|
|
109
114
|
"""
|
|
@@ -158,6 +163,9 @@ class AnalyticsIndex:
|
|
|
158
163
|
)
|
|
159
164
|
|
|
160
165
|
# Indexes for typical dashboard queries
|
|
166
|
+
conn.execute(
|
|
167
|
+
"CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id)"
|
|
168
|
+
)
|
|
161
169
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts)")
|
|
162
170
|
conn.execute(
|
|
163
171
|
"CREATE INDEX IF NOT EXISTS idx_events_session_ts ON events(session_id, ts)"
|
|
@@ -165,7 +173,9 @@ class AnalyticsIndex:
|
|
|
165
173
|
conn.execute(
|
|
166
174
|
"CREATE INDEX IF NOT EXISTS idx_events_feature_ts ON events(feature_id, ts)"
|
|
167
175
|
)
|
|
168
|
-
conn.execute(
|
|
176
|
+
conn.execute(
|
|
177
|
+
"CREATE INDEX IF NOT EXISTS idx_events_tool_ts ON events(tool, ts)"
|
|
178
|
+
)
|
|
169
179
|
conn.execute(
|
|
170
180
|
"CREATE INDEX IF NOT EXISTS idx_events_success_ts ON events(success, ts)"
|
|
171
181
|
)
|
|
@@ -175,8 +185,12 @@ class AnalyticsIndex:
|
|
|
175
185
|
conn.execute(
|
|
176
186
|
"CREATE UNIQUE INDEX IF NOT EXISTS idx_event_files_event_path ON event_files(event_id, path)"
|
|
177
187
|
)
|
|
178
|
-
conn.execute(
|
|
179
|
-
|
|
188
|
+
conn.execute(
|
|
189
|
+
"CREATE INDEX IF NOT EXISTS idx_git_commits_ts ON git_commits(ts)"
|
|
190
|
+
)
|
|
191
|
+
conn.execute(
|
|
192
|
+
"CREATE INDEX IF NOT EXISTS idx_git_commit_features_feature ON git_commit_features(feature_id)"
|
|
193
|
+
)
|
|
180
194
|
|
|
181
195
|
def upsert_session(self, session: dict[str, Any]) -> None:
|
|
182
196
|
"""
|
|
@@ -185,15 +199,17 @@ class AnalyticsIndex:
|
|
|
185
199
|
with self.connect() as conn:
|
|
186
200
|
conn.execute(
|
|
187
201
|
"""
|
|
188
|
-
INSERT INTO sessions(session_id,
|
|
189
|
-
VALUES(
|
|
202
|
+
INSERT INTO sessions(session_id, agent_assigned, start_commit, continued_from, status, started_at, ended_at, parent_session_id, parent_event_id)
|
|
203
|
+
VALUES(?,?,?,?,?,?,?,?,?)
|
|
190
204
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
191
|
-
|
|
205
|
+
agent_assigned=excluded.agent_assigned,
|
|
192
206
|
start_commit=excluded.start_commit,
|
|
193
207
|
continued_from=excluded.continued_from,
|
|
194
208
|
status=excluded.status,
|
|
195
209
|
started_at=excluded.started_at,
|
|
196
|
-
ended_at=excluded.ended_at
|
|
210
|
+
ended_at=excluded.ended_at,
|
|
211
|
+
parent_session_id=excluded.parent_session_id,
|
|
212
|
+
parent_event_id=excluded.parent_event_id
|
|
197
213
|
""",
|
|
198
214
|
(
|
|
199
215
|
session.get("session_id"),
|
|
@@ -203,6 +219,8 @@ class AnalyticsIndex:
|
|
|
203
219
|
session.get("status"),
|
|
204
220
|
session.get("started_at"),
|
|
205
221
|
session.get("ended_at"),
|
|
222
|
+
session.get("parent_session_id"),
|
|
223
|
+
session.get("parent_event_id"),
|
|
206
224
|
),
|
|
207
225
|
)
|
|
208
226
|
|
|
@@ -221,7 +239,9 @@ class AnalyticsIndex:
|
|
|
221
239
|
|
|
222
240
|
payload = event.get("payload")
|
|
223
241
|
payload_json = (
|
|
224
|
-
json.dumps(payload, ensure_ascii=False, default=str)
|
|
242
|
+
json.dumps(payload, ensure_ascii=False, default=str)
|
|
243
|
+
if payload is not None
|
|
244
|
+
else None
|
|
225
245
|
)
|
|
226
246
|
|
|
227
247
|
file_paths = event.get("file_paths") or []
|
|
@@ -231,8 +251,8 @@ class AnalyticsIndex:
|
|
|
231
251
|
with self.connect() as conn:
|
|
232
252
|
conn.execute(
|
|
233
253
|
"""
|
|
234
|
-
INSERT OR IGNORE INTO events(event_id, session_id, ts, tool, summary, success, feature_id, drift_score, payload_json)
|
|
235
|
-
VALUES(
|
|
254
|
+
INSERT OR IGNORE INTO events(event_id, session_id, ts, tool, summary, success, feature_id, drift_score, payload_json, parent_event_id, cost_tokens, execution_duration_seconds)
|
|
255
|
+
VALUES(?,?,?,?,?,?,?,?,?,?,?,?)
|
|
236
256
|
""",
|
|
237
257
|
(
|
|
238
258
|
event_id,
|
|
@@ -244,6 +264,9 @@ class AnalyticsIndex:
|
|
|
244
264
|
event.get("feature_id"),
|
|
245
265
|
event.get("drift_score"),
|
|
246
266
|
payload_json,
|
|
267
|
+
event.get("parent_event_id"),
|
|
268
|
+
event.get("cost_tokens"),
|
|
269
|
+
event.get("execution_duration_seconds"),
|
|
247
270
|
),
|
|
248
271
|
)
|
|
249
272
|
# Insert file path rows, idempotent by (event_id, path)
|
|
@@ -291,20 +314,34 @@ class AnalyticsIndex:
|
|
|
291
314
|
if not ts:
|
|
292
315
|
return None
|
|
293
316
|
|
|
294
|
-
features =
|
|
317
|
+
features = (
|
|
318
|
+
event.get("features")
|
|
319
|
+
if isinstance(event.get("features"), list)
|
|
320
|
+
else []
|
|
321
|
+
)
|
|
295
322
|
feature_id = features[0] if features else None
|
|
296
323
|
|
|
297
324
|
# Best-effort deterministic IDs for GitCommit (by hash), otherwise timestamp-based.
|
|
298
325
|
if legacy_type == "GitCommit" and event.get("commit_hash"):
|
|
299
326
|
base = f"git-commit-{event.get('commit_hash')}"
|
|
300
|
-
event_id =
|
|
301
|
-
|
|
302
|
-
|
|
327
|
+
event_id = (
|
|
328
|
+
base if feature_id is None else f"{base}-{feature_id}"
|
|
329
|
+
)
|
|
330
|
+
msg = (
|
|
331
|
+
(event.get("commit_message") or "").strip().splitlines()[0]
|
|
332
|
+
if event.get("commit_message")
|
|
333
|
+
else ""
|
|
334
|
+
)
|
|
335
|
+
summary = f"Commit {event.get('commit_hash_short', '')}: {msg}".strip()
|
|
303
336
|
else:
|
|
304
337
|
event_id = f"legacy-{legacy_type.lower()}-{ts}"
|
|
305
338
|
summary = legacy_type
|
|
306
339
|
|
|
307
|
-
file_paths =
|
|
340
|
+
file_paths = (
|
|
341
|
+
event.get("files_changed")
|
|
342
|
+
if isinstance(event.get("files_changed"), list)
|
|
343
|
+
else []
|
|
344
|
+
)
|
|
308
345
|
|
|
309
346
|
return {
|
|
310
347
|
"event_id": event_id,
|
|
@@ -336,15 +373,20 @@ class AnalyticsIndex:
|
|
|
336
373
|
continue
|
|
337
374
|
|
|
338
375
|
# Track session metadata from events (best-effort)
|
|
339
|
-
meta = session_meta.setdefault(
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
376
|
+
meta = session_meta.setdefault(
|
|
377
|
+
session_id,
|
|
378
|
+
{
|
|
379
|
+
"session_id": session_id,
|
|
380
|
+
"agent": event.get("agent"),
|
|
381
|
+
"start_commit": event.get("start_commit"),
|
|
382
|
+
"continued_from": event.get("continued_from"),
|
|
383
|
+
"status": event.get("session_status"),
|
|
384
|
+
"started_at": None,
|
|
385
|
+
"ended_at": None,
|
|
386
|
+
"parent_session_id": event.get("parent_session_id"),
|
|
387
|
+
"parent_event_id": event.get("parent_event_id"),
|
|
388
|
+
},
|
|
389
|
+
)
|
|
348
390
|
if meta.get("agent") is None and event.get("agent"):
|
|
349
391
|
meta["agent"] = event.get("agent")
|
|
350
392
|
if meta.get("start_commit") is None and event.get("start_commit"):
|
|
@@ -353,6 +395,12 @@ class AnalyticsIndex:
|
|
|
353
395
|
meta["continued_from"] = event.get("continued_from")
|
|
354
396
|
if meta.get("status") is None and event.get("session_status"):
|
|
355
397
|
meta["status"] = event.get("session_status")
|
|
398
|
+
if meta.get("parent_session_id") is None and event.get(
|
|
399
|
+
"parent_session_id"
|
|
400
|
+
):
|
|
401
|
+
meta["parent_session_id"] = event.get("parent_session_id")
|
|
402
|
+
if meta.get("parent_event_id") is None and event.get("parent_event_id"):
|
|
403
|
+
meta["parent_event_id"] = event.get("parent_event_id")
|
|
356
404
|
|
|
357
405
|
# Track time range (treat earliest event as started_at, latest as ended_at if session is ended)
|
|
358
406
|
if meta["started_at"] is None or ts < meta["started_at"]:
|
|
@@ -362,13 +410,15 @@ class AnalyticsIndex:
|
|
|
362
410
|
|
|
363
411
|
payload = event.get("payload")
|
|
364
412
|
payload_json = (
|
|
365
|
-
json.dumps(payload, ensure_ascii=False, default=str)
|
|
413
|
+
json.dumps(payload, ensure_ascii=False, default=str)
|
|
414
|
+
if payload is not None
|
|
415
|
+
else None
|
|
366
416
|
)
|
|
367
417
|
|
|
368
418
|
conn.execute(
|
|
369
419
|
"""
|
|
370
|
-
INSERT OR IGNORE INTO events(event_id, session_id, ts, tool, summary, success, feature_id, drift_score, payload_json)
|
|
371
|
-
VALUES(
|
|
420
|
+
INSERT OR IGNORE INTO events(event_id, session_id, ts, tool, summary, success, feature_id, drift_score, payload_json, parent_event_id, cost_tokens, execution_duration_seconds)
|
|
421
|
+
VALUES(?,?,?,?,?,?,?,?,?,?,?,?)
|
|
372
422
|
""",
|
|
373
423
|
(
|
|
374
424
|
event_id,
|
|
@@ -380,6 +430,9 @@ class AnalyticsIndex:
|
|
|
380
430
|
event.get("feature_id"),
|
|
381
431
|
event.get("drift_score"),
|
|
382
432
|
payload_json,
|
|
433
|
+
event.get("parent_event_id"),
|
|
434
|
+
event.get("cost_tokens"),
|
|
435
|
+
event.get("execution_duration_seconds"),
|
|
383
436
|
),
|
|
384
437
|
)
|
|
385
438
|
|
|
@@ -457,17 +510,19 @@ class AnalyticsIndex:
|
|
|
457
510
|
for meta in session_meta.values():
|
|
458
511
|
conn.execute(
|
|
459
512
|
"""
|
|
460
|
-
INSERT INTO sessions(session_id,
|
|
461
|
-
VALUES(
|
|
513
|
+
INSERT INTO sessions(session_id, agent_assigned, start_commit, continued_from, status, started_at, ended_at, parent_session_id, parent_event_id)
|
|
514
|
+
VALUES(?,?,?,?,?,?,?,?,?)
|
|
462
515
|
""",
|
|
463
516
|
(
|
|
464
517
|
meta.get("session_id"),
|
|
465
|
-
meta.get("agent"),
|
|
518
|
+
meta.get("agent"), # Source data still uses 'agent' key
|
|
466
519
|
meta.get("start_commit"),
|
|
467
520
|
meta.get("continued_from"),
|
|
468
521
|
meta.get("status"),
|
|
469
522
|
meta.get("started_at"),
|
|
470
523
|
meta.get("ended_at"),
|
|
524
|
+
meta.get("parent_session_id"),
|
|
525
|
+
meta.get("parent_event_id"),
|
|
471
526
|
),
|
|
472
527
|
)
|
|
473
528
|
|
|
@@ -477,7 +532,9 @@ class AnalyticsIndex:
|
|
|
477
532
|
# Git continuity queries
|
|
478
533
|
# ---------------------------------------------------------------------
|
|
479
534
|
|
|
480
|
-
def feature_commits(
|
|
535
|
+
def feature_commits(
|
|
536
|
+
self, feature_id: str, limit: int = 200
|
|
537
|
+
) -> list[dict[str, Any]]:
|
|
481
538
|
"""
|
|
482
539
|
Return commit timeline for a feature based on GitCommit events.
|
|
483
540
|
"""
|
|
@@ -541,9 +598,28 @@ class AnalyticsIndex:
|
|
|
541
598
|
external.add(parent)
|
|
542
599
|
edges.append({"from": parent, "to": r["commit_hash"]})
|
|
543
600
|
|
|
544
|
-
nodes = [
|
|
601
|
+
nodes = [
|
|
602
|
+
{
|
|
603
|
+
"id": c["commit_hash"],
|
|
604
|
+
**{
|
|
605
|
+
k: c.get(k)
|
|
606
|
+
for k in (
|
|
607
|
+
"commit_hash_short",
|
|
608
|
+
"ts",
|
|
609
|
+
"branch",
|
|
610
|
+
"subject",
|
|
611
|
+
"is_merge",
|
|
612
|
+
"insertions",
|
|
613
|
+
"deletions",
|
|
614
|
+
)
|
|
615
|
+
},
|
|
616
|
+
}
|
|
617
|
+
for c in commits
|
|
618
|
+
]
|
|
545
619
|
for parent in sorted(external):
|
|
546
|
-
nodes.append(
|
|
620
|
+
nodes.append(
|
|
621
|
+
{"id": parent, "commit_hash_short": parent[:7], "external": True}
|
|
622
|
+
)
|
|
547
623
|
|
|
548
624
|
return {"nodes": nodes, "edges": edges}
|
|
549
625
|
|
|
@@ -551,7 +627,9 @@ class AnalyticsIndex:
|
|
|
551
627
|
# Query helpers for API
|
|
552
628
|
# ---------------------------------------------------------------------
|
|
553
629
|
|
|
554
|
-
def overview(
|
|
630
|
+
def overview(
|
|
631
|
+
self, since: str | None = None, until: str | None = None
|
|
632
|
+
) -> dict[str, Any]:
|
|
555
633
|
"""
|
|
556
634
|
Return overview stats.
|
|
557
635
|
since/until should be ISO8601 timestamps.
|
|
@@ -586,14 +664,14 @@ class AnalyticsIndex:
|
|
|
586
664
|
return {
|
|
587
665
|
"events": int(row["events"] or 0),
|
|
588
666
|
"failures": int(row["failures"] or 0),
|
|
589
|
-
"failure_rate": (
|
|
590
|
-
float(row["failures"] or 0) / float(row["events"] or 1)
|
|
591
|
-
),
|
|
667
|
+
"failure_rate": (float(row["failures"] or 0) / float(row["events"] or 1)),
|
|
592
668
|
"avg_drift": row["avg_drift"],
|
|
593
669
|
"top_tools": [dict(r) for r in by_tool],
|
|
594
670
|
}
|
|
595
671
|
|
|
596
|
-
def top_features(
|
|
672
|
+
def top_features(
|
|
673
|
+
self, since: str | None = None, until: str | None = None, limit: int = 50
|
|
674
|
+
) -> list[dict[str, Any]]:
|
|
597
675
|
self.ensure_schema()
|
|
598
676
|
clauses = []
|
|
599
677
|
params: list[Any] = []
|
|
@@ -627,13 +705,17 @@ class AnalyticsIndex:
|
|
|
627
705
|
with self.connect() as conn:
|
|
628
706
|
rows = conn.execute(
|
|
629
707
|
"""
|
|
630
|
-
SELECT event_id, session_id, ts, tool, summary, success, feature_id, drift_score
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
708
|
+
SELECT e.event_id, e.session_id, e.ts, e.tool, e.summary, e.success, e.feature_id, e.drift_score,
|
|
709
|
+
COALESCE(e.parent_event_id, s.parent_event_id) as parent_event_id,
|
|
710
|
+
e.cost_tokens, e.execution_duration_seconds
|
|
711
|
+
FROM events e
|
|
712
|
+
JOIN sessions s ON e.session_id = s.session_id
|
|
713
|
+
WHERE e.session_id = ?
|
|
714
|
+
OR s.parent_session_id = ?
|
|
715
|
+
ORDER BY e.ts DESC
|
|
634
716
|
LIMIT ?
|
|
635
717
|
""",
|
|
636
|
-
(session_id, int(limit)),
|
|
718
|
+
(session_id, session_id, int(limit)),
|
|
637
719
|
).fetchall()
|
|
638
720
|
return [dict(r) for r in rows]
|
|
639
721
|
|
|
@@ -725,7 +807,9 @@ class AnalyticsIndex:
|
|
|
725
807
|
return [dict(r) for r in rows]
|
|
726
808
|
|
|
727
809
|
|
|
728
|
-
def _time_where_clause(
|
|
810
|
+
def _time_where_clause(
|
|
811
|
+
column: str, since: str | None, until: str | None
|
|
812
|
+
) -> tuple[str, tuple[Any, ...]]:
|
|
729
813
|
clauses = []
|
|
730
814
|
params: list[Any] = []
|
|
731
815
|
if since:
|