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
@@ -0,0 +1,858 @@
1
+ """
2
+ SQLiteFeatureRepository - SQLite database-based Feature storage.
3
+
4
+ Stores features 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 Edge, Node, Step
17
+ from htmlgraph.repositories.feature_repository import (
18
+ FeatureNotFoundError,
19
+ FeatureRepository,
20
+ FeatureValidationError,
21
+ RepositoryQuery,
22
+ )
23
+
24
+
25
+ class SQLiteRepositoryQuery(RepositoryQuery):
26
+ """Query builder for SQLite filtering."""
27
+
28
+ def __init__(self, repo: "SQLiteFeatureRepository", filters: dict[str, Any]):
29
+ super().__init__(filters)
30
+ self._repo = repo
31
+
32
+ def where(self, **kwargs: Any) -> "SQLiteRepositoryQuery":
33
+ """Chain additional filters."""
34
+ # Validate filter keys
35
+ valid_attrs = {
36
+ "status",
37
+ "priority",
38
+ "track_id",
39
+ "assigned_to",
40
+ "type",
41
+ "title",
42
+ "id",
43
+ "created",
44
+ "updated",
45
+ }
46
+ for key in kwargs:
47
+ if key not in valid_attrs:
48
+ raise FeatureValidationError(f"Invalid filter attribute: {key}")
49
+
50
+ # Merge filters
51
+ new_filters = {**self.filters, **kwargs}
52
+ return SQLiteRepositoryQuery(self._repo, new_filters)
53
+
54
+ def execute(self) -> list[Any]:
55
+ """Execute the query and return results."""
56
+ return self._repo.list(self.filters)
57
+
58
+
59
+ class SQLiteFeatureRepository(FeatureRepository):
60
+ """
61
+ SQLite database-based FeatureRepository implementation.
62
+
63
+ Stores features in a SQLite database for fast queries and transactions.
64
+ Uses parameterized queries to prevent SQL injection.
65
+
66
+ Database schema:
67
+ Table: features
68
+ Columns: id, type, title, description, status, priority,
69
+ assigned_to, track_id, created_at, updated_at, completed_at,
70
+ steps_total, steps_completed, metadata (JSON)
71
+
72
+ Performance:
73
+ - get(id): O(1) with cache, O(log n) from database (indexed)
74
+ - list(): O(n) with SQL WHERE clauses
75
+ - batch operations: O(k) vectorized SQL
76
+
77
+ Example:
78
+ >>> db_path = Path(".htmlgraph/htmlgraph.db")
79
+ >>> repo = SQLiteFeatureRepository(db_path)
80
+ >>> feature = repo.create("User Authentication", priority="high")
81
+ >>> feature.status = "in-progress"
82
+ >>> repo.save(feature)
83
+ """
84
+
85
+ def __init__(self, db_path: Path | str, auto_load: bool = True):
86
+ """
87
+ Initialize SQLite repository.
88
+
89
+ Args:
90
+ db_path: Path to SQLite database file
91
+ auto_load: Whether to enable auto-loading (always True for DB)
92
+ """
93
+ self._db_path = Path(db_path)
94
+ self._auto_load = auto_load
95
+
96
+ # Identity cache: feature_id -> Node instance
97
+ self._cache: dict[str, Node] = {}
98
+
99
+ # Initialize database connection
100
+ self._db = HtmlGraphDB(str(self._db_path))
101
+ self._db.connect()
102
+ self._db.create_tables()
103
+
104
+ # Disable foreign key constraints for testing
105
+ # (allows inserting features with track_ids that don't exist)
106
+ if self._db.connection:
107
+ self._db.connection.execute("PRAGMA foreign_keys = OFF")
108
+
109
+ def _get_connection(self) -> sqlite3.Connection:
110
+ """Get database connection."""
111
+ if not self._db.connection:
112
+ self._db.connect()
113
+ assert self._db.connection is not None
114
+ return self._db.connection
115
+
116
+ def _row_to_node(self, row: sqlite3.Row) -> Node:
117
+ """Convert database row to Node object."""
118
+ # Parse JSON fields
119
+ metadata = json.loads(row["metadata"]) if row["metadata"] else {}
120
+ json.loads(row["tags"]) if row["tags"] else []
121
+
122
+ # Extract steps from metadata
123
+ steps_data = metadata.get("steps", [])
124
+ steps = [
125
+ Step(
126
+ description=s.get("description", ""),
127
+ completed=s.get("completed", False),
128
+ agent=s.get("agent"),
129
+ )
130
+ for s in steps_data
131
+ ]
132
+
133
+ # Extract edges from metadata
134
+ edges_data = metadata.get("edges", {})
135
+ edges = {}
136
+ for rel_type, edge_list in edges_data.items():
137
+ edges[rel_type] = [
138
+ Edge(
139
+ target_id=e.get("target_id", ""),
140
+ relationship=e.get("relationship", rel_type),
141
+ title=e.get("title"),
142
+ since=e.get("since"),
143
+ properties=e.get("properties", {}),
144
+ )
145
+ for e in edge_list
146
+ ]
147
+
148
+ # Create Node
149
+ node = Node(
150
+ id=row["id"],
151
+ title=row["title"],
152
+ type=row["type"],
153
+ status=row["status"],
154
+ priority=row["priority"],
155
+ created=datetime.fromisoformat(row["created_at"])
156
+ if row["created_at"]
157
+ else datetime.now(),
158
+ updated=datetime.fromisoformat(row["updated_at"])
159
+ if row["updated_at"]
160
+ else datetime.now(),
161
+ content=row["description"] or "",
162
+ agent_assigned=row["assigned_to"],
163
+ track_id=row["track_id"],
164
+ steps=steps,
165
+ edges=edges,
166
+ properties=metadata.get("properties", {}),
167
+ )
168
+
169
+ return node
170
+
171
+ def _node_to_row(self, node: Node) -> dict[str, Any]:
172
+ """Convert Node object to database row dict."""
173
+ # Serialize steps
174
+ steps_data = [
175
+ {"description": s.description, "completed": s.completed, "agent": s.agent}
176
+ for s in (node.steps or [])
177
+ ]
178
+
179
+ # Serialize edges (handle both Edge objects and dicts)
180
+ edges_data = {}
181
+ for rel_type, edge_list in (node.edges or {}).items():
182
+ serialized_edges = []
183
+ for e in edge_list:
184
+ if isinstance(e, dict):
185
+ # Already a dict, use as-is with defaults
186
+ serialized_edges.append(
187
+ {
188
+ "target_id": e.get("target_id"),
189
+ "relationship": e.get("relationship", rel_type),
190
+ "title": e.get("title", ""),
191
+ "since": e.get("since"),
192
+ "properties": e.get("properties", {}),
193
+ }
194
+ )
195
+ else:
196
+ # Edge object
197
+ serialized_edges.append(
198
+ {
199
+ "target_id": e.target_id,
200
+ "relationship": e.relationship,
201
+ "title": e.title,
202
+ "since": e.since.isoformat() if e.since else None,
203
+ "properties": e.properties,
204
+ }
205
+ )
206
+ edges_data[rel_type] = serialized_edges
207
+
208
+ # Build metadata
209
+ metadata = {
210
+ "steps": steps_data,
211
+ "edges": edges_data,
212
+ "properties": node.properties or {},
213
+ }
214
+
215
+ return {
216
+ "id": node.id,
217
+ "type": node.type,
218
+ "title": node.title,
219
+ "description": node.content,
220
+ "status": node.status,
221
+ "priority": node.priority,
222
+ "assigned_to": node.agent_assigned,
223
+ "track_id": node.track_id,
224
+ "created_at": node.created.isoformat()
225
+ if node.created
226
+ else datetime.now().isoformat(),
227
+ "updated_at": node.updated.isoformat()
228
+ if node.updated
229
+ else datetime.now().isoformat(),
230
+ "completed_at": None, # TODO: extract from metadata
231
+ "steps_total": len(node.steps) if node.steps else 0,
232
+ "steps_completed": sum(1 for s in (node.steps or []) if s.completed),
233
+ "tags": json.dumps([]),
234
+ "metadata": json.dumps(metadata),
235
+ }
236
+
237
+ def _generate_id(self) -> str:
238
+ """Generate unique feature ID."""
239
+ import uuid
240
+
241
+ return f"feat-{uuid.uuid4().hex[:8]}"
242
+
243
+ def _validate_feature(self, feature: Any) -> None:
244
+ """Validate feature object."""
245
+ if not hasattr(feature, "id"):
246
+ raise FeatureValidationError("Feature must have 'id' attribute")
247
+ if not hasattr(feature, "title"):
248
+ raise FeatureValidationError("Feature must have 'title' attribute")
249
+ if not feature.id or not str(feature.id).strip():
250
+ raise FeatureValidationError("Feature ID cannot be empty")
251
+ if not feature.title or not str(feature.title).strip():
252
+ raise FeatureValidationError("Feature title cannot be empty")
253
+
254
+ def _build_where_clause(self, filters: dict[str, Any]) -> tuple[str, list]:
255
+ """Build SQL WHERE clause from filters."""
256
+ if not filters:
257
+ return "", []
258
+
259
+ conditions = []
260
+ params = []
261
+
262
+ # Map filter keys to database columns
263
+ column_map = {
264
+ "status": "status",
265
+ "priority": "priority",
266
+ "track_id": "track_id",
267
+ "agent_assigned": "assigned_to",
268
+ "type": "type",
269
+ "title": "title",
270
+ "id": "id",
271
+ }
272
+
273
+ for key, value in filters.items():
274
+ if key in column_map:
275
+ conditions.append(f"{column_map[key]} = ?")
276
+ params.append(value)
277
+
278
+ where_clause = " AND ".join(conditions)
279
+ return f"WHERE {where_clause}" if where_clause else "", params
280
+
281
+ # ===== READ OPERATIONS =====
282
+
283
+ def get(self, feature_id: str) -> Node | None:
284
+ """
285
+ Get single feature by ID.
286
+
287
+ Returns same object instance for multiple calls (identity caching).
288
+
289
+ Args:
290
+ feature_id: Feature ID to retrieve
291
+
292
+ Returns:
293
+ Feature object if found, None if not found
294
+
295
+ Raises:
296
+ ValueError: If feature_id is invalid format
297
+
298
+ Performance: O(1) with cache, O(log n) from database
299
+
300
+ Examples:
301
+ >>> feature = repo.get("feat-001")
302
+ >>> feature2 = repo.get("feat-001")
303
+ >>> assert feature is feature2 # Same instance
304
+ """
305
+ if not feature_id or not isinstance(feature_id, str):
306
+ raise ValueError(f"Invalid feature_id: {feature_id}")
307
+
308
+ # Check cache first
309
+ if feature_id in self._cache:
310
+ return self._cache[feature_id]
311
+
312
+ # Query database
313
+ conn = self._get_connection()
314
+ cursor = conn.execute("SELECT * FROM features WHERE id = ?", (feature_id,))
315
+ row = cursor.fetchone()
316
+
317
+ if not row:
318
+ return None
319
+
320
+ # Convert to Node and cache
321
+ node = self._row_to_node(row)
322
+ self._cache[feature_id] = node
323
+ return node
324
+
325
+ def list(self, filters: dict[str, Any] | None = None) -> list[Node]:
326
+ """
327
+ List all features with optional filters.
328
+
329
+ Args:
330
+ filters: Optional dict of attribute->value filters
331
+
332
+ Returns:
333
+ List of Feature objects (empty list if no matches)
334
+
335
+ Raises:
336
+ FeatureValidationError: If filter keys are invalid
337
+
338
+ Performance: O(n) with SQL WHERE clauses
339
+
340
+ Examples:
341
+ >>> all_features = repo.list()
342
+ >>> todo_features = repo.list({"status": "todo"})
343
+ """
344
+ if filters:
345
+ # Validate filter keys
346
+ valid_attrs = {
347
+ "status",
348
+ "priority",
349
+ "track_id",
350
+ "agent_assigned",
351
+ "type",
352
+ "title",
353
+ "id",
354
+ "created",
355
+ "updated",
356
+ }
357
+ for key in filters:
358
+ if key not in valid_attrs:
359
+ raise FeatureValidationError(f"Invalid filter attribute: {key}")
360
+
361
+ # Build query
362
+ where_clause, params = self._build_where_clause(filters or {})
363
+ sql = f"SELECT * FROM features {where_clause} ORDER BY created_at DESC"
364
+
365
+ # Execute query
366
+ conn = self._get_connection()
367
+ cursor = conn.execute(sql, params)
368
+
369
+ # Convert rows to nodes
370
+ results = []
371
+ for row in cursor.fetchall():
372
+ node_id = row["id"]
373
+ if node_id in self._cache:
374
+ results.append(self._cache[node_id])
375
+ else:
376
+ node = self._row_to_node(row)
377
+ self._cache[node_id] = node
378
+ results.append(node)
379
+
380
+ return results
381
+
382
+ def where(self, **kwargs: Any) -> RepositoryQuery:
383
+ """Build a filtered query with chaining support."""
384
+ return SQLiteRepositoryQuery(self, kwargs)
385
+
386
+ def by_track(self, track_id: str) -> builtins.list[Node]:
387
+ """Get all features belonging to a track."""
388
+ if not track_id:
389
+ raise ValueError("track_id cannot be empty")
390
+ return self.list({"track_id": track_id})
391
+
392
+ def by_status(self, status: str) -> builtins.list[Node]:
393
+ """Filter features by status."""
394
+ return self.list({"status": status})
395
+
396
+ def by_priority(self, priority: str) -> builtins.list[Node]:
397
+ """Filter features by priority."""
398
+ return self.list({"priority": priority})
399
+
400
+ def by_assigned_to(self, agent: str) -> builtins.list[Node]:
401
+ """Get features assigned to an agent."""
402
+ return self.list({"agent_assigned": agent})
403
+
404
+ def batch_get(self, feature_ids: builtins.list[str]) -> builtins.list[Node]:
405
+ """
406
+ Bulk retrieve multiple features.
407
+
408
+ Args:
409
+ feature_ids: List of feature IDs
410
+
411
+ Returns:
412
+ List of found features
413
+
414
+ Raises:
415
+ ValueError: If feature_ids is not a list
416
+
417
+ Performance: O(k) where k = batch size
418
+ """
419
+ if not isinstance(feature_ids, list):
420
+ raise ValueError("feature_ids must be a list")
421
+
422
+ if not feature_ids:
423
+ return []
424
+
425
+ # Build IN clause
426
+ placeholders = ",".join("?" * len(feature_ids))
427
+ sql = f"SELECT * FROM features WHERE id IN ({placeholders})"
428
+
429
+ conn = self._get_connection()
430
+ cursor = conn.execute(sql, feature_ids)
431
+
432
+ results = []
433
+ for row in cursor.fetchall():
434
+ node_id = row["id"]
435
+ if node_id in self._cache:
436
+ results.append(self._cache[node_id])
437
+ else:
438
+ node = self._row_to_node(row)
439
+ self._cache[node_id] = node
440
+ results.append(node)
441
+
442
+ return results
443
+
444
+ # ===== WRITE OPERATIONS =====
445
+
446
+ def create(self, title: str, **kwargs: Any) -> Node:
447
+ """
448
+ Create new feature.
449
+
450
+ Args:
451
+ title: Feature title (required)
452
+ **kwargs: Additional properties
453
+
454
+ Returns:
455
+ Created Feature object (with generated ID)
456
+
457
+ Raises:
458
+ FeatureValidationError: If invalid data provided
459
+
460
+ Performance: O(1)
461
+ """
462
+ if not title or not title.strip():
463
+ raise FeatureValidationError("Feature title cannot be empty")
464
+
465
+ # Generate ID if not provided
466
+ feature_id = kwargs.pop("id", None) or self._generate_id()
467
+
468
+ # Extract known fields from kwargs to avoid conflicts
469
+ node_type = kwargs.pop("type", "feature")
470
+ status = kwargs.pop("status", "todo")
471
+ priority = kwargs.pop("priority", "medium")
472
+ created = kwargs.pop("created", datetime.now())
473
+ updated = kwargs.pop("updated", datetime.now())
474
+
475
+ # Remove title from kwargs if present (already have it as parameter)
476
+ kwargs.pop("title", None)
477
+
478
+ # Create Node object
479
+ feature = Node(
480
+ id=feature_id,
481
+ title=title,
482
+ type=node_type,
483
+ status=status,
484
+ priority=priority,
485
+ created=created,
486
+ updated=updated,
487
+ **kwargs,
488
+ )
489
+
490
+ # Validate and save
491
+ self._validate_feature(feature)
492
+ self.save(feature)
493
+
494
+ return feature
495
+
496
+ def save(self, feature: Node) -> Node:
497
+ """
498
+ Save existing feature (update or insert).
499
+
500
+ Args:
501
+ feature: Feature object to save
502
+
503
+ Returns:
504
+ Saved feature (same instance)
505
+
506
+ Raises:
507
+ FeatureValidationError: If feature is invalid
508
+
509
+ Performance: O(1)
510
+ """
511
+ self._validate_feature(feature)
512
+
513
+ # Update timestamp
514
+ feature.updated = datetime.now()
515
+
516
+ # Convert to row
517
+ row = self._node_to_row(feature)
518
+
519
+ # Insert or replace
520
+ conn = self._get_connection()
521
+ conn.execute(
522
+ """
523
+ INSERT OR REPLACE INTO features (
524
+ id, type, title, description, status, priority,
525
+ assigned_to, track_id, created_at, updated_at,
526
+ completed_at, steps_total, steps_completed,
527
+ tags, metadata
528
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
529
+ """,
530
+ (
531
+ row["id"],
532
+ row["type"],
533
+ row["title"],
534
+ row["description"],
535
+ row["status"],
536
+ row["priority"],
537
+ row["assigned_to"],
538
+ row["track_id"],
539
+ row["created_at"],
540
+ row["updated_at"],
541
+ row["completed_at"],
542
+ row["steps_total"],
543
+ row["steps_completed"],
544
+ row["tags"],
545
+ row["metadata"],
546
+ ),
547
+ )
548
+ conn.commit()
549
+
550
+ # Update cache
551
+ self._cache[feature.id] = feature
552
+
553
+ return feature
554
+
555
+ def batch_update(
556
+ self, feature_ids: builtins.list[str], updates: dict[str, Any]
557
+ ) -> int:
558
+ """
559
+ Vectorized batch update operation.
560
+
561
+ Args:
562
+ feature_ids: List of feature IDs to update
563
+ updates: Dict of attribute->value to set
564
+
565
+ Returns:
566
+ Number of features successfully updated
567
+
568
+ Raises:
569
+ FeatureValidationError: If invalid updates
570
+
571
+ Performance: O(k) vectorized
572
+ """
573
+ if not isinstance(feature_ids, list):
574
+ raise ValueError("feature_ids must be a list")
575
+ if not isinstance(updates, dict):
576
+ raise FeatureValidationError("updates must be a dict")
577
+
578
+ if not feature_ids:
579
+ return 0
580
+
581
+ # Map update keys to columns
582
+ column_map = {
583
+ "status": "status",
584
+ "priority": "priority",
585
+ "agent_assigned": "assigned_to",
586
+ "track_id": "track_id",
587
+ }
588
+
589
+ set_clauses = []
590
+ params = []
591
+ for key, value in updates.items():
592
+ if key in column_map:
593
+ set_clauses.append(f"{column_map[key]} = ?")
594
+ params.append(value)
595
+
596
+ if not set_clauses:
597
+ return 0
598
+
599
+ # Add updated_at
600
+ set_clauses.append("updated_at = ?")
601
+ params.append(datetime.now().isoformat())
602
+
603
+ # Add feature IDs
604
+ placeholders = ",".join("?" * len(feature_ids))
605
+ params.extend(feature_ids)
606
+
607
+ # Execute update
608
+ sql = f"""
609
+ UPDATE features
610
+ SET {", ".join(set_clauses)}
611
+ WHERE id IN ({placeholders})
612
+ """
613
+
614
+ conn = self._get_connection()
615
+ cursor = conn.execute(sql, params)
616
+ conn.commit()
617
+
618
+ # Invalidate cache for updated features
619
+ for fid in feature_ids:
620
+ self._cache.pop(fid, None)
621
+
622
+ return cursor.rowcount
623
+
624
+ def delete(self, feature_id: str) -> bool:
625
+ """
626
+ Delete a feature by ID.
627
+
628
+ Args:
629
+ feature_id: Feature ID to delete
630
+
631
+ Returns:
632
+ True if deleted, False if not found
633
+
634
+ Performance: O(1)
635
+ """
636
+ if not feature_id:
637
+ raise FeatureValidationError("feature_id cannot be empty")
638
+
639
+ conn = self._get_connection()
640
+ cursor = conn.execute("DELETE FROM features WHERE id = ?", (feature_id,))
641
+ conn.commit()
642
+
643
+ # Remove from cache
644
+ self._cache.pop(feature_id, None)
645
+
646
+ return cursor.rowcount > 0
647
+
648
+ def batch_delete(self, feature_ids: builtins.list[str]) -> int:
649
+ """
650
+ Delete multiple features.
651
+
652
+ Args:
653
+ feature_ids: List of feature IDs to delete
654
+
655
+ Returns:
656
+ Number of features successfully deleted
657
+
658
+ Performance: O(k) where k = batch size
659
+ """
660
+ if not isinstance(feature_ids, list):
661
+ raise ValueError("feature_ids must be a list")
662
+
663
+ if not feature_ids:
664
+ return 0
665
+
666
+ placeholders = ",".join("?" * len(feature_ids))
667
+ sql = f"DELETE FROM features WHERE id IN ({placeholders})"
668
+
669
+ conn = self._get_connection()
670
+ cursor = conn.execute(sql, feature_ids)
671
+ conn.commit()
672
+
673
+ # Remove from cache
674
+ for fid in feature_ids:
675
+ self._cache.pop(fid, None)
676
+
677
+ return cursor.rowcount
678
+
679
+ # ===== ADVANCED QUERIES =====
680
+
681
+ def find_dependencies(self, feature_id: str) -> builtins.list[Node]:
682
+ """
683
+ Find transitive feature dependencies.
684
+
685
+ Args:
686
+ feature_id: Feature to find dependencies for
687
+
688
+ Returns:
689
+ List of features this feature depends on
690
+
691
+ Raises:
692
+ FeatureNotFoundError: If feature not found
693
+
694
+ Performance: O(n) graph traversal
695
+ """
696
+ feature = self.get(feature_id)
697
+ if not feature:
698
+ raise FeatureNotFoundError(feature_id)
699
+
700
+ dependencies = []
701
+ visited = set()
702
+
703
+ def traverse(f: Node) -> None:
704
+ if f.id in visited:
705
+ return
706
+ visited.add(f.id)
707
+
708
+ # Check edges for dependencies
709
+ if hasattr(f, "edges") and f.edges:
710
+ depends_on = (
711
+ f.edges.get("depends_on", []) if isinstance(f.edges, dict) else []
712
+ )
713
+ for edge in depends_on:
714
+ target_id = (
715
+ edge.target_id
716
+ if hasattr(edge, "target_id")
717
+ else edge.get("target_id")
718
+ if isinstance(edge, dict)
719
+ else None
720
+ )
721
+ if target_id:
722
+ dep = self.get(target_id)
723
+ if dep and dep not in dependencies:
724
+ dependencies.append(dep)
725
+ traverse(dep)
726
+
727
+ traverse(feature)
728
+ return dependencies
729
+
730
+ def find_blocking(self, feature_id: str) -> builtins.list[Node]:
731
+ """
732
+ Find what blocks this feature.
733
+
734
+ Args:
735
+ feature_id: Feature to find blockers for
736
+
737
+ Returns:
738
+ Features that depend on this feature
739
+
740
+ Raises:
741
+ FeatureNotFoundError: If feature not found
742
+ """
743
+ feature = self.get(feature_id)
744
+ if not feature:
745
+ raise FeatureNotFoundError(feature_id)
746
+
747
+ # Query all features and check dependencies
748
+ all_features = self.list()
749
+ blocking = []
750
+
751
+ for f in all_features:
752
+ if hasattr(f, "edges") and f.edges:
753
+ depends_on = (
754
+ f.edges.get("depends_on", []) if isinstance(f.edges, dict) else []
755
+ )
756
+ for edge in depends_on:
757
+ target_id = (
758
+ edge.target_id
759
+ if hasattr(edge, "target_id")
760
+ else edge.get("target_id")
761
+ if isinstance(edge, dict)
762
+ else None
763
+ )
764
+ if target_id == feature_id:
765
+ blocking.append(f)
766
+
767
+ return blocking
768
+
769
+ def filter(self, predicate: Callable[[Node], bool]) -> builtins.list[Node]:
770
+ """
771
+ Filter features with custom predicate function.
772
+
773
+ Args:
774
+ predicate: Function that takes Feature and returns True/False
775
+
776
+ Returns:
777
+ Features matching predicate
778
+ """
779
+ all_features = self.list()
780
+ return [f for f in all_features if predicate(f)]
781
+
782
+ # ===== CACHE/LIFECYCLE MANAGEMENT =====
783
+
784
+ def invalidate_cache(self, feature_id: str | None = None) -> None:
785
+ """
786
+ Invalidate cache for single feature or all features.
787
+
788
+ Args:
789
+ feature_id: Specific feature to invalidate, or None for all
790
+ """
791
+ if feature_id:
792
+ self._cache.pop(feature_id, None)
793
+ else:
794
+ self._cache.clear()
795
+
796
+ def reload(self) -> None:
797
+ """
798
+ Force reload all features from storage.
799
+
800
+ Invalidates all caches and reloads from database.
801
+ """
802
+ self._cache.clear()
803
+ # Cache will be populated on next query
804
+
805
+ @property
806
+ def auto_load(self) -> bool:
807
+ """Whether auto-loading is enabled."""
808
+ return self._auto_load
809
+
810
+ @auto_load.setter
811
+ def auto_load(self, enabled: bool) -> None:
812
+ """Enable/disable auto-loading."""
813
+ self._auto_load = enabled
814
+
815
+ # ===== UTILITY METHODS =====
816
+
817
+ def count(self, filters: dict[str, Any] | None = None) -> int:
818
+ """
819
+ Count features matching filters.
820
+
821
+ Args:
822
+ filters: Optional filters
823
+
824
+ Returns:
825
+ Number of matching features
826
+
827
+ Performance: O(1) with SQL COUNT, O(n) without filters
828
+ """
829
+ where_clause, params = self._build_where_clause(filters or {})
830
+ sql = f"SELECT COUNT(*) FROM features {where_clause}"
831
+
832
+ conn = self._get_connection()
833
+ cursor = conn.execute(sql, params)
834
+ result = cursor.fetchone()[0]
835
+ return int(result)
836
+
837
+ def exists(self, feature_id: str) -> bool:
838
+ """
839
+ Check if feature exists without loading it.
840
+
841
+ Args:
842
+ feature_id: Feature ID to check
843
+
844
+ Returns:
845
+ True if exists, False otherwise
846
+
847
+ Performance: O(1)
848
+ """
849
+ # Check cache first
850
+ if feature_id in self._cache:
851
+ return True
852
+
853
+ # Check database
854
+ conn = self._get_connection()
855
+ cursor = conn.execute(
856
+ "SELECT 1 FROM features WHERE id = ? LIMIT 1", (feature_id,)
857
+ )
858
+ return cursor.fetchone() is not None