htmlgraph 0.2.2__tar.gz → 0.3.0__tar.gz
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-0.2.2 → htmlgraph-0.3.0}/PKG-INFO +1 -1
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/pyproject.toml +1 -1
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/__init__.py +1 -1
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/converter.py +17 -7
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/file_watcher.py +3 -1
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/graph.py +16 -12
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/models.py +4 -1
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/sdk.py +2 -1
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/server.py +48 -51
- htmlgraph-0.3.0/src/python/htmlgraph/track_builder.py +261 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/.gitignore +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/README.md +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/examples/create_auth_track_demo.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/examples/create_htmlgraph_dev_track.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/examples/sdk_demo.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/examples/todo-list/demo.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/examples/todo-list/index.html +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/examples/todo-list/styles.css +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/examples/todo-list/task-001.html +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/examples/todo-list/task-002.html +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/examples/todo-list/task-003.html +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/examples/todo-list/task-004.html +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/examples/todo-list/task-005.html +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/agents.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/analytics_index.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/cli.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/dashboard.html +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/event_log.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/event_migration.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/git_events.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/mcp_server.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/parser.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/planning.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/session_manager.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/setup.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/styles.css +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/track_manager.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/watch.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/tests/python/__init__.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/tests/python/test_cli_commands.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/tests/python/test_dashboard_ui.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/tests/python/test_event_index.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/tests/python/test_git_events.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/tests/python/test_mcp_server.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/tests/python/test_mcp_stdio_transport.py +0 -0
- {htmlgraph-0.2.2 → htmlgraph-0.3.0}/tests/python/test_models.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: htmlgraph
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: HTML is All You Need - Graph database on web standards
|
|
5
5
|
Project-URL: Homepage, https://github.com/Shakes-tzd/htmlgraph
|
|
6
6
|
Project-URL: Documentation, https://github.com/Shakes-tzd/htmlgraph#readme
|
|
@@ -253,14 +253,24 @@ class NodeConverter:
|
|
|
253
253
|
return html_to_node(filepath)
|
|
254
254
|
return None
|
|
255
255
|
|
|
256
|
-
def load_all(self, pattern: str = "*.html") -> list[Node]:
|
|
257
|
-
"""
|
|
256
|
+
def load_all(self, pattern: str | list[str] = "*.html") -> list[Node]:
|
|
257
|
+
"""
|
|
258
|
+
Load all nodes matching pattern(s).
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
pattern: Glob pattern(s) to match. Can be a single pattern or list of patterns.
|
|
262
|
+
Examples: "*.html", ["*.html", "*/index.html"]
|
|
263
|
+
"""
|
|
258
264
|
nodes = []
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
265
|
+
patterns = [pattern] if isinstance(pattern, str) else pattern
|
|
266
|
+
|
|
267
|
+
for pat in patterns:
|
|
268
|
+
for filepath in self.directory.glob(pat):
|
|
269
|
+
if filepath.is_file(): # Skip directories
|
|
270
|
+
try:
|
|
271
|
+
nodes.append(html_to_node(filepath))
|
|
272
|
+
except (ValueError, KeyError):
|
|
273
|
+
continue # Skip malformed files
|
|
264
274
|
return nodes
|
|
265
275
|
|
|
266
276
|
def save(self, node: Node) -> Path:
|
|
@@ -99,7 +99,9 @@ class GraphWatcher:
|
|
|
99
99
|
self.handlers[collection] = handler
|
|
100
100
|
|
|
101
101
|
# Watch the collection directory
|
|
102
|
-
|
|
102
|
+
# Use recursive=True for tracks since they're stored in subdirectories
|
|
103
|
+
recursive = (collection == "tracks")
|
|
104
|
+
self.observer.schedule(handler, str(collection_dir), recursive=recursive)
|
|
103
105
|
|
|
104
106
|
self.observer.start()
|
|
105
107
|
print(f"[FileWatcher] Watching {self.graph_dir} for changes...")
|
|
@@ -36,7 +36,7 @@ class HtmlGraph:
|
|
|
36
36
|
directory: Path | str,
|
|
37
37
|
stylesheet_path: str = "../styles.css",
|
|
38
38
|
auto_load: bool = True,
|
|
39
|
-
pattern: str = "*.html"
|
|
39
|
+
pattern: str | list[str] = "*.html"
|
|
40
40
|
):
|
|
41
41
|
"""
|
|
42
42
|
Initialize graph from a directory.
|
|
@@ -45,7 +45,8 @@ class HtmlGraph:
|
|
|
45
45
|
directory: Directory containing HTML node files
|
|
46
46
|
stylesheet_path: Default stylesheet path for new files
|
|
47
47
|
auto_load: Whether to load all nodes on init
|
|
48
|
-
pattern: Glob pattern for node files
|
|
48
|
+
pattern: Glob pattern(s) for node files. Can be a single pattern or list.
|
|
49
|
+
Examples: "*.html", ["*.html", "*/index.html"]
|
|
49
50
|
"""
|
|
50
51
|
self.directory = Path(directory)
|
|
51
52
|
self.directory.mkdir(parents=True, exist_ok=True)
|
|
@@ -195,16 +196,19 @@ class HtmlGraph:
|
|
|
195
196
|
"""
|
|
196
197
|
matching = []
|
|
197
198
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
199
|
+
patterns = [self.pattern] if isinstance(self.pattern, str) else self.pattern
|
|
200
|
+
for pat in patterns:
|
|
201
|
+
for filepath in self.directory.glob(pat):
|
|
202
|
+
if filepath.is_file():
|
|
203
|
+
try:
|
|
204
|
+
parser = HtmlParser.from_file(filepath)
|
|
205
|
+
# Query for article matching selector
|
|
206
|
+
if parser.query(f"article{selector}"):
|
|
207
|
+
node_id = parser.get_node_id()
|
|
208
|
+
if node_id and node_id in self._nodes:
|
|
209
|
+
matching.append(self._nodes[node_id])
|
|
210
|
+
except Exception:
|
|
211
|
+
continue
|
|
208
212
|
|
|
209
213
|
return matching
|
|
210
214
|
|
|
@@ -235,6 +235,9 @@ class Node(BaseModel):
|
|
|
235
235
|
# Agent attribute
|
|
236
236
|
agent_attr = f' data-agent-assigned="{self.agent_assigned}"' if self.agent_assigned else ""
|
|
237
237
|
|
|
238
|
+
# Track ID attribute
|
|
239
|
+
track_attr = f' data-track-id="{self.track_id}"' if self.track_id else ""
|
|
240
|
+
|
|
238
241
|
return f'''<!DOCTYPE html>
|
|
239
242
|
<html lang="en">
|
|
240
243
|
<head>
|
|
@@ -250,7 +253,7 @@ class Node(BaseModel):
|
|
|
250
253
|
data-status="{self.status}"
|
|
251
254
|
data-priority="{self.priority}"
|
|
252
255
|
data-created="{self.created.isoformat()}"
|
|
253
|
-
data-updated="{self.updated.isoformat()}"{agent_attr}>
|
|
256
|
+
data-updated="{self.updated.isoformat()}"{agent_attr}{track_attr}>
|
|
254
257
|
|
|
255
258
|
<header>
|
|
256
259
|
<h1>{self.title}</h1>
|
|
@@ -47,6 +47,7 @@ from dataclasses import dataclass
|
|
|
47
47
|
from htmlgraph.models import Node, Step, Edge
|
|
48
48
|
from htmlgraph.graph import HtmlGraph
|
|
49
49
|
from htmlgraph.agents import AgentInterface
|
|
50
|
+
from htmlgraph.track_builder import TrackCollection
|
|
50
51
|
|
|
51
52
|
|
|
52
53
|
@dataclass
|
|
@@ -409,7 +410,7 @@ class SDK:
|
|
|
409
410
|
|
|
410
411
|
# Non-work collections
|
|
411
412
|
self.sessions = Collection(self, "sessions", "session")
|
|
412
|
-
self.tracks =
|
|
413
|
+
self.tracks = TrackCollection(self) # Use specialized collection with builder support
|
|
413
414
|
self.agents = Collection(self, "agents", "agent")
|
|
414
415
|
|
|
415
416
|
@staticmethod
|
|
@@ -54,65 +54,62 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
|
|
|
54
54
|
# Tracks support both file-based (track-xxx.html) and directory-based (track-xxx/index.html)
|
|
55
55
|
if collection == "tracks":
|
|
56
56
|
from htmlgraph.planning import Track
|
|
57
|
+
from htmlgraph.converter import html_to_node
|
|
57
58
|
|
|
58
59
|
graph = HtmlGraph(
|
|
59
60
|
collection_dir,
|
|
60
61
|
stylesheet_path="../styles.css",
|
|
61
|
-
auto_load=False # Manual load to
|
|
62
|
+
auto_load=False, # Manual load to convert to Track objects
|
|
63
|
+
pattern=["*.html", "*/index.html"]
|
|
62
64
|
)
|
|
63
65
|
|
|
64
|
-
# Helper
|
|
66
|
+
# Helper to convert Node to Track with has_spec/has_plan detection
|
|
65
67
|
def node_to_track(node: Node, filepath: Path) -> Track:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"description": node.content or "",
|
|
84
|
-
"status": node.status if node.status in ["planned", "active", "completed", "abandoned"] else "planned",
|
|
85
|
-
"priority": node.priority,
|
|
86
|
-
"created": node.created,
|
|
87
|
-
"updated": node.updated,
|
|
88
|
-
"has_spec": has_spec,
|
|
89
|
-
"has_plan": has_plan,
|
|
90
|
-
"features": [], # Will be populated from properties if present
|
|
91
|
-
"sessions": [], # Will be populated from properties if present
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return Track(**track_data)
|
|
95
|
-
|
|
96
|
-
# Load file-based tracks (track-xxx.html)
|
|
97
|
-
for filepath in collection_dir.glob("*.html"):
|
|
98
|
-
try:
|
|
99
|
-
from htmlgraph.converter import html_to_node
|
|
100
|
-
node = html_to_node(filepath)
|
|
101
|
-
track = node_to_track(node, filepath)
|
|
102
|
-
graph._nodes[track.id] = track
|
|
103
|
-
except Exception:
|
|
104
|
-
continue
|
|
105
|
-
|
|
106
|
-
# Load directory-based tracks (track-xxx/index.html)
|
|
107
|
-
for filepath in collection_dir.glob("*/index.html"):
|
|
108
|
-
try:
|
|
109
|
-
from htmlgraph.converter import html_to_node
|
|
110
|
-
node = html_to_node(filepath)
|
|
111
|
-
track = node_to_track(node, filepath)
|
|
112
|
-
graph._nodes[track.id] = track
|
|
113
|
-
except Exception:
|
|
114
|
-
continue
|
|
68
|
+
track_dir = filepath.parent if filepath.name == "index.html" else None
|
|
69
|
+
has_spec = (track_dir / "spec.html").exists() if track_dir else False
|
|
70
|
+
has_plan = (track_dir / "plan.html").exists() if track_dir else False
|
|
71
|
+
|
|
72
|
+
return Track(
|
|
73
|
+
id=node.id,
|
|
74
|
+
title=node.title,
|
|
75
|
+
description=node.content or "",
|
|
76
|
+
status=node.status if node.status in ["planned", "active", "completed", "abandoned"] else "planned",
|
|
77
|
+
priority=node.priority,
|
|
78
|
+
created=node.created,
|
|
79
|
+
updated=node.updated,
|
|
80
|
+
has_spec=has_spec,
|
|
81
|
+
has_plan=has_plan,
|
|
82
|
+
features=[],
|
|
83
|
+
sessions=[]
|
|
84
|
+
)
|
|
115
85
|
|
|
86
|
+
# Load and convert tracks
|
|
87
|
+
patterns = graph.pattern if isinstance(graph.pattern, list) else [graph.pattern]
|
|
88
|
+
for pat in patterns:
|
|
89
|
+
for filepath in collection_dir.glob(pat):
|
|
90
|
+
if filepath.is_file():
|
|
91
|
+
try:
|
|
92
|
+
node = html_to_node(filepath)
|
|
93
|
+
track = node_to_track(node, filepath)
|
|
94
|
+
graph._nodes[track.id] = track
|
|
95
|
+
except Exception:
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
# Override reload to maintain Track conversion
|
|
99
|
+
def reload_tracks():
|
|
100
|
+
graph._nodes.clear()
|
|
101
|
+
for pat in patterns:
|
|
102
|
+
for filepath in collection_dir.glob(pat):
|
|
103
|
+
if filepath.is_file():
|
|
104
|
+
try:
|
|
105
|
+
node = html_to_node(filepath)
|
|
106
|
+
track = node_to_track(node, filepath)
|
|
107
|
+
graph._nodes[track.id] = track
|
|
108
|
+
except Exception:
|
|
109
|
+
continue
|
|
110
|
+
return len(graph._nodes)
|
|
111
|
+
|
|
112
|
+
graph.reload = reload_tracks
|
|
116
113
|
self.graphs[collection] = graph
|
|
117
114
|
else:
|
|
118
115
|
self.graphs[collection] = HtmlGraph(
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Track Builder for agent-friendly track creation.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from htmlgraph.sdk import SDK
|
|
12
|
+
|
|
13
|
+
from htmlgraph.planning import Track, Spec, Plan, Phase, Task, Requirement, AcceptanceCriterion
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TrackBuilder:
|
|
17
|
+
"""
|
|
18
|
+
Fluent builder for creating tracks with spec and plan.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
track = sdk.tracks.builder() \\
|
|
22
|
+
.title("Multi-Agent Collaboration") \\
|
|
23
|
+
.description("Enable seamless agent collaboration") \\
|
|
24
|
+
.priority("high") \\
|
|
25
|
+
.with_spec(
|
|
26
|
+
overview="Agents can work together...",
|
|
27
|
+
requirements=[
|
|
28
|
+
("Add assigned_agent field", "must-have"),
|
|
29
|
+
("Implement claim CLI", "must-have")
|
|
30
|
+
]
|
|
31
|
+
) \\
|
|
32
|
+
.with_plan_phases([
|
|
33
|
+
("Phase 1", ["Add field (1h)", "Implement CLI (2h)"]),
|
|
34
|
+
("Phase 2", ["Add notes (1h)", "Update hooks (2h)"])
|
|
35
|
+
]) \\
|
|
36
|
+
.create()
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, sdk: SDK):
|
|
40
|
+
self.sdk = sdk
|
|
41
|
+
self._title = None
|
|
42
|
+
self._description = ""
|
|
43
|
+
self._priority = "medium"
|
|
44
|
+
self._spec_data = {}
|
|
45
|
+
self._plan_phases = []
|
|
46
|
+
|
|
47
|
+
def title(self, title: str) -> 'TrackBuilder':
|
|
48
|
+
"""Set track title."""
|
|
49
|
+
self._title = title
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
def description(self, desc: str) -> 'TrackBuilder':
|
|
53
|
+
"""Set track description."""
|
|
54
|
+
self._description = desc
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
def priority(self, priority: str) -> 'TrackBuilder':
|
|
58
|
+
"""Set track priority (low/medium/high/critical)."""
|
|
59
|
+
self._priority = priority
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def with_spec(
|
|
63
|
+
self,
|
|
64
|
+
overview: str = "",
|
|
65
|
+
context: str = "",
|
|
66
|
+
requirements: list = None,
|
|
67
|
+
acceptance_criteria: list = None
|
|
68
|
+
) -> 'TrackBuilder':
|
|
69
|
+
"""
|
|
70
|
+
Add spec content to track.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
overview: High-level summary
|
|
74
|
+
context: Background and current state
|
|
75
|
+
requirements: List of (description, priority) tuples or strings
|
|
76
|
+
acceptance_criteria: List of strings or (description, test_case) tuples
|
|
77
|
+
"""
|
|
78
|
+
self._spec_data = {
|
|
79
|
+
"overview": overview,
|
|
80
|
+
"context": context,
|
|
81
|
+
"requirements": requirements or [],
|
|
82
|
+
"acceptance_criteria": acceptance_criteria or []
|
|
83
|
+
}
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def with_plan_phases(self, phases: list[tuple[str, list[str]]]) -> 'TrackBuilder':
|
|
87
|
+
"""
|
|
88
|
+
Add plan phases with tasks.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
phases: List of (phase_name, [task_descriptions]) tuples
|
|
92
|
+
Task descriptions can include estimates like "Task name (2h)"
|
|
93
|
+
"""
|
|
94
|
+
self._plan_phases = phases
|
|
95
|
+
return self
|
|
96
|
+
|
|
97
|
+
def _generate_track_html(self, track: Track, track_dir: Path) -> str:
|
|
98
|
+
"""Generate track index.html content."""
|
|
99
|
+
spec_link = '<li><a href="spec.html">📝 Specification</a></li>' if track.has_spec else ''
|
|
100
|
+
plan_link = '<li><a href="plan.html">📋 Implementation Plan</a></li>' if track.has_plan else ''
|
|
101
|
+
|
|
102
|
+
return f'''<!DOCTYPE html>
|
|
103
|
+
<html lang="en">
|
|
104
|
+
<head>
|
|
105
|
+
<meta charset="UTF-8">
|
|
106
|
+
<title>{track.title}</title>
|
|
107
|
+
<link rel="stylesheet" href="../../styles.css">
|
|
108
|
+
</head>
|
|
109
|
+
<body>
|
|
110
|
+
<article id="{track.id}" data-type="track" data-status="{track.status}" data-priority="{track.priority}">
|
|
111
|
+
<header>
|
|
112
|
+
<h1>{track.title}</h1>
|
|
113
|
+
<div class="metadata">
|
|
114
|
+
<span class="badge status-{track.status}">{track.status.title()}</span>
|
|
115
|
+
<span class="badge priority-{track.priority}">{track.priority.title()} Priority</span>
|
|
116
|
+
</div>
|
|
117
|
+
</header>
|
|
118
|
+
|
|
119
|
+
<section data-description>
|
|
120
|
+
<p>{track.description}</p>
|
|
121
|
+
</section>
|
|
122
|
+
|
|
123
|
+
<nav data-track-components>
|
|
124
|
+
<h2>Components</h2>
|
|
125
|
+
<ul>
|
|
126
|
+
{spec_link}
|
|
127
|
+
{plan_link}
|
|
128
|
+
</ul>
|
|
129
|
+
</nav>
|
|
130
|
+
</article>
|
|
131
|
+
</body>
|
|
132
|
+
</html>'''
|
|
133
|
+
|
|
134
|
+
def create(self) -> Track:
|
|
135
|
+
"""Execute the build and create track+spec+plan."""
|
|
136
|
+
if not self._title:
|
|
137
|
+
raise ValueError("Track title is required")
|
|
138
|
+
|
|
139
|
+
# Generate track ID
|
|
140
|
+
track_id = f"track-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
|
141
|
+
|
|
142
|
+
# Create track
|
|
143
|
+
track = Track(
|
|
144
|
+
id=track_id,
|
|
145
|
+
title=f"Track: {self._title}",
|
|
146
|
+
description=self._description,
|
|
147
|
+
priority=self._priority,
|
|
148
|
+
has_spec=bool(self._spec_data),
|
|
149
|
+
has_plan=bool(self._plan_phases)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Save track index.html
|
|
153
|
+
track_dir = Path(".htmlgraph") / "tracks" / track_id
|
|
154
|
+
track_dir.mkdir(parents=True, exist_ok=True)
|
|
155
|
+
|
|
156
|
+
# Generate track index HTML
|
|
157
|
+
track_html = self._generate_track_html(track, track_dir)
|
|
158
|
+
(track_dir / "index.html").write_text(track_html, encoding="utf-8")
|
|
159
|
+
|
|
160
|
+
# Create spec if provided
|
|
161
|
+
requirements = []
|
|
162
|
+
if self._spec_data:
|
|
163
|
+
for i, req in enumerate(self._spec_data.get("requirements", [])):
|
|
164
|
+
if isinstance(req, tuple):
|
|
165
|
+
desc, priority = req
|
|
166
|
+
else:
|
|
167
|
+
desc, priority = req, "must-have"
|
|
168
|
+
|
|
169
|
+
requirements.append(Requirement(
|
|
170
|
+
id=f"req-{i+1}",
|
|
171
|
+
description=desc,
|
|
172
|
+
priority=priority
|
|
173
|
+
))
|
|
174
|
+
|
|
175
|
+
criteria = []
|
|
176
|
+
for crit in self._spec_data.get("acceptance_criteria", []):
|
|
177
|
+
if isinstance(crit, tuple):
|
|
178
|
+
desc, test_case = crit
|
|
179
|
+
criteria.append(AcceptanceCriterion(description=desc, test_case=test_case))
|
|
180
|
+
else:
|
|
181
|
+
criteria.append(AcceptanceCriterion(description=crit))
|
|
182
|
+
|
|
183
|
+
spec = Spec(
|
|
184
|
+
id=f"{track_id}-spec",
|
|
185
|
+
title=f"{self._title} Specification",
|
|
186
|
+
track_id=track_id,
|
|
187
|
+
overview=self._spec_data.get("overview", ""),
|
|
188
|
+
context=self._spec_data.get("context", ""),
|
|
189
|
+
requirements=requirements,
|
|
190
|
+
acceptance_criteria=criteria
|
|
191
|
+
)
|
|
192
|
+
(track_dir / "spec.html").write_text(spec.to_html(), encoding="utf-8")
|
|
193
|
+
|
|
194
|
+
# Create plan if provided
|
|
195
|
+
if self._plan_phases:
|
|
196
|
+
phases = []
|
|
197
|
+
for i, (phase_name, tasks) in enumerate(self._plan_phases):
|
|
198
|
+
phase_tasks = []
|
|
199
|
+
for j, task_desc in enumerate(tasks):
|
|
200
|
+
# Parse estimate from task description
|
|
201
|
+
estimate = None
|
|
202
|
+
if "(" in task_desc and "h)" in task_desc:
|
|
203
|
+
match = re.search(r'\((\d+(?:\.\d+)?)\s*h\)', task_desc)
|
|
204
|
+
if match:
|
|
205
|
+
estimate = float(match.group(1))
|
|
206
|
+
task_desc = re.sub(r'\s*\(\d+(?:\.\d+)?\s*h\)', '', task_desc).strip()
|
|
207
|
+
|
|
208
|
+
phase_tasks.append(Task(
|
|
209
|
+
id=f"task-{i+1}-{j+1}",
|
|
210
|
+
description=task_desc,
|
|
211
|
+
estimate_hours=estimate
|
|
212
|
+
))
|
|
213
|
+
|
|
214
|
+
phases.append(Phase(
|
|
215
|
+
id=f"phase-{i+1}",
|
|
216
|
+
name=phase_name,
|
|
217
|
+
tasks=phase_tasks
|
|
218
|
+
))
|
|
219
|
+
|
|
220
|
+
plan = Plan(
|
|
221
|
+
id=f"{track_id}-plan",
|
|
222
|
+
title=f"{self._title} Implementation Plan",
|
|
223
|
+
track_id=track_id,
|
|
224
|
+
phases=phases
|
|
225
|
+
)
|
|
226
|
+
(track_dir / "plan.html").write_text(plan.to_html(), encoding="utf-8")
|
|
227
|
+
|
|
228
|
+
print(f"✓ Created track: {track_id}")
|
|
229
|
+
if self._spec_data:
|
|
230
|
+
print(f" - Spec with {len(requirements)} requirements")
|
|
231
|
+
if self._plan_phases:
|
|
232
|
+
total_tasks = sum(len(tasks) for _, tasks in self._plan_phases)
|
|
233
|
+
print(f" - Plan with {len(self._plan_phases)} phases, {total_tasks} tasks")
|
|
234
|
+
|
|
235
|
+
return track
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class TrackCollection:
|
|
239
|
+
"""Collection interface for tracks with builder support."""
|
|
240
|
+
|
|
241
|
+
def __init__(self, sdk: 'SDK'):
|
|
242
|
+
self._sdk = sdk
|
|
243
|
+
self.collection_name = "tracks"
|
|
244
|
+
self.id_prefix = "track"
|
|
245
|
+
|
|
246
|
+
def builder(self) -> TrackBuilder:
|
|
247
|
+
"""
|
|
248
|
+
Create a new track builder with fluent interface.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
TrackBuilder for method chaining
|
|
252
|
+
|
|
253
|
+
Example:
|
|
254
|
+
track = sdk.tracks.builder() \\
|
|
255
|
+
.title("Multi-Agent Collaboration") \\
|
|
256
|
+
.priority("high") \\
|
|
257
|
+
.with_spec(overview="...") \\
|
|
258
|
+
.with_plan_phases([...]) \\
|
|
259
|
+
.create()
|
|
260
|
+
"""
|
|
261
|
+
return TrackBuilder(self._sdk)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|