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,607 @@
1
+ """
2
+ MemoryFeatureRepository - In-memory Feature storage.
3
+
4
+ Pure in-memory implementation for testing and development.
5
+ All operations are O(1) or O(n) with no disk I/O.
6
+ """
7
+
8
+ import builtins
9
+ from collections.abc import Callable
10
+ from datetime import datetime
11
+ from typing import Any
12
+
13
+ from htmlgraph.models import Node
14
+ from htmlgraph.repositories.feature_repository import (
15
+ FeatureNotFoundError,
16
+ FeatureRepository,
17
+ FeatureValidationError,
18
+ RepositoryQuery,
19
+ )
20
+
21
+
22
+ class MemoryRepositoryQuery(RepositoryQuery):
23
+ """Query builder for in-memory filtering."""
24
+
25
+ def __init__(self, repo: "MemoryFeatureRepository", filters: dict[str, Any]):
26
+ super().__init__(filters)
27
+ self._repo = repo
28
+
29
+ def where(self, **kwargs: Any) -> "MemoryRepositoryQuery":
30
+ """Chain additional filters."""
31
+ # Validate filter keys
32
+ valid_attrs = {
33
+ "status",
34
+ "priority",
35
+ "track_id",
36
+ "agent_assigned",
37
+ "type",
38
+ "title",
39
+ "id",
40
+ "created",
41
+ "updated",
42
+ }
43
+ for key in kwargs:
44
+ if key not in valid_attrs:
45
+ raise FeatureValidationError(f"Invalid filter attribute: {key}")
46
+
47
+ # Merge filters
48
+ new_filters = {**self.filters, **kwargs}
49
+ return MemoryRepositoryQuery(self._repo, new_filters)
50
+
51
+ def execute(self) -> list[Any]:
52
+ """Execute the query and return results."""
53
+ return self._repo.list(self.filters)
54
+
55
+
56
+ class MemoryFeatureRepository(FeatureRepository):
57
+ """
58
+ In-memory FeatureRepository implementation.
59
+
60
+ Stores features in a dictionary with identity caching.
61
+ All operations are fast (O(1) or O(n)) with no disk I/O.
62
+
63
+ Perfect for testing and development.
64
+
65
+ Performance:
66
+ - get(id): O(1)
67
+ - list(): O(n)
68
+ - create/save/delete: O(1)
69
+ - All batch operations: O(k) where k = batch size
70
+
71
+ Example:
72
+ >>> repo = MemoryFeatureRepository()
73
+ >>> feature = repo.create("User Authentication", priority="high")
74
+ >>> feature.status = "in-progress"
75
+ >>> repo.save(feature)
76
+ >>> all_features = repo.list()
77
+ """
78
+
79
+ def __init__(self, auto_load: bool = True):
80
+ """
81
+ Initialize in-memory repository.
82
+
83
+ Args:
84
+ auto_load: Whether to enable auto-loading (always True for memory)
85
+ """
86
+ self._features: dict[str, Node] = {} # Identity cache
87
+ self._auto_load = auto_load
88
+ self._counter = 0 # For generating IDs
89
+
90
+ def _generate_id(self) -> str:
91
+ """Generate unique feature ID."""
92
+ import uuid
93
+
94
+ return f"feat-{uuid.uuid4().hex[:8]}"
95
+
96
+ def _validate_feature(self, feature: Any) -> None:
97
+ """Validate feature object."""
98
+ if not hasattr(feature, "id"):
99
+ raise FeatureValidationError("Feature must have 'id' attribute")
100
+ if not hasattr(feature, "title"):
101
+ raise FeatureValidationError("Feature must have 'title' attribute")
102
+ if not feature.id or not str(feature.id).strip():
103
+ raise FeatureValidationError("Feature ID cannot be empty")
104
+ if not feature.title or not str(feature.title).strip():
105
+ raise FeatureValidationError("Feature title cannot be empty")
106
+
107
+ def _matches_filters(self, feature: Node, filters: dict[str, Any]) -> bool:
108
+ """Check if feature matches all filters."""
109
+ if not filters:
110
+ return True
111
+
112
+ for key, value in filters.items():
113
+ if not hasattr(feature, key):
114
+ return False
115
+ if getattr(feature, key) != value:
116
+ return False
117
+ return True
118
+
119
+ # ===== READ OPERATIONS =====
120
+
121
+ def get(self, feature_id: str) -> Node | None:
122
+ """
123
+ Get single feature by ID.
124
+
125
+ Returns same object instance for multiple calls (identity caching).
126
+
127
+ Args:
128
+ feature_id: Feature ID to retrieve
129
+
130
+ Returns:
131
+ Feature object if found, None if not found
132
+
133
+ Raises:
134
+ ValueError: If feature_id is invalid format
135
+
136
+ Performance: O(1)
137
+
138
+ Examples:
139
+ >>> feature = repo.get("feat-001")
140
+ >>> feature2 = repo.get("feat-001")
141
+ >>> assert feature is feature2 # Same instance
142
+ """
143
+ if not feature_id or not isinstance(feature_id, str):
144
+ raise ValueError(f"Invalid feature_id: {feature_id}")
145
+
146
+ return self._features.get(feature_id)
147
+
148
+ def list(self, filters: dict[str, Any] | None = None) -> list[Node]:
149
+ """
150
+ List all features with optional filters.
151
+
152
+ Args:
153
+ filters: Optional dict of attribute->value filters
154
+
155
+ Returns:
156
+ List of Feature objects (empty list if no matches)
157
+
158
+ Raises:
159
+ FeatureValidationError: If filter keys are invalid
160
+
161
+ Performance: O(n) where n = total features
162
+
163
+ Examples:
164
+ >>> all_features = repo.list()
165
+ >>> todo_features = repo.list({"status": "todo"})
166
+ """
167
+ if filters:
168
+ # Validate filter keys
169
+ valid_attrs = {
170
+ "status",
171
+ "priority",
172
+ "track_id",
173
+ "agent_assigned",
174
+ "type",
175
+ "title",
176
+ "id",
177
+ "created",
178
+ "updated",
179
+ }
180
+ for key in filters:
181
+ if key not in valid_attrs:
182
+ raise FeatureValidationError(f"Invalid filter attribute: {key}")
183
+
184
+ results = []
185
+ for feature in self._features.values():
186
+ if self._matches_filters(feature, filters or {}):
187
+ results.append(feature)
188
+ return results
189
+
190
+ def where(self, **kwargs: Any) -> RepositoryQuery:
191
+ """
192
+ Build a filtered query with chaining support.
193
+
194
+ Args:
195
+ **kwargs: Attribute->value filter pairs
196
+
197
+ Returns:
198
+ RepositoryQuery object that can be further filtered
199
+
200
+ Raises:
201
+ FeatureValidationError: If invalid attribute names
202
+
203
+ Examples:
204
+ >>> query = repo.where(status='todo')
205
+ >>> results = query.where(priority='high').execute()
206
+ """
207
+ return MemoryRepositoryQuery(self, kwargs)
208
+
209
+ def by_track(self, track_id: str) -> builtins.list[Node]:
210
+ """
211
+ Get all features belonging to a track.
212
+
213
+ Args:
214
+ track_id: Track ID to filter by
215
+
216
+ Returns:
217
+ List of features in track
218
+
219
+ Performance: O(n)
220
+ """
221
+ if not track_id:
222
+ raise ValueError("track_id cannot be empty")
223
+ return self.list({"track_id": track_id})
224
+
225
+ def by_status(self, status: str) -> builtins.list[Node]:
226
+ """
227
+ Filter features by status.
228
+
229
+ Args:
230
+ status: Status to filter by
231
+
232
+ Returns:
233
+ List of matching features
234
+
235
+ Performance: O(n)
236
+ """
237
+ return self.list({"status": status})
238
+
239
+ def by_priority(self, priority: str) -> builtins.list[Node]:
240
+ """
241
+ Filter features by priority.
242
+
243
+ Args:
244
+ priority: Priority level
245
+
246
+ Returns:
247
+ List of matching features
248
+
249
+ Performance: O(n)
250
+ """
251
+ return self.list({"priority": priority})
252
+
253
+ def by_assigned_to(self, agent: str) -> builtins.list[Node]:
254
+ """
255
+ Get features assigned to an agent.
256
+
257
+ Args:
258
+ agent: Agent ID
259
+
260
+ Returns:
261
+ Features assigned to agent
262
+ """
263
+ return self.list({"agent_assigned": agent})
264
+
265
+ def batch_get(self, feature_ids: builtins.list[str]) -> builtins.list[Node]:
266
+ """
267
+ Bulk retrieve multiple features.
268
+
269
+ Args:
270
+ feature_ids: List of feature IDs
271
+
272
+ Returns:
273
+ List of found features (None for missing ones omitted)
274
+
275
+ Raises:
276
+ ValueError: If feature_ids is not a list
277
+
278
+ Performance: O(k) where k = batch size
279
+ """
280
+ if not isinstance(feature_ids, list):
281
+ raise ValueError("feature_ids must be a list")
282
+
283
+ results = []
284
+ for fid in feature_ids:
285
+ feature = self.get(fid)
286
+ if feature:
287
+ results.append(feature)
288
+ return results
289
+
290
+ # ===== WRITE OPERATIONS =====
291
+
292
+ def create(self, title: str, **kwargs: Any) -> Node:
293
+ """
294
+ Create new feature.
295
+
296
+ Args:
297
+ title: Feature title (required)
298
+ **kwargs: Additional properties
299
+
300
+ Returns:
301
+ Created Feature object (with generated ID)
302
+
303
+ Raises:
304
+ FeatureValidationError: If invalid data provided
305
+
306
+ Performance: O(1)
307
+ """
308
+ if not title or not title.strip():
309
+ raise FeatureValidationError("Feature title cannot be empty")
310
+
311
+ # Generate ID if not provided
312
+ feature_id = kwargs.pop("id", None) or self._generate_id()
313
+
314
+ # Extract known fields from kwargs to avoid conflicts
315
+ node_type = kwargs.pop("type", "feature")
316
+ status = kwargs.pop("status", "todo")
317
+ priority = kwargs.pop("priority", "medium")
318
+ created = kwargs.pop("created", datetime.now())
319
+ updated = kwargs.pop("updated", datetime.now())
320
+
321
+ # Remove title from kwargs if present (already have it as parameter)
322
+ kwargs.pop("title", None)
323
+
324
+ # Create Node object
325
+ feature = Node(
326
+ id=feature_id,
327
+ title=title,
328
+ type=node_type,
329
+ status=status,
330
+ priority=priority,
331
+ created=created,
332
+ updated=updated,
333
+ **kwargs,
334
+ )
335
+
336
+ # Validate and store
337
+ self._validate_feature(feature)
338
+ self._features[feature.id] = feature
339
+
340
+ return feature
341
+
342
+ def save(self, feature: Node) -> Node:
343
+ """
344
+ Save existing feature (update or insert).
345
+
346
+ Args:
347
+ feature: Feature object to save
348
+
349
+ Returns:
350
+ Saved feature (same instance)
351
+
352
+ Raises:
353
+ FeatureValidationError: If feature is invalid
354
+
355
+ Performance: O(1)
356
+ """
357
+ self._validate_feature(feature)
358
+
359
+ # Update timestamp
360
+ feature.updated = datetime.now()
361
+
362
+ # Store (updates existing or inserts new)
363
+ self._features[feature.id] = feature
364
+
365
+ return feature
366
+
367
+ def batch_update(
368
+ self, feature_ids: builtins.list[str], updates: dict[str, Any]
369
+ ) -> int:
370
+ """
371
+ Vectorized batch update operation.
372
+
373
+ Args:
374
+ feature_ids: List of feature IDs to update
375
+ updates: Dict of attribute->value to set
376
+
377
+ Returns:
378
+ Number of features successfully updated
379
+
380
+ Raises:
381
+ FeatureValidationError: If invalid updates
382
+
383
+ Performance: O(k) where k = batch size
384
+ """
385
+ if not isinstance(feature_ids, list):
386
+ raise ValueError("feature_ids must be a list")
387
+ if not isinstance(updates, dict):
388
+ raise FeatureValidationError("updates must be a dict")
389
+
390
+ count = 0
391
+ for fid in feature_ids:
392
+ feature = self.get(fid)
393
+ if feature:
394
+ # Apply updates
395
+ for key, value in updates.items():
396
+ setattr(feature, key, value)
397
+ feature.updated = datetime.now()
398
+ count += 1
399
+
400
+ return count
401
+
402
+ def delete(self, feature_id: str) -> bool:
403
+ """
404
+ Delete a feature by ID.
405
+
406
+ Args:
407
+ feature_id: Feature ID to delete
408
+
409
+ Returns:
410
+ True if deleted, False if not found
411
+
412
+ Performance: O(1)
413
+ """
414
+ if not feature_id:
415
+ raise FeatureValidationError("feature_id cannot be empty")
416
+
417
+ if feature_id in self._features:
418
+ del self._features[feature_id]
419
+ return True
420
+ return False
421
+
422
+ def batch_delete(self, feature_ids: builtins.list[str]) -> int:
423
+ """
424
+ Delete multiple features.
425
+
426
+ Args:
427
+ feature_ids: List of feature IDs to delete
428
+
429
+ Returns:
430
+ Number of features successfully deleted
431
+
432
+ Performance: O(k) where k = batch size
433
+ """
434
+ if not isinstance(feature_ids, list):
435
+ raise ValueError("feature_ids must be a list")
436
+
437
+ count = 0
438
+ for fid in feature_ids:
439
+ if self.delete(fid):
440
+ count += 1
441
+ return count
442
+
443
+ # ===== ADVANCED QUERIES =====
444
+
445
+ def find_dependencies(self, feature_id: str) -> builtins.list[Node]:
446
+ """
447
+ Find transitive feature dependencies.
448
+
449
+ Args:
450
+ feature_id: Feature to find dependencies for
451
+
452
+ Returns:
453
+ List of features this feature depends on
454
+
455
+ Raises:
456
+ FeatureNotFoundError: If feature not found
457
+
458
+ Performance: O(n) graph traversal
459
+ """
460
+ feature = self.get(feature_id)
461
+ if not feature:
462
+ raise FeatureNotFoundError(feature_id)
463
+
464
+ dependencies = []
465
+ visited = set()
466
+
467
+ def traverse(f: Node) -> None:
468
+ if f.id in visited:
469
+ return
470
+ visited.add(f.id)
471
+
472
+ # Check edges for dependencies
473
+ if hasattr(f, "edges") and f.edges:
474
+ depends_on = (
475
+ f.edges.get("depends_on", []) if isinstance(f.edges, dict) else []
476
+ )
477
+ for edge in depends_on:
478
+ target_id = (
479
+ edge.target_id
480
+ if hasattr(edge, "target_id")
481
+ else edge.get("target_id")
482
+ if isinstance(edge, dict)
483
+ else None
484
+ )
485
+ if target_id:
486
+ dep = self.get(target_id)
487
+ if dep and dep not in dependencies:
488
+ dependencies.append(dep)
489
+ traverse(dep)
490
+
491
+ traverse(feature)
492
+ return dependencies
493
+
494
+ def find_blocking(self, feature_id: str) -> builtins.list[Node]:
495
+ """
496
+ Find what blocks this feature.
497
+
498
+ Args:
499
+ feature_id: Feature to find blockers for
500
+
501
+ Returns:
502
+ Features that depend on this feature
503
+
504
+ Raises:
505
+ FeatureNotFoundError: If feature not found
506
+ """
507
+ feature = self.get(feature_id)
508
+ if not feature:
509
+ raise FeatureNotFoundError(feature_id)
510
+
511
+ blocking = []
512
+ for f in self._features.values():
513
+ if hasattr(f, "edges") and f.edges:
514
+ depends_on = (
515
+ f.edges.get("depends_on", []) if isinstance(f.edges, dict) else []
516
+ )
517
+ for edge in depends_on:
518
+ target_id = (
519
+ edge.target_id
520
+ if hasattr(edge, "target_id")
521
+ else edge.get("target_id")
522
+ if isinstance(edge, dict)
523
+ else None
524
+ )
525
+ if target_id == feature_id:
526
+ blocking.append(f)
527
+
528
+ return blocking
529
+
530
+ def filter(self, predicate: Callable[[Node], bool]) -> builtins.list[Node]:
531
+ """
532
+ Filter features with custom predicate function.
533
+
534
+ Args:
535
+ predicate: Function that takes Feature and returns True/False
536
+
537
+ Returns:
538
+ Features matching predicate
539
+ """
540
+ return [f for f in self._features.values() if predicate(f)]
541
+
542
+ # ===== CACHE/LIFECYCLE MANAGEMENT =====
543
+
544
+ def invalidate_cache(self, feature_id: str | None = None) -> None:
545
+ """
546
+ Invalidate cache for single feature or all features.
547
+
548
+ For memory repository, this is a no-op since we don't have
549
+ external storage to reload from.
550
+
551
+ Args:
552
+ feature_id: Specific feature to invalidate, or None for all
553
+ """
554
+ # No-op for memory repository
555
+ pass
556
+
557
+ def reload(self) -> None:
558
+ """
559
+ Force reload all features from storage.
560
+
561
+ For memory repository, this is a no-op since we don't have
562
+ external storage.
563
+ """
564
+ # No-op for memory repository
565
+ pass
566
+
567
+ @property
568
+ def auto_load(self) -> bool:
569
+ """Whether auto-loading is enabled."""
570
+ return self._auto_load
571
+
572
+ @auto_load.setter
573
+ def auto_load(self, enabled: bool) -> None:
574
+ """Enable/disable auto-loading."""
575
+ self._auto_load = enabled
576
+
577
+ # ===== UTILITY METHODS =====
578
+
579
+ def count(self, filters: dict[str, Any] | None = None) -> int:
580
+ """
581
+ Count features matching filters.
582
+
583
+ Args:
584
+ filters: Optional filters
585
+
586
+ Returns:
587
+ Number of matching features
588
+
589
+ Performance: O(n) or O(1) if no filters
590
+ """
591
+ if not filters:
592
+ return len(self._features)
593
+ return len(self.list(filters))
594
+
595
+ def exists(self, feature_id: str) -> bool:
596
+ """
597
+ Check if feature exists without loading it.
598
+
599
+ Args:
600
+ feature_id: Feature ID to check
601
+
602
+ Returns:
603
+ True if exists, False otherwise
604
+
605
+ Performance: O(1)
606
+ """
607
+ return feature_id in self._features