htmlgraph 0.21.0__py3-none-any.whl → 0.23.0__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/__init__.py +1 -1
- htmlgraph/agent_detection.py +41 -2
- htmlgraph/analytics/cli.py +86 -20
- htmlgraph/cli.py +519 -87
- htmlgraph/collections/base.py +68 -4
- 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 +161 -0
- htmlgraph/git_events.py +61 -7
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +61 -0
- htmlgraph/operations/analytics.py +338 -0
- htmlgraph/operations/events.py +243 -0
- htmlgraph/operations/hooks.py +349 -0
- htmlgraph/operations/server.py +302 -0
- htmlgraph/orchestration/__init__.py +39 -0
- htmlgraph/orchestration/headless_spawner.py +566 -0
- htmlgraph/orchestration/model_selection.py +323 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +47 -0
- htmlgraph/parser.py +56 -1
- htmlgraph/sdk.py +529 -7
- htmlgraph/server.py +153 -60
- {htmlgraph-0.21.0.dist-info → htmlgraph-0.23.0.dist-info}/METADATA +3 -1
- {htmlgraph-0.21.0.dist-info → htmlgraph-0.23.0.dist-info}/RECORD +40 -19
- /htmlgraph/{orchestration.py → orchestration/task_coordination.py} +0 -0
- {htmlgraph-0.21.0.data → htmlgraph-0.23.0.data}/data/htmlgraph/dashboard.html +0 -0
- {htmlgraph-0.21.0.data → htmlgraph-0.23.0.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.21.0.data → htmlgraph-0.23.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.21.0.data → htmlgraph-0.23.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.21.0.data → htmlgraph-0.23.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.21.0.dist-info → htmlgraph-0.23.0.dist-info}/WHEEL +0 -0
- {htmlgraph-0.21.0.dist-info → htmlgraph-0.23.0.dist-info}/entry_points.txt +0 -0
htmlgraph/collections/base.py
CHANGED
|
@@ -91,6 +91,48 @@ class BaseCollection(Generic[CollectionT]):
|
|
|
91
91
|
|
|
92
92
|
return self._graph
|
|
93
93
|
|
|
94
|
+
def __getattribute__(self, name: str) -> Any:
|
|
95
|
+
"""Override to provide helpful error messages for missing attributes."""
|
|
96
|
+
try:
|
|
97
|
+
return object.__getattribute__(self, name)
|
|
98
|
+
except AttributeError as e:
|
|
99
|
+
# Get available methods
|
|
100
|
+
available = [m for m in dir(self) if not m.startswith("_")]
|
|
101
|
+
|
|
102
|
+
# Common mistakes mapping
|
|
103
|
+
common_mistakes = {
|
|
104
|
+
"mark_complete": "mark_done",
|
|
105
|
+
"complete": "Use complete(node_id) for single item or mark_done([ids]) for batch",
|
|
106
|
+
"finish": "mark_done",
|
|
107
|
+
"end": "mark_done",
|
|
108
|
+
"update_status": "edit() context manager or batch_update()",
|
|
109
|
+
"mark_as_done": "mark_done",
|
|
110
|
+
"set_done": "mark_done",
|
|
111
|
+
"complete_all": "mark_done",
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
suggestions = []
|
|
115
|
+
if name in common_mistakes:
|
|
116
|
+
suggestions.append(f"Did you mean: {common_mistakes[name]}")
|
|
117
|
+
|
|
118
|
+
# Find similar method names
|
|
119
|
+
similar = [
|
|
120
|
+
m
|
|
121
|
+
for m in available
|
|
122
|
+
if name.lower() in m.lower() or m.lower() in name.lower()
|
|
123
|
+
]
|
|
124
|
+
if similar:
|
|
125
|
+
suggestions.append(f"Similar methods: {', '.join(similar[:5])}")
|
|
126
|
+
|
|
127
|
+
# Build helpful error message
|
|
128
|
+
error_msg = f"'{type(self).__name__}' has no attribute '{name}'."
|
|
129
|
+
if suggestions:
|
|
130
|
+
error_msg += "\n\n" + "\n".join(suggestions)
|
|
131
|
+
error_msg += f"\n\nAvailable methods: {', '.join(available[:15])}"
|
|
132
|
+
error_msg += "\n\nTip: Use sdk.help() to see all available operations."
|
|
133
|
+
|
|
134
|
+
raise AttributeError(error_msg) from e
|
|
135
|
+
|
|
94
136
|
def __dir__(self) -> list[str]:
|
|
95
137
|
"""Return attributes with most useful ones first for discoverability."""
|
|
96
138
|
priority = [
|
|
@@ -418,7 +460,7 @@ class BaseCollection(Generic[CollectionT]):
|
|
|
418
460
|
|
|
419
461
|
return count
|
|
420
462
|
|
|
421
|
-
def mark_done(self, node_ids: list[str]) ->
|
|
463
|
+
def mark_done(self, node_ids: list[str]) -> dict[str, Any]:
|
|
422
464
|
"""
|
|
423
465
|
Batch mark nodes as done.
|
|
424
466
|
|
|
@@ -426,12 +468,34 @@ class BaseCollection(Generic[CollectionT]):
|
|
|
426
468
|
node_ids: List of node IDs to mark as done
|
|
427
469
|
|
|
428
470
|
Returns:
|
|
429
|
-
|
|
471
|
+
Dict with 'success_count', 'failed_ids', and 'warnings'
|
|
430
472
|
|
|
431
473
|
Example:
|
|
432
|
-
>>> sdk.features.mark_done(["feat-001", "feat-002"])
|
|
474
|
+
>>> result = sdk.features.mark_done(["feat-001", "feat-002"])
|
|
475
|
+
>>> print(f"Completed {result['success_count']} of {len(node_ids)}")
|
|
476
|
+
>>> if result['failed_ids']:
|
|
477
|
+
... print(f"Failed: {result['failed_ids']}")
|
|
433
478
|
"""
|
|
434
|
-
|
|
479
|
+
graph = self._ensure_graph()
|
|
480
|
+
results: dict[str, Any] = {"success_count": 0, "failed_ids": [], "warnings": []}
|
|
481
|
+
|
|
482
|
+
for node_id in node_ids:
|
|
483
|
+
try:
|
|
484
|
+
node = graph.get(node_id)
|
|
485
|
+
if not node:
|
|
486
|
+
results["failed_ids"].append(node_id)
|
|
487
|
+
results["warnings"].append(f"Node {node_id} not found")
|
|
488
|
+
continue
|
|
489
|
+
|
|
490
|
+
node.status = "done"
|
|
491
|
+
node.updated = datetime.now()
|
|
492
|
+
graph.update(node)
|
|
493
|
+
results["success_count"] += 1
|
|
494
|
+
except Exception as e:
|
|
495
|
+
results["failed_ids"].append(node_id)
|
|
496
|
+
results["warnings"].append(f"Failed to mark {node_id}: {str(e)}")
|
|
497
|
+
|
|
498
|
+
return results
|
|
435
499
|
|
|
436
500
|
def assign(self, node_ids: list[str], agent: str) -> int:
|
|
437
501
|
"""
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Documentation version tracking and migration system with template-based user customization.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from htmlgraph import __version__
|
|
8
|
+
from htmlgraph.docs.docs_version import (
|
|
9
|
+
DOC_VERSIONS,
|
|
10
|
+
DocVersion,
|
|
11
|
+
get_current_doc_version,
|
|
12
|
+
is_compatible,
|
|
13
|
+
)
|
|
14
|
+
from htmlgraph.docs.metadata import DocsMetadata
|
|
15
|
+
from htmlgraph.docs.migrations import MIGRATIONS, MigrationScript, get_migration
|
|
16
|
+
from htmlgraph.docs.template_engine import DocTemplateEngine
|
|
17
|
+
from htmlgraph.docs.version_check import check_docs_version, upgrade_docs_interactive
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"DOC_VERSIONS",
|
|
21
|
+
"DocVersion",
|
|
22
|
+
"get_current_doc_version",
|
|
23
|
+
"is_compatible",
|
|
24
|
+
"DocsMetadata",
|
|
25
|
+
"MigrationScript",
|
|
26
|
+
"MIGRATIONS",
|
|
27
|
+
"get_migration",
|
|
28
|
+
"check_docs_version",
|
|
29
|
+
"upgrade_docs_interactive",
|
|
30
|
+
"DocTemplateEngine",
|
|
31
|
+
"get_agents_md",
|
|
32
|
+
"sync_docs_to_file",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_agents_md(htmlgraph_dir: Path, platform: str = "claude") -> str:
|
|
37
|
+
"""Get AGENTS.md content with user customizations merged.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
htmlgraph_dir: Path to .htmlgraph directory
|
|
41
|
+
platform: Platform name (claude, gemini, etc.)
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Merged documentation content
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
>>> from pathlib import Path
|
|
48
|
+
>>> content = get_agents_md(Path(".htmlgraph"), "claude")
|
|
49
|
+
"""
|
|
50
|
+
engine = DocTemplateEngine(htmlgraph_dir)
|
|
51
|
+
return engine.render_agents_md(__version__, platform)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def sync_docs_to_file(
|
|
55
|
+
htmlgraph_dir: Path, output_file: Path, platform: str = "claude"
|
|
56
|
+
) -> Path:
|
|
57
|
+
"""Generate and write documentation to file.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
htmlgraph_dir: Path to .htmlgraph directory
|
|
61
|
+
output_file: Path where documentation should be written
|
|
62
|
+
platform: Platform name (claude, gemini, etc.)
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Path to written file
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
>>> from pathlib import Path
|
|
69
|
+
>>> sync_docs_to_file(
|
|
70
|
+
... Path(".htmlgraph"),
|
|
71
|
+
... Path("AGENTS.md"),
|
|
72
|
+
... "claude"
|
|
73
|
+
... )
|
|
74
|
+
"""
|
|
75
|
+
content = get_agents_md(htmlgraph_dir, platform)
|
|
76
|
+
output_file.write_text(content)
|
|
77
|
+
return output_file
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Documentation schema version management.
|
|
3
|
+
|
|
4
|
+
This module defines version compatibility rules for HtmlGraph documentation files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class DocVersion:
|
|
12
|
+
"""Documentation schema version."""
|
|
13
|
+
|
|
14
|
+
version: int # Schema version (1, 2, 3)
|
|
15
|
+
package_version: str # Minimum package version (e.g., "0.20.0")
|
|
16
|
+
breaking_changes: list[str]
|
|
17
|
+
migration_required: bool = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Version compatibility matrix
|
|
21
|
+
DOC_VERSIONS = {
|
|
22
|
+
1: DocVersion(
|
|
23
|
+
version=1,
|
|
24
|
+
package_version="0.1.0",
|
|
25
|
+
breaking_changes=[],
|
|
26
|
+
migration_required=False,
|
|
27
|
+
),
|
|
28
|
+
2: DocVersion(
|
|
29
|
+
version=2,
|
|
30
|
+
package_version="0.20.0",
|
|
31
|
+
breaking_changes=[
|
|
32
|
+
"AGENTS.md structure changed to use Jinja2 templates",
|
|
33
|
+
"Removed root-level CLAUDE.md/GEMINI.md (moved to .htmlgraph/docs/)",
|
|
34
|
+
],
|
|
35
|
+
migration_required=True,
|
|
36
|
+
),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_current_doc_version() -> int:
|
|
41
|
+
"""Get current documentation schema version for this package."""
|
|
42
|
+
return 2 # Current version
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def is_compatible(user_version: int, package_version: int) -> bool:
|
|
46
|
+
"""Check if user's doc version is compatible with package.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
user_version: User's documentation schema version
|
|
50
|
+
package_version: Package's required documentation schema version
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
True if compatible (supports N-1 versions)
|
|
54
|
+
"""
|
|
55
|
+
return user_version >= package_version - 1 # Support N-1 versions
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Documentation metadata storage and management.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DocsMetadata(BaseModel):
|
|
12
|
+
"""Metadata for project documentation."""
|
|
13
|
+
|
|
14
|
+
schema_version: int = 2
|
|
15
|
+
last_updated: datetime = Field(default_factory=datetime.now)
|
|
16
|
+
customizations: list[str] = [] # List of user-customized sections
|
|
17
|
+
base_version_on_last_update: str = "0.21.0"
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def load(cls, htmlgraph_dir: Path) -> "DocsMetadata":
|
|
21
|
+
"""Load metadata from .docs-metadata.json.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
htmlgraph_dir: Path to .htmlgraph directory
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
DocsMetadata instance (default if file doesn't exist)
|
|
28
|
+
"""
|
|
29
|
+
import json
|
|
30
|
+
|
|
31
|
+
metadata_file = htmlgraph_dir / ".docs-metadata.json"
|
|
32
|
+
if metadata_file.exists():
|
|
33
|
+
data = json.loads(metadata_file.read_text())
|
|
34
|
+
return cls(**data)
|
|
35
|
+
return cls() # Default
|
|
36
|
+
|
|
37
|
+
def save(self, htmlgraph_dir: Path) -> None:
|
|
38
|
+
"""Save metadata to .docs-metadata.json.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
htmlgraph_dir: Path to .htmlgraph directory
|
|
42
|
+
"""
|
|
43
|
+
metadata_file = htmlgraph_dir / ".docs-metadata.json"
|
|
44
|
+
metadata_file.write_text(self.model_dump_json(indent=2))
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def create_initial(
|
|
48
|
+
cls, htmlgraph_dir: Path, schema_version: int = 2
|
|
49
|
+
) -> "DocsMetadata":
|
|
50
|
+
"""Create initial metadata file.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
htmlgraph_dir: Path to .htmlgraph directory
|
|
54
|
+
schema_version: Documentation schema version
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
New DocsMetadata instance
|
|
58
|
+
"""
|
|
59
|
+
from htmlgraph import __version__
|
|
60
|
+
|
|
61
|
+
metadata = cls(
|
|
62
|
+
schema_version=schema_version,
|
|
63
|
+
base_version_on_last_update=__version__,
|
|
64
|
+
customizations=[],
|
|
65
|
+
)
|
|
66
|
+
metadata.save(htmlgraph_dir)
|
|
67
|
+
return metadata
|
|
68
|
+
|
|
69
|
+
def add_customization(self, section_name: str) -> None:
|
|
70
|
+
"""Mark a section as customized.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
section_name: Name of the customized section
|
|
74
|
+
"""
|
|
75
|
+
if section_name not in self.customizations:
|
|
76
|
+
self.customizations.append(section_name)
|
|
77
|
+
|
|
78
|
+
def remove_customization(self, section_name: str) -> None:
|
|
79
|
+
"""Remove a customization marker.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
section_name: Name of the section to unmark
|
|
83
|
+
"""
|
|
84
|
+
if section_name in self.customizations:
|
|
85
|
+
self.customizations.remove(section_name)
|
|
86
|
+
|
|
87
|
+
def has_customizations(self) -> bool:
|
|
88
|
+
"""Check if any customizations exist.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
True if user has customized documentation
|
|
92
|
+
"""
|
|
93
|
+
return len(self.customizations) > 0
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Documentation migration system.
|
|
3
|
+
|
|
4
|
+
Handles version migrations with automatic customization preservation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import shutil
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from htmlgraph.docs.metadata import DocsMetadata
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MigrationScript(ABC):
|
|
16
|
+
"""Base class for documentation migrations."""
|
|
17
|
+
|
|
18
|
+
from_version: int
|
|
19
|
+
to_version: int
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def migrate(self, htmlgraph_dir: Path, backup_dir: Path) -> bool:
|
|
23
|
+
"""Execute migration. Returns True if successful.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
htmlgraph_dir: Path to .htmlgraph directory
|
|
27
|
+
backup_dir: Path to backup directory
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
True if migration successful
|
|
31
|
+
"""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def rollback(self, htmlgraph_dir: Path, backup_dir: Path) -> None:
|
|
36
|
+
"""Rollback migration to previous state.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
htmlgraph_dir: Path to .htmlgraph directory
|
|
40
|
+
backup_dir: Path to backup directory
|
|
41
|
+
"""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
def backup_docs(self, htmlgraph_dir: Path, backup_dir: Path) -> Path:
|
|
45
|
+
"""Backup current documentation before migration.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
htmlgraph_dir: Path to .htmlgraph directory
|
|
49
|
+
backup_dir: Path to backup directory
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Path to created backup subdirectory
|
|
53
|
+
"""
|
|
54
|
+
backup_subdir = (
|
|
55
|
+
backup_dir / f"v{self.from_version}_{datetime.now():%Y%m%d_%H%M%S}"
|
|
56
|
+
)
|
|
57
|
+
backup_subdir.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
|
|
59
|
+
# Backup all docs
|
|
60
|
+
docs_dir = htmlgraph_dir / "docs"
|
|
61
|
+
if docs_dir.exists():
|
|
62
|
+
for doc_file in docs_dir.glob("*.md"):
|
|
63
|
+
shutil.copy2(doc_file, backup_subdir / doc_file.name)
|
|
64
|
+
|
|
65
|
+
# Also backup root-level docs (legacy)
|
|
66
|
+
for doc_file in htmlgraph_dir.glob("*.md"):
|
|
67
|
+
shutil.copy2(doc_file, backup_subdir / doc_file.name)
|
|
68
|
+
|
|
69
|
+
return backup_subdir
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class V1toV2Migration(MigrationScript):
|
|
73
|
+
"""Migrate from v1 (root-level docs) to v2 (template-based docs)."""
|
|
74
|
+
|
|
75
|
+
from_version = 1
|
|
76
|
+
to_version = 2
|
|
77
|
+
|
|
78
|
+
def migrate(self, htmlgraph_dir: Path, backup_dir: Path) -> bool:
|
|
79
|
+
"""Migrate v1 docs to v2 template structure.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
htmlgraph_dir: Path to .htmlgraph directory
|
|
83
|
+
backup_dir: Path to backup directory
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
True if migration successful
|
|
87
|
+
"""
|
|
88
|
+
# Backup first
|
|
89
|
+
backup_path = self.backup_docs(htmlgraph_dir, backup_dir)
|
|
90
|
+
print(f"✅ Backed up to {backup_path}")
|
|
91
|
+
|
|
92
|
+
# Detect user customizations in old AGENTS.md
|
|
93
|
+
customizations = self._detect_customizations(htmlgraph_dir / "AGENTS.md")
|
|
94
|
+
if customizations:
|
|
95
|
+
print(f"📝 Detected customizations: {', '.join(customizations)}")
|
|
96
|
+
|
|
97
|
+
# Move old docs to archive
|
|
98
|
+
old_agents_md = htmlgraph_dir / "AGENTS.md"
|
|
99
|
+
if old_agents_md.exists():
|
|
100
|
+
old_agents_md.rename(htmlgraph_dir / "AGENTS.md.v1.backup")
|
|
101
|
+
print("📦 Archived old AGENTS.md")
|
|
102
|
+
|
|
103
|
+
# Generate new v2 docs with customizations preserved
|
|
104
|
+
self._generate_v2_docs(htmlgraph_dir, customizations)
|
|
105
|
+
print("✨ Generated v2 documentation")
|
|
106
|
+
|
|
107
|
+
# Update metadata
|
|
108
|
+
metadata = DocsMetadata.load(htmlgraph_dir)
|
|
109
|
+
metadata.schema_version = 2
|
|
110
|
+
metadata.customizations = customizations
|
|
111
|
+
metadata.save(htmlgraph_dir)
|
|
112
|
+
print("💾 Updated metadata")
|
|
113
|
+
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
def _detect_customizations(self, agents_md_path: Path) -> list[str]:
|
|
117
|
+
"""Detect user-customized sections in old AGENTS.md.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
agents_md_path: Path to AGENTS.md file
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
List of customized section names
|
|
124
|
+
"""
|
|
125
|
+
if not agents_md_path.exists():
|
|
126
|
+
return []
|
|
127
|
+
|
|
128
|
+
content = agents_md_path.read_text()
|
|
129
|
+
customizations = []
|
|
130
|
+
|
|
131
|
+
# Simple heuristic: sections not in base template
|
|
132
|
+
if "## Our Team" in content:
|
|
133
|
+
customizations.append("custom_workflows")
|
|
134
|
+
if "## Project Conventions" in content:
|
|
135
|
+
customizations.append("project_conventions")
|
|
136
|
+
if "## Custom Workflows" in content:
|
|
137
|
+
customizations.append("custom_workflows")
|
|
138
|
+
|
|
139
|
+
return customizations
|
|
140
|
+
|
|
141
|
+
def _generate_v2_docs(self, htmlgraph_dir: Path, customizations: list[str]) -> None:
|
|
142
|
+
"""Generate v2 template-based docs with customizations.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
htmlgraph_dir: Path to .htmlgraph directory
|
|
146
|
+
customizations: List of customized sections to preserve
|
|
147
|
+
"""
|
|
148
|
+
# NOTE: This would integrate with sync_docs.py
|
|
149
|
+
# For now, just create placeholder
|
|
150
|
+
docs_dir = htmlgraph_dir / "docs"
|
|
151
|
+
docs_dir.mkdir(exist_ok=True)
|
|
152
|
+
|
|
153
|
+
placeholder = docs_dir / "AGENTS.md"
|
|
154
|
+
placeholder.write_text(
|
|
155
|
+
"""# HtmlGraph Agent Documentation (v2)
|
|
156
|
+
|
|
157
|
+
This file was migrated from v1 to v2.
|
|
158
|
+
|
|
159
|
+
Run `uv run htmlgraph sync-docs` to regenerate from templates.
|
|
160
|
+
"""
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Inject customizations as template overrides if needed
|
|
164
|
+
if customizations:
|
|
165
|
+
self._create_override_template(htmlgraph_dir, customizations)
|
|
166
|
+
|
|
167
|
+
def _create_override_template(
|
|
168
|
+
self, htmlgraph_dir: Path, customizations: list[str]
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Create template overrides for customizations.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
htmlgraph_dir: Path to .htmlgraph directory
|
|
174
|
+
customizations: List of customizations to preserve
|
|
175
|
+
"""
|
|
176
|
+
overrides_file = htmlgraph_dir / "docs" / "overrides.md"
|
|
177
|
+
overrides_file.write_text(
|
|
178
|
+
f"""# Documentation Customizations
|
|
179
|
+
|
|
180
|
+
The following sections were customized in v1:
|
|
181
|
+
|
|
182
|
+
{chr(10).join(f"- {c}" for c in customizations)}
|
|
183
|
+
|
|
184
|
+
To preserve these customizations, add them back manually after running sync-docs.
|
|
185
|
+
"""
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def rollback(self, htmlgraph_dir: Path, backup_dir: Path) -> None:
|
|
189
|
+
"""Rollback to v1 docs from backup.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
htmlgraph_dir: Path to .htmlgraph directory
|
|
193
|
+
backup_dir: Path to backup directory
|
|
194
|
+
"""
|
|
195
|
+
# Find latest backup
|
|
196
|
+
backups = sorted(backup_dir.glob("v1_*"))
|
|
197
|
+
if not backups:
|
|
198
|
+
raise ValueError("No backup found for rollback")
|
|
199
|
+
|
|
200
|
+
latest_backup = backups[-1]
|
|
201
|
+
print(f"🔄 Rolling back from {latest_backup}")
|
|
202
|
+
|
|
203
|
+
# Restore old AGENTS.md
|
|
204
|
+
backup_agents = latest_backup / "AGENTS.md"
|
|
205
|
+
if backup_agents.exists():
|
|
206
|
+
shutil.copy2(backup_agents, htmlgraph_dir / "AGENTS.md")
|
|
207
|
+
print("✅ Restored AGENTS.md")
|
|
208
|
+
|
|
209
|
+
# Update metadata
|
|
210
|
+
metadata = DocsMetadata.load(htmlgraph_dir)
|
|
211
|
+
metadata.schema_version = 1
|
|
212
|
+
metadata.save(htmlgraph_dir)
|
|
213
|
+
print("💾 Updated metadata to v1")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# Migration registry
|
|
217
|
+
MIGRATIONS: dict[tuple[int, int], MigrationScript] = {
|
|
218
|
+
(1, 2): V1toV2Migration(),
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def get_migration(from_version: int, to_version: int) -> MigrationScript | None:
|
|
223
|
+
"""Get migration script for version transition.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
from_version: Source schema version
|
|
227
|
+
to_version: Target schema version
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
MigrationScript instance or None if no migration exists
|
|
231
|
+
"""
|
|
232
|
+
return MIGRATIONS.get((from_version, to_version))
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Jinja2-based template engine for documentation with user customization."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from jinja2 import ChoiceLoader, Environment, FileSystemLoader
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DocTemplateEngine:
|
|
11
|
+
"""Renders documentation templates with user customization support.
|
|
12
|
+
|
|
13
|
+
Template Priority:
|
|
14
|
+
1. User templates in .htmlgraph/docs/templates/ (highest priority)
|
|
15
|
+
2. Package templates in htmlgraph/docs/templates/ (fallback)
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
>>> engine = DocTemplateEngine(Path(".htmlgraph"))
|
|
19
|
+
>>> content = engine.render_agents_md("0.21.0", "claude")
|
|
20
|
+
>>> print(content)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, htmlgraph_dir: Path):
|
|
24
|
+
"""Initialize template engine with multi-loader.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
htmlgraph_dir: Path to .htmlgraph directory
|
|
28
|
+
"""
|
|
29
|
+
# Package templates (bundled with pip install)
|
|
30
|
+
package_templates = Path(__file__).parent / "templates"
|
|
31
|
+
|
|
32
|
+
# User templates (project-specific customizations)
|
|
33
|
+
user_templates = htmlgraph_dir / "docs" / "templates"
|
|
34
|
+
|
|
35
|
+
# Multi-loader: User templates have priority over package templates
|
|
36
|
+
loaders = []
|
|
37
|
+
if user_templates.exists():
|
|
38
|
+
loaders.append(FileSystemLoader(str(user_templates)))
|
|
39
|
+
loaders.append(FileSystemLoader(str(package_templates)))
|
|
40
|
+
|
|
41
|
+
self.env = Environment(loader=ChoiceLoader(loaders))
|
|
42
|
+
|
|
43
|
+
# Add custom filters
|
|
44
|
+
self.env.filters["format_date"] = self._format_date
|
|
45
|
+
self.env.filters["highlight_code"] = self._highlight_code
|
|
46
|
+
|
|
47
|
+
def render(self, template_name: str, context: dict[str, Any]) -> str:
|
|
48
|
+
"""Render template with context, merging user overrides.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
template_name: Name of template file (e.g., "agents.md.j2")
|
|
52
|
+
context: Template variables
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Rendered content with user customizations applied
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
>>> engine.render("agents.md.j2", {"platform": "claude"})
|
|
59
|
+
"""
|
|
60
|
+
template = self.env.get_template(template_name)
|
|
61
|
+
return template.render(**context)
|
|
62
|
+
|
|
63
|
+
def render_agents_md(self, sdk_version: str, platform: str = "claude") -> str:
|
|
64
|
+
"""Render AGENTS.md with platform-specific customizations.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
sdk_version: HtmlGraph SDK version (e.g., "0.21.0")
|
|
68
|
+
platform: Platform name (claude, gemini, api, etc.)
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Rendered AGENTS.md content
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
>>> content = engine.render_agents_md("0.21.0", "claude")
|
|
75
|
+
"""
|
|
76
|
+
context = {
|
|
77
|
+
"sdk_version": sdk_version,
|
|
78
|
+
"platform": platform,
|
|
79
|
+
"features_enabled": self._get_enabled_features(),
|
|
80
|
+
"custom_workflows": self._load_custom_workflows(),
|
|
81
|
+
"generated_at": datetime.now().isoformat(),
|
|
82
|
+
}
|
|
83
|
+
# User templates should extend "base_agents.md.j2" to avoid recursion
|
|
84
|
+
# Priority: agents.md.j2 (user override) → base_agents.md.j2 (package default)
|
|
85
|
+
template_name = "agents.md.j2"
|
|
86
|
+
try:
|
|
87
|
+
# Try to get user template first (will succeed if it exists in user dir)
|
|
88
|
+
self.env.get_template(template_name)
|
|
89
|
+
return self.render(template_name, context)
|
|
90
|
+
except: # noqa
|
|
91
|
+
# Fall back to base template (always exists in package)
|
|
92
|
+
return self.render("base_agents.md.j2", context)
|
|
93
|
+
|
|
94
|
+
def _format_date(self, value: str) -> str:
|
|
95
|
+
"""Format ISO date string for display.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
value: ISO 8601 date string
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Formatted date string
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
dt = datetime.fromisoformat(value)
|
|
105
|
+
return dt.strftime("%Y-%m-%d %H:%M")
|
|
106
|
+
except (ValueError, AttributeError):
|
|
107
|
+
return value
|
|
108
|
+
|
|
109
|
+
def _highlight_code(self, code: str, language: str = "python") -> str:
|
|
110
|
+
"""Add markdown code fence with syntax highlighting.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
code: Code snippet
|
|
114
|
+
language: Programming language for syntax highlighting
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Markdown code block
|
|
118
|
+
"""
|
|
119
|
+
return f"```{language}\n{code}\n```"
|
|
120
|
+
|
|
121
|
+
def _get_enabled_features(self) -> dict[str, bool]:
|
|
122
|
+
"""Get enabled features for this installation.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Dictionary of feature flags
|
|
126
|
+
"""
|
|
127
|
+
# TODO: Read from config or detect dynamically
|
|
128
|
+
return {
|
|
129
|
+
"sessions": True,
|
|
130
|
+
"tracks": True,
|
|
131
|
+
"analytics": True,
|
|
132
|
+
"mcp": True,
|
|
133
|
+
"cli": True,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
def _load_custom_workflows(self) -> str | None:
|
|
137
|
+
"""Load custom workflows from user template.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Custom workflow markdown or None
|
|
141
|
+
"""
|
|
142
|
+
# TODO: Load from .htmlgraph/docs/workflows.md if exists
|
|
143
|
+
return None
|