htmlgraph 0.20.1__py3-none-any.whl → 0.27.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +51 -1
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +26 -10
  7. htmlgraph/agent_registry.py +2 -1
  8. htmlgraph/analytics/__init__.py +8 -1
  9. htmlgraph/analytics/cli.py +86 -20
  10. htmlgraph/analytics/cost_analyzer.py +391 -0
  11. htmlgraph/analytics/cost_monitor.py +664 -0
  12. htmlgraph/analytics/cost_reporter.py +675 -0
  13. htmlgraph/analytics/cross_session.py +617 -0
  14. htmlgraph/analytics/dependency.py +10 -6
  15. htmlgraph/analytics/pattern_learning.py +771 -0
  16. htmlgraph/analytics/session_graph.py +707 -0
  17. htmlgraph/analytics/strategic/__init__.py +80 -0
  18. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  19. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  20. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  21. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  22. htmlgraph/analytics/work_type.py +67 -27
  23. htmlgraph/analytics_index.py +53 -20
  24. htmlgraph/api/__init__.py +3 -0
  25. htmlgraph/api/cost_alerts_websocket.py +416 -0
  26. htmlgraph/api/main.py +2498 -0
  27. htmlgraph/api/static/htmx.min.js +1 -0
  28. htmlgraph/api/static/style-redesign.css +1344 -0
  29. htmlgraph/api/static/style.css +1079 -0
  30. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  31. htmlgraph/api/templates/dashboard.html +794 -0
  32. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  33. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  34. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  35. htmlgraph/api/templates/partials/agents.html +317 -0
  36. htmlgraph/api/templates/partials/event-traces.html +373 -0
  37. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  38. htmlgraph/api/templates/partials/features.html +578 -0
  39. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  40. htmlgraph/api/templates/partials/metrics.html +346 -0
  41. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  42. htmlgraph/api/templates/partials/orchestration.html +198 -0
  43. htmlgraph/api/templates/partials/spawners.html +375 -0
  44. htmlgraph/api/templates/partials/work-items.html +613 -0
  45. htmlgraph/api/websocket.py +538 -0
  46. htmlgraph/archive/__init__.py +24 -0
  47. htmlgraph/archive/bloom.py +234 -0
  48. htmlgraph/archive/fts.py +297 -0
  49. htmlgraph/archive/manager.py +583 -0
  50. htmlgraph/archive/search.py +244 -0
  51. htmlgraph/atomic_ops.py +560 -0
  52. htmlgraph/attribute_index.py +2 -1
  53. htmlgraph/bounded_paths.py +539 -0
  54. htmlgraph/builders/base.py +57 -2
  55. htmlgraph/builders/bug.py +19 -3
  56. htmlgraph/builders/chore.py +19 -3
  57. htmlgraph/builders/epic.py +19 -3
  58. htmlgraph/builders/feature.py +27 -3
  59. htmlgraph/builders/insight.py +2 -1
  60. htmlgraph/builders/metric.py +2 -1
  61. htmlgraph/builders/pattern.py +2 -1
  62. htmlgraph/builders/phase.py +19 -3
  63. htmlgraph/builders/spike.py +29 -3
  64. htmlgraph/builders/track.py +42 -1
  65. htmlgraph/cigs/__init__.py +81 -0
  66. htmlgraph/cigs/autonomy.py +385 -0
  67. htmlgraph/cigs/cost.py +475 -0
  68. htmlgraph/cigs/messages_basic.py +472 -0
  69. htmlgraph/cigs/messaging.py +365 -0
  70. htmlgraph/cigs/models.py +771 -0
  71. htmlgraph/cigs/pattern_storage.py +427 -0
  72. htmlgraph/cigs/patterns.py +503 -0
  73. htmlgraph/cigs/posttool_analyzer.py +234 -0
  74. htmlgraph/cigs/reporter.py +818 -0
  75. htmlgraph/cigs/tracker.py +317 -0
  76. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  77. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  78. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  79. htmlgraph/cli/__init__.py +42 -0
  80. htmlgraph/cli/__main__.py +6 -0
  81. htmlgraph/cli/analytics.py +1424 -0
  82. htmlgraph/cli/base.py +685 -0
  83. htmlgraph/cli/constants.py +206 -0
  84. htmlgraph/cli/core.py +954 -0
  85. htmlgraph/cli/main.py +147 -0
  86. htmlgraph/cli/models.py +475 -0
  87. htmlgraph/cli/templates/__init__.py +1 -0
  88. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  89. htmlgraph/cli/work/__init__.py +239 -0
  90. htmlgraph/cli/work/browse.py +115 -0
  91. htmlgraph/cli/work/features.py +568 -0
  92. htmlgraph/cli/work/orchestration.py +676 -0
  93. htmlgraph/cli/work/report.py +728 -0
  94. htmlgraph/cli/work/sessions.py +466 -0
  95. htmlgraph/cli/work/snapshot.py +559 -0
  96. htmlgraph/cli/work/tracks.py +486 -0
  97. htmlgraph/cli_commands/__init__.py +1 -0
  98. htmlgraph/cli_commands/feature.py +195 -0
  99. htmlgraph/cli_framework.py +115 -0
  100. htmlgraph/collections/__init__.py +2 -0
  101. htmlgraph/collections/base.py +197 -14
  102. htmlgraph/collections/bug.py +2 -1
  103. htmlgraph/collections/chore.py +2 -1
  104. htmlgraph/collections/epic.py +2 -1
  105. htmlgraph/collections/feature.py +2 -1
  106. htmlgraph/collections/insight.py +2 -1
  107. htmlgraph/collections/metric.py +2 -1
  108. htmlgraph/collections/pattern.py +2 -1
  109. htmlgraph/collections/phase.py +2 -1
  110. htmlgraph/collections/session.py +194 -0
  111. htmlgraph/collections/spike.py +13 -2
  112. htmlgraph/collections/task_delegation.py +241 -0
  113. htmlgraph/collections/todo.py +14 -1
  114. htmlgraph/collections/traces.py +487 -0
  115. htmlgraph/config/cost_models.json +56 -0
  116. htmlgraph/config.py +190 -0
  117. htmlgraph/context_analytics.py +2 -1
  118. htmlgraph/converter.py +116 -7
  119. htmlgraph/cost_analysis/__init__.py +5 -0
  120. htmlgraph/cost_analysis/analyzer.py +438 -0
  121. htmlgraph/dashboard.html +2246 -248
  122. htmlgraph/dashboard.html.backup +6592 -0
  123. htmlgraph/dashboard.html.bak +7181 -0
  124. htmlgraph/dashboard.html.bak2 +7231 -0
  125. htmlgraph/dashboard.html.bak3 +7232 -0
  126. htmlgraph/db/__init__.py +38 -0
  127. htmlgraph/db/queries.py +790 -0
  128. htmlgraph/db/schema.py +1788 -0
  129. htmlgraph/decorators.py +317 -0
  130. htmlgraph/dependency_models.py +2 -1
  131. htmlgraph/deploy.py +26 -27
  132. htmlgraph/docs/API_REFERENCE.md +841 -0
  133. htmlgraph/docs/HTTP_API.md +750 -0
  134. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  135. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  136. htmlgraph/docs/README.md +532 -0
  137. htmlgraph/docs/__init__.py +77 -0
  138. htmlgraph/docs/docs_version.py +55 -0
  139. htmlgraph/docs/metadata.py +93 -0
  140. htmlgraph/docs/migrations.py +232 -0
  141. htmlgraph/docs/template_engine.py +143 -0
  142. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  143. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  144. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  145. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  146. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  147. htmlgraph/docs/version_check.py +163 -0
  148. htmlgraph/edge_index.py +2 -1
  149. htmlgraph/error_handler.py +544 -0
  150. htmlgraph/event_log.py +86 -37
  151. htmlgraph/event_migration.py +2 -1
  152. htmlgraph/file_watcher.py +12 -8
  153. htmlgraph/find_api.py +2 -1
  154. htmlgraph/git_events.py +67 -9
  155. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  156. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  157. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  158. htmlgraph/hooks/__init__.py +8 -0
  159. htmlgraph/hooks/bootstrap.py +169 -0
  160. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  161. htmlgraph/hooks/concurrent_sessions.py +208 -0
  162. htmlgraph/hooks/context.py +350 -0
  163. htmlgraph/hooks/drift_handler.py +525 -0
  164. htmlgraph/hooks/event_tracker.py +790 -99
  165. htmlgraph/hooks/git_commands.py +175 -0
  166. htmlgraph/hooks/installer.py +5 -1
  167. htmlgraph/hooks/orchestrator.py +327 -76
  168. htmlgraph/hooks/orchestrator_reflector.py +31 -4
  169. htmlgraph/hooks/post_tool_use_failure.py +32 -7
  170. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  171. htmlgraph/hooks/posttooluse.py +92 -19
  172. htmlgraph/hooks/pretooluse.py +527 -7
  173. htmlgraph/hooks/prompt_analyzer.py +637 -0
  174. htmlgraph/hooks/session_handler.py +668 -0
  175. htmlgraph/hooks/session_summary.py +395 -0
  176. htmlgraph/hooks/state_manager.py +504 -0
  177. htmlgraph/hooks/subagent_detection.py +202 -0
  178. htmlgraph/hooks/subagent_stop.py +369 -0
  179. htmlgraph/hooks/task_enforcer.py +99 -4
  180. htmlgraph/hooks/validator.py +212 -91
  181. htmlgraph/ids.py +2 -1
  182. htmlgraph/learning.py +125 -100
  183. htmlgraph/mcp_server.py +2 -1
  184. htmlgraph/models.py +217 -18
  185. htmlgraph/operations/README.md +62 -0
  186. htmlgraph/operations/__init__.py +79 -0
  187. htmlgraph/operations/analytics.py +339 -0
  188. htmlgraph/operations/bootstrap.py +289 -0
  189. htmlgraph/operations/events.py +244 -0
  190. htmlgraph/operations/fastapi_server.py +231 -0
  191. htmlgraph/operations/hooks.py +350 -0
  192. htmlgraph/operations/initialization.py +597 -0
  193. htmlgraph/operations/initialization.py.backup +228 -0
  194. htmlgraph/operations/server.py +303 -0
  195. htmlgraph/orchestration/__init__.py +58 -0
  196. htmlgraph/orchestration/claude_launcher.py +179 -0
  197. htmlgraph/orchestration/command_builder.py +72 -0
  198. htmlgraph/orchestration/headless_spawner.py +281 -0
  199. htmlgraph/orchestration/live_events.py +377 -0
  200. htmlgraph/orchestration/model_selection.py +327 -0
  201. htmlgraph/orchestration/plugin_manager.py +140 -0
  202. htmlgraph/orchestration/prompts.py +137 -0
  203. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  204. htmlgraph/orchestration/spawners/__init__.py +16 -0
  205. htmlgraph/orchestration/spawners/base.py +194 -0
  206. htmlgraph/orchestration/spawners/claude.py +173 -0
  207. htmlgraph/orchestration/spawners/codex.py +435 -0
  208. htmlgraph/orchestration/spawners/copilot.py +294 -0
  209. htmlgraph/orchestration/spawners/gemini.py +471 -0
  210. htmlgraph/orchestration/subprocess_runner.py +36 -0
  211. htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
  212. htmlgraph/orchestration.md +563 -0
  213. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  214. htmlgraph/orchestrator.py +2 -1
  215. htmlgraph/orchestrator_config.py +357 -0
  216. htmlgraph/orchestrator_mode.py +115 -4
  217. htmlgraph/parallel.py +2 -1
  218. htmlgraph/parser.py +86 -6
  219. htmlgraph/path_query.py +608 -0
  220. htmlgraph/pattern_matcher.py +636 -0
  221. htmlgraph/pydantic_models.py +476 -0
  222. htmlgraph/quality_gates.py +350 -0
  223. htmlgraph/query_builder.py +2 -1
  224. htmlgraph/query_composer.py +509 -0
  225. htmlgraph/reflection.py +443 -0
  226. htmlgraph/refs.py +344 -0
  227. htmlgraph/repo_hash.py +512 -0
  228. htmlgraph/repositories/__init__.py +292 -0
  229. htmlgraph/repositories/analytics_repository.py +455 -0
  230. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  231. htmlgraph/repositories/feature_repository.py +581 -0
  232. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  233. htmlgraph/repositories/feature_repository_memory.py +607 -0
  234. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  235. htmlgraph/repositories/filter_service.py +620 -0
  236. htmlgraph/repositories/filter_service_standard.py +445 -0
  237. htmlgraph/repositories/shared_cache.py +621 -0
  238. htmlgraph/repositories/shared_cache_memory.py +395 -0
  239. htmlgraph/repositories/track_repository.py +552 -0
  240. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  241. htmlgraph/repositories/track_repository_memory.py +508 -0
  242. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  243. htmlgraph/sdk/__init__.py +398 -0
  244. htmlgraph/sdk/__init__.pyi +14 -0
  245. htmlgraph/sdk/analytics/__init__.py +19 -0
  246. htmlgraph/sdk/analytics/engine.py +155 -0
  247. htmlgraph/sdk/analytics/helpers.py +178 -0
  248. htmlgraph/sdk/analytics/registry.py +109 -0
  249. htmlgraph/sdk/base.py +484 -0
  250. htmlgraph/sdk/constants.py +216 -0
  251. htmlgraph/sdk/core.pyi +308 -0
  252. htmlgraph/sdk/discovery.py +120 -0
  253. htmlgraph/sdk/help/__init__.py +12 -0
  254. htmlgraph/sdk/help/mixin.py +699 -0
  255. htmlgraph/sdk/mixins/__init__.py +15 -0
  256. htmlgraph/sdk/mixins/attribution.py +113 -0
  257. htmlgraph/sdk/mixins/mixin.py +410 -0
  258. htmlgraph/sdk/operations/__init__.py +12 -0
  259. htmlgraph/sdk/operations/mixin.py +427 -0
  260. htmlgraph/sdk/orchestration/__init__.py +17 -0
  261. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  262. htmlgraph/sdk/orchestration/spawner.py +204 -0
  263. htmlgraph/sdk/planning/__init__.py +19 -0
  264. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  265. htmlgraph/sdk/planning/mixin.py +211 -0
  266. htmlgraph/sdk/planning/parallel.py +186 -0
  267. htmlgraph/sdk/planning/queue.py +210 -0
  268. htmlgraph/sdk/planning/recommendations.py +87 -0
  269. htmlgraph/sdk/planning/smart_planning.py +319 -0
  270. htmlgraph/sdk/session/__init__.py +19 -0
  271. htmlgraph/sdk/session/continuity.py +57 -0
  272. htmlgraph/sdk/session/handoff.py +110 -0
  273. htmlgraph/sdk/session/info.py +309 -0
  274. htmlgraph/sdk/session/manager.py +103 -0
  275. htmlgraph/sdk/strategic/__init__.py +26 -0
  276. htmlgraph/sdk/strategic/mixin.py +563 -0
  277. htmlgraph/server.py +295 -107
  278. htmlgraph/session_hooks.py +300 -0
  279. htmlgraph/session_manager.py +285 -3
  280. htmlgraph/session_registry.py +587 -0
  281. htmlgraph/session_state.py +436 -0
  282. htmlgraph/session_warning.py +2 -1
  283. htmlgraph/sessions/__init__.py +23 -0
  284. htmlgraph/sessions/handoff.py +756 -0
  285. htmlgraph/system_prompts.py +450 -0
  286. htmlgraph/templates/orchestration-view.html +350 -0
  287. htmlgraph/track_builder.py +33 -1
  288. htmlgraph/track_manager.py +38 -0
  289. htmlgraph/transcript.py +18 -5
  290. htmlgraph/validation.py +115 -0
  291. htmlgraph/watch.py +2 -1
  292. htmlgraph/work_type_utils.py +2 -1
  293. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
  294. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
  295. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  296. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  297. htmlgraph/cli.py +0 -4839
  298. htmlgraph/sdk.py +0 -2359
  299. htmlgraph-0.20.1.dist-info/RECORD +0 -118
  300. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  301. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  302. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  303. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  304. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
