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,583 @@
1
+ """
2
+ Archive manager for consolidating and managing archived entities.
3
+
4
+ Provides:
5
+ - Archive creation with hybrid time+status naming (2024-Q4-completed.html)
6
+ - Archive search with three-tier optimization
7
+ - Unarchive (restore) functionality
8
+ - Cross-reference preservation
9
+ """
10
+
11
+ import json
12
+ from dataclasses import asdict, dataclass
13
+ from datetime import datetime, timedelta
14
+ from pathlib import Path
15
+ from typing import Any, Literal
16
+
17
+ from htmlgraph.archive.bloom import BloomFilter
18
+ from htmlgraph.archive.fts import ArchiveFTS5Index
19
+ from htmlgraph.archive.search import ArchiveSearchEngine
20
+ from htmlgraph.models import Node
21
+ from htmlgraph.parser import HtmlParser
22
+
23
+
24
+ @dataclass
25
+ class ArchiveConfig:
26
+ """
27
+ Configuration for archive management.
28
+
29
+ Attributes:
30
+ retention_days: Days before entities are eligible for archiving
31
+ archive_period: Time period for grouping (quarter, month, year)
32
+ entity_types: Types of entities to archive (feature, bug, etc.)
33
+ status_filter: Only archive entities with these statuses
34
+ auto_archive: Enable automatic archiving
35
+ """
36
+
37
+ retention_days: int = 90
38
+ archive_period: Literal["quarter", "month", "year"] = "quarter"
39
+ entity_types: list[str] = None # type: ignore
40
+ status_filter: list[str] = None # type: ignore
41
+ auto_archive: bool = False
42
+
43
+ def __post_init__(self) -> None:
44
+ """Set defaults for mutable fields."""
45
+ if self.entity_types is None:
46
+ # Support all HtmlGraph entity types
47
+ self.entity_types = [
48
+ "feature",
49
+ "bug",
50
+ "chore",
51
+ "spike",
52
+ "pattern",
53
+ "session",
54
+ "track",
55
+ "epic",
56
+ "phase",
57
+ "insight",
58
+ "metric",
59
+ ]
60
+ if self.status_filter is None:
61
+ self.status_filter = ["done", "cancelled", "obsolete"]
62
+
63
+ def to_dict(self) -> dict[str, Any]:
64
+ """Convert to dictionary."""
65
+ return asdict(self)
66
+
67
+ @classmethod
68
+ def from_dict(cls, data: dict[str, Any]) -> "ArchiveConfig":
69
+ """Create from dictionary."""
70
+ return cls(**data)
71
+
72
+
73
+ class ArchiveManager:
74
+ """
75
+ Manages entity archiving and search.
76
+
77
+ Workflow:
78
+ 1. Identify entities eligible for archiving (age + status)
79
+ 2. Group by time period (quarter, month, year)
80
+ 3. Consolidate into archive HTML files
81
+ 4. Build Bloom filters and FTS5 index
82
+ 5. Update cross-references in active entities
83
+ """
84
+
85
+ def __init__(self, htmlgraph_dir: Path) -> None:
86
+ """
87
+ Initialize archive manager.
88
+
89
+ Args:
90
+ htmlgraph_dir: Path to .htmlgraph directory
91
+ """
92
+ self.htmlgraph_dir = htmlgraph_dir
93
+ self.archive_dir = htmlgraph_dir / "archives"
94
+ self.index_dir = htmlgraph_dir / "archive-index"
95
+ self.config_path = htmlgraph_dir / "config.json"
96
+
97
+ # Create directories
98
+ self.archive_dir.mkdir(parents=True, exist_ok=True)
99
+ self.index_dir.mkdir(parents=True, exist_ok=True)
100
+
101
+ # Load configuration
102
+ self.config = self._load_config()
103
+
104
+ # Initialize search engine
105
+ self.search_engine = ArchiveSearchEngine(self.archive_dir, self.index_dir)
106
+
107
+ def _load_config(self) -> ArchiveConfig:
108
+ """Load configuration from disk."""
109
+ if self.config_path.exists():
110
+ with open(self.config_path) as f:
111
+ data = json.load(f)
112
+ return ArchiveConfig.from_dict(data.get("archive", {}))
113
+ return ArchiveConfig()
114
+
115
+ def _save_config(self) -> None:
116
+ """Save configuration to disk."""
117
+ # Load existing config or create new
118
+ if self.config_path.exists():
119
+ with open(self.config_path) as f:
120
+ config_data = json.load(f)
121
+ else:
122
+ config_data = {}
123
+
124
+ # Update archive section
125
+ config_data["archive"] = self.config.to_dict()
126
+
127
+ # Save
128
+ with open(self.config_path, "w") as f:
129
+ json.dump(config_data, f, indent=2)
130
+
131
+ def set_config(self, config: ArchiveConfig) -> None:
132
+ """
133
+ Update configuration.
134
+
135
+ Args:
136
+ config: New configuration
137
+ """
138
+ self.config = config
139
+ self._save_config()
140
+
141
+ def _get_period_name(self, dt: datetime) -> str:
142
+ """
143
+ Get period name for a datetime.
144
+
145
+ Args:
146
+ dt: Datetime to get period for
147
+
148
+ Returns:
149
+ Period string (e.g., "2024-Q4", "2024-12", "2024")
150
+ """
151
+ if self.config.archive_period == "quarter":
152
+ quarter = (dt.month - 1) // 3 + 1
153
+ return f"{dt.year}-Q{quarter}"
154
+ elif self.config.archive_period == "month":
155
+ return f"{dt.year}-{dt.month:02d}"
156
+ else: # year
157
+ return str(dt.year)
158
+
159
+ def _get_eligible_entities(
160
+ self, entity_type: str, older_than_days: int | None = None
161
+ ) -> list[tuple[Path, Node]]:
162
+ """
163
+ Get entities eligible for archiving.
164
+
165
+ Args:
166
+ entity_type: Type of entity (feature, bug, etc.)
167
+ older_than_days: Override retention days
168
+
169
+ Returns:
170
+ List of (filepath, node) tuples
171
+ """
172
+ days = (
173
+ older_than_days
174
+ if older_than_days is not None
175
+ else self.config.retention_days
176
+ )
177
+ cutoff_date = datetime.now() - timedelta(days=days)
178
+
179
+ entity_dir = self.htmlgraph_dir / f"{entity_type}s"
180
+ if not entity_dir.exists():
181
+ return []
182
+
183
+ eligible = []
184
+
185
+ for filepath in entity_dir.glob("*.html"):
186
+ try:
187
+ # Parse entity
188
+ from htmlgraph.converter import html_to_node
189
+
190
+ node = html_to_node(filepath)
191
+
192
+ # Check status filter
193
+ if node.status not in self.config.status_filter:
194
+ continue
195
+
196
+ # Check age (use updated timestamp)
197
+ if node.updated < cutoff_date:
198
+ eligible.append((filepath, node))
199
+
200
+ except Exception:
201
+ # Skip unparseable files
202
+ continue
203
+
204
+ return eligible
205
+
206
+ def _create_archive_html(
207
+ self,
208
+ archive_file: str,
209
+ entities: list[Node],
210
+ period: str,
211
+ status: str,
212
+ ) -> str:
213
+ """
214
+ Create consolidated archive HTML file.
215
+
216
+ Args:
217
+ archive_file: Archive filename
218
+ entities: List of entities to include
219
+ period: Time period (e.g., "2024-Q4")
220
+ status: Status filter (e.g., "completed")
221
+
222
+ Returns:
223
+ HTML content as string
224
+ """
225
+ # Sort entities by updated date (newest first)
226
+ sorted_entities = sorted(entities, key=lambda e: e.updated, reverse=True)
227
+
228
+ # Build table of contents
229
+ toc_items = "\n".join(
230
+ f'<li><a href="#{e.id}">{e.title}</a> ({e.type})</li>'
231
+ for e in sorted_entities
232
+ )
233
+
234
+ # Build entity sections
235
+ entity_sections = []
236
+ for entity in sorted_entities:
237
+ # Generate full HTML for entity
238
+ entity_html = entity.to_html()
239
+
240
+ # Extract the <article> content
241
+ # We'll wrap it in a section for better structure
242
+ entity_sections.append(
243
+ f'<section id="{entity.id}" class="archived-entity">\n{entity_html}\n</section>'
244
+ )
245
+
246
+ entities_html = "\n\n".join(entity_sections)
247
+
248
+ # Create archive HTML
249
+ html = f"""<!DOCTYPE html>
250
+ <html lang="en">
251
+ <head>
252
+ <meta charset="UTF-8">
253
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
254
+ <title>Archive: {period} ({status})</title>
255
+ <link rel="stylesheet" href="../styles.css">
256
+ <style>
257
+ .archive-banner {{
258
+ background: #f0f0f0;
259
+ border: 2px solid #ccc;
260
+ padding: 1rem;
261
+ margin-bottom: 2rem;
262
+ border-radius: 4px;
263
+ }}
264
+ .archive-search {{
265
+ width: 100%;
266
+ padding: 0.5rem;
267
+ font-size: 1rem;
268
+ border: 1px solid #ccc;
269
+ border-radius: 4px;
270
+ }}
271
+ .archived-entity {{
272
+ border-bottom: 2px solid #eee;
273
+ margin-bottom: 2rem;
274
+ padding-bottom: 2rem;
275
+ }}
276
+ .toc {{
277
+ background: #f9f9f9;
278
+ padding: 1rem;
279
+ border-radius: 4px;
280
+ margin-bottom: 2rem;
281
+ }}
282
+ </style>
283
+ </head>
284
+ <body>
285
+ <div class="archive-banner">
286
+ <h1>📦 Archive: {period} - {status.title()}</h1>
287
+ <p><strong>{len(entities)}</strong> archived entities</p>
288
+ <input type="text" class="archive-search" id="searchInput" placeholder="Search this archive...">
289
+ </div>
290
+
291
+ <div class="toc">
292
+ <h2>Contents</h2>
293
+ <ul>
294
+ {toc_items}
295
+ </ul>
296
+ </div>
297
+
298
+ {entities_html}
299
+
300
+ <script>
301
+ // Client-side search
302
+ const searchInput = document.getElementById('searchInput');
303
+ const entities = document.querySelectorAll('.archived-entity');
304
+
305
+ searchInput.addEventListener('input', (e) => {{
306
+ const query = e.target.value.toLowerCase();
307
+
308
+ entities.forEach(entity => {{
309
+ const text = entity.textContent.toLowerCase();
310
+ if (text.includes(query)) {{
311
+ entity.style.display = 'block';
312
+ }} else {{
313
+ entity.style.display = 'none';
314
+ }}
315
+ }});
316
+ }});
317
+ </script>
318
+ </body>
319
+ </html>"""
320
+
321
+ return html
322
+
323
+ def archive_entities(
324
+ self,
325
+ entity_types: list[str] | None = None,
326
+ status_filter: list[str] | None = None,
327
+ older_than_days: int | None = None,
328
+ period: Literal["quarter", "month", "year"] | None = None,
329
+ dry_run: bool = False,
330
+ ) -> dict[str, Any]:
331
+ """
332
+ Archive eligible entities.
333
+
334
+ Args:
335
+ entity_types: Types to archive (default: config)
336
+ status_filter: Statuses to archive (default: config)
337
+ older_than_days: Age threshold (default: config)
338
+ period: Grouping period (default: config)
339
+ dry_run: Preview without making changes
340
+
341
+ Returns:
342
+ Dictionary with archived_count, archive_files, etc.
343
+ """
344
+ types = entity_types or self.config.entity_types
345
+ statuses = status_filter or self.config.status_filter
346
+ period_type = period or self.config.archive_period
347
+
348
+ # Temporarily override config for this operation
349
+ original_period = self.config.archive_period
350
+ self.config.archive_period = period_type
351
+
352
+ # Group entities by period and status
353
+ archives: dict[str, list[Node]] = {}
354
+ entity_files: dict[str, list[Path]] = {}
355
+
356
+ for entity_type in types:
357
+ eligible = self._get_eligible_entities(entity_type, older_than_days)
358
+
359
+ for filepath, node in eligible:
360
+ if node.status not in statuses:
361
+ continue
362
+
363
+ period_name = self._get_period_name(node.updated)
364
+ archive_key = f"{period_name}-{node.status}"
365
+
366
+ if archive_key not in archives:
367
+ archives[archive_key] = []
368
+ entity_files[archive_key] = []
369
+
370
+ archives[archive_key].append(node)
371
+ entity_files[archive_key].append(filepath)
372
+
373
+ # Restore original config
374
+ self.config.archive_period = original_period
375
+
376
+ if dry_run:
377
+ return {
378
+ "dry_run": True,
379
+ "would_archive": sum(len(entities) for entities in archives.values()),
380
+ "archive_files": list(archives.keys()),
381
+ "details": {key: len(entities) for key, entities in archives.items()},
382
+ }
383
+
384
+ # Create archive files
385
+ created_archives = []
386
+
387
+ for archive_key, entities in archives.items():
388
+ archive_file = f"{archive_key}.html"
389
+ archive_path = self.archive_dir / archive_file
390
+
391
+ # Extract period and status from key
392
+ parts = archive_key.rsplit("-", 1)
393
+ period_name = parts[0]
394
+ status = parts[1]
395
+
396
+ # Create HTML
397
+ html = self._create_archive_html(
398
+ archive_file, entities, period_name, status
399
+ )
400
+
401
+ # Write to disk
402
+ with open(archive_path, "w") as f:
403
+ f.write(html)
404
+
405
+ # Build Bloom filter
406
+ bloom = BloomFilter(expected_items=len(entities))
407
+ entity_dicts = [
408
+ {
409
+ "id": e.id,
410
+ "title": e.title,
411
+ "description": e.content or "",
412
+ }
413
+ for e in entities
414
+ ]
415
+ bloom.build_for_archive(entity_dicts)
416
+ bloom.save(self.index_dir / f"{archive_file}.bloom")
417
+
418
+ # Index in FTS5
419
+ fts_index = ArchiveFTS5Index(self.index_dir / "archives.db")
420
+ content_dicts = [
421
+ {
422
+ "id": e.id,
423
+ "title": e.title,
424
+ "description": e.content or "",
425
+ "content": " ".join(s.description for s in e.steps),
426
+ "type": e.type,
427
+ "status": e.status,
428
+ "created": e.created.isoformat(),
429
+ "updated": e.updated.isoformat(),
430
+ }
431
+ for e in entities
432
+ ]
433
+ fts_index.index_archive(archive_file, content_dicts)
434
+ fts_index.close()
435
+
436
+ # Delete original files
437
+ for filepath in entity_files[archive_key]:
438
+ filepath.unlink()
439
+
440
+ created_archives.append(archive_file)
441
+
442
+ # Clear search cache (new archives added)
443
+ self.search_engine.clear_cache()
444
+
445
+ return {
446
+ "dry_run": False,
447
+ "archived_count": sum(len(entities) for entities in archives.values()),
448
+ "archive_files": created_archives,
449
+ "details": {key: len(entities) for key, entities in archives.items()},
450
+ }
451
+
452
+ def unarchive(self, entity_id: str) -> bool:
453
+ """
454
+ Restore an archived entity to active status.
455
+
456
+ Args:
457
+ entity_id: Entity to restore
458
+
459
+ Returns:
460
+ True if restored, False if not found
461
+ """
462
+
463
+ # Find entity in FTS5 index
464
+ fts_index = ArchiveFTS5Index(self.index_dir / "archives.db")
465
+ metadata = fts_index.get_entity_metadata(entity_id)
466
+ fts_index.close()
467
+
468
+ if not metadata:
469
+ return False
470
+
471
+ archive_file = metadata["archive_file"]
472
+ archive_path = self.archive_dir / archive_file
473
+
474
+ if not archive_path.exists():
475
+ return False
476
+
477
+ # Parse archive HTML
478
+ parser = HtmlParser.from_file(archive_path)
479
+
480
+ # Find entity section by ID
481
+ sections = parser.query(f"section#{entity_id}")
482
+ if not sections:
483
+ return False
484
+
485
+ # Extract the article element from section
486
+ articles = sections[0].query("article")
487
+ if not articles:
488
+ return False
489
+
490
+ # Get article HTML string and create temp file
491
+ import tempfile
492
+
493
+ from htmlgraph.converter import html_to_node
494
+
495
+ article_html = str(articles[0])
496
+
497
+ # Write to temp file for parsing
498
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False) as tmp:
499
+ # Wrap article in minimal HTML document
500
+ tmp.write(
501
+ f"<!DOCTYPE html><html><head><meta charset='UTF-8'></head><body>{article_html}</body></html>"
502
+ )
503
+ tmp_path = Path(tmp.name)
504
+
505
+ try:
506
+ node = html_to_node(tmp_path)
507
+ finally:
508
+ tmp_path.unlink(missing_ok=True)
509
+
510
+ # Determine target directory
511
+ entity_type = node.type
512
+ target_dir = self.htmlgraph_dir / f"{entity_type}s"
513
+ target_dir.mkdir(parents=True, exist_ok=True)
514
+
515
+ # Generate filename
516
+ target_path = target_dir / f"{entity_id}.html"
517
+
518
+ # Write entity file
519
+ with open(target_path, "w") as f:
520
+ f.write(node.to_html())
521
+
522
+ # TODO: Remove from archive HTML and update indexes
523
+ # For now, leave in archive (duplicate is acceptable)
524
+
525
+ return True
526
+
527
+ def get_archive_stats(self) -> dict[str, Any]:
528
+ """
529
+ Get statistics about archives.
530
+
531
+ Returns:
532
+ Dictionary with archive_count, entity_count, size_mb, etc.
533
+ """
534
+ archive_files = list(self.archive_dir.glob("*.html"))
535
+ total_size = sum(f.stat().st_size for f in archive_files)
536
+
537
+ # Get FTS5 stats
538
+ fts_index = ArchiveFTS5Index(self.index_dir / "archives.db")
539
+ fts_stats = fts_index.get_stats()
540
+ fts_index.close()
541
+
542
+ # Get Bloom filter stats
543
+ bloom_files = list(self.index_dir.glob("*.bloom"))
544
+ bloom_size = sum(f.stat().st_size for f in bloom_files)
545
+
546
+ return {
547
+ "archive_count": len(archive_files),
548
+ "entity_count": fts_stats["entity_count"],
549
+ "total_size_mb": total_size / (1024 * 1024),
550
+ "fts_size_mb": fts_stats["db_size_mb"],
551
+ "bloom_size_kb": bloom_size / 1024,
552
+ "bloom_count": len(bloom_files),
553
+ }
554
+
555
+ def search(self, query: str, limit: int = 10) -> list[dict[str, Any]]:
556
+ """
557
+ Search archived entities.
558
+
559
+ Args:
560
+ query: Search query
561
+ limit: Maximum results
562
+
563
+ Returns:
564
+ List of search results
565
+ """
566
+ results = self.search_engine.search(query, include_archived=True, limit=limit)
567
+
568
+ return [
569
+ {
570
+ "entity_id": r.entity_id,
571
+ "archive_file": r.archive_file,
572
+ "entity_type": r.entity_type,
573
+ "status": r.status,
574
+ "title_snippet": r.title_snippet,
575
+ "description_snippet": r.description_snippet,
576
+ "rank": r.rank,
577
+ }
578
+ for r in results
579
+ ]
580
+
581
+ def close(self) -> None:
582
+ """Close all resources."""
583
+ self.search_engine.close()