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,619 @@
1
+ """
2
+ HTMLFileTrackRepository - HTML file-based Track storage.
3
+
4
+ Loads and saves tracks from HTML files using HtmlGraph's existing parser.
5
+ """
6
+
7
+ import builtins
8
+ from collections.abc import Callable
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from htmlgraph.converter import html_to_node, node_to_html
14
+ from htmlgraph.models import Node
15
+ from htmlgraph.repositories.track_repository import (
16
+ RepositoryQuery,
17
+ TrackRepository,
18
+ TrackValidationError,
19
+ )
20
+
21
+
22
+ class HTMLFileRepositoryQuery(RepositoryQuery):
23
+ """Query builder for HTML file filtering."""
24
+
25
+ def __init__(self, repo: "HTMLFileTrackRepository", filters: dict[str, Any]):
26
+ super().__init__(filters)
27
+ self._repo = repo
28
+
29
+ def where(self, **kwargs: Any) -> "HTMLFileRepositoryQuery":
30
+ """Chain additional filters."""
31
+ # Validate filter keys
32
+ valid_attrs = {
33
+ "status",
34
+ "priority",
35
+ "has_spec",
36
+ "has_plan",
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 TrackValidationError(f"Invalid filter attribute: {key}")
46
+
47
+ # Merge filters
48
+ new_filters = {**self.filters, **kwargs}
49
+ return HTMLFileRepositoryQuery(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 HTMLFileTrackRepository(TrackRepository):
57
+ """
58
+ HTML file-based TrackRepository implementation.
59
+
60
+ Loads and saves tracks from HTML files in a directory.
61
+ Uses HtmlGraph's existing HTML parsing and serialization.
62
+
63
+ Tracks are stored as: `.htmlgraph/tracks/trk-XXXXX.html`
64
+
65
+ Performance:
66
+ - get(id): O(1) with cache, O(n) cold start
67
+ - list(): O(n) where n = total tracks
68
+ - save(): O(1) file write
69
+ - Cache hit ratio: >90% in steady state
70
+
71
+ Example:
72
+ >>> repo = HTMLFileTrackRepository(Path(".htmlgraph/tracks"))
73
+ >>> track = repo.get("trk-001")
74
+ >>> track.status = "completed"
75
+ >>> repo.save(track)
76
+ """
77
+
78
+ def __init__(
79
+ self,
80
+ directory: Path | str,
81
+ auto_load: bool = True,
82
+ stylesheet_path: str = "../styles.css",
83
+ ):
84
+ """
85
+ Initialize HTML file repository.
86
+
87
+ Args:
88
+ directory: Directory containing track HTML files
89
+ auto_load: Whether to auto-load tracks on first access
90
+ stylesheet_path: Relative path to CSS stylesheet for new files
91
+ """
92
+ self._directory = Path(directory)
93
+ self._directory.mkdir(parents=True, exist_ok=True)
94
+ self._auto_load = auto_load
95
+ self._stylesheet_path = stylesheet_path
96
+
97
+ # Identity cache: track_id -> Node instance
98
+ self._cache: dict[str, Node] = {}
99
+ self._loaded = False
100
+
101
+ def _ensure_loaded(self) -> None:
102
+ """Ensure tracks are loaded from disk."""
103
+ if not self._loaded and self._auto_load:
104
+ self.reload()
105
+
106
+ def _load_from_file(self, filepath: Path) -> Node:
107
+ """Load a single track from HTML file."""
108
+ try:
109
+ node = html_to_node(filepath)
110
+ if node.type != "track":
111
+ raise TrackValidationError(
112
+ f"File {filepath} contains node of type '{node.type}', not 'track'"
113
+ )
114
+ return node
115
+ except Exception as e:
116
+ raise TrackValidationError(f"Failed to load {filepath}: {e}") from e
117
+
118
+ def _find_file(self, track_id: str) -> Path | None:
119
+ """Find HTML file for a track ID."""
120
+ # Try direct match: trk-abc123.html
121
+ direct = self._directory / f"{track_id}.html"
122
+ if direct.exists():
123
+ return direct
124
+
125
+ # Try scanning all files (fallback)
126
+ for filepath in self._directory.glob("*.html"):
127
+ if filepath.stem == track_id:
128
+ return filepath
129
+
130
+ return None
131
+
132
+ def _generate_id(self) -> str:
133
+ """Generate unique track ID."""
134
+ import uuid
135
+
136
+ return f"trk-{uuid.uuid4().hex[:8]}"
137
+
138
+ def _validate_track(self, track: Any) -> None:
139
+ """Validate track object."""
140
+ if not hasattr(track, "id"):
141
+ raise TrackValidationError("Track must have 'id' attribute")
142
+ if not hasattr(track, "title"):
143
+ raise TrackValidationError("Track must have 'title' attribute")
144
+ if not track.id or not str(track.id).strip():
145
+ raise TrackValidationError("Track ID cannot be empty")
146
+ if not track.title or not str(track.title).strip():
147
+ raise TrackValidationError("Track title cannot be empty")
148
+
149
+ def _matches_filters(self, track: Node, filters: dict[str, Any]) -> bool:
150
+ """Check if track matches all filters."""
151
+ if not filters:
152
+ return True
153
+
154
+ for key, value in filters.items():
155
+ if not hasattr(track, key):
156
+ return False
157
+ if getattr(track, key) != value:
158
+ return False
159
+ return True
160
+
161
+ # ===== READ OPERATIONS =====
162
+
163
+ def get(self, track_id: str) -> Node | None:
164
+ """
165
+ Get single track by ID.
166
+
167
+ Returns same object instance for multiple calls (identity caching).
168
+
169
+ Args:
170
+ track_id: Track ID to retrieve
171
+
172
+ Returns:
173
+ Track object if found, None if not found
174
+
175
+ Raises:
176
+ ValueError: If track_id is invalid format
177
+
178
+ Performance: O(1) if cached, O(log n) if uncached
179
+ """
180
+ if not track_id or not isinstance(track_id, str):
181
+ raise ValueError(f"Invalid track_id: {track_id}")
182
+
183
+ self._ensure_loaded()
184
+
185
+ # Check cache first
186
+ if track_id in self._cache:
187
+ return self._cache[track_id]
188
+
189
+ # Load from file
190
+ filepath = self._find_file(track_id)
191
+ if not filepath:
192
+ return None
193
+
194
+ track = self._load_from_file(filepath)
195
+ self._cache[track_id] = track
196
+ return track
197
+
198
+ def list(self, filters: dict[str, Any] | None = None) -> list[Node]:
199
+ """
200
+ List all tracks with optional filters.
201
+
202
+ Args:
203
+ filters: Optional dict of attribute->value filters
204
+
205
+ Returns:
206
+ List of Track objects (empty list if no matches)
207
+
208
+ Raises:
209
+ TrackValidationError: If filter keys are invalid
210
+
211
+ Performance: O(n) where n = total tracks
212
+ """
213
+ if filters:
214
+ # Validate filter keys
215
+ valid_attrs = {
216
+ "status",
217
+ "priority",
218
+ "has_spec",
219
+ "has_plan",
220
+ "type",
221
+ "title",
222
+ "id",
223
+ "created",
224
+ "updated",
225
+ }
226
+ for key in filters:
227
+ if key not in valid_attrs:
228
+ raise TrackValidationError(f"Invalid filter attribute: {key}")
229
+
230
+ self._ensure_loaded()
231
+
232
+ results = []
233
+ for track in self._cache.values():
234
+ if self._matches_filters(track, filters or {}):
235
+ results.append(track)
236
+ return results
237
+
238
+ def where(self, **kwargs: Any) -> RepositoryQuery:
239
+ """
240
+ Build a filtered query with chaining support.
241
+
242
+ Args:
243
+ **kwargs: Attribute->value filter pairs
244
+
245
+ Returns:
246
+ RepositoryQuery object that can be further filtered
247
+
248
+ Raises:
249
+ TrackValidationError: If invalid attribute names
250
+ """
251
+ # Validate filter keys upfront
252
+ valid_attrs = {
253
+ "status",
254
+ "priority",
255
+ "has_spec",
256
+ "has_plan",
257
+ "type",
258
+ "title",
259
+ "id",
260
+ "created",
261
+ "updated",
262
+ }
263
+ for key in kwargs:
264
+ if key not in valid_attrs:
265
+ raise TrackValidationError(f"Invalid filter attribute: {key}")
266
+ return HTMLFileRepositoryQuery(self, kwargs)
267
+
268
+ def by_status(self, status: str) -> builtins.list[Node]:
269
+ """Filter tracks by status."""
270
+ return self.list({"status": status})
271
+
272
+ def by_priority(self, priority: str) -> builtins.list[Node]:
273
+ """Filter tracks by priority."""
274
+ return self.list({"priority": priority})
275
+
276
+ def active_tracks(self) -> builtins.list[Node]:
277
+ """Get all tracks currently in progress."""
278
+ return self.by_status("active")
279
+
280
+ def batch_get(self, track_ids: builtins.list[str]) -> builtins.list[Node]:
281
+ """
282
+ Bulk retrieve multiple tracks.
283
+
284
+ Args:
285
+ track_ids: List of track IDs
286
+
287
+ Returns:
288
+ List of found tracks
289
+
290
+ Raises:
291
+ ValueError: If track_ids is not a list
292
+
293
+ Performance: O(k) where k = batch size
294
+ """
295
+ if not isinstance(track_ids, list):
296
+ raise ValueError("track_ids must be a list")
297
+
298
+ results = []
299
+ for tid in track_ids:
300
+ track = self.get(tid)
301
+ if track:
302
+ results.append(track)
303
+ return results
304
+
305
+ # ===== WRITE OPERATIONS =====
306
+
307
+ def create(self, title: str, **kwargs: Any) -> Node:
308
+ """
309
+ Create new track.
310
+
311
+ Args:
312
+ title: Track title (required)
313
+ **kwargs: Additional properties
314
+
315
+ Returns:
316
+ Created Track object (with generated ID)
317
+
318
+ Raises:
319
+ TrackValidationError: If invalid data provided
320
+
321
+ Performance: O(1)
322
+ """
323
+ if not title or not title.strip():
324
+ raise TrackValidationError("Track title cannot be empty")
325
+
326
+ # Generate ID if not provided
327
+ track_id = kwargs.pop("id", None) or self._generate_id()
328
+
329
+ # Extract known fields from kwargs to avoid conflicts
330
+ node_type = kwargs.pop("type", "track")
331
+ status = kwargs.pop("status", "todo")
332
+ priority = kwargs.pop("priority", "medium")
333
+ created = kwargs.pop("created", datetime.now())
334
+ updated = kwargs.pop("updated", datetime.now())
335
+
336
+ # Remove title from kwargs if present (already have it as parameter)
337
+ kwargs.pop("title", None)
338
+
339
+ # Create Node object
340
+ track = Node(
341
+ id=track_id,
342
+ title=title,
343
+ type=node_type,
344
+ status=status,
345
+ priority=priority,
346
+ created=created,
347
+ updated=updated,
348
+ **kwargs,
349
+ )
350
+
351
+ # Validate and save
352
+ self._validate_track(track)
353
+ self.save(track)
354
+
355
+ return track
356
+
357
+ def save(self, track: Node) -> Node:
358
+ """
359
+ Save existing track (update or insert).
360
+
361
+ Args:
362
+ track: Track object to save
363
+
364
+ Returns:
365
+ Saved track (same instance)
366
+
367
+ Raises:
368
+ TrackValidationError: If track is invalid
369
+
370
+ Performance: O(1)
371
+ """
372
+ self._validate_track(track)
373
+
374
+ # Update timestamp
375
+ track.updated = datetime.now()
376
+
377
+ # Write to file
378
+ filepath = self._directory / f"{track.id}.html"
379
+ node_to_html(track, filepath, stylesheet_path=self._stylesheet_path)
380
+
381
+ # Update cache
382
+ self._cache[track.id] = track
383
+
384
+ return track
385
+
386
+ def batch_update(
387
+ self, track_ids: builtins.list[str], updates: dict[str, Any]
388
+ ) -> int:
389
+ """
390
+ Vectorized batch update operation.
391
+
392
+ Args:
393
+ track_ids: List of track IDs to update
394
+ updates: Dict of attribute->value to set
395
+
396
+ Returns:
397
+ Number of tracks successfully updated
398
+
399
+ Raises:
400
+ TrackValidationError: If invalid updates
401
+
402
+ Performance: O(k) vectorized where k = batch size
403
+ """
404
+ if not isinstance(track_ids, list):
405
+ raise ValueError("track_ids must be a list")
406
+ if not isinstance(updates, dict):
407
+ raise TrackValidationError("updates must be a dict")
408
+
409
+ count = 0
410
+ for tid in track_ids:
411
+ track = self.get(tid)
412
+ if track:
413
+ # Apply updates
414
+ for key, value in updates.items():
415
+ setattr(track, key, value)
416
+ self.save(track)
417
+ count += 1
418
+
419
+ return count
420
+
421
+ def delete(self, track_id: str) -> bool:
422
+ """
423
+ Delete a track by ID.
424
+
425
+ Args:
426
+ track_id: Track ID to delete
427
+
428
+ Returns:
429
+ True if deleted, False if not found
430
+
431
+ Performance: O(1) cache removal, O(log n) storage deletion
432
+ """
433
+ if not track_id:
434
+ raise TrackValidationError("track_id cannot be empty")
435
+
436
+ # Find and delete file
437
+ filepath = self._find_file(track_id)
438
+ if not filepath:
439
+ return False
440
+
441
+ filepath.unlink()
442
+
443
+ # Remove from cache
444
+ self._cache.pop(track_id, None)
445
+
446
+ return True
447
+
448
+ def batch_delete(self, track_ids: builtins.list[str]) -> int:
449
+ """
450
+ Delete multiple tracks.
451
+
452
+ Args:
453
+ track_ids: List of track IDs to delete
454
+
455
+ Returns:
456
+ Number of tracks successfully deleted
457
+
458
+ Performance: O(k) where k = batch size
459
+ """
460
+ if not isinstance(track_ids, list):
461
+ raise ValueError("track_ids must be a list")
462
+
463
+ count = 0
464
+ for tid in track_ids:
465
+ if self.delete(tid):
466
+ count += 1
467
+ return count
468
+
469
+ # ===== ADVANCED QUERIES =====
470
+
471
+ def find_by_features(self, feature_ids: builtins.list[str]) -> builtins.list[Node]:
472
+ """
473
+ Find tracks containing any of the specified features.
474
+
475
+ Args:
476
+ feature_ids: List of feature IDs to search for
477
+
478
+ Returns:
479
+ Tracks that contain at least one of these features
480
+
481
+ Raises:
482
+ ValueError: If feature_ids is not a list
483
+
484
+ Performance: O(n) with early termination
485
+ """
486
+ if not isinstance(feature_ids, list):
487
+ raise ValueError("feature_ids must be a list")
488
+
489
+ self._ensure_loaded()
490
+
491
+ results = []
492
+ for track in self._cache.values():
493
+ # Check both track.features attribute and properties["features"]
494
+ features = None
495
+ if hasattr(track, "features") and track.features:
496
+ features = track.features
497
+ elif hasattr(track, "properties") and track.properties.get("features"):
498
+ features = track.properties["features"]
499
+
500
+ if features and any(fid in features for fid in feature_ids):
501
+ results.append(track)
502
+ return results
503
+
504
+ def with_feature_count(self) -> builtins.list[Node]:
505
+ """
506
+ Get all tracks with feature count calculated.
507
+
508
+ Returns:
509
+ All tracks with feature_count property set
510
+ """
511
+ self._ensure_loaded()
512
+ return list(self._cache.values())
513
+
514
+ def filter(self, predicate: Callable[[Node], bool]) -> builtins.list[Node]:
515
+ """
516
+ Filter tracks with custom predicate function.
517
+
518
+ Args:
519
+ predicate: Function that takes Track and returns True/False
520
+
521
+ Returns:
522
+ Tracks matching predicate
523
+ """
524
+ self._ensure_loaded()
525
+ return [t for t in self._cache.values() if predicate(t)]
526
+
527
+ # ===== CACHE/LIFECYCLE MANAGEMENT =====
528
+
529
+ def invalidate_cache(self, track_id: str | None = None) -> None:
530
+ """
531
+ Invalidate cache for single track or all tracks.
532
+
533
+ Forces reload from storage on next access.
534
+
535
+ Args:
536
+ track_id: Specific track to invalidate, or None for all
537
+ """
538
+ if track_id:
539
+ self._cache.pop(track_id, None)
540
+ else:
541
+ self._cache.clear()
542
+
543
+ def reload(self) -> None:
544
+ """
545
+ Force reload all tracks from storage.
546
+
547
+ Invalidates all caches and reloads from disk.
548
+ Note: Preserves existing cache entries to maintain object identity
549
+ for tracks that have been created but not yet persisted to disk.
550
+ """
551
+ # Keep track of existing cached entries to preserve object identity
552
+ existing_cache = dict(self._cache)
553
+ self._cache.clear()
554
+
555
+ # Load all HTML files
556
+ for filepath in self._directory.glob("*.html"):
557
+ try:
558
+ track_id = filepath.stem
559
+ # If we already have this track in cache, keep the existing instance
560
+ if track_id in existing_cache:
561
+ self._cache[track_id] = existing_cache[track_id]
562
+ else:
563
+ track = self._load_from_file(filepath)
564
+ self._cache[track.id] = track
565
+ except Exception as e:
566
+ # Log and skip invalid files
567
+ import logging
568
+
569
+ logging.warning(f"Failed to load {filepath}: {e}")
570
+
571
+ self._loaded = True
572
+
573
+ @property
574
+ def auto_load(self) -> bool:
575
+ """Whether auto-loading is enabled."""
576
+ return self._auto_load
577
+
578
+ @auto_load.setter
579
+ def auto_load(self, enabled: bool) -> None:
580
+ """Enable/disable auto-loading."""
581
+ self._auto_load = enabled
582
+
583
+ # ===== UTILITY METHODS =====
584
+
585
+ def count(self, filters: dict[str, Any] | None = None) -> int:
586
+ """
587
+ Count tracks matching filters.
588
+
589
+ Args:
590
+ filters: Optional filters
591
+
592
+ Returns:
593
+ Number of matching tracks
594
+
595
+ Performance: O(n) or O(1) if cached and no filters
596
+ """
597
+ if not filters:
598
+ self._ensure_loaded()
599
+ return len(self._cache)
600
+ return len(self.list(filters))
601
+
602
+ def exists(self, track_id: str) -> bool:
603
+ """
604
+ Check if track exists without loading it.
605
+
606
+ Args:
607
+ track_id: Track ID to check
608
+
609
+ Returns:
610
+ True if exists, False otherwise
611
+
612
+ Performance: O(1)
613
+ """
614
+ # Check cache first
615
+ if track_id in self._cache:
616
+ return True
617
+
618
+ # Check file system
619
+ return self._find_file(track_id) is not None