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,508 @@
1
+ """
2
+ MemoryTrackRepository - In-memory Track storage for testing.
3
+
4
+ Provides fast in-memory Track storage with full repository interface support.
5
+ """
6
+
7
+ import builtins
8
+ from collections.abc import Callable
9
+ from datetime import datetime
10
+ from typing import Any
11
+
12
+ from htmlgraph.models import Node
13
+ from htmlgraph.repositories.track_repository import (
14
+ RepositoryQuery,
15
+ TrackRepository,
16
+ TrackValidationError,
17
+ )
18
+
19
+
20
+ class MemoryRepositoryQuery(RepositoryQuery):
21
+ """Query builder for in-memory filtering."""
22
+
23
+ def __init__(self, repo: "MemoryTrackRepository", filters: dict[str, Any]):
24
+ super().__init__(filters)
25
+ self._repo = repo
26
+
27
+ def where(self, **kwargs: Any) -> "MemoryRepositoryQuery":
28
+ """Chain additional filters."""
29
+ # Validate filter keys
30
+ valid_attrs = {
31
+ "status",
32
+ "priority",
33
+ "has_spec",
34
+ "has_plan",
35
+ "type",
36
+ "title",
37
+ "id",
38
+ "created",
39
+ "updated",
40
+ }
41
+ for key in kwargs:
42
+ if key not in valid_attrs:
43
+ raise TrackValidationError(f"Invalid filter attribute: {key}")
44
+
45
+ # Merge filters
46
+ new_filters = {**self.filters, **kwargs}
47
+ return MemoryRepositoryQuery(self._repo, new_filters)
48
+
49
+ def execute(self) -> list[Any]:
50
+ """Execute the query and return results."""
51
+ return self._repo.list(self.filters)
52
+
53
+
54
+ class MemoryTrackRepository(TrackRepository):
55
+ """
56
+ In-memory TrackRepository implementation for testing.
57
+
58
+ Fast, ephemeral storage with full repository interface support.
59
+ All data lost when instance is destroyed.
60
+
61
+ Performance:
62
+ - All operations: O(1) or O(n)
63
+ - No disk I/O
64
+ - Fast for test suites
65
+
66
+ Example:
67
+ >>> repo = MemoryTrackRepository()
68
+ >>> track = repo.create("Planning Phase 1", status="active")
69
+ >>> track.status = "completed"
70
+ >>> repo.save(track)
71
+ """
72
+
73
+ def __init__(self, auto_load: bool = True):
74
+ """
75
+ Initialize in-memory repository.
76
+
77
+ Args:
78
+ auto_load: Whether to auto-load tracks (no-op for memory repo)
79
+ """
80
+ self._auto_load = auto_load
81
+ self._tracks: dict[str, Node] = {}
82
+
83
+ def _generate_id(self) -> str:
84
+ """Generate unique track ID."""
85
+ import uuid
86
+
87
+ return f"trk-{uuid.uuid4().hex[:8]}"
88
+
89
+ def _validate_track(self, track: Any) -> None:
90
+ """Validate track object."""
91
+ if not hasattr(track, "id"):
92
+ raise TrackValidationError("Track must have 'id' attribute")
93
+ if not hasattr(track, "title"):
94
+ raise TrackValidationError("Track must have 'title' attribute")
95
+ if not track.id or not str(track.id).strip():
96
+ raise TrackValidationError("Track ID cannot be empty")
97
+ if not track.title or not str(track.title).strip():
98
+ raise TrackValidationError("Track title cannot be empty")
99
+
100
+ def _matches_filters(self, track: Node, filters: dict[str, Any]) -> bool:
101
+ """Check if track matches all filters."""
102
+ if not filters:
103
+ return True
104
+
105
+ for key, value in filters.items():
106
+ if not hasattr(track, key):
107
+ return False
108
+ if getattr(track, key) != value:
109
+ return False
110
+ return True
111
+
112
+ # ===== READ OPERATIONS =====
113
+
114
+ def get(self, track_id: str) -> Any | None:
115
+ """
116
+ Get single track by ID.
117
+
118
+ Returns same object instance for multiple calls (identity caching).
119
+
120
+ Args:
121
+ track_id: Track ID to retrieve
122
+
123
+ Returns:
124
+ Track object if found, None if not found
125
+
126
+ Raises:
127
+ ValueError: If track_id is invalid format
128
+
129
+ Performance: O(1)
130
+ """
131
+ if not track_id or not isinstance(track_id, str):
132
+ raise ValueError(f"Invalid track_id: {track_id}")
133
+
134
+ return self._tracks.get(track_id)
135
+
136
+ def list(self, filters: dict[str, Any] | None = None) -> list[Any]:
137
+ """
138
+ List all tracks with optional filters.
139
+
140
+ Args:
141
+ filters: Optional dict of attribute->value filters
142
+
143
+ Returns:
144
+ List of Track objects (empty list if no matches)
145
+
146
+ Raises:
147
+ TrackValidationError: If filter keys are invalid
148
+
149
+ Performance: O(n) where n = total tracks
150
+ """
151
+ if filters:
152
+ # Validate filter keys
153
+ valid_attrs = {
154
+ "status",
155
+ "priority",
156
+ "has_spec",
157
+ "has_plan",
158
+ "type",
159
+ "title",
160
+ "id",
161
+ "created",
162
+ "updated",
163
+ }
164
+ for key in filters:
165
+ if key not in valid_attrs:
166
+ raise TrackValidationError(f"Invalid filter attribute: {key}")
167
+
168
+ results = []
169
+ for track in self._tracks.values():
170
+ if self._matches_filters(track, filters or {}):
171
+ results.append(track)
172
+ return results
173
+
174
+ def where(self, **kwargs: Any) -> RepositoryQuery:
175
+ """
176
+ Build a filtered query with chaining support.
177
+
178
+ Args:
179
+ **kwargs: Attribute->value filter pairs
180
+
181
+ Returns:
182
+ RepositoryQuery object that can be further filtered
183
+
184
+ Raises:
185
+ TrackValidationError: If invalid attribute names
186
+ """
187
+ # Validate filter keys upfront
188
+ valid_attrs = {
189
+ "status",
190
+ "priority",
191
+ "has_spec",
192
+ "has_plan",
193
+ "type",
194
+ "title",
195
+ "id",
196
+ "created",
197
+ "updated",
198
+ }
199
+ for key in kwargs:
200
+ if key not in valid_attrs:
201
+ raise TrackValidationError(f"Invalid filter attribute: {key}")
202
+ return MemoryRepositoryQuery(self, kwargs)
203
+
204
+ def by_status(self, status: str) -> builtins.list[Any]:
205
+ """Filter tracks by status."""
206
+ return self.list({"status": status})
207
+
208
+ def by_priority(self, priority: str) -> builtins.list[Any]:
209
+ """Filter tracks by priority."""
210
+ return self.list({"priority": priority})
211
+
212
+ def active_tracks(self) -> builtins.list[Any]:
213
+ """Get all tracks currently in progress."""
214
+ return self.by_status("active")
215
+
216
+ def batch_get(self, track_ids: builtins.list[str]) -> builtins.list[Any]:
217
+ """
218
+ Bulk retrieve multiple tracks.
219
+
220
+ Args:
221
+ track_ids: List of track IDs
222
+
223
+ Returns:
224
+ List of found tracks
225
+
226
+ Raises:
227
+ ValueError: If track_ids is not a list
228
+
229
+ Performance: O(k) where k = batch size
230
+ """
231
+ if not isinstance(track_ids, list):
232
+ raise ValueError("track_ids must be a list")
233
+
234
+ results = []
235
+ for tid in track_ids:
236
+ track = self.get(tid)
237
+ if track:
238
+ results.append(track)
239
+ return results
240
+
241
+ # ===== WRITE OPERATIONS =====
242
+
243
+ def create(self, title: str, **kwargs: Any) -> Any:
244
+ """
245
+ Create new track.
246
+
247
+ Args:
248
+ title: Track title (required)
249
+ **kwargs: Additional properties
250
+
251
+ Returns:
252
+ Created Track object (with generated ID)
253
+
254
+ Raises:
255
+ TrackValidationError: If invalid data provided
256
+
257
+ Performance: O(1)
258
+ """
259
+ if not title or not title.strip():
260
+ raise TrackValidationError("Track title cannot be empty")
261
+
262
+ # Generate ID if not provided
263
+ track_id = kwargs.pop("id", None) or self._generate_id()
264
+
265
+ # Extract known fields
266
+ node_type = kwargs.pop("type", "track")
267
+ status = kwargs.pop("status", "todo")
268
+ priority = kwargs.pop("priority", "medium")
269
+ created = kwargs.pop("created", datetime.now())
270
+ updated = kwargs.pop("updated", datetime.now())
271
+
272
+ # Remove title from kwargs if present
273
+ kwargs.pop("title", None)
274
+
275
+ # Create Node object
276
+ track = Node(
277
+ id=track_id,
278
+ title=title,
279
+ type=node_type,
280
+ status=status,
281
+ priority=priority,
282
+ created=created,
283
+ updated=updated,
284
+ **kwargs,
285
+ )
286
+
287
+ # Validate and save
288
+ self._validate_track(track)
289
+ self._tracks[track_id] = track
290
+
291
+ return track
292
+
293
+ def save(self, track: Any) -> Any:
294
+ """
295
+ Save existing track (update or insert).
296
+
297
+ Args:
298
+ track: Track object to save
299
+
300
+ Returns:
301
+ Saved track (same instance)
302
+
303
+ Raises:
304
+ TrackValidationError: If track is invalid
305
+
306
+ Performance: O(1)
307
+ """
308
+ self._validate_track(track)
309
+
310
+ # Update timestamp
311
+ track.updated = datetime.now()
312
+
313
+ # Save to dict
314
+ self._tracks[track.id] = track
315
+
316
+ return track
317
+
318
+ def batch_update(
319
+ self, track_ids: builtins.list[str], updates: dict[str, Any]
320
+ ) -> int:
321
+ """
322
+ Vectorized batch update operation.
323
+
324
+ Args:
325
+ track_ids: List of track IDs to update
326
+ updates: Dict of attribute->value to set
327
+
328
+ Returns:
329
+ Number of tracks successfully updated
330
+
331
+ Raises:
332
+ TrackValidationError: If invalid updates
333
+
334
+ Performance: O(k) where k = batch size
335
+ """
336
+ if not isinstance(track_ids, list):
337
+ raise ValueError("track_ids must be a list")
338
+ if not isinstance(updates, dict):
339
+ raise TrackValidationError("updates must be a dict")
340
+
341
+ count = 0
342
+ for tid in track_ids:
343
+ track = self.get(tid)
344
+ if track:
345
+ # Apply updates
346
+ for key, value in updates.items():
347
+ setattr(track, key, value)
348
+ self.save(track)
349
+ count += 1
350
+
351
+ return count
352
+
353
+ def delete(self, track_id: str) -> bool:
354
+ """
355
+ Delete a track by ID.
356
+
357
+ Args:
358
+ track_id: Track ID to delete
359
+
360
+ Returns:
361
+ True if deleted, False if not found
362
+
363
+ Performance: O(1)
364
+ """
365
+ if not track_id:
366
+ raise TrackValidationError("track_id cannot be empty")
367
+
368
+ if track_id in self._tracks:
369
+ del self._tracks[track_id]
370
+ return True
371
+ return False
372
+
373
+ def batch_delete(self, track_ids: builtins.list[str]) -> int:
374
+ """
375
+ Delete multiple tracks.
376
+
377
+ Args:
378
+ track_ids: List of track IDs to delete
379
+
380
+ Returns:
381
+ Number of tracks successfully deleted
382
+
383
+ Performance: O(k) where k = batch size
384
+ """
385
+ if not isinstance(track_ids, list):
386
+ raise ValueError("track_ids must be a list")
387
+
388
+ count = 0
389
+ for tid in track_ids:
390
+ if self.delete(tid):
391
+ count += 1
392
+ return count
393
+
394
+ # ===== ADVANCED QUERIES =====
395
+
396
+ def find_by_features(self, feature_ids: builtins.list[str]) -> builtins.list[Any]:
397
+ """
398
+ Find tracks containing any of the specified features.
399
+
400
+ Args:
401
+ feature_ids: List of feature IDs to search for
402
+
403
+ Returns:
404
+ Tracks that contain at least one of these features
405
+
406
+ Raises:
407
+ ValueError: If feature_ids is not a list
408
+
409
+ Performance: O(n)
410
+ """
411
+ if not isinstance(feature_ids, list):
412
+ raise ValueError("feature_ids must be a list")
413
+
414
+ results = []
415
+ for track in self._tracks.values():
416
+ # Check both track.features attribute and properties["features"]
417
+ features = None
418
+ if hasattr(track, "features") and track.features:
419
+ features = track.features
420
+ elif hasattr(track, "properties") and track.properties.get("features"):
421
+ features = track.properties["features"]
422
+
423
+ if features and any(fid in features for fid in feature_ids):
424
+ results.append(track)
425
+ return results
426
+
427
+ def with_feature_count(self) -> builtins.list[Any]:
428
+ """
429
+ Get all tracks with feature count calculated.
430
+
431
+ Returns:
432
+ All tracks with feature_count property set
433
+ """
434
+ return list(self._tracks.values())
435
+
436
+ def filter(self, predicate: Callable[[Any], bool]) -> builtins.list[Any]:
437
+ """
438
+ Filter tracks with custom predicate function.
439
+
440
+ Args:
441
+ predicate: Function that takes Track and returns True/False
442
+
443
+ Returns:
444
+ Tracks matching predicate
445
+ """
446
+ return [t for t in self._tracks.values() if predicate(t)]
447
+
448
+ # ===== CACHE/LIFECYCLE MANAGEMENT =====
449
+
450
+ def invalidate_cache(self, track_id: str | None = None) -> None:
451
+ """
452
+ Invalidate cache for single track or all tracks.
453
+
454
+ Args:
455
+ track_id: Specific track to invalidate, or None for all
456
+ """
457
+ # No-op for memory repository (no cache separate from storage)
458
+ pass
459
+
460
+ def reload(self) -> None:
461
+ """
462
+ Force reload all tracks from storage.
463
+
464
+ No-op for memory repository (no external storage).
465
+ """
466
+ pass
467
+
468
+ @property
469
+ def auto_load(self) -> bool:
470
+ """Whether auto-loading is enabled."""
471
+ return self._auto_load
472
+
473
+ @auto_load.setter
474
+ def auto_load(self, enabled: bool) -> None:
475
+ """Enable/disable auto-loading."""
476
+ self._auto_load = enabled
477
+
478
+ # ===== UTILITY METHODS =====
479
+
480
+ def count(self, filters: dict[str, Any] | None = None) -> int:
481
+ """
482
+ Count tracks matching filters.
483
+
484
+ Args:
485
+ filters: Optional filters
486
+
487
+ Returns:
488
+ Number of matching tracks
489
+
490
+ Performance: O(1) if no filters, O(n) with filters
491
+ """
492
+ if not filters:
493
+ return len(self._tracks)
494
+ return len(self.list(filters))
495
+
496
+ def exists(self, track_id: str) -> bool:
497
+ """
498
+ Check if track exists without loading it.
499
+
500
+ Args:
501
+ track_id: Track ID to check
502
+
503
+ Returns:
504
+ True if exists, False otherwise
505
+
506
+ Performance: O(1)
507
+ """
508
+ return track_id in self._tracks