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.
- htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +51 -1
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +26 -10
- htmlgraph/agent_registry.py +2 -1
- htmlgraph/analytics/__init__.py +8 -1
- htmlgraph/analytics/cli.py +86 -20
- 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 +10 -6
- 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 +67 -27
- htmlgraph/analytics_index.py +53 -20
- 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 +2 -1
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/base.py +57 -2
- htmlgraph/builders/bug.py +19 -3
- htmlgraph/builders/chore.py +19 -3
- htmlgraph/builders/epic.py +19 -3
- htmlgraph/builders/feature.py +27 -3
- htmlgraph/builders/insight.py +2 -1
- htmlgraph/builders/metric.py +2 -1
- htmlgraph/builders/pattern.py +2 -1
- htmlgraph/builders/phase.py +19 -3
- htmlgraph/builders/spike.py +29 -3
- htmlgraph/builders/track.py +42 -1
- 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 +2 -0
- htmlgraph/collections/base.py +197 -14
- htmlgraph/collections/bug.py +2 -1
- htmlgraph/collections/chore.py +2 -1
- htmlgraph/collections/epic.py +2 -1
- htmlgraph/collections/feature.py +2 -1
- htmlgraph/collections/insight.py +2 -1
- htmlgraph/collections/metric.py +2 -1
- htmlgraph/collections/pattern.py +2 -1
- htmlgraph/collections/phase.py +2 -1
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +13 -2
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +14 -1
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +2 -1
- htmlgraph/converter.py +116 -7
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2246 -248
- 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 +2 -1
- htmlgraph/deploy.py +26 -27
- 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 +2 -1
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +86 -37
- htmlgraph/event_migration.py +2 -1
- htmlgraph/file_watcher.py +12 -8
- htmlgraph/find_api.py +2 -1
- htmlgraph/git_events.py +67 -9
- 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 +8 -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 +790 -99
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/installer.py +5 -1
- htmlgraph/hooks/orchestrator.py +327 -76
- htmlgraph/hooks/orchestrator_reflector.py +31 -4
- htmlgraph/hooks/post_tool_use_failure.py +32 -7
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +92 -19
- htmlgraph/hooks/pretooluse.py +527 -7
- 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 +99 -4
- htmlgraph/hooks/validator.py +212 -91
- htmlgraph/ids.py +2 -1
- htmlgraph/learning.py +125 -100
- htmlgraph/mcp_server.py +2 -1
- htmlgraph/models.py +217 -18
- 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.py → orchestration/task_coordination.py} +16 -8
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +2 -1
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +115 -4
- htmlgraph/parallel.py +2 -1
- htmlgraph/parser.py +86 -6
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +2 -1
- 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/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 +295 -107
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +285 -3
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +2 -1
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +33 -1
- htmlgraph/track_manager.py +38 -0
- htmlgraph/transcript.py +18 -5
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +2 -1
- htmlgraph/work_type_utils.py +2 -1
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -4839
- htmlgraph/sdk.py +0 -2359
- htmlgraph-0.20.1.dist-info/RECORD +0 -118
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MemoryFeatureRepository - In-memory Feature storage.
|
|
3
|
+
|
|
4
|
+
Pure in-memory implementation for testing and development.
|
|
5
|
+
All operations are O(1) or O(n) with no disk I/O.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import builtins
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from htmlgraph.models import Node
|
|
14
|
+
from htmlgraph.repositories.feature_repository import (
|
|
15
|
+
FeatureNotFoundError,
|
|
16
|
+
FeatureRepository,
|
|
17
|
+
FeatureValidationError,
|
|
18
|
+
RepositoryQuery,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MemoryRepositoryQuery(RepositoryQuery):
|
|
23
|
+
"""Query builder for in-memory filtering."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, repo: "MemoryFeatureRepository", filters: dict[str, Any]):
|
|
26
|
+
super().__init__(filters)
|
|
27
|
+
self._repo = repo
|
|
28
|
+
|
|
29
|
+
def where(self, **kwargs: Any) -> "MemoryRepositoryQuery":
|
|
30
|
+
"""Chain additional filters."""
|
|
31
|
+
# Validate filter keys
|
|
32
|
+
valid_attrs = {
|
|
33
|
+
"status",
|
|
34
|
+
"priority",
|
|
35
|
+
"track_id",
|
|
36
|
+
"agent_assigned",
|
|
37
|
+
"type",
|
|
38
|
+
"title",
|
|
39
|
+
"id",
|
|
40
|
+
"created",
|
|
41
|
+
"updated",
|
|
42
|
+
}
|
|
43
|
+
for key in kwargs:
|
|
44
|
+
if key not in valid_attrs:
|
|
45
|
+
raise FeatureValidationError(f"Invalid filter attribute: {key}")
|
|
46
|
+
|
|
47
|
+
# Merge filters
|
|
48
|
+
new_filters = {**self.filters, **kwargs}
|
|
49
|
+
return MemoryRepositoryQuery(self._repo, new_filters)
|
|
50
|
+
|
|
51
|
+
def execute(self) -> list[Any]:
|
|
52
|
+
"""Execute the query and return results."""
|
|
53
|
+
return self._repo.list(self.filters)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class MemoryFeatureRepository(FeatureRepository):
|
|
57
|
+
"""
|
|
58
|
+
In-memory FeatureRepository implementation.
|
|
59
|
+
|
|
60
|
+
Stores features in a dictionary with identity caching.
|
|
61
|
+
All operations are fast (O(1) or O(n)) with no disk I/O.
|
|
62
|
+
|
|
63
|
+
Perfect for testing and development.
|
|
64
|
+
|
|
65
|
+
Performance:
|
|
66
|
+
- get(id): O(1)
|
|
67
|
+
- list(): O(n)
|
|
68
|
+
- create/save/delete: O(1)
|
|
69
|
+
- All batch operations: O(k) where k = batch size
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
>>> repo = MemoryFeatureRepository()
|
|
73
|
+
>>> feature = repo.create("User Authentication", priority="high")
|
|
74
|
+
>>> feature.status = "in-progress"
|
|
75
|
+
>>> repo.save(feature)
|
|
76
|
+
>>> all_features = repo.list()
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(self, auto_load: bool = True):
|
|
80
|
+
"""
|
|
81
|
+
Initialize in-memory repository.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
auto_load: Whether to enable auto-loading (always True for memory)
|
|
85
|
+
"""
|
|
86
|
+
self._features: dict[str, Node] = {} # Identity cache
|
|
87
|
+
self._auto_load = auto_load
|
|
88
|
+
self._counter = 0 # For generating IDs
|
|
89
|
+
|
|
90
|
+
def _generate_id(self) -> str:
|
|
91
|
+
"""Generate unique feature ID."""
|
|
92
|
+
import uuid
|
|
93
|
+
|
|
94
|
+
return f"feat-{uuid.uuid4().hex[:8]}"
|
|
95
|
+
|
|
96
|
+
def _validate_feature(self, feature: Any) -> None:
|
|
97
|
+
"""Validate feature object."""
|
|
98
|
+
if not hasattr(feature, "id"):
|
|
99
|
+
raise FeatureValidationError("Feature must have 'id' attribute")
|
|
100
|
+
if not hasattr(feature, "title"):
|
|
101
|
+
raise FeatureValidationError("Feature must have 'title' attribute")
|
|
102
|
+
if not feature.id or not str(feature.id).strip():
|
|
103
|
+
raise FeatureValidationError("Feature ID cannot be empty")
|
|
104
|
+
if not feature.title or not str(feature.title).strip():
|
|
105
|
+
raise FeatureValidationError("Feature title cannot be empty")
|
|
106
|
+
|
|
107
|
+
def _matches_filters(self, feature: Node, filters: dict[str, Any]) -> bool:
|
|
108
|
+
"""Check if feature matches all filters."""
|
|
109
|
+
if not filters:
|
|
110
|
+
return True
|
|
111
|
+
|
|
112
|
+
for key, value in filters.items():
|
|
113
|
+
if not hasattr(feature, key):
|
|
114
|
+
return False
|
|
115
|
+
if getattr(feature, key) != value:
|
|
116
|
+
return False
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
# ===== READ OPERATIONS =====
|
|
120
|
+
|
|
121
|
+
def get(self, feature_id: str) -> Node | None:
|
|
122
|
+
"""
|
|
123
|
+
Get single feature by ID.
|
|
124
|
+
|
|
125
|
+
Returns same object instance for multiple calls (identity caching).
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
feature_id: Feature ID to retrieve
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Feature object if found, None if not found
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
ValueError: If feature_id is invalid format
|
|
135
|
+
|
|
136
|
+
Performance: O(1)
|
|
137
|
+
|
|
138
|
+
Examples:
|
|
139
|
+
>>> feature = repo.get("feat-001")
|
|
140
|
+
>>> feature2 = repo.get("feat-001")
|
|
141
|
+
>>> assert feature is feature2 # Same instance
|
|
142
|
+
"""
|
|
143
|
+
if not feature_id or not isinstance(feature_id, str):
|
|
144
|
+
raise ValueError(f"Invalid feature_id: {feature_id}")
|
|
145
|
+
|
|
146
|
+
return self._features.get(feature_id)
|
|
147
|
+
|
|
148
|
+
def list(self, filters: dict[str, Any] | None = None) -> list[Node]:
|
|
149
|
+
"""
|
|
150
|
+
List all features with optional filters.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
filters: Optional dict of attribute->value filters
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
List of Feature objects (empty list if no matches)
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
FeatureValidationError: If filter keys are invalid
|
|
160
|
+
|
|
161
|
+
Performance: O(n) where n = total features
|
|
162
|
+
|
|
163
|
+
Examples:
|
|
164
|
+
>>> all_features = repo.list()
|
|
165
|
+
>>> todo_features = repo.list({"status": "todo"})
|
|
166
|
+
"""
|
|
167
|
+
if filters:
|
|
168
|
+
# Validate filter keys
|
|
169
|
+
valid_attrs = {
|
|
170
|
+
"status",
|
|
171
|
+
"priority",
|
|
172
|
+
"track_id",
|
|
173
|
+
"agent_assigned",
|
|
174
|
+
"type",
|
|
175
|
+
"title",
|
|
176
|
+
"id",
|
|
177
|
+
"created",
|
|
178
|
+
"updated",
|
|
179
|
+
}
|
|
180
|
+
for key in filters:
|
|
181
|
+
if key not in valid_attrs:
|
|
182
|
+
raise FeatureValidationError(f"Invalid filter attribute: {key}")
|
|
183
|
+
|
|
184
|
+
results = []
|
|
185
|
+
for feature in self._features.values():
|
|
186
|
+
if self._matches_filters(feature, filters or {}):
|
|
187
|
+
results.append(feature)
|
|
188
|
+
return results
|
|
189
|
+
|
|
190
|
+
def where(self, **kwargs: Any) -> RepositoryQuery:
|
|
191
|
+
"""
|
|
192
|
+
Build a filtered query with chaining support.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
**kwargs: Attribute->value filter pairs
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
RepositoryQuery object that can be further filtered
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
FeatureValidationError: If invalid attribute names
|
|
202
|
+
|
|
203
|
+
Examples:
|
|
204
|
+
>>> query = repo.where(status='todo')
|
|
205
|
+
>>> results = query.where(priority='high').execute()
|
|
206
|
+
"""
|
|
207
|
+
return MemoryRepositoryQuery(self, kwargs)
|
|
208
|
+
|
|
209
|
+
def by_track(self, track_id: str) -> builtins.list[Node]:
|
|
210
|
+
"""
|
|
211
|
+
Get all features belonging to a track.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
track_id: Track ID to filter by
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
List of features in track
|
|
218
|
+
|
|
219
|
+
Performance: O(n)
|
|
220
|
+
"""
|
|
221
|
+
if not track_id:
|
|
222
|
+
raise ValueError("track_id cannot be empty")
|
|
223
|
+
return self.list({"track_id": track_id})
|
|
224
|
+
|
|
225
|
+
def by_status(self, status: str) -> builtins.list[Node]:
|
|
226
|
+
"""
|
|
227
|
+
Filter features by status.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
status: Status to filter by
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
List of matching features
|
|
234
|
+
|
|
235
|
+
Performance: O(n)
|
|
236
|
+
"""
|
|
237
|
+
return self.list({"status": status})
|
|
238
|
+
|
|
239
|
+
def by_priority(self, priority: str) -> builtins.list[Node]:
|
|
240
|
+
"""
|
|
241
|
+
Filter features by priority.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
priority: Priority level
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
List of matching features
|
|
248
|
+
|
|
249
|
+
Performance: O(n)
|
|
250
|
+
"""
|
|
251
|
+
return self.list({"priority": priority})
|
|
252
|
+
|
|
253
|
+
def by_assigned_to(self, agent: str) -> builtins.list[Node]:
|
|
254
|
+
"""
|
|
255
|
+
Get features assigned to an agent.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
agent: Agent ID
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Features assigned to agent
|
|
262
|
+
"""
|
|
263
|
+
return self.list({"agent_assigned": agent})
|
|
264
|
+
|
|
265
|
+
def batch_get(self, feature_ids: builtins.list[str]) -> builtins.list[Node]:
|
|
266
|
+
"""
|
|
267
|
+
Bulk retrieve multiple features.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
feature_ids: List of feature IDs
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
List of found features (None for missing ones omitted)
|
|
274
|
+
|
|
275
|
+
Raises:
|
|
276
|
+
ValueError: If feature_ids is not a list
|
|
277
|
+
|
|
278
|
+
Performance: O(k) where k = batch size
|
|
279
|
+
"""
|
|
280
|
+
if not isinstance(feature_ids, list):
|
|
281
|
+
raise ValueError("feature_ids must be a list")
|
|
282
|
+
|
|
283
|
+
results = []
|
|
284
|
+
for fid in feature_ids:
|
|
285
|
+
feature = self.get(fid)
|
|
286
|
+
if feature:
|
|
287
|
+
results.append(feature)
|
|
288
|
+
return results
|
|
289
|
+
|
|
290
|
+
# ===== WRITE OPERATIONS =====
|
|
291
|
+
|
|
292
|
+
def create(self, title: str, **kwargs: Any) -> Node:
|
|
293
|
+
"""
|
|
294
|
+
Create new feature.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
title: Feature title (required)
|
|
298
|
+
**kwargs: Additional properties
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
Created Feature object (with generated ID)
|
|
302
|
+
|
|
303
|
+
Raises:
|
|
304
|
+
FeatureValidationError: If invalid data provided
|
|
305
|
+
|
|
306
|
+
Performance: O(1)
|
|
307
|
+
"""
|
|
308
|
+
if not title or not title.strip():
|
|
309
|
+
raise FeatureValidationError("Feature title cannot be empty")
|
|
310
|
+
|
|
311
|
+
# Generate ID if not provided
|
|
312
|
+
feature_id = kwargs.pop("id", None) or self._generate_id()
|
|
313
|
+
|
|
314
|
+
# Extract known fields from kwargs to avoid conflicts
|
|
315
|
+
node_type = kwargs.pop("type", "feature")
|
|
316
|
+
status = kwargs.pop("status", "todo")
|
|
317
|
+
priority = kwargs.pop("priority", "medium")
|
|
318
|
+
created = kwargs.pop("created", datetime.now())
|
|
319
|
+
updated = kwargs.pop("updated", datetime.now())
|
|
320
|
+
|
|
321
|
+
# Remove title from kwargs if present (already have it as parameter)
|
|
322
|
+
kwargs.pop("title", None)
|
|
323
|
+
|
|
324
|
+
# Create Node object
|
|
325
|
+
feature = Node(
|
|
326
|
+
id=feature_id,
|
|
327
|
+
title=title,
|
|
328
|
+
type=node_type,
|
|
329
|
+
status=status,
|
|
330
|
+
priority=priority,
|
|
331
|
+
created=created,
|
|
332
|
+
updated=updated,
|
|
333
|
+
**kwargs,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Validate and store
|
|
337
|
+
self._validate_feature(feature)
|
|
338
|
+
self._features[feature.id] = feature
|
|
339
|
+
|
|
340
|
+
return feature
|
|
341
|
+
|
|
342
|
+
def save(self, feature: Node) -> Node:
|
|
343
|
+
"""
|
|
344
|
+
Save existing feature (update or insert).
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
feature: Feature object to save
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Saved feature (same instance)
|
|
351
|
+
|
|
352
|
+
Raises:
|
|
353
|
+
FeatureValidationError: If feature is invalid
|
|
354
|
+
|
|
355
|
+
Performance: O(1)
|
|
356
|
+
"""
|
|
357
|
+
self._validate_feature(feature)
|
|
358
|
+
|
|
359
|
+
# Update timestamp
|
|
360
|
+
feature.updated = datetime.now()
|
|
361
|
+
|
|
362
|
+
# Store (updates existing or inserts new)
|
|
363
|
+
self._features[feature.id] = feature
|
|
364
|
+
|
|
365
|
+
return feature
|
|
366
|
+
|
|
367
|
+
def batch_update(
|
|
368
|
+
self, feature_ids: builtins.list[str], updates: dict[str, Any]
|
|
369
|
+
) -> int:
|
|
370
|
+
"""
|
|
371
|
+
Vectorized batch update operation.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
feature_ids: List of feature IDs to update
|
|
375
|
+
updates: Dict of attribute->value to set
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
Number of features successfully updated
|
|
379
|
+
|
|
380
|
+
Raises:
|
|
381
|
+
FeatureValidationError: If invalid updates
|
|
382
|
+
|
|
383
|
+
Performance: O(k) where k = batch size
|
|
384
|
+
"""
|
|
385
|
+
if not isinstance(feature_ids, list):
|
|
386
|
+
raise ValueError("feature_ids must be a list")
|
|
387
|
+
if not isinstance(updates, dict):
|
|
388
|
+
raise FeatureValidationError("updates must be a dict")
|
|
389
|
+
|
|
390
|
+
count = 0
|
|
391
|
+
for fid in feature_ids:
|
|
392
|
+
feature = self.get(fid)
|
|
393
|
+
if feature:
|
|
394
|
+
# Apply updates
|
|
395
|
+
for key, value in updates.items():
|
|
396
|
+
setattr(feature, key, value)
|
|
397
|
+
feature.updated = datetime.now()
|
|
398
|
+
count += 1
|
|
399
|
+
|
|
400
|
+
return count
|
|
401
|
+
|
|
402
|
+
def delete(self, feature_id: str) -> bool:
|
|
403
|
+
"""
|
|
404
|
+
Delete a feature by ID.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
feature_id: Feature ID to delete
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
True if deleted, False if not found
|
|
411
|
+
|
|
412
|
+
Performance: O(1)
|
|
413
|
+
"""
|
|
414
|
+
if not feature_id:
|
|
415
|
+
raise FeatureValidationError("feature_id cannot be empty")
|
|
416
|
+
|
|
417
|
+
if feature_id in self._features:
|
|
418
|
+
del self._features[feature_id]
|
|
419
|
+
return True
|
|
420
|
+
return False
|
|
421
|
+
|
|
422
|
+
def batch_delete(self, feature_ids: builtins.list[str]) -> int:
|
|
423
|
+
"""
|
|
424
|
+
Delete multiple features.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
feature_ids: List of feature IDs to delete
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
Number of features successfully deleted
|
|
431
|
+
|
|
432
|
+
Performance: O(k) where k = batch size
|
|
433
|
+
"""
|
|
434
|
+
if not isinstance(feature_ids, list):
|
|
435
|
+
raise ValueError("feature_ids must be a list")
|
|
436
|
+
|
|
437
|
+
count = 0
|
|
438
|
+
for fid in feature_ids:
|
|
439
|
+
if self.delete(fid):
|
|
440
|
+
count += 1
|
|
441
|
+
return count
|
|
442
|
+
|
|
443
|
+
# ===== ADVANCED QUERIES =====
|
|
444
|
+
|
|
445
|
+
def find_dependencies(self, feature_id: str) -> builtins.list[Node]:
|
|
446
|
+
"""
|
|
447
|
+
Find transitive feature dependencies.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
feature_id: Feature to find dependencies for
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
List of features this feature depends on
|
|
454
|
+
|
|
455
|
+
Raises:
|
|
456
|
+
FeatureNotFoundError: If feature not found
|
|
457
|
+
|
|
458
|
+
Performance: O(n) graph traversal
|
|
459
|
+
"""
|
|
460
|
+
feature = self.get(feature_id)
|
|
461
|
+
if not feature:
|
|
462
|
+
raise FeatureNotFoundError(feature_id)
|
|
463
|
+
|
|
464
|
+
dependencies = []
|
|
465
|
+
visited = set()
|
|
466
|
+
|
|
467
|
+
def traverse(f: Node) -> None:
|
|
468
|
+
if f.id in visited:
|
|
469
|
+
return
|
|
470
|
+
visited.add(f.id)
|
|
471
|
+
|
|
472
|
+
# Check edges for dependencies
|
|
473
|
+
if hasattr(f, "edges") and f.edges:
|
|
474
|
+
depends_on = (
|
|
475
|
+
f.edges.get("depends_on", []) if isinstance(f.edges, dict) else []
|
|
476
|
+
)
|
|
477
|
+
for edge in depends_on:
|
|
478
|
+
target_id = (
|
|
479
|
+
edge.target_id
|
|
480
|
+
if hasattr(edge, "target_id")
|
|
481
|
+
else edge.get("target_id")
|
|
482
|
+
if isinstance(edge, dict)
|
|
483
|
+
else None
|
|
484
|
+
)
|
|
485
|
+
if target_id:
|
|
486
|
+
dep = self.get(target_id)
|
|
487
|
+
if dep and dep not in dependencies:
|
|
488
|
+
dependencies.append(dep)
|
|
489
|
+
traverse(dep)
|
|
490
|
+
|
|
491
|
+
traverse(feature)
|
|
492
|
+
return dependencies
|
|
493
|
+
|
|
494
|
+
def find_blocking(self, feature_id: str) -> builtins.list[Node]:
|
|
495
|
+
"""
|
|
496
|
+
Find what blocks this feature.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
feature_id: Feature to find blockers for
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
Features that depend on this feature
|
|
503
|
+
|
|
504
|
+
Raises:
|
|
505
|
+
FeatureNotFoundError: If feature not found
|
|
506
|
+
"""
|
|
507
|
+
feature = self.get(feature_id)
|
|
508
|
+
if not feature:
|
|
509
|
+
raise FeatureNotFoundError(feature_id)
|
|
510
|
+
|
|
511
|
+
blocking = []
|
|
512
|
+
for f in self._features.values():
|
|
513
|
+
if hasattr(f, "edges") and f.edges:
|
|
514
|
+
depends_on = (
|
|
515
|
+
f.edges.get("depends_on", []) if isinstance(f.edges, dict) else []
|
|
516
|
+
)
|
|
517
|
+
for edge in depends_on:
|
|
518
|
+
target_id = (
|
|
519
|
+
edge.target_id
|
|
520
|
+
if hasattr(edge, "target_id")
|
|
521
|
+
else edge.get("target_id")
|
|
522
|
+
if isinstance(edge, dict)
|
|
523
|
+
else None
|
|
524
|
+
)
|
|
525
|
+
if target_id == feature_id:
|
|
526
|
+
blocking.append(f)
|
|
527
|
+
|
|
528
|
+
return blocking
|
|
529
|
+
|
|
530
|
+
def filter(self, predicate: Callable[[Node], bool]) -> builtins.list[Node]:
|
|
531
|
+
"""
|
|
532
|
+
Filter features with custom predicate function.
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
predicate: Function that takes Feature and returns True/False
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
Features matching predicate
|
|
539
|
+
"""
|
|
540
|
+
return [f for f in self._features.values() if predicate(f)]
|
|
541
|
+
|
|
542
|
+
# ===== CACHE/LIFECYCLE MANAGEMENT =====
|
|
543
|
+
|
|
544
|
+
def invalidate_cache(self, feature_id: str | None = None) -> None:
|
|
545
|
+
"""
|
|
546
|
+
Invalidate cache for single feature or all features.
|
|
547
|
+
|
|
548
|
+
For memory repository, this is a no-op since we don't have
|
|
549
|
+
external storage to reload from.
|
|
550
|
+
|
|
551
|
+
Args:
|
|
552
|
+
feature_id: Specific feature to invalidate, or None for all
|
|
553
|
+
"""
|
|
554
|
+
# No-op for memory repository
|
|
555
|
+
pass
|
|
556
|
+
|
|
557
|
+
def reload(self) -> None:
|
|
558
|
+
"""
|
|
559
|
+
Force reload all features from storage.
|
|
560
|
+
|
|
561
|
+
For memory repository, this is a no-op since we don't have
|
|
562
|
+
external storage.
|
|
563
|
+
"""
|
|
564
|
+
# No-op for memory repository
|
|
565
|
+
pass
|
|
566
|
+
|
|
567
|
+
@property
|
|
568
|
+
def auto_load(self) -> bool:
|
|
569
|
+
"""Whether auto-loading is enabled."""
|
|
570
|
+
return self._auto_load
|
|
571
|
+
|
|
572
|
+
@auto_load.setter
|
|
573
|
+
def auto_load(self, enabled: bool) -> None:
|
|
574
|
+
"""Enable/disable auto-loading."""
|
|
575
|
+
self._auto_load = enabled
|
|
576
|
+
|
|
577
|
+
# ===== UTILITY METHODS =====
|
|
578
|
+
|
|
579
|
+
def count(self, filters: dict[str, Any] | None = None) -> int:
|
|
580
|
+
"""
|
|
581
|
+
Count features matching filters.
|
|
582
|
+
|
|
583
|
+
Args:
|
|
584
|
+
filters: Optional filters
|
|
585
|
+
|
|
586
|
+
Returns:
|
|
587
|
+
Number of matching features
|
|
588
|
+
|
|
589
|
+
Performance: O(n) or O(1) if no filters
|
|
590
|
+
"""
|
|
591
|
+
if not filters:
|
|
592
|
+
return len(self._features)
|
|
593
|
+
return len(self.list(filters))
|
|
594
|
+
|
|
595
|
+
def exists(self, feature_id: str) -> bool:
|
|
596
|
+
"""
|
|
597
|
+
Check if feature exists without loading it.
|
|
598
|
+
|
|
599
|
+
Args:
|
|
600
|
+
feature_id: Feature ID to check
|
|
601
|
+
|
|
602
|
+
Returns:
|
|
603
|
+
True if exists, False otherwise
|
|
604
|
+
|
|
605
|
+
Performance: O(1)
|
|
606
|
+
"""
|
|
607
|
+
return feature_id in self._features
|