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.
- htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +51 -1
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +26 -10
- htmlgraph/agent_registry.py +2 -1
- htmlgraph/analytics/__init__.py +8 -1
- htmlgraph/analytics/cli.py +86 -20
- 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 +10 -6
- 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 +67 -27
- htmlgraph/analytics_index.py +53 -20
- 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 +2 -1
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/base.py +57 -2
- htmlgraph/builders/bug.py +19 -3
- htmlgraph/builders/chore.py +19 -3
- htmlgraph/builders/epic.py +19 -3
- htmlgraph/builders/feature.py +27 -3
- htmlgraph/builders/insight.py +2 -1
- htmlgraph/builders/metric.py +2 -1
- htmlgraph/builders/pattern.py +2 -1
- htmlgraph/builders/phase.py +19 -3
- htmlgraph/builders/spike.py +29 -3
- htmlgraph/builders/track.py +42 -1
- 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 +2 -0
- htmlgraph/collections/base.py +197 -14
- htmlgraph/collections/bug.py +2 -1
- htmlgraph/collections/chore.py +2 -1
- htmlgraph/collections/epic.py +2 -1
- htmlgraph/collections/feature.py +2 -1
- htmlgraph/collections/insight.py +2 -1
- htmlgraph/collections/metric.py +2 -1
- htmlgraph/collections/pattern.py +2 -1
- htmlgraph/collections/phase.py +2 -1
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +13 -2
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +14 -1
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +2 -1
- htmlgraph/converter.py +116 -7
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2246 -248
- 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 +2 -1
- htmlgraph/deploy.py +26 -27
- 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 +2 -1
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +86 -37
- htmlgraph/event_migration.py +2 -1
- htmlgraph/file_watcher.py +12 -8
- htmlgraph/find_api.py +2 -1
- htmlgraph/git_events.py +67 -9
- 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 +8 -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 +790 -99
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/installer.py +5 -1
- htmlgraph/hooks/orchestrator.py +327 -76
- htmlgraph/hooks/orchestrator_reflector.py +31 -4
- htmlgraph/hooks/post_tool_use_failure.py +32 -7
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +92 -19
- htmlgraph/hooks/pretooluse.py +527 -7
- 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 +99 -4
- htmlgraph/hooks/validator.py +212 -91
- htmlgraph/ids.py +2 -1
- htmlgraph/learning.py +125 -100
- htmlgraph/mcp_server.py +2 -1
- htmlgraph/models.py +217 -18
- 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.py → orchestration/task_coordination.py} +16 -8
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +2 -1
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +115 -4
- htmlgraph/parallel.py +2 -1
- htmlgraph/parser.py +86 -6
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +2 -1
- 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/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 +295 -107
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +285 -3
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +2 -1
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +33 -1
- htmlgraph/track_manager.py +38 -0
- htmlgraph/transcript.py +18 -5
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +2 -1
- htmlgraph/work_type_utils.py +2 -1
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -4839
- htmlgraph/sdk.py +0 -2359
- htmlgraph-0.20.1.dist-info/RECORD +0 -118
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]
|