htmlgraph 0.26.25__py3-none-any.whl → 0.27.1__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 (175) hide show
  1. htmlgraph/__init__.py +23 -1
  2. htmlgraph/__init__.pyi +123 -0
  3. htmlgraph/agent_registry.py +2 -1
  4. htmlgraph/analytics/cli.py +3 -3
  5. htmlgraph/analytics/cost_analyzer.py +5 -1
  6. htmlgraph/analytics/cost_monitor.py +664 -0
  7. htmlgraph/analytics/cross_session.py +13 -9
  8. htmlgraph/analytics/dependency.py +10 -6
  9. htmlgraph/analytics/strategic/__init__.py +80 -0
  10. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  11. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  12. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  13. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  14. htmlgraph/analytics/work_type.py +15 -11
  15. htmlgraph/analytics_index.py +2 -1
  16. htmlgraph/api/cost_alerts_websocket.py +416 -0
  17. htmlgraph/api/main.py +167 -62
  18. htmlgraph/api/websocket.py +538 -0
  19. htmlgraph/attribute_index.py +2 -1
  20. htmlgraph/builders/base.py +2 -1
  21. htmlgraph/builders/bug.py +2 -1
  22. htmlgraph/builders/chore.py +2 -1
  23. htmlgraph/builders/epic.py +2 -1
  24. htmlgraph/builders/feature.py +2 -1
  25. htmlgraph/builders/insight.py +2 -1
  26. htmlgraph/builders/metric.py +2 -1
  27. htmlgraph/builders/pattern.py +2 -1
  28. htmlgraph/builders/phase.py +2 -1
  29. htmlgraph/builders/spike.py +2 -1
  30. htmlgraph/builders/track.py +2 -1
  31. htmlgraph/cli/analytics.py +2 -1
  32. htmlgraph/cli/base.py +2 -1
  33. htmlgraph/cli/core.py +2 -1
  34. htmlgraph/cli/main.py +2 -1
  35. htmlgraph/cli/models.py +2 -1
  36. htmlgraph/cli/templates/cost_dashboard.py +2 -1
  37. htmlgraph/cli/work/__init__.py +2 -1
  38. htmlgraph/cli/work/browse.py +2 -1
  39. htmlgraph/cli/work/features.py +2 -1
  40. htmlgraph/cli/work/orchestration.py +2 -1
  41. htmlgraph/cli/work/report.py +2 -1
  42. htmlgraph/cli/work/sessions.py +2 -1
  43. htmlgraph/cli/work/snapshot.py +2 -1
  44. htmlgraph/cli/work/tracks.py +2 -1
  45. htmlgraph/collections/base.py +10 -5
  46. htmlgraph/collections/bug.py +2 -1
  47. htmlgraph/collections/chore.py +2 -1
  48. htmlgraph/collections/epic.py +2 -1
  49. htmlgraph/collections/feature.py +2 -1
  50. htmlgraph/collections/insight.py +2 -1
  51. htmlgraph/collections/metric.py +2 -1
  52. htmlgraph/collections/pattern.py +2 -1
  53. htmlgraph/collections/phase.py +2 -1
  54. htmlgraph/collections/session.py +12 -7
  55. htmlgraph/collections/spike.py +6 -1
  56. htmlgraph/collections/task_delegation.py +7 -2
  57. htmlgraph/collections/todo.py +2 -1
  58. htmlgraph/collections/traces.py +15 -10
  59. htmlgraph/config/cost_models.json +56 -0
  60. htmlgraph/context_analytics.py +2 -1
  61. htmlgraph/db/schema.py +67 -6
  62. htmlgraph/dependency_models.py +2 -1
  63. htmlgraph/edge_index.py +2 -1
  64. htmlgraph/event_log.py +83 -64
  65. htmlgraph/event_migration.py +2 -1
  66. htmlgraph/file_watcher.py +12 -8
  67. htmlgraph/find_api.py +2 -1
  68. htmlgraph/git_events.py +6 -2
  69. htmlgraph/hooks/cigs_pretool_enforcer.py +5 -1
  70. htmlgraph/hooks/drift_handler.py +3 -3
  71. htmlgraph/hooks/event_tracker.py +40 -61
  72. htmlgraph/hooks/installer.py +5 -1
  73. htmlgraph/hooks/orchestrator.py +4 -0
  74. htmlgraph/hooks/orchestrator_reflector.py +4 -0
  75. htmlgraph/hooks/post_tool_use_failure.py +7 -3
  76. htmlgraph/hooks/posttooluse.py +4 -0
  77. htmlgraph/hooks/prompt_analyzer.py +5 -5
  78. htmlgraph/hooks/session_handler.py +2 -1
  79. htmlgraph/hooks/session_summary.py +6 -2
  80. htmlgraph/hooks/validator.py +8 -4
  81. htmlgraph/ids.py +2 -1
  82. htmlgraph/learning.py +2 -1
  83. htmlgraph/mcp_server.py +2 -1
  84. htmlgraph/operations/analytics.py +2 -1
  85. htmlgraph/operations/bootstrap.py +2 -1
  86. htmlgraph/operations/events.py +2 -1
  87. htmlgraph/operations/fastapi_server.py +2 -1
  88. htmlgraph/operations/hooks.py +2 -1
  89. htmlgraph/operations/initialization.py +2 -1
  90. htmlgraph/operations/server.py +2 -1
  91. htmlgraph/orchestration/claude_launcher.py +23 -20
  92. htmlgraph/orchestration/command_builder.py +2 -1
  93. htmlgraph/orchestration/headless_spawner.py +6 -2
  94. htmlgraph/orchestration/model_selection.py +7 -3
  95. htmlgraph/orchestration/plugin_manager.py +24 -19
  96. htmlgraph/orchestration/spawners/claude.py +5 -2
  97. htmlgraph/orchestration/spawners/codex.py +12 -19
  98. htmlgraph/orchestration/spawners/copilot.py +13 -18
  99. htmlgraph/orchestration/spawners/gemini.py +12 -19
  100. htmlgraph/orchestration/subprocess_runner.py +6 -3
  101. htmlgraph/orchestration/task_coordination.py +16 -8
  102. htmlgraph/orchestrator.py +2 -1
  103. htmlgraph/parallel.py +2 -1
  104. htmlgraph/query_builder.py +2 -1
  105. htmlgraph/reflection.py +2 -1
  106. htmlgraph/refs.py +2 -1
  107. htmlgraph/repo_hash.py +2 -1
  108. htmlgraph/repositories/__init__.py +292 -0
  109. htmlgraph/repositories/analytics_repository.py +455 -0
  110. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  111. htmlgraph/repositories/feature_repository.py +581 -0
  112. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  113. htmlgraph/repositories/feature_repository_memory.py +607 -0
  114. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  115. htmlgraph/repositories/filter_service.py +620 -0
  116. htmlgraph/repositories/filter_service_standard.py +445 -0
  117. htmlgraph/repositories/shared_cache.py +621 -0
  118. htmlgraph/repositories/shared_cache_memory.py +395 -0
  119. htmlgraph/repositories/track_repository.py +552 -0
  120. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  121. htmlgraph/repositories/track_repository_memory.py +508 -0
  122. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  123. htmlgraph/sdk/__init__.py +398 -0
  124. htmlgraph/sdk/__init__.pyi +14 -0
  125. htmlgraph/sdk/analytics/__init__.py +19 -0
  126. htmlgraph/sdk/analytics/engine.py +155 -0
  127. htmlgraph/sdk/analytics/helpers.py +178 -0
  128. htmlgraph/sdk/analytics/registry.py +109 -0
  129. htmlgraph/sdk/base.py +484 -0
  130. htmlgraph/sdk/constants.py +216 -0
  131. htmlgraph/sdk/core.pyi +308 -0
  132. htmlgraph/sdk/discovery.py +120 -0
  133. htmlgraph/sdk/help/__init__.py +12 -0
  134. htmlgraph/sdk/help/mixin.py +699 -0
  135. htmlgraph/sdk/mixins/__init__.py +15 -0
  136. htmlgraph/sdk/mixins/attribution.py +113 -0
  137. htmlgraph/sdk/mixins/mixin.py +410 -0
  138. htmlgraph/sdk/operations/__init__.py +12 -0
  139. htmlgraph/sdk/operations/mixin.py +427 -0
  140. htmlgraph/sdk/orchestration/__init__.py +17 -0
  141. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  142. htmlgraph/sdk/orchestration/spawner.py +204 -0
  143. htmlgraph/sdk/planning/__init__.py +19 -0
  144. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  145. htmlgraph/sdk/planning/mixin.py +211 -0
  146. htmlgraph/sdk/planning/parallel.py +186 -0
  147. htmlgraph/sdk/planning/queue.py +210 -0
  148. htmlgraph/sdk/planning/recommendations.py +87 -0
  149. htmlgraph/sdk/planning/smart_planning.py +319 -0
  150. htmlgraph/sdk/session/__init__.py +19 -0
  151. htmlgraph/sdk/session/continuity.py +57 -0
  152. htmlgraph/sdk/session/handoff.py +110 -0
  153. htmlgraph/sdk/session/info.py +309 -0
  154. htmlgraph/sdk/session/manager.py +103 -0
  155. htmlgraph/sdk/strategic/__init__.py +26 -0
  156. htmlgraph/sdk/strategic/mixin.py +563 -0
  157. htmlgraph/server.py +21 -17
  158. htmlgraph/session_warning.py +2 -1
  159. htmlgraph/sessions/handoff.py +4 -3
  160. htmlgraph/system_prompts.py +2 -1
  161. htmlgraph/track_builder.py +2 -1
  162. htmlgraph/transcript.py +2 -1
  163. htmlgraph/watch.py +2 -1
  164. htmlgraph/work_type_utils.py +2 -1
  165. {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.1.dist-info}/METADATA +1 -1
  166. htmlgraph-0.27.1.dist-info/RECORD +332 -0
  167. htmlgraph/sdk.py +0 -3500
  168. htmlgraph-0.26.25.dist-info/RECORD +0 -274
  169. {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/dashboard.html +0 -0
  170. {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/styles.css +0 -0
  171. {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  172. {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  173. {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  174. {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.1.dist-info}/WHEEL +0 -0
  175. {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,581 @@
1
+ """
2
+ FeatureRepository - Abstract interface for Feature data access.
3
+
4
+ Unifies all data access patterns for Features across HtmlGraph.
5
+ Implementations handle:
6
+ - HTML file storage + SQLite database
7
+ - Lazy loading and caching
8
+ - Query building and filtering
9
+ - Concurrent access safety
10
+ - Event logging and session tracking
11
+
12
+ All implementations MUST pass FeatureRepositoryComplianceTests.
13
+ """
14
+
15
+ import builtins
16
+ from abc import ABC, abstractmethod
17
+ from collections.abc import Callable
18
+ from dataclasses import dataclass
19
+ from typing import Any
20
+
21
+
22
+ @dataclass
23
+ class RepositoryQuery:
24
+ """
25
+ Query builder for chaining filters.
26
+
27
+ Supports method chaining:
28
+ repo.where(status='todo').where(priority='high').execute()
29
+ """
30
+
31
+ filters: dict[str, Any]
32
+
33
+ def execute(self) -> list[Any]:
34
+ """Execute the query and return results."""
35
+ raise NotImplementedError("Subclass must implement")
36
+
37
+
38
+ class FeatureRepositoryError(Exception):
39
+ """Base exception for repository operations."""
40
+
41
+ pass
42
+
43
+
44
+ class FeatureNotFoundError(FeatureRepositoryError):
45
+ """Raised when a feature is not found."""
46
+
47
+ def __init__(self, feature_id: str):
48
+ self.feature_id = feature_id
49
+ super().__init__(f"Feature not found: {feature_id}")
50
+
51
+
52
+ class FeatureValidationError(FeatureRepositoryError):
53
+ """Raised when feature data fails validation."""
54
+
55
+ pass
56
+
57
+
58
+ class FeatureConcurrencyError(FeatureRepositoryError):
59
+ """Raised when concurrent modification detected."""
60
+
61
+ pass
62
+
63
+
64
+ class FeatureRepository(ABC):
65
+ """
66
+ Abstract interface for Feature data access.
67
+
68
+ Unifies access to Features stored in HTML files and SQLite database.
69
+
70
+ CONTRACT:
71
+ 1. **Identity Invariant**: get(id) returns same object instance for same feature
72
+ 2. **Atomicity**: write operations are atomic (all-or-nothing)
73
+ 3. **Consistency**: cache stays in sync with storage
74
+ 4. **Isolation**: concurrent operations don't corrupt state
75
+ 5. **Error Handling**: all errors preserve full context
76
+
77
+ CACHING BEHAVIOR:
78
+ - Single object instances per feature (identity, not just equality)
79
+ - Automatic cache invalidation on writes
80
+ - Optional auto-load on first access
81
+
82
+ PERFORMANCE:
83
+ - get(id): O(1) cached, O(log n) uncached
84
+ - list(): O(n) where n = features
85
+ - where(**kwargs): O(n) with early termination
86
+ - batch_get(): O(k) where k = batch size
87
+ - batch_update(): O(k) vectorized
88
+
89
+ THREAD SAFETY:
90
+ - Implementations should be thread-safe
91
+ - Concurrent reads allowed
92
+ - Concurrent writes serialized (via database locks or explicit locking)
93
+ """
94
+
95
+ # ===== READ OPERATIONS =====
96
+
97
+ @abstractmethod
98
+ def get(self, feature_id: str) -> Any | None:
99
+ """
100
+ Get single feature by ID.
101
+
102
+ Returns same object instance for multiple calls with same ID.
103
+ Implements identity caching (is, not ==).
104
+
105
+ Args:
106
+ feature_id: Feature ID to retrieve (e.g., "feat-abc123")
107
+
108
+ Returns:
109
+ Feature object if found, None if not found
110
+
111
+ Raises:
112
+ ValueError: If feature_id is invalid format
113
+
114
+ Performance: O(1) if cached, O(log n) if uncached
115
+
116
+ Examples:
117
+ >>> feature = repo.get("feat-001")
118
+ >>> feature2 = repo.get("feat-001")
119
+ >>> assert feature is feature2 # Identity, not just equality
120
+ >>> assert feature is not None
121
+ """
122
+ ...
123
+
124
+ @abstractmethod
125
+ def list(self, filters: dict[str, Any] | None = None) -> list[Any]:
126
+ """
127
+ List all features with optional filters.
128
+
129
+ Returns empty list if no matches, never None.
130
+
131
+ Args:
132
+ filters: Optional dict of attribute->value filters.
133
+ Empty/None dict means no filters (returns all).
134
+
135
+ Returns:
136
+ List of Feature objects (empty list if no matches)
137
+
138
+ Raises:
139
+ FeatureValidationError: If filter keys are invalid
140
+
141
+ Performance: O(n) where n = total features
142
+
143
+ Examples:
144
+ >>> all_features = repo.list()
145
+ >>> assert isinstance(all_features, list)
146
+ >>> todo_features = repo.list({"status": "todo"})
147
+ >>> multiple = repo.list({"status": "todo", "priority": "high"})
148
+ """
149
+ ...
150
+
151
+ @abstractmethod
152
+ def where(self, **kwargs: Any) -> RepositoryQuery:
153
+ """
154
+ Build a filtered query with chaining support.
155
+
156
+ Supports method chaining for composable queries:
157
+ repo.where(status='todo').where(priority='high').execute()
158
+
159
+ Args:
160
+ **kwargs: Attribute->value filter pairs.
161
+ Common: status, priority, assigned_to, track_id
162
+
163
+ Returns:
164
+ RepositoryQuery object that can be further filtered or executed
165
+
166
+ Raises:
167
+ FeatureValidationError: If invalid attribute names
168
+
169
+ Examples:
170
+ >>> query = repo.where(status='todo')
171
+ >>> query2 = query.where(priority='high') # Chaining
172
+ >>> results = query2.execute()
173
+ >>> assert all(f.status == 'todo' for f in results)
174
+ >>> assert all(f.priority == 'high' for f in results)
175
+ """
176
+ ...
177
+
178
+ @abstractmethod
179
+ def by_track(self, track_id: str) -> builtins.list[Any]:
180
+ """
181
+ Get all features belonging to a track.
182
+
183
+ Args:
184
+ track_id: Track ID to filter by
185
+
186
+ Returns:
187
+ List of features in track (empty if track has no features)
188
+
189
+ Raises:
190
+ ValueError: If track_id is invalid format
191
+
192
+ Performance: O(n) with early termination on match
193
+
194
+ Examples:
195
+ >>> features = repo.by_track("track-planning")
196
+ >>> assert all(f.track_id == "track-planning" for f in features)
197
+ """
198
+ ...
199
+
200
+ @abstractmethod
201
+ def by_status(self, status: str) -> builtins.list[Any]:
202
+ """
203
+ Filter features by status.
204
+
205
+ Args:
206
+ status: Status to filter by (e.g., 'todo', 'in-progress', 'done')
207
+
208
+ Returns:
209
+ List of matching features (empty if no matches)
210
+
211
+ Performance: O(n) with early termination
212
+
213
+ Examples:
214
+ >>> done_features = repo.by_status("done")
215
+ >>> active = repo.by_status("in-progress")
216
+ """
217
+ ...
218
+
219
+ @abstractmethod
220
+ def by_priority(self, priority: str) -> builtins.list[Any]:
221
+ """
222
+ Filter features by priority.
223
+
224
+ Args:
225
+ priority: Priority level (e.g., 'low', 'medium', 'high', 'critical')
226
+
227
+ Returns:
228
+ List of matching features
229
+
230
+ Performance: O(n)
231
+
232
+ Examples:
233
+ >>> critical = repo.by_priority("critical")
234
+ >>> important = repo.by_priority("high")
235
+ """
236
+ ...
237
+
238
+ @abstractmethod
239
+ def by_assigned_to(self, agent: str) -> builtins.list[Any]:
240
+ """
241
+ Get features assigned to an agent.
242
+
243
+ Args:
244
+ agent: Agent ID (e.g., 'claude', 'gpt4')
245
+
246
+ Returns:
247
+ Features assigned to agent
248
+
249
+ Examples:
250
+ >>> my_work = repo.by_assigned_to("claude")
251
+ """
252
+ ...
253
+
254
+ @abstractmethod
255
+ def batch_get(self, feature_ids: builtins.list[str]) -> builtins.list[Any]:
256
+ """
257
+ Bulk retrieve multiple features.
258
+
259
+ More efficient than multiple get() calls (vectorized).
260
+ Returns partial results if some features not found.
261
+
262
+ Args:
263
+ feature_ids: List of feature IDs
264
+
265
+ Returns:
266
+ List of found features (in order of input, with None for missing)
267
+ or list of only found features (implementation-dependent)
268
+
269
+ Raises:
270
+ ValueError: If feature_ids is not a list
271
+
272
+ Performance: O(k) where k = batch size
273
+
274
+ Examples:
275
+ >>> ids = ["feat-001", "feat-002", "feat-003"]
276
+ >>> features = repo.batch_get(ids)
277
+ >>> assert len(features) <= len(ids)
278
+ """
279
+ ...
280
+
281
+ # ===== WRITE OPERATIONS =====
282
+
283
+ @abstractmethod
284
+ def create(self, title: str, **kwargs: Any) -> Any:
285
+ """
286
+ Create new feature.
287
+
288
+ Generates ID if not provided.
289
+ Saves to storage immediately.
290
+
291
+ Args:
292
+ title: Feature title (required)
293
+ **kwargs: Additional properties (priority, status, track_id, etc.)
294
+
295
+ Returns:
296
+ Created Feature object (with generated ID)
297
+
298
+ Raises:
299
+ FeatureValidationError: If invalid data provided
300
+ FeatureRepositoryError: If create fails
301
+
302
+ Performance: O(1) cached write
303
+
304
+ Examples:
305
+ >>> feature = repo.create("User Authentication")
306
+ >>> assert feature.id is not None
307
+ >>> feature2 = repo.create("API Rate Limiting", priority="high")
308
+ """
309
+ ...
310
+
311
+ @abstractmethod
312
+ def save(self, feature: Any) -> Any:
313
+ """
314
+ Save existing feature (update or insert).
315
+
316
+ If feature.id exists in repo, updates. Otherwise inserts.
317
+
318
+ Args:
319
+ feature: Feature object to save
320
+
321
+ Returns:
322
+ Saved feature (same instance)
323
+
324
+ Raises:
325
+ FeatureValidationError: If feature is invalid
326
+ FeatureConcurrencyError: If feature was modified elsewhere
327
+
328
+ Performance: O(1)
329
+
330
+ Examples:
331
+ >>> feature = repo.get("feat-001")
332
+ >>> feature.status = "in-progress"
333
+ >>> repo.save(feature)
334
+ """
335
+ ...
336
+
337
+ @abstractmethod
338
+ def batch_update(
339
+ self, feature_ids: builtins.list[str], updates: dict[str, Any]
340
+ ) -> int:
341
+ """
342
+ Vectorized batch update operation.
343
+
344
+ Updates all specified features with same values.
345
+ More efficient than individual saves.
346
+
347
+ Args:
348
+ feature_ids: List of feature IDs to update
349
+ updates: Dict of attribute->value to set
350
+
351
+ Returns:
352
+ Number of features successfully updated
353
+
354
+ Raises:
355
+ FeatureValidationError: If invalid updates
356
+
357
+ Performance: O(k) vectorized where k = batch size
358
+
359
+ Examples:
360
+ >>> count = repo.batch_update(
361
+ ... ["feat-1", "feat-2", "feat-3"],
362
+ ... {"status": "done", "priority": "low"}
363
+ ... )
364
+ >>> assert count == 3
365
+ """
366
+ ...
367
+
368
+ @abstractmethod
369
+ def delete(self, feature_id: str) -> bool:
370
+ """
371
+ Delete a feature by ID.
372
+
373
+ Args:
374
+ feature_id: Feature ID to delete
375
+
376
+ Returns:
377
+ True if deleted, False if not found
378
+
379
+ Raises:
380
+ FeatureValidationError: If feature_id invalid
381
+
382
+ Performance: O(1) cache removal, O(log n) storage deletion
383
+
384
+ Examples:
385
+ >>> success = repo.delete("feat-001")
386
+ >>> assert success is True or success is False
387
+ """
388
+ ...
389
+
390
+ @abstractmethod
391
+ def batch_delete(self, feature_ids: builtins.list[str]) -> int:
392
+ """
393
+ Delete multiple features.
394
+
395
+ Args:
396
+ feature_ids: List of feature IDs to delete
397
+
398
+ Returns:
399
+ Number of features successfully deleted
400
+
401
+ Raises:
402
+ ValueError: If feature_ids not a list
403
+
404
+ Performance: O(k) where k = batch size
405
+
406
+ Examples:
407
+ >>> count = repo.batch_delete(["feat-1", "feat-2"])
408
+ >>> assert count == 2
409
+ """
410
+ ...
411
+
412
+ # ===== ADVANCED QUERIES =====
413
+
414
+ @abstractmethod
415
+ def find_dependencies(self, feature_id: str) -> builtins.list[Any]:
416
+ """
417
+ Find transitive feature dependencies.
418
+
419
+ Returns features that MUST be completed before given feature.
420
+ Traverses dependency graph to find all transitive deps.
421
+
422
+ Args:
423
+ feature_id: Feature to find dependencies for
424
+
425
+ Returns:
426
+ List of features this feature depends on (transitive closure)
427
+
428
+ Raises:
429
+ FeatureNotFoundError: If feature not found
430
+
431
+ Performance: O(n) graph traversal
432
+
433
+ Examples:
434
+ >>> deps = repo.find_dependencies("feat-auth")
435
+ >>> # Returns all features that must be done before auth
436
+ >>> assert all(f.id != "feat-auth" for f in deps)
437
+ """
438
+ ...
439
+
440
+ @abstractmethod
441
+ def find_blocking(self, feature_id: str) -> builtins.list[Any]:
442
+ """
443
+ Find what blocks this feature.
444
+
445
+ Inverse of dependencies: features that depend ON this feature.
446
+ Returns features blocked by given feature.
447
+
448
+ Args:
449
+ feature_id: Feature to find blockers for
450
+
451
+ Returns:
452
+ Features that depend on this feature
453
+
454
+ Raises:
455
+ FeatureNotFoundError: If feature not found
456
+
457
+ Examples:
458
+ >>> blockers = repo.find_blocking("feat-database-migration")
459
+ >>> # Returns all features waiting on this one
460
+ """
461
+ ...
462
+
463
+ @abstractmethod
464
+ def filter(self, predicate: Callable[[Any], bool]) -> builtins.list[Any]:
465
+ """
466
+ Filter features with custom predicate function.
467
+
468
+ For complex queries not covered by standard filters.
469
+
470
+ Args:
471
+ predicate: Function that takes Feature and returns True/False
472
+
473
+ Returns:
474
+ Features matching predicate
475
+
476
+ Examples:
477
+ >>> recent = repo.filter(
478
+ ... lambda f: (datetime.now() - f.created).days < 7
479
+ ... )
480
+ >>> contains_auth = repo.filter(
481
+ ... lambda f: "auth" in f.title.lower()
482
+ ... )
483
+ """
484
+ ...
485
+
486
+ # ===== CACHE/LIFECYCLE MANAGEMENT =====
487
+
488
+ @abstractmethod
489
+ def invalidate_cache(self, feature_id: str | None = None) -> None:
490
+ """
491
+ Invalidate cache for single feature or all features.
492
+
493
+ Forces reload from storage on next access.
494
+ Used when external process modifies storage.
495
+
496
+ Args:
497
+ feature_id: Specific feature to invalidate, or None for all
498
+
499
+ Examples:
500
+ >>> repo.invalidate_cache("feat-001") # Single feature
501
+ >>> repo.invalidate_cache() # Clear entire cache
502
+ """
503
+ ...
504
+
505
+ @abstractmethod
506
+ def reload(self) -> None:
507
+ """
508
+ Force reload all features from storage.
509
+
510
+ Invalidates all caches and reloads from disk/database.
511
+ Useful for external changes or cache reconciliation.
512
+
513
+ Examples:
514
+ >>> repo.reload() # Force refresh from storage
515
+ """
516
+ ...
517
+
518
+ @property
519
+ @abstractmethod
520
+ def auto_load(self) -> bool:
521
+ """
522
+ Whether auto-loading is enabled.
523
+
524
+ If True, features auto-load on first access.
525
+ If False, manual reload() required.
526
+
527
+ Returns:
528
+ True if auto-loading enabled, False otherwise
529
+ """
530
+ ...
531
+
532
+ @auto_load.setter
533
+ @abstractmethod
534
+ def auto_load(self, enabled: bool) -> None:
535
+ """
536
+ Enable/disable auto-loading.
537
+
538
+ Args:
539
+ enabled: True to enable auto-load, False to disable
540
+ """
541
+ ...
542
+
543
+ # ===== UTILITY METHODS =====
544
+
545
+ @abstractmethod
546
+ def count(self, filters: dict[str, Any] | None = None) -> int:
547
+ """
548
+ Count features matching filters.
549
+
550
+ Args:
551
+ filters: Optional filters (same as list())
552
+
553
+ Returns:
554
+ Number of matching features
555
+
556
+ Performance: O(n) or O(1) if optimized with SQL count
557
+
558
+ Examples:
559
+ >>> total = repo.count()
560
+ >>> todo_count = repo.count({"status": "todo"})
561
+ """
562
+ ...
563
+
564
+ @abstractmethod
565
+ def exists(self, feature_id: str) -> bool:
566
+ """
567
+ Check if feature exists without loading it.
568
+
569
+ Args:
570
+ feature_id: Feature ID to check
571
+
572
+ Returns:
573
+ True if exists, False otherwise
574
+
575
+ Performance: O(1) if optimized
576
+
577
+ Examples:
578
+ >>> if repo.exists("feat-001"):
579
+ ... feature = repo.get("feat-001")
580
+ """
581
+ ...