htmlgraph 0.9.3__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.
- htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +173 -17
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +127 -0
- htmlgraph/agent_registry.py +45 -30
- htmlgraph/agents.py +160 -107
- htmlgraph/analytics/__init__.py +9 -2
- htmlgraph/analytics/cli.py +190 -51
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +192 -100
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +190 -14
- htmlgraph/analytics_index.py +135 -51
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +578 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +208 -0
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/__init__.py +14 -0
- htmlgraph/builders/base.py +118 -29
- htmlgraph/builders/bug.py +150 -0
- htmlgraph/builders/chore.py +119 -0
- htmlgraph/builders/epic.py +150 -0
- htmlgraph/builders/feature.py +31 -6
- htmlgraph/builders/insight.py +195 -0
- htmlgraph/builders/metric.py +217 -0
- htmlgraph/builders/pattern.py +202 -0
- htmlgraph/builders/phase.py +162 -0
- htmlgraph/builders/spike.py +52 -19
- htmlgraph/builders/track.py +148 -72
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +18 -0
- htmlgraph/collections/base.py +415 -98
- htmlgraph/collections/bug.py +53 -0
- htmlgraph/collections/chore.py +53 -0
- htmlgraph/collections/epic.py +53 -0
- htmlgraph/collections/feature.py +12 -26
- htmlgraph/collections/insight.py +100 -0
- htmlgraph/collections/metric.py +92 -0
- htmlgraph/collections/pattern.py +97 -0
- htmlgraph/collections/phase.py +53 -0
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +56 -16
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +511 -0
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +344 -0
- htmlgraph/converter.py +216 -28
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2406 -307
- htmlgraph/dashboard.html.backup +6592 -0
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +19 -2
- htmlgraph/deploy.py +142 -125
- htmlgraph/deployment_models.py +474 -0
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +182 -27
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +100 -52
- htmlgraph/event_migration.py +13 -4
- htmlgraph/exceptions.py +49 -0
- htmlgraph/file_watcher.py +101 -28
- htmlgraph/find_api.py +75 -63
- htmlgraph/git_events.py +145 -63
- htmlgraph/graph.py +1122 -106
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +45 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +1314 -0
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/hooks-config.example.json +12 -0
- htmlgraph/hooks/installer.py +343 -0
- htmlgraph/hooks/orchestrator.py +674 -0
- htmlgraph/hooks/orchestrator_reflector.py +223 -0
- htmlgraph/hooks/post-checkout.sh +28 -0
- htmlgraph/hooks/post-commit.sh +24 -0
- htmlgraph/hooks/post-merge.sh +26 -0
- htmlgraph/hooks/post_tool_use_failure.py +273 -0
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +408 -0
- htmlgraph/hooks/pre-commit.sh +94 -0
- htmlgraph/hooks/pre-push.sh +28 -0
- htmlgraph/hooks/pretooluse.py +819 -0
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +255 -0
- htmlgraph/hooks/task_validator.py +177 -0
- htmlgraph/hooks/validator.py +628 -0
- htmlgraph/ids.py +41 -27
- htmlgraph/index.d.ts +286 -0
- htmlgraph/learning.py +767 -0
- htmlgraph/mcp_server.py +69 -23
- htmlgraph/models.py +1586 -87
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/orchestration/task_coordination.py +343 -0
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +669 -0
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +328 -0
- htmlgraph/orchestrator_validator.py +133 -0
- htmlgraph/parallel.py +646 -0
- htmlgraph/parser.py +160 -35
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/planning.py +147 -52
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +109 -72
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/routing.py +8 -19
- htmlgraph/scripts/deploy.py +1 -2
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +685 -180
- htmlgraph/services/__init__.py +10 -0
- htmlgraph/services/claiming.py +199 -0
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +1392 -175
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +201 -0
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/setup.py +34 -17
- htmlgraph/spike_index.py +143 -0
- htmlgraph/sync_docs.py +12 -15
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph/templates/GEMINI.md.template +87 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +146 -15
- htmlgraph/track_manager.py +69 -21
- htmlgraph/transcript.py +890 -0
- htmlgraph/transcript_analytics.py +699 -0
- htmlgraph/types.py +323 -0
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +8 -5
- htmlgraph/work_type_utils.py +3 -2
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
- htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -2688
- htmlgraph/sdk.py +0 -709
- htmlgraph-0.9.3.dist-info/RECORD +0 -61
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.9.3.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()
|