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.
Files changed (304) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +51 -1
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +26 -10
  7. htmlgraph/agent_registry.py +2 -1
  8. htmlgraph/analytics/__init__.py +8 -1
  9. htmlgraph/analytics/cli.py +86 -20
  10. htmlgraph/analytics/cost_analyzer.py +391 -0
  11. htmlgraph/analytics/cost_monitor.py +664 -0
  12. htmlgraph/analytics/cost_reporter.py +675 -0
  13. htmlgraph/analytics/cross_session.py +617 -0
  14. htmlgraph/analytics/dependency.py +10 -6
  15. htmlgraph/analytics/pattern_learning.py +771 -0
  16. htmlgraph/analytics/session_graph.py +707 -0
  17. htmlgraph/analytics/strategic/__init__.py +80 -0
  18. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  19. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  20. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  21. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  22. htmlgraph/analytics/work_type.py +67 -27
  23. htmlgraph/analytics_index.py +53 -20
  24. htmlgraph/api/__init__.py +3 -0
  25. htmlgraph/api/cost_alerts_websocket.py +416 -0
  26. htmlgraph/api/main.py +2498 -0
  27. htmlgraph/api/static/htmx.min.js +1 -0
  28. htmlgraph/api/static/style-redesign.css +1344 -0
  29. htmlgraph/api/static/style.css +1079 -0
  30. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  31. htmlgraph/api/templates/dashboard.html +794 -0
  32. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  33. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  34. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  35. htmlgraph/api/templates/partials/agents.html +317 -0
  36. htmlgraph/api/templates/partials/event-traces.html +373 -0
  37. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  38. htmlgraph/api/templates/partials/features.html +578 -0
  39. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  40. htmlgraph/api/templates/partials/metrics.html +346 -0
  41. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  42. htmlgraph/api/templates/partials/orchestration.html +198 -0
  43. htmlgraph/api/templates/partials/spawners.html +375 -0
  44. htmlgraph/api/templates/partials/work-items.html +613 -0
  45. htmlgraph/api/websocket.py +538 -0
  46. htmlgraph/archive/__init__.py +24 -0
  47. htmlgraph/archive/bloom.py +234 -0
  48. htmlgraph/archive/fts.py +297 -0
  49. htmlgraph/archive/manager.py +583 -0
  50. htmlgraph/archive/search.py +244 -0
  51. htmlgraph/atomic_ops.py +560 -0
  52. htmlgraph/attribute_index.py +2 -1
  53. htmlgraph/bounded_paths.py +539 -0
  54. htmlgraph/builders/base.py +57 -2
  55. htmlgraph/builders/bug.py +19 -3
  56. htmlgraph/builders/chore.py +19 -3
  57. htmlgraph/builders/epic.py +19 -3
  58. htmlgraph/builders/feature.py +27 -3
  59. htmlgraph/builders/insight.py +2 -1
  60. htmlgraph/builders/metric.py +2 -1
  61. htmlgraph/builders/pattern.py +2 -1
  62. htmlgraph/builders/phase.py +19 -3
  63. htmlgraph/builders/spike.py +29 -3
  64. htmlgraph/builders/track.py +42 -1
  65. htmlgraph/cigs/__init__.py +81 -0
  66. htmlgraph/cigs/autonomy.py +385 -0
  67. htmlgraph/cigs/cost.py +475 -0
  68. htmlgraph/cigs/messages_basic.py +472 -0
  69. htmlgraph/cigs/messaging.py +365 -0
  70. htmlgraph/cigs/models.py +771 -0
  71. htmlgraph/cigs/pattern_storage.py +427 -0
  72. htmlgraph/cigs/patterns.py +503 -0
  73. htmlgraph/cigs/posttool_analyzer.py +234 -0
  74. htmlgraph/cigs/reporter.py +818 -0
  75. htmlgraph/cigs/tracker.py +317 -0
  76. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  77. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  78. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  79. htmlgraph/cli/__init__.py +42 -0
  80. htmlgraph/cli/__main__.py +6 -0
  81. htmlgraph/cli/analytics.py +1424 -0
  82. htmlgraph/cli/base.py +685 -0
  83. htmlgraph/cli/constants.py +206 -0
  84. htmlgraph/cli/core.py +954 -0
  85. htmlgraph/cli/main.py +147 -0
  86. htmlgraph/cli/models.py +475 -0
  87. htmlgraph/cli/templates/__init__.py +1 -0
  88. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  89. htmlgraph/cli/work/__init__.py +239 -0
  90. htmlgraph/cli/work/browse.py +115 -0
  91. htmlgraph/cli/work/features.py +568 -0
  92. htmlgraph/cli/work/orchestration.py +676 -0
  93. htmlgraph/cli/work/report.py +728 -0
  94. htmlgraph/cli/work/sessions.py +466 -0
  95. htmlgraph/cli/work/snapshot.py +559 -0
  96. htmlgraph/cli/work/tracks.py +486 -0
  97. htmlgraph/cli_commands/__init__.py +1 -0
  98. htmlgraph/cli_commands/feature.py +195 -0
  99. htmlgraph/cli_framework.py +115 -0
  100. htmlgraph/collections/__init__.py +2 -0
  101. htmlgraph/collections/base.py +197 -14
  102. htmlgraph/collections/bug.py +2 -1
  103. htmlgraph/collections/chore.py +2 -1
  104. htmlgraph/collections/epic.py +2 -1
  105. htmlgraph/collections/feature.py +2 -1
  106. htmlgraph/collections/insight.py +2 -1
  107. htmlgraph/collections/metric.py +2 -1
  108. htmlgraph/collections/pattern.py +2 -1
  109. htmlgraph/collections/phase.py +2 -1
  110. htmlgraph/collections/session.py +194 -0
  111. htmlgraph/collections/spike.py +13 -2
  112. htmlgraph/collections/task_delegation.py +241 -0
  113. htmlgraph/collections/todo.py +14 -1
  114. htmlgraph/collections/traces.py +487 -0
  115. htmlgraph/config/cost_models.json +56 -0
  116. htmlgraph/config.py +190 -0
  117. htmlgraph/context_analytics.py +2 -1
  118. htmlgraph/converter.py +116 -7
  119. htmlgraph/cost_analysis/__init__.py +5 -0
  120. htmlgraph/cost_analysis/analyzer.py +438 -0
  121. htmlgraph/dashboard.html +2246 -248
  122. htmlgraph/dashboard.html.backup +6592 -0
  123. htmlgraph/dashboard.html.bak +7181 -0
  124. htmlgraph/dashboard.html.bak2 +7231 -0
  125. htmlgraph/dashboard.html.bak3 +7232 -0
  126. htmlgraph/db/__init__.py +38 -0
  127. htmlgraph/db/queries.py +790 -0
  128. htmlgraph/db/schema.py +1788 -0
  129. htmlgraph/decorators.py +317 -0
  130. htmlgraph/dependency_models.py +2 -1
  131. htmlgraph/deploy.py +26 -27
  132. htmlgraph/docs/API_REFERENCE.md +841 -0
  133. htmlgraph/docs/HTTP_API.md +750 -0
  134. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  135. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  136. htmlgraph/docs/README.md +532 -0
  137. htmlgraph/docs/__init__.py +77 -0
  138. htmlgraph/docs/docs_version.py +55 -0
  139. htmlgraph/docs/metadata.py +93 -0
  140. htmlgraph/docs/migrations.py +232 -0
  141. htmlgraph/docs/template_engine.py +143 -0
  142. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  143. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  144. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  145. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  146. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  147. htmlgraph/docs/version_check.py +163 -0
  148. htmlgraph/edge_index.py +2 -1
  149. htmlgraph/error_handler.py +544 -0
  150. htmlgraph/event_log.py +86 -37
  151. htmlgraph/event_migration.py +2 -1
  152. htmlgraph/file_watcher.py +12 -8
  153. htmlgraph/find_api.py +2 -1
  154. htmlgraph/git_events.py +67 -9
  155. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  156. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  157. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  158. htmlgraph/hooks/__init__.py +8 -0
  159. htmlgraph/hooks/bootstrap.py +169 -0
  160. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  161. htmlgraph/hooks/concurrent_sessions.py +208 -0
  162. htmlgraph/hooks/context.py +350 -0
  163. htmlgraph/hooks/drift_handler.py +525 -0
  164. htmlgraph/hooks/event_tracker.py +790 -99
  165. htmlgraph/hooks/git_commands.py +175 -0
  166. htmlgraph/hooks/installer.py +5 -1
  167. htmlgraph/hooks/orchestrator.py +327 -76
  168. htmlgraph/hooks/orchestrator_reflector.py +31 -4
  169. htmlgraph/hooks/post_tool_use_failure.py +32 -7
  170. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  171. htmlgraph/hooks/posttooluse.py +92 -19
  172. htmlgraph/hooks/pretooluse.py +527 -7
  173. htmlgraph/hooks/prompt_analyzer.py +637 -0
  174. htmlgraph/hooks/session_handler.py +668 -0
  175. htmlgraph/hooks/session_summary.py +395 -0
  176. htmlgraph/hooks/state_manager.py +504 -0
  177. htmlgraph/hooks/subagent_detection.py +202 -0
  178. htmlgraph/hooks/subagent_stop.py +369 -0
  179. htmlgraph/hooks/task_enforcer.py +99 -4
  180. htmlgraph/hooks/validator.py +212 -91
  181. htmlgraph/ids.py +2 -1
  182. htmlgraph/learning.py +125 -100
  183. htmlgraph/mcp_server.py +2 -1
  184. htmlgraph/models.py +217 -18
  185. htmlgraph/operations/README.md +62 -0
  186. htmlgraph/operations/__init__.py +79 -0
  187. htmlgraph/operations/analytics.py +339 -0
  188. htmlgraph/operations/bootstrap.py +289 -0
  189. htmlgraph/operations/events.py +244 -0
  190. htmlgraph/operations/fastapi_server.py +231 -0
  191. htmlgraph/operations/hooks.py +350 -0
  192. htmlgraph/operations/initialization.py +597 -0
  193. htmlgraph/operations/initialization.py.backup +228 -0
  194. htmlgraph/operations/server.py +303 -0
  195. htmlgraph/orchestration/__init__.py +58 -0
  196. htmlgraph/orchestration/claude_launcher.py +179 -0
  197. htmlgraph/orchestration/command_builder.py +72 -0
  198. htmlgraph/orchestration/headless_spawner.py +281 -0
  199. htmlgraph/orchestration/live_events.py +377 -0
  200. htmlgraph/orchestration/model_selection.py +327 -0
  201. htmlgraph/orchestration/plugin_manager.py +140 -0
  202. htmlgraph/orchestration/prompts.py +137 -0
  203. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  204. htmlgraph/orchestration/spawners/__init__.py +16 -0
  205. htmlgraph/orchestration/spawners/base.py +194 -0
  206. htmlgraph/orchestration/spawners/claude.py +173 -0
  207. htmlgraph/orchestration/spawners/codex.py +435 -0
  208. htmlgraph/orchestration/spawners/copilot.py +294 -0
  209. htmlgraph/orchestration/spawners/gemini.py +471 -0
  210. htmlgraph/orchestration/subprocess_runner.py +36 -0
  211. htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
  212. htmlgraph/orchestration.md +563 -0
  213. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  214. htmlgraph/orchestrator.py +2 -1
  215. htmlgraph/orchestrator_config.py +357 -0
  216. htmlgraph/orchestrator_mode.py +115 -4
  217. htmlgraph/parallel.py +2 -1
  218. htmlgraph/parser.py +86 -6
  219. htmlgraph/path_query.py +608 -0
  220. htmlgraph/pattern_matcher.py +636 -0
  221. htmlgraph/pydantic_models.py +476 -0
  222. htmlgraph/quality_gates.py +350 -0
  223. htmlgraph/query_builder.py +2 -1
  224. htmlgraph/query_composer.py +509 -0
  225. htmlgraph/reflection.py +443 -0
  226. htmlgraph/refs.py +344 -0
  227. htmlgraph/repo_hash.py +512 -0
  228. htmlgraph/repositories/__init__.py +292 -0
  229. htmlgraph/repositories/analytics_repository.py +455 -0
  230. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  231. htmlgraph/repositories/feature_repository.py +581 -0
  232. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  233. htmlgraph/repositories/feature_repository_memory.py +607 -0
  234. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  235. htmlgraph/repositories/filter_service.py +620 -0
  236. htmlgraph/repositories/filter_service_standard.py +445 -0
  237. htmlgraph/repositories/shared_cache.py +621 -0
  238. htmlgraph/repositories/shared_cache_memory.py +395 -0
  239. htmlgraph/repositories/track_repository.py +552 -0
  240. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  241. htmlgraph/repositories/track_repository_memory.py +508 -0
  242. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  243. htmlgraph/sdk/__init__.py +398 -0
  244. htmlgraph/sdk/__init__.pyi +14 -0
  245. htmlgraph/sdk/analytics/__init__.py +19 -0
  246. htmlgraph/sdk/analytics/engine.py +155 -0
  247. htmlgraph/sdk/analytics/helpers.py +178 -0
  248. htmlgraph/sdk/analytics/registry.py +109 -0
  249. htmlgraph/sdk/base.py +484 -0
  250. htmlgraph/sdk/constants.py +216 -0
  251. htmlgraph/sdk/core.pyi +308 -0
  252. htmlgraph/sdk/discovery.py +120 -0
  253. htmlgraph/sdk/help/__init__.py +12 -0
  254. htmlgraph/sdk/help/mixin.py +699 -0
  255. htmlgraph/sdk/mixins/__init__.py +15 -0
  256. htmlgraph/sdk/mixins/attribution.py +113 -0
  257. htmlgraph/sdk/mixins/mixin.py +410 -0
  258. htmlgraph/sdk/operations/__init__.py +12 -0
  259. htmlgraph/sdk/operations/mixin.py +427 -0
  260. htmlgraph/sdk/orchestration/__init__.py +17 -0
  261. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  262. htmlgraph/sdk/orchestration/spawner.py +204 -0
  263. htmlgraph/sdk/planning/__init__.py +19 -0
  264. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  265. htmlgraph/sdk/planning/mixin.py +211 -0
  266. htmlgraph/sdk/planning/parallel.py +186 -0
  267. htmlgraph/sdk/planning/queue.py +210 -0
  268. htmlgraph/sdk/planning/recommendations.py +87 -0
  269. htmlgraph/sdk/planning/smart_planning.py +319 -0
  270. htmlgraph/sdk/session/__init__.py +19 -0
  271. htmlgraph/sdk/session/continuity.py +57 -0
  272. htmlgraph/sdk/session/handoff.py +110 -0
  273. htmlgraph/sdk/session/info.py +309 -0
  274. htmlgraph/sdk/session/manager.py +103 -0
  275. htmlgraph/sdk/strategic/__init__.py +26 -0
  276. htmlgraph/sdk/strategic/mixin.py +563 -0
  277. htmlgraph/server.py +295 -107
  278. htmlgraph/session_hooks.py +300 -0
  279. htmlgraph/session_manager.py +285 -3
  280. htmlgraph/session_registry.py +587 -0
  281. htmlgraph/session_state.py +436 -0
  282. htmlgraph/session_warning.py +2 -1
  283. htmlgraph/sessions/__init__.py +23 -0
  284. htmlgraph/sessions/handoff.py +756 -0
  285. htmlgraph/system_prompts.py +450 -0
  286. htmlgraph/templates/orchestration-view.html +350 -0
  287. htmlgraph/track_builder.py +33 -1
  288. htmlgraph/track_manager.py +38 -0
  289. htmlgraph/transcript.py +18 -5
  290. htmlgraph/validation.py +115 -0
  291. htmlgraph/watch.py +2 -1
  292. htmlgraph/work_type_utils.py +2 -1
  293. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
  294. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
  295. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  296. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  297. htmlgraph/cli.py +0 -4839
  298. htmlgraph/sdk.py +0 -2359
  299. htmlgraph-0.20.1.dist-info/RECORD +0 -118
  300. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  301. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  302. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  303. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  304. {htmlgraph-0.20.1.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()