@@ -0,0 +1,711 @@
1
+ """
2
+ SQLiteTrackRepository - SQLite database-based Track storage.
3
+
4
+ Stores tracks in SQLite database for fast queries and unified storage.
5
+ """
6
+
7
+ import builtins
8
+ import json
9
+ import sqlite3
10
+ from collections.abc import Callable
11
+ from datetime import datetime
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from htmlgraph.db.schema import HtmlGraphDB
16
+ from htmlgraph.models import Node
17
+ from htmlgraph.repositories.track_repository import (
18
+ RepositoryQuery,
19
+ TrackRepository,
20
+ TrackValidationError,
21
+ )
22
+
23
+
24
+ class SQLiteRepositoryQuery(RepositoryQuery):
25
+ """Query builder for SQLite filtering."""
26
+
27
+ def __init__(self, repo: "SQLiteTrackRepository", filters: dict[str, Any]):
28
+ super().__init__(filters)
29
+ self._repo = repo
30
+
31
+ def where(self, **kwargs: Any) -> "SQLiteRepositoryQuery":
32
+ """Chain additional filters."""
33
+ # Validate filter keys
34
+ valid_attrs = {
35
+ "status",
36
+ "priority",
37
+ "has_spec",
38
+ "has_plan",
39
+ "type",
40
+ "title",
41
+ "id",
42
+ "created",
43
+ "updated",
44
+ }
45
+ for key in kwargs:
46
+ if key not in valid_attrs:
47
+ raise TrackValidationError(f"Invalid filter attribute: {key}")
48
+
49
+ # Merge filters
50
+ new_filters = {**self.filters, **kwargs}
51
+ return SQLiteRepositoryQuery(self._repo, new_filters)
52
+
53
+ def execute(self) -> list[Any]:
54
+ """Execute the query and return results."""
55
+ return self._repo.list(self.filters)
56
+
57
+
58
+ class SQLiteTrackRepository(TrackRepository):
59
+ """
60
+ SQLite database-based TrackRepository implementation.
61
+
62
+ Stores tracks in a SQLite database for fast queries and transactions.
63
+ Uses parameterized queries to prevent SQL injection.
64
+
65
+ Database schema:
66
+ Table: tracks
67
+ Columns: id, type, title, description, status, priority,
68
+ created_at, updated_at, has_spec, has_plan,
69
+ metadata (JSON)
70
+
71
+ Performance:
72
+ - get(id): O(1) with cache, O(log n) from database (indexed)
73
+ - list(): O(n) with SQL WHERE clauses
74
+ - batch operations: O(k) vectorized SQL
75
+
76
+ Example:
77
+ >>> db_path = Path(".htmlgraph/htmlgraph.db")
78
+ >>> repo = SQLiteTrackRepository(db_path)
79
+ >>> track = repo.create("Planning Phase 1", priority="high")
80
+ >>> track.status = "active"
81
+ >>> repo.save(track)
82
+ """
83
+
84
+ def __init__(self, db_path: Path | str, auto_load: bool = True):
85
+ """
86
+ Initialize SQLite repository.
87
+
88
+ Args:
89
+ db_path: Path to SQLite database file
90
+ auto_load: Whether to enable auto-loading (always True for DB)
91
+ """
92
+ self._db_path = Path(db_path)
93
+ self._auto_load = auto_load
94
+
95
+ # Identity cache: track_id -> Node instance
96
+ self._cache: dict[str, Node] = {}
97
+
98
+ # Initialize database connection
99
+ self._db = HtmlGraphDB(str(self._db_path))
100
+ self._db.connect()
101
+ self._db.create_tables()
102
+
103
+ # Disable foreign key constraints for testing
104
+ if self._db.connection:
105
+ self._db.connection.execute("PRAGMA foreign_keys = OFF")
106
+
107
+ def _get_connection(self) -> sqlite3.Connection:
108
+ """Get database connection."""
109
+ if not self._db.connection:
110
+ self._db.connect()
111
+ assert self._db.connection is not None
112
+ return self._db.connection
113
+
114
+ def _generate_id(self) -> str:
115
+ """Generate unique track ID."""
116
+ import uuid
117
+
118
+ return f"trk-{uuid.uuid4().hex[:8]}"
119
+
120
+ def _validate_track(self, track: Any) -> None:
121
+ """Validate track object."""
122
+ if not hasattr(track, "id"):
123
+ raise TrackValidationError("Track must have 'id' attribute")
124
+ if not hasattr(track, "title"):
125
+ raise TrackValidationError("Track must have 'title' attribute")
126
+ if not track.id or not str(track.id).strip():
127
+ raise TrackValidationError("Track ID cannot be empty")
128
+ if not track.title or not str(track.title).strip():
129
+ raise TrackValidationError("Track title cannot be empty")
130
+
131
+ def _row_to_node(self, row: sqlite3.Row) -> Node:
132
+ """Convert database row to Node object."""
133
+ # Parse metadata JSON
134
+ metadata = json.loads(row["metadata"]) if row["metadata"] else {}
135
+
136
+ # Map database status to Node status (handle legacy values)
137
+ db_status = row["status"] or "todo"
138
+ status_map: dict[str, str] = {
139
+ "in_progress": "in-progress",
140
+ "cancelled": "done",
141
+ "planned": "todo",
142
+ "completed": "done",
143
+ }
144
+ status = status_map.get(db_status, db_status)
145
+
146
+ # Cast to valid status literal - Node validates this
147
+ from typing import Literal, cast
148
+
149
+ valid_status = cast(
150
+ Literal[
151
+ "todo", "in-progress", "blocked", "done", "active", "ended", "stale"
152
+ ],
153
+ status,
154
+ )
155
+
156
+ # Create Node object
157
+ node = Node(
158
+ id=row["id"],
159
+ title=row["title"],
160
+ type=row["type"] or "track",
161
+ status=valid_status,
162
+ priority=row["priority"] or "medium",
163
+ created=datetime.fromisoformat(row["created_at"])
164
+ if row["created_at"]
165
+ else datetime.now(),
166
+ updated=datetime.fromisoformat(row["updated_at"])
167
+ if row["updated_at"]
168
+ else datetime.now(),
169
+ content=row["description"] or "",
170
+ properties=metadata,
171
+ )
172
+
173
+ return node
174
+
175
+ def _node_to_dict(self, track: Node) -> dict[str, Any]:
176
+ """Convert Node object to database dict."""
177
+ # Extract track-specific metadata
178
+ metadata = (
179
+ dict(track.properties)
180
+ if hasattr(track, "properties") and track.properties
181
+ else {}
182
+ )
183
+
184
+ if hasattr(track, "has_spec"):
185
+ metadata["has_spec"] = track.has_spec
186
+ if hasattr(track, "has_plan"):
187
+ metadata["has_plan"] = track.has_plan
188
+ if hasattr(track, "features"):
189
+ metadata["features"] = track.features
190
+ if hasattr(track, "phases"):
191
+ metadata["phases"] = track.phases
192
+
193
+ return {
194
+ "id": track.id,
195
+ "type": track.type,
196
+ "title": track.title,
197
+ "description": track.content if hasattr(track, "content") else "",
198
+ "status": track.status,
199
+ "priority": track.priority,
200
+ "created_at": track.created.isoformat()
201
+ if hasattr(track, "created")
202
+ else datetime.now().isoformat(),
203
+ "updated_at": track.updated.isoformat()
204
+ if hasattr(track, "updated")
205
+ else datetime.now().isoformat(),
206
+ "metadata": json.dumps(metadata),
207
+ }
208
+
209
+ # ===== READ OPERATIONS =====
210
+
211
+ def get(self, track_id: str) -> Node | None:
212
+ """
213
+ Get single track by ID.
214
+
215
+ Returns same object instance for multiple calls (identity caching).
216
+
217
+ Args:
218
+ track_id: Track ID to retrieve
219
+
220
+ Returns:
221
+ Track object if found, None if not found
222
+
223
+ Raises:
224
+ ValueError: If track_id is invalid format
225
+
226
+ Performance: O(1) if cached, O(log n) from database
227
+ """
228
+ if not track_id or not isinstance(track_id, str):
229
+ raise ValueError(f"Invalid track_id: {track_id}")
230
+
231
+ # Check cache first
232
+ if track_id in self._cache:
233
+ return self._cache[track_id]
234
+
235
+ # Query database
236
+ conn = self._get_connection()
237
+ cursor = conn.execute("SELECT * FROM tracks WHERE id = ?", (track_id,))
238
+ row = cursor.fetchone()
239
+
240
+ if not row:
241
+ return None
242
+
243
+ # Convert to Node and cache
244
+ track = self._row_to_node(row)
245
+ self._cache[track_id] = track
246
+ return track
247
+
248
+ def list(self, filters: dict[str, Any] | None = None) -> list[Node]:
249
+ """
250
+ List all tracks with optional filters.
251
+
252
+ Args:
253
+ filters: Optional dict of attribute->value filters
254
+
255
+ Returns:
256
+ List of Track objects (empty list if no matches)
257
+
258
+ Raises:
259
+ TrackValidationError: If filter keys are invalid
260
+
261
+ Performance: O(n) with SQL WHERE clauses
262
+ """
263
+ if filters:
264
+ # Validate filter keys
265
+ valid_attrs = {
266
+ "status",
267
+ "priority",
268
+ "has_spec",
269
+ "has_plan",
270
+ "type",
271
+ "title",
272
+ "id",
273
+ "created",
274
+ "updated",
275
+ }
276
+ for key in filters:
277
+ if key not in valid_attrs:
278
+ raise TrackValidationError(f"Invalid filter attribute: {key}")
279
+
280
+ # Build SQL query
281
+ query = "SELECT * FROM tracks"
282
+ params = []
283
+
284
+ if filters:
285
+ where_clauses = []
286
+ for key, value in filters.items():
287
+ if key in ["status", "priority", "type", "title", "id"]:
288
+ where_clauses.append(f"{key} = ?")
289
+ params.append(value)
290
+ elif key in ["has_spec", "has_plan"]:
291
+ # Query JSON metadata
292
+ where_clauses.append(f"json_extract(metadata, '$.{key}') = ?")
293
+ params.append(1 if value else 0)
294
+
295
+ if where_clauses:
296
+ query += " WHERE " + " AND ".join(where_clauses)
297
+
298
+ # Execute query
299
+ conn = self._get_connection()
300
+ cursor = conn.execute(query, params)
301
+ rows = cursor.fetchall()
302
+
303
+ # Convert to Node objects
304
+ results = []
305
+ for row in rows:
306
+ track_id = row["id"]
307
+ if track_id in self._cache:
308
+ results.append(self._cache[track_id])
309
+ else:
310
+ track = self._row_to_node(row)
311
+ self._cache[track_id] = track
312
+ results.append(track)
313
+
314
+ return results
315
+
316
+ def where(self, **kwargs: Any) -> RepositoryQuery:
317
+ """
318
+ Build a filtered query with chaining support.
319
+
320
+ Args:
321
+ **kwargs: Attribute->value filter pairs
322
+
323
+ Returns:
324
+ RepositoryQuery object that can be further filtered
325
+
326
+ Raises:
327
+ TrackValidationError: If invalid attribute names
328
+ """
329
+ # Validate filter keys upfront
330
+ valid_attrs = {
331
+ "status",
332
+ "priority",
333
+ "has_spec",
334
+ "has_plan",
335
+ "type",
336
+ "title",
337
+ "id",
338
+ "created",
339
+ "updated",
340
+ }
341
+ for key in kwargs:
342
+ if key not in valid_attrs:
343
+ raise TrackValidationError(f"Invalid filter attribute: {key}")
344
+ return SQLiteRepositoryQuery(self, kwargs)
345
+
346
+ def by_status(self, status: str) -> builtins.list[Node]:
347
+ """Filter tracks by status."""
348
+ return self.list({"status": status})
349
+
350
+ def by_priority(self, priority: str) -> builtins.list[Node]:
351
+ """Filter tracks by priority."""
352
+ return self.list({"priority": priority})
353
+
354
+ def active_tracks(self) -> builtins.list[Node]:
355
+ """Get all tracks currently in progress."""
356
+ return self.by_status("active")
357
+
358
+ def batch_get(self, track_ids: builtins.list[str]) -> builtins.list[Node]:
359
+ """
360
+ Bulk retrieve multiple tracks.
361
+
362
+ Args:
363
+ track_ids: List of track IDs
364
+
365
+ Returns:
366
+ List of found tracks
367
+
368
+ Raises:
369
+ ValueError: If track_ids is not a list
370
+
371
+ Performance: O(k) where k = batch size
372
+ """
373
+ if not isinstance(track_ids, list):
374
+ raise ValueError("track_ids must be a list")
375
+
376
+ results = []
377
+ for tid in track_ids:
378
+ track = self.get(tid)
379
+ if track:
380
+ results.append(track)
381
+ return results
382
+
383
+ # ===== WRITE OPERATIONS =====
384
+
385
+ def create(self, title: str, **kwargs: Any) -> Node:
386
+ """
387
+ Create new track.
388
+
389
+ Args:
390
+ title: Track title (required)
391
+ **kwargs: Additional properties
392
+
393
+ Returns:
394
+ Created Track object (with generated ID)
395
+
396
+ Raises:
397
+ TrackValidationError: If invalid data provided
398
+
399
+ Performance: O(1)
400
+ """
401
+ if not title or not title.strip():
402
+ raise TrackValidationError("Track title cannot be empty")
403
+
404
+ # Generate ID if not provided
405
+ track_id = kwargs.pop("id", None) or self._generate_id()
406
+
407
+ # Extract known fields
408
+ node_type = kwargs.pop("type", "track")
409
+ status = kwargs.pop("status", "todo")
410
+ priority = kwargs.pop("priority", "medium")
411
+ created = kwargs.pop("created", datetime.now())
412
+ updated = kwargs.pop("updated", datetime.now())
413
+
414
+ # Remove title from kwargs if present
415
+ kwargs.pop("title", None)
416
+
417
+ # Create Node object
418
+ track = Node(
419
+ id=track_id,
420
+ title=title,
421
+ type=node_type,
422
+ status=status,
423
+ priority=priority,
424
+ created=created,
425
+ updated=updated,
426
+ **kwargs,
427
+ )
428
+
429
+ # Validate and save
430
+ self._validate_track(track)
431
+ self.save(track)
432
+
433
+ return track
434
+
435
+ def save(self, track: Node) -> Node:
436
+ """
437
+ Save existing track (update or insert).
438
+
439
+ Args:
440
+ track: Track object to save
441
+
442
+ Returns:
443
+ Saved track (same instance)
444
+
445
+ Raises:
446
+ TrackValidationError: If track is invalid
447
+
448
+ Performance: O(1)
449
+ """
450
+ self._validate_track(track)
451
+
452
+ # Update timestamp
453
+ track.updated = datetime.now()
454
+
455
+ # Convert to dict
456
+ data = self._node_to_dict(track)
457
+
458
+ # Upsert into database
459
+ conn = self._get_connection()
460
+ conn.execute(
461
+ """
462
+ INSERT OR REPLACE INTO tracks
463
+ (id, type, title, description, status, priority, created_at, updated_at, metadata)
464
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
465
+ """,
466
+ (
467
+ data["id"],
468
+ data["type"],
469
+ data["title"],
470
+ data["description"],
471
+ data["status"],
472
+ data["priority"],
473
+ data["created_at"],
474
+ data["updated_at"],
475
+ data["metadata"],
476
+ ),
477
+ )
478
+ conn.commit()
479
+
480
+ # Update cache
481
+ self._cache[track.id] = track
482
+
483
+ return track
484
+
485
+ def batch_update(
486
+ self, track_ids: builtins.list[str], updates: dict[str, Any]
487
+ ) -> int:
488
+ """
489
+ Vectorized batch update operation.
490
+
491
+ Args:
492
+ track_ids: List of track IDs to update
493
+ updates: Dict of attribute->value to set
494
+
495
+ Returns:
496
+ Number of tracks successfully updated
497
+
498
+ Raises:
499
+ TrackValidationError: If invalid updates
500
+
501
+ Performance: O(k) vectorized SQL where k = batch size
502
+ """
503
+ if not isinstance(track_ids, list):
504
+ raise ValueError("track_ids must be a list")
505
+ if not isinstance(updates, dict):
506
+ raise TrackValidationError("updates must be a dict")
507
+
508
+ count = 0
509
+ for tid in track_ids:
510
+ track = self.get(tid)
511
+ if track:
512
+ # Apply updates
513
+ for key, value in updates.items():
514
+ setattr(track, key, value)
515
+ self.save(track)
516
+ count += 1
517
+
518
+ return count
519
+
520
+ def delete(self, track_id: str) -> bool:
521
+ """
522
+ Delete a track by ID.
523
+
524
+ Args:
525
+ track_id: Track ID to delete
526
+
527
+ Returns:
528
+ True if deleted, False if not found
529
+
530
+ Performance: O(1)
531
+ """
532
+ if not track_id:
533
+ raise TrackValidationError("track_id cannot be empty")
534
+
535
+ # Delete from database
536
+ conn = self._get_connection()
537
+ cursor = conn.execute("DELETE FROM tracks WHERE id = ?", (track_id,))
538
+ conn.commit()
539
+
540
+ # Remove from cache
541
+ self._cache.pop(track_id, None)
542
+
543
+ return cursor.rowcount > 0
544
+
545
+ def batch_delete(self, track_ids: builtins.list[str]) -> int:
546
+ """
547
+ Delete multiple tracks.
548
+
549
+ Args:
550
+ track_ids: List of track IDs to delete
551
+
552
+ Returns:
553
+ Number of tracks successfully deleted
554
+
555
+ Performance: O(k) where k = batch size
556
+ """
557
+ if not isinstance(track_ids, list):
558
+ raise ValueError("track_ids must be a list")
559
+
560
+ count = 0
561
+ for tid in track_ids:
562
+ if self.delete(tid):
563
+ count += 1
564
+ return count
565
+
566
+ # ===== ADVANCED QUERIES =====
567
+
568
+ def find_by_features(self, feature_ids: builtins.list[str]) -> builtins.list[Node]:
569
+ """
570
+ Find tracks containing any of the specified features.
571
+
572
+ Args:
573
+ feature_ids: List of feature IDs to search for
574
+
575
+ Returns:
576
+ Tracks that contain at least one of these features
577
+
578
+ Raises:
579
+ ValueError: If feature_ids is not a list
580
+
581
+ Performance: O(n) with JSON queries
582
+ """
583
+ if not isinstance(feature_ids, list):
584
+ raise ValueError("feature_ids must be a list")
585
+
586
+ # Query tracks with features in metadata
587
+ conn = self._get_connection()
588
+
589
+ # Build query to check if any feature_id is in the features array
590
+ results = []
591
+ for feature_id in feature_ids:
592
+ cursor = conn.execute(
593
+ """
594
+ SELECT * FROM tracks
595
+ WHERE json_extract(metadata, '$.features') LIKE ?
596
+ """,
597
+ (f"%{feature_id}%",),
598
+ )
599
+
600
+ for row in cursor.fetchall():
601
+ track_id = row["id"]
602
+ if track_id in self._cache:
603
+ track = self._cache[track_id]
604
+ else:
605
+ track = self._row_to_node(row)
606
+ self._cache[track_id] = track
607
+
608
+ if track not in results:
609
+ results.append(track)
610
+
611
+ return results
612
+
613
+ def with_feature_count(self) -> builtins.list[Node]:
614
+ """
615
+ Get all tracks with feature count calculated.
616
+
617
+ Returns:
618
+ All tracks with feature_count property set
619
+ """
620
+ return self.list()
621
+
622
+ def filter(self, predicate: Callable[[Node], bool]) -> builtins.list[Node]:
623
+ """
624
+ Filter tracks with custom predicate function.
625
+
626
+ Args:
627
+ predicate: Function that takes Track and returns True/False
628
+
629
+ Returns:
630
+ Tracks matching predicate
631
+ """
632
+ all_tracks = self.list()
633
+ return [t for t in all_tracks if predicate(t)]
634
+
635
+ # ===== CACHE/LIFECYCLE MANAGEMENT =====
636
+
637
+ def invalidate_cache(self, track_id: str | None = None) -> None:
638
+ """
639
+ Invalidate cache for single track or all tracks.
640
+
641
+ Forces reload from storage on next access.
642
+
643
+ Args:
644
+ track_id: Specific track to invalidate, or None for all
645
+ """
646
+ if track_id:
647
+ self._cache.pop(track_id, None)
648
+ else:
649
+ self._cache.clear()
650
+
651
+ def reload(self) -> None:
652
+ """
653
+ Force reload all tracks from storage.
654
+
655
+ Invalidates all caches and reloads from database.
656
+ """
657
+ self._cache.clear()
658
+ # Database is always up-to-date, no need to reload
659
+
660
+ @property
661
+ def auto_load(self) -> bool:
662
+ """Whether auto-loading is enabled."""
663
+ return self._auto_load
664
+
665
+ @auto_load.setter
666
+ def auto_load(self, enabled: bool) -> None:
667
+ """Enable/disable auto-loading."""
668
+ self._auto_load = enabled
669
+
670
+ # ===== UTILITY METHODS =====
671
+
672
+ def count(self, filters: dict[str, Any] | None = None) -> int:
673
+ """
674
+ Count tracks matching filters.
675
+
676
+ Args:
677
+ filters: Optional filters
678
+
679
+ Returns:
680
+ Number of matching tracks
681
+
682
+ Performance: O(1) with SQL COUNT, O(n) with filters
683
+ """
684
+ if not filters:
685
+ conn = self._get_connection()
686
+ cursor = conn.execute("SELECT COUNT(*) FROM tracks")
687
+ result = cursor.fetchone()[0]
688
+ return int(result)
689
+
690
+ return len(self.list(filters))
691
+
692
+ def exists(self, track_id: str) -> bool:
693
+ """
694
+ Check if track exists without loading it.
695
+
696
+ Args:
697
+ track_id: Track ID to check
698
+
699
+ Returns:
700
+ True if exists, False otherwise
701
+
702
+ Performance: O(1)
703
+ """
704
+ # Check cache first
705
+ if track_id in self._cache:
706
+ return True
707
+
708
+ # Check database
709
+ conn = self._get_connection()
710
+ cursor = conn.execute("SELECT 1 FROM tracks WHERE id = ? LIMIT 1", (track_id,))
711
+ return cursor.fetchone() is not None