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,395 @@
1
+ """
2
+ MemorySharedCache - In-memory singleton cache with LRU eviction and TTL.
3
+
4
+ Thread-safe implementation using RLock for concurrent access.
5
+ Provides O(1) operations for get/set/delete with pattern-based invalidation.
6
+ """
7
+
8
+ import fnmatch
9
+ import threading
10
+ import time
11
+ from collections.abc import Callable
12
+ from typing import Any, Optional
13
+
14
+ from .shared_cache import (
15
+ CacheCapacityError,
16
+ CacheKeyError,
17
+ SharedCache,
18
+ )
19
+
20
+
21
+ class MemorySharedCache(SharedCache):
22
+ """
23
+ In-memory cache with LRU eviction and TTL support.
24
+
25
+ Features:
26
+ - Thread-safe singleton pattern
27
+ - O(1) get/set/delete operations
28
+ - LRU eviction when max_size exceeded
29
+ - TTL-based automatic expiration
30
+ - Pattern-based invalidation (prefix matching)
31
+ - Comprehensive statistics tracking
32
+
33
+ Performance:
34
+ - get(key): O(1) average
35
+ - set(key, value, ttl): O(1) average
36
+ - delete(key): O(1)
37
+ - delete_pattern(pattern): O(n) where n = total keys
38
+ - clear(): O(n)
39
+ """
40
+
41
+ _instance: Optional["MemorySharedCache"] = None
42
+ _lock_class = threading.RLock()
43
+
44
+ def __init__(
45
+ self,
46
+ max_size: int = 1000,
47
+ default_ttl: int = 3600,
48
+ metrics_enabled: bool = True,
49
+ ):
50
+ """Initialize cache with configuration."""
51
+ self._cache: dict[str, tuple[Any, float | None]] = {}
52
+ self._access_times: dict[str, float] = {}
53
+ self._max_size = max_size
54
+ self._default_ttl = default_ttl
55
+ self._metrics_enabled = metrics_enabled
56
+ self._lock = threading.RLock()
57
+
58
+ # Statistics
59
+ self._stats = {
60
+ "hits": 0,
61
+ "misses": 0,
62
+ "evictions": 0,
63
+ "total_load_time_ms": 0.0,
64
+ "load_count": 0,
65
+ }
66
+
67
+ # ===== SINGLETON MANAGEMENT =====
68
+
69
+ @classmethod
70
+ def initialize(
71
+ cls, max_size: int = 1000, default_ttl: int = 3600, metrics_enabled: bool = True
72
+ ) -> "MemorySharedCache":
73
+ """Initialize singleton cache instance."""
74
+ with cls._lock_class:
75
+ if cls._instance is None:
76
+ cls._instance = cls(max_size, default_ttl, metrics_enabled)
77
+ return cls._instance
78
+
79
+ @classmethod
80
+ def get_instance(cls) -> "MemorySharedCache":
81
+ """Get singleton instance."""
82
+ with cls._lock_class:
83
+ if cls._instance is None:
84
+ raise RuntimeError("Cache not initialized. Call initialize() first.")
85
+ return cls._instance
86
+
87
+ @classmethod
88
+ def reset_instance(cls) -> None:
89
+ """Reset singleton (for testing only)."""
90
+ with cls._lock_class:
91
+ cls._instance = None
92
+
93
+ # ===== GET OPERATIONS =====
94
+
95
+ def get(self, key: str) -> Any | None:
96
+ """Retrieve cached value by key."""
97
+ if not key:
98
+ raise CacheKeyError("Cache key cannot be empty")
99
+
100
+ with self._lock:
101
+ if key not in self._cache:
102
+ if self._metrics_enabled:
103
+ self._stats["misses"] += 1
104
+ return None
105
+
106
+ value, ttl_expiry = self._cache[key]
107
+
108
+ # Check TTL expiration
109
+ if ttl_expiry is not None and ttl_expiry < time.time():
110
+ del self._cache[key]
111
+ del self._access_times[key]
112
+ if self._metrics_enabled:
113
+ self._stats["misses"] += 1
114
+ return None
115
+
116
+ # Update LRU tracking
117
+ self._access_times[key] = time.time()
118
+
119
+ if self._metrics_enabled:
120
+ self._stats["hits"] += 1
121
+
122
+ return value
123
+
124
+ def get_or_compute(
125
+ self, key: str, compute_fn: Callable[[], Any], ttl: int | None = None
126
+ ) -> Any:
127
+ """Get cached value or compute and cache if missing."""
128
+ value = self.get(key)
129
+ if value is not None:
130
+ return value
131
+
132
+ # Compute and cache
133
+ start_time = time.time()
134
+ value = compute_fn()
135
+ elapsed_ms = (time.time() - start_time) * 1000
136
+
137
+ if self._metrics_enabled:
138
+ self._stats["total_load_time_ms"] += elapsed_ms
139
+ self._stats["load_count"] += 1
140
+
141
+ self.set(key, value, ttl)
142
+ return value
143
+
144
+ def exists(self, key: str) -> bool:
145
+ """Check if key exists in cache."""
146
+ with self._lock:
147
+ if key not in self._cache:
148
+ return False
149
+
150
+ value, ttl_expiry = self._cache[key]
151
+
152
+ # Check TTL expiration
153
+ if ttl_expiry is not None and ttl_expiry < time.time():
154
+ del self._cache[key]
155
+ del self._access_times[key]
156
+ return False
157
+
158
+ return True
159
+
160
+ # ===== SET OPERATIONS =====
161
+
162
+ def set(self, key: str, value: Any, ttl: int | None = None) -> None:
163
+ """Cache a value with optional time-to-live."""
164
+ if not key:
165
+ raise CacheKeyError("Cache key cannot be empty")
166
+
167
+ with self._lock:
168
+ # Evict LRU if needed and key doesn't already exist
169
+ if len(self._cache) >= self._max_size and key not in self._cache:
170
+ if not self._access_times:
171
+ raise CacheCapacityError("Cache at capacity and can't evict")
172
+
173
+ # Find and evict LRU item
174
+ lru_key = min(self._access_times, key=lambda k: self._access_times[k])
175
+ del self._cache[lru_key]
176
+ del self._access_times[lru_key]
177
+
178
+ if self._metrics_enabled:
179
+ self._stats["evictions"] += 1
180
+
181
+ # Calculate TTL expiry
182
+ ttl_seconds = ttl if ttl is not None else self._default_ttl
183
+ ttl_expiry = time.time() + ttl_seconds if ttl_seconds else None
184
+
185
+ # Store value and update access time
186
+ self._cache[key] = (value, ttl_expiry)
187
+ self._access_times[key] = time.time()
188
+
189
+ def set_many(self, items: dict[str, Any], ttl: int | None = None) -> None:
190
+ """Cache multiple key-value pairs at once."""
191
+ for key, value in items.items():
192
+ self.set(key, value, ttl)
193
+
194
+ # ===== DELETE OPERATIONS =====
195
+
196
+ def delete(self, key: str) -> bool:
197
+ """Delete single cached value."""
198
+ with self._lock:
199
+ if key in self._cache:
200
+ del self._cache[key]
201
+ del self._access_times[key]
202
+ return True
203
+ return False
204
+
205
+ def delete_pattern(self, pattern: str) -> int:
206
+ """Delete all cached values matching pattern."""
207
+ with self._lock:
208
+ # Convert pattern to prefix if using wildcard syntax
209
+ if pattern.endswith("*"):
210
+ prefix = pattern[:-1]
211
+ matching_keys = [k for k in self._cache if k.startswith(prefix)]
212
+ else:
213
+ matching_keys = [k for k in self._cache if fnmatch.fnmatch(k, pattern)]
214
+
215
+ for key in matching_keys:
216
+ del self._cache[key]
217
+ del self._access_times[key]
218
+
219
+ return len(matching_keys)
220
+
221
+ def clear(self) -> int:
222
+ """Clear all cached values."""
223
+ with self._lock:
224
+ count = len(self._cache)
225
+ self._cache.clear()
226
+ self._access_times.clear()
227
+ return count
228
+
229
+ # ===== BATCH OPERATIONS =====
230
+
231
+ def get_many(self, keys: list[str]) -> dict[str, Any]:
232
+ """Retrieve multiple cached values at once."""
233
+ result = {}
234
+ for key in keys:
235
+ value = self.get(key)
236
+ if value is not None:
237
+ result[key] = value
238
+ return result
239
+
240
+ def delete_many(self, keys: list[str]) -> int:
241
+ """Delete multiple cached values at once."""
242
+ count = 0
243
+ for key in keys:
244
+ if self.delete(key):
245
+ count += 1
246
+ return count
247
+
248
+ # ===== INVALIDATION HELPERS =====
249
+
250
+ def invalidate_feature(self, feature_id: str) -> None:
251
+ """Invalidate all caches related to a feature."""
252
+ with self._lock:
253
+ self.delete(f"feature:{feature_id}")
254
+ self.delete_pattern("feature:list:*")
255
+ self.delete(f"dependency:{feature_id}")
256
+ self.delete_pattern(f"dependency:*:blocking_for_{feature_id}")
257
+ self.delete(f"priority:{feature_id}")
258
+ self.delete_pattern("recommendation:*")
259
+
260
+ def invalidate_track(self, track_id: str) -> None:
261
+ """Invalidate all caches related to a track."""
262
+ with self._lock:
263
+ self.delete(f"track:{track_id}")
264
+ self.delete(f"track:{track_id}:features")
265
+ self.delete_pattern("track:list:*")
266
+ # Tracks can affect features, so invalidate feature analytics
267
+ self.delete_pattern("recommendation:*")
268
+
269
+ def invalidate_analytics(self) -> None:
270
+ """Invalidate all analytics caches."""
271
+ with self._lock:
272
+ self.delete_pattern("dependency:*")
273
+ self.delete_pattern("priority:*")
274
+ self.delete_pattern("recommendation:*")
275
+ self.delete_pattern("critical_path:*")
276
+ self.delete_pattern("blocking:*")
277
+
278
+ # ===== OBSERVABILITY =====
279
+
280
+ def size(self) -> int:
281
+ """Get current number of cached items."""
282
+ with self._lock:
283
+ return len(self._cache)
284
+
285
+ def stats(self) -> dict[str, Any]:
286
+ """Get detailed cache statistics."""
287
+ with self._lock:
288
+ total_requests = self._stats["hits"] + self._stats["misses"]
289
+ hit_rate = (
290
+ self._stats["hits"] / total_requests if total_requests > 0 else 0.0
291
+ )
292
+
293
+ avg_load_ms = (
294
+ self._stats["total_load_time_ms"] / self._stats["load_count"]
295
+ if self._stats["load_count"] > 0
296
+ else 0.0
297
+ )
298
+
299
+ # Estimate memory usage (rough approximation)
300
+ # Assume average 1KB per cached item
301
+ memory_bytes = len(self._cache) * 1024
302
+
303
+ return {
304
+ "hits": self._stats["hits"],
305
+ "misses": self._stats["misses"],
306
+ "hit_rate": hit_rate,
307
+ "evictions": self._stats["evictions"],
308
+ "size": len(self._cache),
309
+ "capacity": self._max_size,
310
+ "memory_bytes": memory_bytes,
311
+ "avg_load_ms": avg_load_ms,
312
+ }
313
+
314
+ def reset_stats(self) -> None:
315
+ """Reset cache statistics to zero."""
316
+ with self._lock:
317
+ self._stats = {
318
+ "hits": 0,
319
+ "misses": 0,
320
+ "evictions": 0,
321
+ "total_load_time_ms": 0.0,
322
+ "load_count": 0,
323
+ }
324
+
325
+ # ===== CONFIGURATION =====
326
+
327
+ def configure(
328
+ self,
329
+ max_size: int | None = None,
330
+ default_ttl: int | None = None,
331
+ metrics_enabled: bool | None = None,
332
+ ) -> None:
333
+ """Configure cache behavior."""
334
+ with self._lock:
335
+ if max_size is not None:
336
+ self._max_size = max_size
337
+ if default_ttl is not None:
338
+ self._default_ttl = default_ttl
339
+ if metrics_enabled is not None:
340
+ self._metrics_enabled = metrics_enabled
341
+
342
+ def is_configured(self) -> bool:
343
+ """Check if cache is properly configured."""
344
+ return self._max_size > 0 and self._default_ttl >= 0 and self._cache is not None
345
+
346
+ # ===== DEBUG / UTILITY =====
347
+
348
+ def debug_info(self) -> dict[str, Any]:
349
+ """Get detailed debug information."""
350
+ with self._lock:
351
+ keys_info = {}
352
+ for key in self._cache:
353
+ value, ttl_expiry = self._cache[key]
354
+ keys_info[key] = {
355
+ "ttl_remaining": (
356
+ ttl_expiry - time.time() if ttl_expiry is not None else None
357
+ ),
358
+ "access_time": self._access_times.get(key),
359
+ }
360
+
361
+ # Sort by LRU order (oldest first)
362
+ lru_order = sorted(
363
+ self._access_times.keys(), key=lambda k: self._access_times[k]
364
+ )
365
+
366
+ return {
367
+ "keys": list(self._cache.keys()),
368
+ "key_info": keys_info,
369
+ "lru_order": lru_order,
370
+ "size": len(self._cache),
371
+ "capacity": self._max_size,
372
+ "stats": self.stats(),
373
+ }
374
+
375
+ def validate_integrity(self) -> bool:
376
+ """Validate cache internal consistency."""
377
+ with self._lock:
378
+ try:
379
+ # Check no expired items
380
+ current_time = time.time()
381
+ for key, (value, ttl_expiry) in self._cache.items():
382
+ if ttl_expiry is not None and ttl_expiry < current_time:
383
+ return False # Expired item found
384
+
385
+ # Check all cache keys have access times
386
+ if set(self._cache.keys()) != set(self._access_times.keys()):
387
+ return False
388
+
389
+ # Check size tracking
390
+ if len(self._cache) > self._max_size:
391
+ return False
392
+
393
+ return True
394
+ except Exception:
395
+ return False