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.
Files changed (331) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +173 -17
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +127 -0
  7. htmlgraph/agent_registry.py +45 -30
  8. htmlgraph/agents.py +160 -107
  9. htmlgraph/analytics/__init__.py +9 -2
  10. htmlgraph/analytics/cli.py +190 -51
  11. htmlgraph/analytics/cost_analyzer.py +391 -0
  12. htmlgraph/analytics/cost_monitor.py +664 -0
  13. htmlgraph/analytics/cost_reporter.py +675 -0
  14. htmlgraph/analytics/cross_session.py +617 -0
  15. htmlgraph/analytics/dependency.py +192 -100
  16. htmlgraph/analytics/pattern_learning.py +771 -0
  17. htmlgraph/analytics/session_graph.py +707 -0
  18. htmlgraph/analytics/strategic/__init__.py +80 -0
  19. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  20. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  21. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  22. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  23. htmlgraph/analytics/work_type.py +190 -14
  24. htmlgraph/analytics_index.py +135 -51
  25. htmlgraph/api/__init__.py +3 -0
  26. htmlgraph/api/cost_alerts_websocket.py +416 -0
  27. htmlgraph/api/main.py +2498 -0
  28. htmlgraph/api/static/htmx.min.js +1 -0
  29. htmlgraph/api/static/style-redesign.css +1344 -0
  30. htmlgraph/api/static/style.css +1079 -0
  31. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  32. htmlgraph/api/templates/dashboard.html +794 -0
  33. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  34. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  35. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  36. htmlgraph/api/templates/partials/agents.html +317 -0
  37. htmlgraph/api/templates/partials/event-traces.html +373 -0
  38. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  39. htmlgraph/api/templates/partials/features.html +578 -0
  40. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  41. htmlgraph/api/templates/partials/metrics.html +346 -0
  42. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  43. htmlgraph/api/templates/partials/orchestration.html +198 -0
  44. htmlgraph/api/templates/partials/spawners.html +375 -0
  45. htmlgraph/api/templates/partials/work-items.html +613 -0
  46. htmlgraph/api/websocket.py +538 -0
  47. htmlgraph/archive/__init__.py +24 -0
  48. htmlgraph/archive/bloom.py +234 -0
  49. htmlgraph/archive/fts.py +297 -0
  50. htmlgraph/archive/manager.py +583 -0
  51. htmlgraph/archive/search.py +244 -0
  52. htmlgraph/atomic_ops.py +560 -0
  53. htmlgraph/attribute_index.py +208 -0
  54. htmlgraph/bounded_paths.py +539 -0
  55. htmlgraph/builders/__init__.py +14 -0
  56. htmlgraph/builders/base.py +118 -29
  57. htmlgraph/builders/bug.py +150 -0
  58. htmlgraph/builders/chore.py +119 -0
  59. htmlgraph/builders/epic.py +150 -0
  60. htmlgraph/builders/feature.py +31 -6
  61. htmlgraph/builders/insight.py +195 -0
  62. htmlgraph/builders/metric.py +217 -0
  63. htmlgraph/builders/pattern.py +202 -0
  64. htmlgraph/builders/phase.py +162 -0
  65. htmlgraph/builders/spike.py +52 -19
  66. htmlgraph/builders/track.py +148 -72
  67. htmlgraph/cigs/__init__.py +81 -0
  68. htmlgraph/cigs/autonomy.py +385 -0
  69. htmlgraph/cigs/cost.py +475 -0
  70. htmlgraph/cigs/messages_basic.py +472 -0
  71. htmlgraph/cigs/messaging.py +365 -0
  72. htmlgraph/cigs/models.py +771 -0
  73. htmlgraph/cigs/pattern_storage.py +427 -0
  74. htmlgraph/cigs/patterns.py +503 -0
  75. htmlgraph/cigs/posttool_analyzer.py +234 -0
  76. htmlgraph/cigs/reporter.py +818 -0
  77. htmlgraph/cigs/tracker.py +317 -0
  78. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  79. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  80. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  81. htmlgraph/cli/__init__.py +42 -0
  82. htmlgraph/cli/__main__.py +6 -0
  83. htmlgraph/cli/analytics.py +1424 -0
  84. htmlgraph/cli/base.py +685 -0
  85. htmlgraph/cli/constants.py +206 -0
  86. htmlgraph/cli/core.py +954 -0
  87. htmlgraph/cli/main.py +147 -0
  88. htmlgraph/cli/models.py +475 -0
  89. htmlgraph/cli/templates/__init__.py +1 -0
  90. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  91. htmlgraph/cli/work/__init__.py +239 -0
  92. htmlgraph/cli/work/browse.py +115 -0
  93. htmlgraph/cli/work/features.py +568 -0
  94. htmlgraph/cli/work/orchestration.py +676 -0
  95. htmlgraph/cli/work/report.py +728 -0
  96. htmlgraph/cli/work/sessions.py +466 -0
  97. htmlgraph/cli/work/snapshot.py +559 -0
  98. htmlgraph/cli/work/tracks.py +486 -0
  99. htmlgraph/cli_commands/__init__.py +1 -0
  100. htmlgraph/cli_commands/feature.py +195 -0
  101. htmlgraph/cli_framework.py +115 -0
  102. htmlgraph/collections/__init__.py +18 -0
  103. htmlgraph/collections/base.py +415 -98
  104. htmlgraph/collections/bug.py +53 -0
  105. htmlgraph/collections/chore.py +53 -0
  106. htmlgraph/collections/epic.py +53 -0
  107. htmlgraph/collections/feature.py +12 -26
  108. htmlgraph/collections/insight.py +100 -0
  109. htmlgraph/collections/metric.py +92 -0
  110. htmlgraph/collections/pattern.py +97 -0
  111. htmlgraph/collections/phase.py +53 -0
  112. htmlgraph/collections/session.py +194 -0
  113. htmlgraph/collections/spike.py +56 -16
  114. htmlgraph/collections/task_delegation.py +241 -0
  115. htmlgraph/collections/todo.py +511 -0
  116. htmlgraph/collections/traces.py +487 -0
  117. htmlgraph/config/cost_models.json +56 -0
  118. htmlgraph/config.py +190 -0
  119. htmlgraph/context_analytics.py +344 -0
  120. htmlgraph/converter.py +216 -28
  121. htmlgraph/cost_analysis/__init__.py +5 -0
  122. htmlgraph/cost_analysis/analyzer.py +438 -0
  123. htmlgraph/dashboard.html +2406 -307
  124. htmlgraph/dashboard.html.backup +6592 -0
  125. htmlgraph/dashboard.html.bak +7181 -0
  126. htmlgraph/dashboard.html.bak2 +7231 -0
  127. htmlgraph/dashboard.html.bak3 +7232 -0
  128. htmlgraph/db/__init__.py +38 -0
  129. htmlgraph/db/queries.py +790 -0
  130. htmlgraph/db/schema.py +1788 -0
  131. htmlgraph/decorators.py +317 -0
  132. htmlgraph/dependency_models.py +19 -2
  133. htmlgraph/deploy.py +142 -125
  134. htmlgraph/deployment_models.py +474 -0
  135. htmlgraph/docs/API_REFERENCE.md +841 -0
  136. htmlgraph/docs/HTTP_API.md +750 -0
  137. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  138. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  139. htmlgraph/docs/README.md +532 -0
  140. htmlgraph/docs/__init__.py +77 -0
  141. htmlgraph/docs/docs_version.py +55 -0
  142. htmlgraph/docs/metadata.py +93 -0
  143. htmlgraph/docs/migrations.py +232 -0
  144. htmlgraph/docs/template_engine.py +143 -0
  145. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  146. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  147. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  148. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  149. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  150. htmlgraph/docs/version_check.py +163 -0
  151. htmlgraph/edge_index.py +182 -27
  152. htmlgraph/error_handler.py +544 -0
  153. htmlgraph/event_log.py +100 -52
  154. htmlgraph/event_migration.py +13 -4
  155. htmlgraph/exceptions.py +49 -0
  156. htmlgraph/file_watcher.py +101 -28
  157. htmlgraph/find_api.py +75 -63
  158. htmlgraph/git_events.py +145 -63
  159. htmlgraph/graph.py +1122 -106
  160. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  161. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  162. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  163. htmlgraph/hooks/__init__.py +45 -0
  164. htmlgraph/hooks/bootstrap.py +169 -0
  165. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  166. htmlgraph/hooks/concurrent_sessions.py +208 -0
  167. htmlgraph/hooks/context.py +350 -0
  168. htmlgraph/hooks/drift_handler.py +525 -0
  169. htmlgraph/hooks/event_tracker.py +1314 -0
  170. htmlgraph/hooks/git_commands.py +175 -0
  171. htmlgraph/hooks/hooks-config.example.json +12 -0
  172. htmlgraph/hooks/installer.py +343 -0
  173. htmlgraph/hooks/orchestrator.py +674 -0
  174. htmlgraph/hooks/orchestrator_reflector.py +223 -0
  175. htmlgraph/hooks/post-checkout.sh +28 -0
  176. htmlgraph/hooks/post-commit.sh +24 -0
  177. htmlgraph/hooks/post-merge.sh +26 -0
  178. htmlgraph/hooks/post_tool_use_failure.py +273 -0
  179. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  180. htmlgraph/hooks/posttooluse.py +408 -0
  181. htmlgraph/hooks/pre-commit.sh +94 -0
  182. htmlgraph/hooks/pre-push.sh +28 -0
  183. htmlgraph/hooks/pretooluse.py +819 -0
  184. htmlgraph/hooks/prompt_analyzer.py +637 -0
  185. htmlgraph/hooks/session_handler.py +668 -0
  186. htmlgraph/hooks/session_summary.py +395 -0
  187. htmlgraph/hooks/state_manager.py +504 -0
  188. htmlgraph/hooks/subagent_detection.py +202 -0
  189. htmlgraph/hooks/subagent_stop.py +369 -0
  190. htmlgraph/hooks/task_enforcer.py +255 -0
  191. htmlgraph/hooks/task_validator.py +177 -0
  192. htmlgraph/hooks/validator.py +628 -0
  193. htmlgraph/ids.py +41 -27
  194. htmlgraph/index.d.ts +286 -0
  195. htmlgraph/learning.py +767 -0
  196. htmlgraph/mcp_server.py +69 -23
  197. htmlgraph/models.py +1586 -87
  198. htmlgraph/operations/README.md +62 -0
  199. htmlgraph/operations/__init__.py +79 -0
  200. htmlgraph/operations/analytics.py +339 -0
  201. htmlgraph/operations/bootstrap.py +289 -0
  202. htmlgraph/operations/events.py +244 -0
  203. htmlgraph/operations/fastapi_server.py +231 -0
  204. htmlgraph/operations/hooks.py +350 -0
  205. htmlgraph/operations/initialization.py +597 -0
  206. htmlgraph/operations/initialization.py.backup +228 -0
  207. htmlgraph/operations/server.py +303 -0
  208. htmlgraph/orchestration/__init__.py +58 -0
  209. htmlgraph/orchestration/claude_launcher.py +179 -0
  210. htmlgraph/orchestration/command_builder.py +72 -0
  211. htmlgraph/orchestration/headless_spawner.py +281 -0
  212. htmlgraph/orchestration/live_events.py +377 -0
  213. htmlgraph/orchestration/model_selection.py +327 -0
  214. htmlgraph/orchestration/plugin_manager.py +140 -0
  215. htmlgraph/orchestration/prompts.py +137 -0
  216. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  217. htmlgraph/orchestration/spawners/__init__.py +16 -0
  218. htmlgraph/orchestration/spawners/base.py +194 -0
  219. htmlgraph/orchestration/spawners/claude.py +173 -0
  220. htmlgraph/orchestration/spawners/codex.py +435 -0
  221. htmlgraph/orchestration/spawners/copilot.py +294 -0
  222. htmlgraph/orchestration/spawners/gemini.py +471 -0
  223. htmlgraph/orchestration/subprocess_runner.py +36 -0
  224. htmlgraph/orchestration/task_coordination.py +343 -0
  225. htmlgraph/orchestration.md +563 -0
  226. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  227. htmlgraph/orchestrator.py +669 -0
  228. htmlgraph/orchestrator_config.py +357 -0
  229. htmlgraph/orchestrator_mode.py +328 -0
  230. htmlgraph/orchestrator_validator.py +133 -0
  231. htmlgraph/parallel.py +646 -0
  232. htmlgraph/parser.py +160 -35
  233. htmlgraph/path_query.py +608 -0
  234. htmlgraph/pattern_matcher.py +636 -0
  235. htmlgraph/planning.py +147 -52
  236. htmlgraph/pydantic_models.py +476 -0
  237. htmlgraph/quality_gates.py +350 -0
  238. htmlgraph/query_builder.py +109 -72
  239. htmlgraph/query_composer.py +509 -0
  240. htmlgraph/reflection.py +443 -0
  241. htmlgraph/refs.py +344 -0
  242. htmlgraph/repo_hash.py +512 -0
  243. htmlgraph/repositories/__init__.py +292 -0
  244. htmlgraph/repositories/analytics_repository.py +455 -0
  245. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  246. htmlgraph/repositories/feature_repository.py +581 -0
  247. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  248. htmlgraph/repositories/feature_repository_memory.py +607 -0
  249. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  250. htmlgraph/repositories/filter_service.py +620 -0
  251. htmlgraph/repositories/filter_service_standard.py +445 -0
  252. htmlgraph/repositories/shared_cache.py +621 -0
  253. htmlgraph/repositories/shared_cache_memory.py +395 -0
  254. htmlgraph/repositories/track_repository.py +552 -0
  255. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  256. htmlgraph/repositories/track_repository_memory.py +508 -0
  257. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  258. htmlgraph/routing.py +8 -19
  259. htmlgraph/scripts/deploy.py +1 -2
  260. htmlgraph/sdk/__init__.py +398 -0
  261. htmlgraph/sdk/__init__.pyi +14 -0
  262. htmlgraph/sdk/analytics/__init__.py +19 -0
  263. htmlgraph/sdk/analytics/engine.py +155 -0
  264. htmlgraph/sdk/analytics/helpers.py +178 -0
  265. htmlgraph/sdk/analytics/registry.py +109 -0
  266. htmlgraph/sdk/base.py +484 -0
  267. htmlgraph/sdk/constants.py +216 -0
  268. htmlgraph/sdk/core.pyi +308 -0
  269. htmlgraph/sdk/discovery.py +120 -0
  270. htmlgraph/sdk/help/__init__.py +12 -0
  271. htmlgraph/sdk/help/mixin.py +699 -0
  272. htmlgraph/sdk/mixins/__init__.py +15 -0
  273. htmlgraph/sdk/mixins/attribution.py +113 -0
  274. htmlgraph/sdk/mixins/mixin.py +410 -0
  275. htmlgraph/sdk/operations/__init__.py +12 -0
  276. htmlgraph/sdk/operations/mixin.py +427 -0
  277. htmlgraph/sdk/orchestration/__init__.py +17 -0
  278. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  279. htmlgraph/sdk/orchestration/spawner.py +204 -0
  280. htmlgraph/sdk/planning/__init__.py +19 -0
  281. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  282. htmlgraph/sdk/planning/mixin.py +211 -0
  283. htmlgraph/sdk/planning/parallel.py +186 -0
  284. htmlgraph/sdk/planning/queue.py +210 -0
  285. htmlgraph/sdk/planning/recommendations.py +87 -0
  286. htmlgraph/sdk/planning/smart_planning.py +319 -0
  287. htmlgraph/sdk/session/__init__.py +19 -0
  288. htmlgraph/sdk/session/continuity.py +57 -0
  289. htmlgraph/sdk/session/handoff.py +110 -0
  290. htmlgraph/sdk/session/info.py +309 -0
  291. htmlgraph/sdk/session/manager.py +103 -0
  292. htmlgraph/sdk/strategic/__init__.py +26 -0
  293. htmlgraph/sdk/strategic/mixin.py +563 -0
  294. htmlgraph/server.py +685 -180
  295. htmlgraph/services/__init__.py +10 -0
  296. htmlgraph/services/claiming.py +199 -0
  297. htmlgraph/session_hooks.py +300 -0
  298. htmlgraph/session_manager.py +1392 -175
  299. htmlgraph/session_registry.py +587 -0
  300. htmlgraph/session_state.py +436 -0
  301. htmlgraph/session_warning.py +201 -0
  302. htmlgraph/sessions/__init__.py +23 -0
  303. htmlgraph/sessions/handoff.py +756 -0
  304. htmlgraph/setup.py +34 -17
  305. htmlgraph/spike_index.py +143 -0
  306. htmlgraph/sync_docs.py +12 -15
  307. htmlgraph/system_prompts.py +450 -0
  308. htmlgraph/templates/AGENTS.md.template +366 -0
  309. htmlgraph/templates/CLAUDE.md.template +97 -0
  310. htmlgraph/templates/GEMINI.md.template +87 -0
  311. htmlgraph/templates/orchestration-view.html +350 -0
  312. htmlgraph/track_builder.py +146 -15
  313. htmlgraph/track_manager.py +69 -21
  314. htmlgraph/transcript.py +890 -0
  315. htmlgraph/transcript_analytics.py +699 -0
  316. htmlgraph/types.py +323 -0
  317. htmlgraph/validation.py +115 -0
  318. htmlgraph/watch.py +8 -5
  319. htmlgraph/work_type_utils.py +3 -2
  320. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
  321. htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
  322. htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
  323. htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
  324. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
  325. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  326. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  327. htmlgraph/cli.py +0 -2688
  328. htmlgraph/sdk.py +0 -709
  329. htmlgraph-0.9.3.dist-info/RECORD +0 -61
  330. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  331. {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 Node, Edge, Step, Session, ActivityEntry
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
- return Node(**data)
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
- return data
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(edge["since"].replace("Z", "+00:00"))
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(step["timestamp"].replace("Z", "+00:00"))
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
- return data
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(entry["timestamp"].replace("Z", "+00:00"))
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
- article = parser.query_one("article[data-type='session']")
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(last_activity.replace("Z", "+00:00"))
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
- title_el = parser.query_one("h1")
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("nav[data-graph-edges] section[data-edge-type='worked-on'] a"):
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
- continued_link = parser.query_one("nav[data-graph-edges] section[data-edge-type='continued-from'] a")
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
- handoff_section = parser.query_one("section[data-handoff]")
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
- notes_el = parser.query_one("section[data-handoff] [data-handoff-notes]")
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
- next_el = parser.query_one("section[data-handoff] [data-recommended-next]")
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
- """Load a single session by ID."""
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 html_to_session(filepath)
545
- return None
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
- """Load all sessions matching pattern."""
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
- sessions.append(html_to_session(filepath))
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
@@ -0,0 +1,5 @@
1
+ """Cost analysis module for HtmlGraph event tracking and token cost calculation."""
2
+
3
+ from .analyzer import CostAnalyzer
4
+
5
+ __all__ = ["CostAnalyzer"]