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/converter.py
CHANGED
|
@@ -8,22 +8,34 @@ Provides:
|
|
|
8
8
|
- Handles edge cases (missing fields, malformed HTML)
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
import logging
|
|
11
12
|
from pathlib import Path
|
|
12
|
-
from typing import Any
|
|
13
|
-
|
|
14
|
-
from htmlgraph.models import
|
|
13
|
+
from typing import Any, cast
|
|
14
|
+
|
|
15
|
+
from htmlgraph.models import (
|
|
16
|
+
ActivityEntry,
|
|
17
|
+
Chore,
|
|
18
|
+
Edge,
|
|
19
|
+
Node,
|
|
20
|
+
Pattern,
|
|
21
|
+
Session,
|
|
22
|
+
Spike,
|
|
23
|
+
Step,
|
|
24
|
+
)
|
|
15
25
|
from htmlgraph.parser import HtmlParser
|
|
16
26
|
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
17
29
|
|
|
18
30
|
def html_to_node(filepath: Path | str) -> Node:
|
|
19
31
|
"""
|
|
20
|
-
Parse HTML file into a Node model.
|
|
32
|
+
Parse HTML file into a Node model (or subclass).
|
|
21
33
|
|
|
22
34
|
Args:
|
|
23
35
|
filepath: Path to HTML file
|
|
24
36
|
|
|
25
37
|
Returns:
|
|
26
|
-
Node instance populated from HTML
|
|
38
|
+
Node instance (or Spike/Chore subclass) populated from HTML
|
|
27
39
|
|
|
28
40
|
Raises:
|
|
29
41
|
FileNotFoundError: If file doesn't exist
|
|
@@ -66,14 +78,24 @@ def html_to_node(filepath: Path | str) -> Node:
|
|
|
66
78
|
]
|
|
67
79
|
data["steps"] = steps
|
|
68
80
|
|
|
69
|
-
|
|
81
|
+
# Map node type to model class
|
|
82
|
+
node_type = data.get("type", "node")
|
|
83
|
+
model_classes: dict[str, type[Node]] = {
|
|
84
|
+
"spike": Spike,
|
|
85
|
+
"chore": Chore,
|
|
86
|
+
"pattern": Pattern,
|
|
87
|
+
"node": Node,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
model_class = model_classes.get(node_type, Node)
|
|
91
|
+
return cast(Node, model_class(**data))
|
|
70
92
|
|
|
71
93
|
|
|
72
94
|
def node_to_html(
|
|
73
95
|
node: Node,
|
|
74
96
|
filepath: Path | str,
|
|
75
97
|
stylesheet_path: str = "../styles.css",
|
|
76
|
-
create_dirs: bool = True
|
|
98
|
+
create_dirs: bool = True,
|
|
77
99
|
) -> Path:
|
|
78
100
|
"""
|
|
79
101
|
Write a Node model to an HTML file.
|
|
@@ -101,7 +123,7 @@ def node_to_html(
|
|
|
101
123
|
def update_node_html(
|
|
102
124
|
filepath: Path | str,
|
|
103
125
|
updates: dict[str, Any],
|
|
104
|
-
stylesheet_path: str = "../styles.css"
|
|
126
|
+
stylesheet_path: str = "../styles.css",
|
|
105
127
|
) -> Node:
|
|
106
128
|
"""
|
|
107
129
|
Update specific fields in an existing HTML node file.
|
|
@@ -195,7 +217,8 @@ def node_to_dict(node: Node) -> dict[str, Any]:
|
|
|
195
217
|
if step.get("timestamp"):
|
|
196
218
|
step["timestamp"] = step["timestamp"].isoformat()
|
|
197
219
|
|
|
198
|
-
|
|
220
|
+
result: dict[str, Any] = data
|
|
221
|
+
return result
|
|
199
222
|
|
|
200
223
|
|
|
201
224
|
def dict_to_node(data: dict[str, Any]) -> Node:
|
|
@@ -215,12 +238,16 @@ def dict_to_node(data: dict[str, Any]) -> Node:
|
|
|
215
238
|
for edges in data.get("edges", {}).values():
|
|
216
239
|
for edge in edges:
|
|
217
240
|
if isinstance(edge.get("since"), str):
|
|
218
|
-
edge["since"] = datetime.fromisoformat(
|
|
241
|
+
edge["since"] = datetime.fromisoformat(
|
|
242
|
+
edge["since"].replace("Z", "+00:00")
|
|
243
|
+
)
|
|
219
244
|
|
|
220
245
|
# Parse step datetimes
|
|
221
246
|
for step in data.get("steps", []):
|
|
222
247
|
if isinstance(step.get("timestamp"), str):
|
|
223
|
-
step["timestamp"] = datetime.fromisoformat(
|
|
248
|
+
step["timestamp"] = datetime.fromisoformat(
|
|
249
|
+
step["timestamp"].replace("Z", "+00:00")
|
|
250
|
+
)
|
|
224
251
|
|
|
225
252
|
return Node.from_dict(data)
|
|
226
253
|
|
|
@@ -299,6 +326,7 @@ class NodeConverter:
|
|
|
299
326
|
# Session Converters
|
|
300
327
|
# =============================================================================
|
|
301
328
|
|
|
329
|
+
|
|
302
330
|
def session_to_dict(session: Session) -> dict[str, Any]:
|
|
303
331
|
"""
|
|
304
332
|
Convert Session to a plain dictionary (JSON-serializable).
|
|
@@ -317,7 +345,8 @@ def session_to_dict(session: Session) -> dict[str, Any]:
|
|
|
317
345
|
if entry.get("timestamp"):
|
|
318
346
|
entry["timestamp"] = entry["timestamp"].isoformat()
|
|
319
347
|
|
|
320
|
-
|
|
348
|
+
result: dict[str, Any] = data
|
|
349
|
+
return result
|
|
321
350
|
|
|
322
351
|
|
|
323
352
|
def dict_to_session(data: dict[str, Any]) -> Session:
|
|
@@ -336,7 +365,9 @@ def dict_to_session(data: dict[str, Any]) -> Session:
|
|
|
336
365
|
# Parse activity log timestamps
|
|
337
366
|
for entry in data.get("activity_log", []):
|
|
338
367
|
if isinstance(entry.get("timestamp"), str):
|
|
339
|
-
entry["timestamp"] = datetime.fromisoformat(
|
|
368
|
+
entry["timestamp"] = datetime.fromisoformat(
|
|
369
|
+
entry["timestamp"].replace("Z", "+00:00")
|
|
370
|
+
)
|
|
340
371
|
|
|
341
372
|
return Session.from_dict(data)
|
|
342
373
|
|
|
@@ -364,7 +395,8 @@ def html_to_session(filepath: Path | str) -> Session:
|
|
|
364
395
|
parser = HtmlParser.from_file(filepath)
|
|
365
396
|
|
|
366
397
|
# Get article element with session data
|
|
367
|
-
|
|
398
|
+
article_results = parser.query("article[data-type='session']")
|
|
399
|
+
article = article_results[0] if article_results else None
|
|
368
400
|
if not article:
|
|
369
401
|
raise ValueError(f"No session article found in: {filepath}")
|
|
370
402
|
|
|
@@ -392,7 +424,9 @@ def html_to_session(filepath: Path | str) -> Session:
|
|
|
392
424
|
|
|
393
425
|
last_activity = article.attrs.get("data-last-activity")
|
|
394
426
|
if last_activity:
|
|
395
|
-
data["last_activity"] = datetime.fromisoformat(
|
|
427
|
+
data["last_activity"] = datetime.fromisoformat(
|
|
428
|
+
last_activity.replace("Z", "+00:00")
|
|
429
|
+
)
|
|
396
430
|
|
|
397
431
|
start_commit = article.attrs.get("data-start-commit")
|
|
398
432
|
if start_commit:
|
|
@@ -406,19 +440,42 @@ def html_to_session(filepath: Path | str) -> Session:
|
|
|
406
440
|
work_breakdown_json = article.attrs.get("data-work-breakdown")
|
|
407
441
|
if work_breakdown_json:
|
|
408
442
|
import json
|
|
443
|
+
|
|
409
444
|
try:
|
|
410
445
|
data["work_breakdown"] = json.loads(work_breakdown_json)
|
|
411
446
|
except (json.JSONDecodeError, ValueError):
|
|
412
447
|
pass # Skip if invalid JSON
|
|
413
448
|
|
|
449
|
+
# Parse transcript integration fields
|
|
450
|
+
transcript_id = article.attrs.get("data-transcript-id")
|
|
451
|
+
if transcript_id:
|
|
452
|
+
data["transcript_id"] = transcript_id
|
|
453
|
+
|
|
454
|
+
transcript_path = article.attrs.get("data-transcript-path")
|
|
455
|
+
if transcript_path:
|
|
456
|
+
data["transcript_path"] = transcript_path
|
|
457
|
+
|
|
458
|
+
transcript_synced = article.attrs.get("data-transcript-synced")
|
|
459
|
+
if transcript_synced:
|
|
460
|
+
data["transcript_synced_at"] = datetime.fromisoformat(
|
|
461
|
+
transcript_synced.replace("Z", "+00:00")
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
transcript_branch = article.attrs.get("data-transcript-branch")
|
|
465
|
+
if transcript_branch:
|
|
466
|
+
data["transcript_git_branch"] = transcript_branch
|
|
467
|
+
|
|
414
468
|
# Parse title
|
|
415
|
-
|
|
469
|
+
title_el_results = parser.query("h1")
|
|
470
|
+
title_el = title_el_results[0] if title_el_results else None
|
|
416
471
|
if title_el:
|
|
417
472
|
data["title"] = title_el.to_text().strip()
|
|
418
473
|
|
|
419
474
|
# Parse worked_on edges
|
|
420
475
|
worked_on = []
|
|
421
|
-
for link in parser.query(
|
|
476
|
+
for link in parser.query(
|
|
477
|
+
"nav[data-graph-edges] section[data-edge-type='worked-on'] a"
|
|
478
|
+
):
|
|
422
479
|
href = link.attrs.get("href") or ""
|
|
423
480
|
# Extract feature ID from href
|
|
424
481
|
feature_id = href.replace("../features/", "").replace(".html", "")
|
|
@@ -427,22 +484,28 @@ def html_to_session(filepath: Path | str) -> Session:
|
|
|
427
484
|
data["worked_on"] = worked_on
|
|
428
485
|
|
|
429
486
|
# Parse continued_from edge
|
|
430
|
-
|
|
487
|
+
continued_link_results = parser.query(
|
|
488
|
+
"nav[data-graph-edges] section[data-edge-type='continued-from'] a"
|
|
489
|
+
)
|
|
490
|
+
continued_link = continued_link_results[0] if continued_link_results else None
|
|
431
491
|
if continued_link:
|
|
432
492
|
href = continued_link.attrs.get("href") or ""
|
|
433
493
|
data["continued_from"] = href.replace(".html", "")
|
|
434
494
|
|
|
435
495
|
# Parse handoff context
|
|
436
|
-
|
|
496
|
+
handoff_section_results = parser.query("section[data-handoff]")
|
|
497
|
+
handoff_section = handoff_section_results[0] if handoff_section_results else None
|
|
437
498
|
if handoff_section:
|
|
438
|
-
|
|
499
|
+
notes_el_results = parser.query("section[data-handoff] [data-handoff-notes]")
|
|
500
|
+
notes_el = notes_el_results[0] if notes_el_results else None
|
|
439
501
|
if notes_el:
|
|
440
502
|
notes_text = notes_el.to_text().strip()
|
|
441
503
|
if notes_text.lower().startswith("notes:"):
|
|
442
504
|
notes_text = notes_text.split(":", 1)[1].strip()
|
|
443
505
|
data["handoff_notes"] = notes_text
|
|
444
506
|
|
|
445
|
-
|
|
507
|
+
next_el_results = parser.query("section[data-handoff] [data-recommended-next]")
|
|
508
|
+
next_el = next_el_results[0] if next_el_results else None
|
|
446
509
|
if next_el:
|
|
447
510
|
next_text = next_el.to_text().strip()
|
|
448
511
|
if next_text.lower().startswith("recommended next:"):
|
|
@@ -457,6 +520,17 @@ def html_to_session(filepath: Path | str) -> Session:
|
|
|
457
520
|
if blockers:
|
|
458
521
|
data["blockers"] = blockers
|
|
459
522
|
|
|
523
|
+
# Parse recommended context files
|
|
524
|
+
recommended_context = []
|
|
525
|
+
for li in parser.query(
|
|
526
|
+
"section[data-handoff] div[data-recommended-context] li"
|
|
527
|
+
):
|
|
528
|
+
file_path = li.to_text().strip()
|
|
529
|
+
if file_path:
|
|
530
|
+
recommended_context.append(file_path)
|
|
531
|
+
if recommended_context:
|
|
532
|
+
data["recommended_context"] = recommended_context
|
|
533
|
+
|
|
460
534
|
# Parse activity log
|
|
461
535
|
activity_log = []
|
|
462
536
|
for li in parser.query("section[data-activity-log] ol li"):
|
|
@@ -491,6 +565,88 @@ def html_to_session(filepath: Path | str) -> Session:
|
|
|
491
565
|
# Activity log in HTML is reversed (newest first), so reverse back
|
|
492
566
|
data["activity_log"] = list(reversed(activity_log))
|
|
493
567
|
|
|
568
|
+
# Parse detected patterns from table (if present)
|
|
569
|
+
detected_patterns = []
|
|
570
|
+
for tr in parser.query("section[data-detected-patterns] table tbody tr"):
|
|
571
|
+
# Extract pattern data from table row
|
|
572
|
+
pattern_type = tr.attrs.get("data-pattern-type", "neutral")
|
|
573
|
+
|
|
574
|
+
# Extract sequence from first <td class="sequence">
|
|
575
|
+
seq_tds = tr.query("td.sequence")
|
|
576
|
+
seq_td = seq_tds[0] if seq_tds else None
|
|
577
|
+
sequence_str = seq_td.to_text().strip() if seq_td else ""
|
|
578
|
+
sequence = [s.strip() for s in sequence_str.split("→")] if sequence_str else []
|
|
579
|
+
|
|
580
|
+
# Extract count from third <td>
|
|
581
|
+
tds = tr.query("td")
|
|
582
|
+
count_td = tds[2] if len(tds) > 2 else None
|
|
583
|
+
count_str = count_td.to_text().strip() if count_td else "0"
|
|
584
|
+
try:
|
|
585
|
+
count = int(count_str)
|
|
586
|
+
except (ValueError, TypeError):
|
|
587
|
+
count = 0
|
|
588
|
+
|
|
589
|
+
# Extract timestamps from fourth <td>
|
|
590
|
+
time_td = tds[3] if len(tds) > 3 else None
|
|
591
|
+
time_str = time_td.to_text().strip() if time_td else ""
|
|
592
|
+
times = time_str.split(" / ")
|
|
593
|
+
first_detected = times[0].strip() if len(times) > 0 else ""
|
|
594
|
+
last_detected = times[1].strip() if len(times) > 1 else ""
|
|
595
|
+
|
|
596
|
+
if sequence: # Only add if we have a valid sequence
|
|
597
|
+
detected_patterns.append(
|
|
598
|
+
{
|
|
599
|
+
"sequence": sequence,
|
|
600
|
+
"pattern_type": pattern_type,
|
|
601
|
+
"detection_count": count,
|
|
602
|
+
"first_detected": first_detected,
|
|
603
|
+
"last_detected": last_detected,
|
|
604
|
+
}
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
data["detected_patterns"] = detected_patterns
|
|
608
|
+
|
|
609
|
+
# Parse error log from error section (if present)
|
|
610
|
+
error_log = []
|
|
611
|
+
for details in parser.query("section[data-error-log] details"):
|
|
612
|
+
error_data = {
|
|
613
|
+
"error_type": details.attrs.get("data-error-type", "Unknown"),
|
|
614
|
+
"message": "",
|
|
615
|
+
"traceback": None,
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
ts = details.attrs.get("data-ts")
|
|
619
|
+
if ts:
|
|
620
|
+
error_data["timestamp"] = datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
621
|
+
|
|
622
|
+
tool = details.attrs.get("data-tool")
|
|
623
|
+
if tool:
|
|
624
|
+
error_data["tool"] = tool
|
|
625
|
+
|
|
626
|
+
# Parse summary text (first line of details)
|
|
627
|
+
summary_el_results = details.query("summary")
|
|
628
|
+
summary_el = summary_el_results[0] if summary_el_results else None
|
|
629
|
+
if summary_el:
|
|
630
|
+
summary_text = summary_el.to_text().strip()
|
|
631
|
+
# Extract message from "ErrorType: message" format
|
|
632
|
+
if ": " in summary_text:
|
|
633
|
+
error_data["message"] = summary_text.split(": ", 1)[1]
|
|
634
|
+
else:
|
|
635
|
+
error_data["message"] = summary_text
|
|
636
|
+
|
|
637
|
+
# Parse traceback (if present)
|
|
638
|
+
traceback_el_results = details.query("pre.traceback")
|
|
639
|
+
traceback_el = traceback_el_results[0] if traceback_el_results else None
|
|
640
|
+
if traceback_el:
|
|
641
|
+
error_data["traceback"] = traceback_el.to_text().strip()
|
|
642
|
+
|
|
643
|
+
if error_data.get("message") or error_data.get("traceback"):
|
|
644
|
+
from htmlgraph.models import ErrorEntry
|
|
645
|
+
|
|
646
|
+
error_log.append(ErrorEntry(**error_data))
|
|
647
|
+
|
|
648
|
+
data["error_log"] = error_log
|
|
649
|
+
|
|
494
650
|
return Session(**data)
|
|
495
651
|
|
|
496
652
|
|
|
@@ -498,7 +654,7 @@ def session_to_html(
|
|
|
498
654
|
session: Session,
|
|
499
655
|
filepath: Path | str,
|
|
500
656
|
stylesheet_path: str = "../styles.css",
|
|
501
|
-
create_dirs: bool = True
|
|
657
|
+
create_dirs: bool = True,
|
|
502
658
|
) -> Path:
|
|
503
659
|
"""
|
|
504
660
|
Write a Session model to an HTML file.
|
|
@@ -538,18 +694,50 @@ class SessionConverter:
|
|
|
538
694
|
self.stylesheet_path = stylesheet_path
|
|
539
695
|
|
|
540
696
|
def load(self, session_id: str) -> Session | None:
|
|
541
|
-
"""
|
|
697
|
+
"""
|
|
698
|
+
Load a single session by ID and cleanup stale work item references.
|
|
699
|
+
|
|
700
|
+
This automatically removes references to deleted/missing work items
|
|
701
|
+
from the session's worked_on list to maintain data integrity.
|
|
702
|
+
"""
|
|
542
703
|
filepath = self.directory / f"{session_id}.html"
|
|
543
|
-
if filepath.exists():
|
|
544
|
-
return
|
|
545
|
-
|
|
704
|
+
if not filepath.exists():
|
|
705
|
+
return None
|
|
706
|
+
|
|
707
|
+
# Load session from HTML
|
|
708
|
+
session = html_to_session(filepath)
|
|
709
|
+
|
|
710
|
+
# Cleanup stale work item references
|
|
711
|
+
# (removes IDs from worked_on that no longer exist in .htmlgraph/)
|
|
712
|
+
graph_dir = self.directory.parent # .htmlgraph directory
|
|
713
|
+
cleanup_result = session.cleanup_missing_references(graph_dir)
|
|
714
|
+
|
|
715
|
+
# Log warning if stale references were removed
|
|
716
|
+
if cleanup_result["removed_count"] > 0:
|
|
717
|
+
logger.warning(
|
|
718
|
+
f"Session {session_id}: Removed {cleanup_result['removed_count']} "
|
|
719
|
+
f"stale work item references: {cleanup_result['removed']}"
|
|
720
|
+
)
|
|
721
|
+
# Save cleaned session back to disk
|
|
722
|
+
self.save(session)
|
|
723
|
+
|
|
724
|
+
return session
|
|
546
725
|
|
|
547
726
|
def load_all(self, pattern: str = "*.html") -> list[Session]:
|
|
548
|
-
"""
|
|
727
|
+
"""
|
|
728
|
+
Load all sessions matching pattern and cleanup stale references.
|
|
729
|
+
|
|
730
|
+
Each session is loaded through self.load() which automatically cleans up
|
|
731
|
+
stale work item references.
|
|
732
|
+
"""
|
|
549
733
|
sessions = []
|
|
550
734
|
for filepath in self.directory.glob(pattern):
|
|
551
735
|
try:
|
|
552
|
-
|
|
736
|
+
# Extract session_id from filename
|
|
737
|
+
session_id = filepath.stem # e.g., "sess-abc123"
|
|
738
|
+
session = self.load(session_id)
|
|
739
|
+
if session:
|
|
740
|
+
sessions.append(session)
|
|
553
741
|
except (ValueError, KeyError):
|
|
554
742
|
continue
|
|
555
743
|
return sessions
|