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
htmlgraph/refs.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Short reference manager for graph nodes.
|
|
5
|
+
|
|
6
|
+
Manages persistent mapping of short refs (@f1, @t2, @b5) to full node IDs,
|
|
7
|
+
enabling AI-friendly snapshots and queries.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RefManager:
|
|
17
|
+
"""
|
|
18
|
+
Manages short references (@f1, @t1, @b5, etc.) for graph nodes.
|
|
19
|
+
|
|
20
|
+
Maintains a persistent refs.json file mapping short refs to full node IDs.
|
|
21
|
+
Refs are stable across sessions and auto-generated on first access.
|
|
22
|
+
|
|
23
|
+
Ref format: @{prefix}{number}
|
|
24
|
+
Prefixes: f=feature, t=track, b=bug, s=spike, c=chore, e=epic, d=todo
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
>>> ref_mgr = RefManager(Path(".htmlgraph"))
|
|
28
|
+
>>> ref = ref_mgr.generate_ref("feat-a1b2c3d4")
|
|
29
|
+
>>> print(ref) # "@f1"
|
|
30
|
+
>>> full_id = ref_mgr.resolve_ref("@f1")
|
|
31
|
+
>>> print(full_id) # "feat-a1b2c3d4"
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# Map node ID prefix to short ref prefix
|
|
35
|
+
PREFIX_MAP = {
|
|
36
|
+
"feat": "f",
|
|
37
|
+
"trk": "t",
|
|
38
|
+
"bug": "b",
|
|
39
|
+
"spk": "s",
|
|
40
|
+
"chr": "c",
|
|
41
|
+
"epc": "e",
|
|
42
|
+
"todo": "d",
|
|
43
|
+
"phs": "p",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Reverse mapping for type lookup
|
|
47
|
+
TYPE_MAP = {
|
|
48
|
+
"f": "feature",
|
|
49
|
+
"t": "track",
|
|
50
|
+
"b": "bug",
|
|
51
|
+
"s": "spike",
|
|
52
|
+
"c": "chore",
|
|
53
|
+
"e": "epic",
|
|
54
|
+
"d": "todo",
|
|
55
|
+
"p": "phase",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
def __init__(self, graph_dir: Path):
|
|
59
|
+
"""
|
|
60
|
+
Initialize RefManager.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
graph_dir: Path to .htmlgraph directory
|
|
64
|
+
"""
|
|
65
|
+
self.graph_dir = Path(graph_dir)
|
|
66
|
+
self.refs_file = self.graph_dir / "refs.json"
|
|
67
|
+
self._refs: dict[str, str] = {} # Maps: "@f1" -> "feat-a1b2c3d4"
|
|
68
|
+
self._reverse_refs: dict[str, str] = {} # Maps: "feat-a1b2c3d4" -> "@f1"
|
|
69
|
+
self._load()
|
|
70
|
+
|
|
71
|
+
def _load(self) -> None:
|
|
72
|
+
"""Load refs.json into memory."""
|
|
73
|
+
if not self.refs_file.exists():
|
|
74
|
+
self._refs = {}
|
|
75
|
+
self._reverse_refs = {}
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
with open(self.refs_file, encoding="utf-8") as f:
|
|
80
|
+
data = json.load(f)
|
|
81
|
+
self._refs = data.get("refs", {})
|
|
82
|
+
|
|
83
|
+
# Build reverse mapping
|
|
84
|
+
self._reverse_refs = {v: k for k, v in self._refs.items()}
|
|
85
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
86
|
+
# Corrupted file - start fresh
|
|
87
|
+
import logging
|
|
88
|
+
|
|
89
|
+
logging.warning(f"Failed to load refs.json: {e}. Starting fresh.")
|
|
90
|
+
self._refs = {}
|
|
91
|
+
self._reverse_refs = {}
|
|
92
|
+
|
|
93
|
+
def _save(self) -> None:
|
|
94
|
+
"""Save refs to refs.json."""
|
|
95
|
+
# Ensure directory exists
|
|
96
|
+
self.graph_dir.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
|
|
98
|
+
data = {
|
|
99
|
+
"refs": self._refs,
|
|
100
|
+
"version": 1,
|
|
101
|
+
"regenerated_at": datetime.now().isoformat(),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
with open(self.refs_file, "w", encoding="utf-8") as f:
|
|
106
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
107
|
+
except OSError as e:
|
|
108
|
+
import logging
|
|
109
|
+
|
|
110
|
+
logging.error(f"Failed to save refs.json: {e}")
|
|
111
|
+
|
|
112
|
+
def _parse_node_type(self, node_id: str) -> str | None:
|
|
113
|
+
"""
|
|
114
|
+
Extract node type prefix from node ID.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
node_id: Full node ID like "feat-a1b2c3d4"
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Node type prefix (e.g., "feat") or None if invalid
|
|
121
|
+
"""
|
|
122
|
+
if "-" not in node_id:
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
prefix = node_id.split("-", 1)[0]
|
|
126
|
+
return prefix if prefix in self.PREFIX_MAP else None
|
|
127
|
+
|
|
128
|
+
def _next_ref_number(self, prefix: str) -> int:
|
|
129
|
+
"""
|
|
130
|
+
Get next available ref number for a type.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
prefix: Short ref prefix (e.g., "f", "t", "b")
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Next available number (1, 2, 3, ...)
|
|
137
|
+
"""
|
|
138
|
+
# Find all refs with this prefix
|
|
139
|
+
existing = [int(ref[2:]) for ref in self._refs if ref.startswith(f"@{prefix}")]
|
|
140
|
+
return max(existing, default=0) + 1
|
|
141
|
+
|
|
142
|
+
def generate_ref(self, node_id: str) -> str:
|
|
143
|
+
"""
|
|
144
|
+
Generate a short ref for a node ID.
|
|
145
|
+
|
|
146
|
+
This method is idempotent - calling it multiple times with the same
|
|
147
|
+
node_id returns the same ref without creating duplicates.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
node_id: Full node ID like "feat-a1b2c3d4"
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Short ref like "@f1"
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
ValueError: If node_id has invalid format
|
|
157
|
+
|
|
158
|
+
Example:
|
|
159
|
+
>>> ref = ref_mgr.generate_ref("feat-abc123")
|
|
160
|
+
>>> print(ref) # "@f1"
|
|
161
|
+
>>> # Second call returns same ref
|
|
162
|
+
>>> ref2 = ref_mgr.generate_ref("feat-abc123")
|
|
163
|
+
>>> assert ref == ref2
|
|
164
|
+
"""
|
|
165
|
+
# Check if already has ref (idempotent)
|
|
166
|
+
if node_id in self._reverse_refs:
|
|
167
|
+
return self._reverse_refs[node_id]
|
|
168
|
+
|
|
169
|
+
# Parse node type
|
|
170
|
+
node_prefix = self._parse_node_type(node_id)
|
|
171
|
+
if not node_prefix:
|
|
172
|
+
raise ValueError(f"Invalid node ID format: {node_id}")
|
|
173
|
+
|
|
174
|
+
# Get short prefix
|
|
175
|
+
short_prefix = self.PREFIX_MAP[node_prefix]
|
|
176
|
+
|
|
177
|
+
# Generate next ref
|
|
178
|
+
number = self._next_ref_number(short_prefix)
|
|
179
|
+
short_ref = f"@{short_prefix}{number}"
|
|
180
|
+
|
|
181
|
+
# Store mappings
|
|
182
|
+
self._refs[short_ref] = node_id
|
|
183
|
+
self._reverse_refs[node_id] = short_ref
|
|
184
|
+
|
|
185
|
+
# Persist
|
|
186
|
+
self._save()
|
|
187
|
+
|
|
188
|
+
return short_ref
|
|
189
|
+
|
|
190
|
+
def get_ref(self, node_id: str) -> str | None:
|
|
191
|
+
"""
|
|
192
|
+
Get existing ref for a node ID (create if not exist).
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
node_id: Full node ID like "feat-a1b2c3d4"
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Short ref like "@f1", or None if node_id invalid
|
|
199
|
+
|
|
200
|
+
Example:
|
|
201
|
+
>>> ref = ref_mgr.get_ref("feat-abc123")
|
|
202
|
+
>>> # Creates ref if doesn't exist
|
|
203
|
+
"""
|
|
204
|
+
# Return existing ref
|
|
205
|
+
if node_id in self._reverse_refs:
|
|
206
|
+
return self._reverse_refs[node_id]
|
|
207
|
+
|
|
208
|
+
# Generate new ref if valid node ID
|
|
209
|
+
try:
|
|
210
|
+
return self.generate_ref(node_id)
|
|
211
|
+
except ValueError:
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
def resolve_ref(self, short_ref: str) -> str | None:
|
|
215
|
+
"""
|
|
216
|
+
Resolve short ref to full node ID.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
short_ref: "@f1", "@t2", etc.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Full node ID or None if not found
|
|
223
|
+
|
|
224
|
+
Example:
|
|
225
|
+
>>> full_id = ref_mgr.resolve_ref("@f1")
|
|
226
|
+
>>> print(full_id) # "feat-abc123"
|
|
227
|
+
"""
|
|
228
|
+
return self._refs.get(short_ref)
|
|
229
|
+
|
|
230
|
+
def get_all_refs(self) -> dict[str, str]:
|
|
231
|
+
"""
|
|
232
|
+
Return all refs.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Dict mapping short refs to full IDs: {"@f1": "feat-a1b2c3d4", ...}
|
|
236
|
+
"""
|
|
237
|
+
return self._refs.copy()
|
|
238
|
+
|
|
239
|
+
def get_refs_by_type(self, node_type: str) -> list[tuple[str, str]]:
|
|
240
|
+
"""
|
|
241
|
+
Get all refs for a specific type.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
node_type: "feature", "track", "bug", "spike", "chore", "epic", "todo", "phase"
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
List of (short_ref, full_id) tuples sorted by ref number
|
|
248
|
+
|
|
249
|
+
Example:
|
|
250
|
+
>>> feature_refs = ref_mgr.get_refs_by_type("feature")
|
|
251
|
+
>>> for ref, full_id in feature_refs:
|
|
252
|
+
... print(f"{ref} -> {full_id}")
|
|
253
|
+
"""
|
|
254
|
+
# Get prefix for this type
|
|
255
|
+
prefix = None
|
|
256
|
+
for short_prefix, type_name in self.TYPE_MAP.items():
|
|
257
|
+
if type_name == node_type:
|
|
258
|
+
prefix = short_prefix
|
|
259
|
+
break
|
|
260
|
+
|
|
261
|
+
if not prefix:
|
|
262
|
+
return []
|
|
263
|
+
|
|
264
|
+
# Filter refs by prefix
|
|
265
|
+
matching = [
|
|
266
|
+
(ref, full_id)
|
|
267
|
+
for ref, full_id in self._refs.items()
|
|
268
|
+
if ref.startswith(f"@{prefix}")
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
# Sort by ref number
|
|
272
|
+
def sort_key(item: tuple[str, str]) -> int:
|
|
273
|
+
ref = item[0]
|
|
274
|
+
try:
|
|
275
|
+
return int(ref[2:]) # Extract number after "@f"
|
|
276
|
+
except (ValueError, IndexError):
|
|
277
|
+
return 0
|
|
278
|
+
|
|
279
|
+
return sorted(matching, key=sort_key)
|
|
280
|
+
|
|
281
|
+
def rebuild_refs(self) -> None:
|
|
282
|
+
"""
|
|
283
|
+
Rebuild refs from all .htmlgraph/ files (recovery tool).
|
|
284
|
+
|
|
285
|
+
Scans all collection directories (features/, tracks/, bugs/, etc.)
|
|
286
|
+
and rebuilds refs.json from scratch. Preserves existing refs where
|
|
287
|
+
possible to maintain stability.
|
|
288
|
+
|
|
289
|
+
This is a recovery tool for when refs.json is corrupted or deleted.
|
|
290
|
+
|
|
291
|
+
Example:
|
|
292
|
+
>>> ref_mgr.rebuild_refs()
|
|
293
|
+
>>> # Refs regenerated from file system
|
|
294
|
+
"""
|
|
295
|
+
# Save current refs for preservation
|
|
296
|
+
old_refs = self._refs.copy()
|
|
297
|
+
|
|
298
|
+
# Clear in-memory refs
|
|
299
|
+
self._refs = {}
|
|
300
|
+
self._reverse_refs = {}
|
|
301
|
+
|
|
302
|
+
# Scan all collection directories
|
|
303
|
+
collections = [
|
|
304
|
+
"features",
|
|
305
|
+
"tracks",
|
|
306
|
+
"bugs",
|
|
307
|
+
"spikes",
|
|
308
|
+
"chores",
|
|
309
|
+
"epics",
|
|
310
|
+
"todos",
|
|
311
|
+
"phases",
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
for collection in collections:
|
|
315
|
+
collection_dir = self.graph_dir / collection
|
|
316
|
+
if not collection_dir.exists():
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
# Scan HTML files
|
|
320
|
+
for html_file in collection_dir.glob("*.html"):
|
|
321
|
+
# Extract node ID from filename (without .html)
|
|
322
|
+
node_id = html_file.stem
|
|
323
|
+
|
|
324
|
+
# Skip if invalid format
|
|
325
|
+
if not self._parse_node_type(node_id):
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
# Try to preserve existing ref
|
|
329
|
+
if node_id in {v for v in old_refs.values()}:
|
|
330
|
+
# Find old ref
|
|
331
|
+
old_ref = next(
|
|
332
|
+
(k for k, v in old_refs.items() if v == node_id), None
|
|
333
|
+
)
|
|
334
|
+
if old_ref:
|
|
335
|
+
# Preserve old ref
|
|
336
|
+
self._refs[old_ref] = node_id
|
|
337
|
+
self._reverse_refs[node_id] = old_ref
|
|
338
|
+
continue
|
|
339
|
+
|
|
340
|
+
# Generate new ref
|
|
341
|
+
self.generate_ref(node_id)
|
|
342
|
+
|
|
343
|
+
# Save to disk
|
|
344
|
+
self._save()
|