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.
Files changed (331) 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 +173 -17
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +127 -0
  7. htmlgraph/agent_registry.py +45 -30
  8. htmlgraph/agents.py +160 -107
  9. htmlgraph/analytics/__init__.py +9 -2
  10. htmlgraph/analytics/cli.py +190 -51
  11. htmlgraph/analytics/cost_analyzer.py +391 -0
  12. htmlgraph/analytics/cost_monitor.py +664 -0
  13. htmlgraph/analytics/cost_reporter.py +675 -0
  14. htmlgraph/analytics/cross_session.py +617 -0
  15. htmlgraph/analytics/dependency.py +192 -100
  16. htmlgraph/analytics/pattern_learning.py +771 -0
  17. htmlgraph/analytics/session_graph.py +707 -0
  18. htmlgraph/analytics/strategic/__init__.py +80 -0
  19. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  20. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  21. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  22. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  23. htmlgraph/analytics/work_type.py +190 -14
  24. htmlgraph/analytics_index.py +135 -51
  25. htmlgraph/api/__init__.py +3 -0
  26. htmlgraph/api/cost_alerts_websocket.py +416 -0
  27. htmlgraph/api/main.py +2498 -0
  28. htmlgraph/api/static/htmx.min.js +1 -0
  29. htmlgraph/api/static/style-redesign.css +1344 -0
  30. htmlgraph/api/static/style.css +1079 -0
  31. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  32. htmlgraph/api/templates/dashboard.html +794 -0
  33. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  34. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  35. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  36. htmlgraph/api/templates/partials/agents.html +317 -0
  37. htmlgraph/api/templates/partials/event-traces.html +373 -0
  38. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  39. htmlgraph/api/templates/partials/features.html +578 -0
  40. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  41. htmlgraph/api/templates/partials/metrics.html +346 -0
  42. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  43. htmlgraph/api/templates/partials/orchestration.html +198 -0
  44. htmlgraph/api/templates/partials/spawners.html +375 -0
  45. htmlgraph/api/templates/partials/work-items.html +613 -0
  46. htmlgraph/api/websocket.py +538 -0
  47. htmlgraph/archive/__init__.py +24 -0
  48. htmlgraph/archive/bloom.py +234 -0
  49. htmlgraph/archive/fts.py +297 -0
  50. htmlgraph/archive/manager.py +583 -0
  51. htmlgraph/archive/search.py +244 -0
  52. htmlgraph/atomic_ops.py +560 -0
  53. htmlgraph/attribute_index.py +208 -0
  54. htmlgraph/bounded_paths.py +539 -0
  55. htmlgraph/builders/__init__.py +14 -0
  56. htmlgraph/builders/base.py +118 -29
  57. htmlgraph/builders/bug.py +150 -0
  58. htmlgraph/builders/chore.py +119 -0
  59. htmlgraph/builders/epic.py +150 -0
  60. htmlgraph/builders/feature.py +31 -6
  61. htmlgraph/builders/insight.py +195 -0
  62. htmlgraph/builders/metric.py +217 -0
  63. htmlgraph/builders/pattern.py +202 -0
  64. htmlgraph/builders/phase.py +162 -0
  65. htmlgraph/builders/spike.py +52 -19
  66. htmlgraph/builders/track.py +148 -72
  67. htmlgraph/cigs/__init__.py +81 -0
  68. htmlgraph/cigs/autonomy.py +385 -0
  69. htmlgraph/cigs/cost.py +475 -0
  70. htmlgraph/cigs/messages_basic.py +472 -0
  71. htmlgraph/cigs/messaging.py +365 -0
  72. htmlgraph/cigs/models.py +771 -0
  73. htmlgraph/cigs/pattern_storage.py +427 -0
  74. htmlgraph/cigs/patterns.py +503 -0
  75. htmlgraph/cigs/posttool_analyzer.py +234 -0
  76. htmlgraph/cigs/reporter.py +818 -0
  77. htmlgraph/cigs/tracker.py +317 -0
  78. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  79. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  80. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  81. htmlgraph/cli/__init__.py +42 -0
  82. htmlgraph/cli/__main__.py +6 -0
  83. htmlgraph/cli/analytics.py +1424 -0
  84. htmlgraph/cli/base.py +685 -0
  85. htmlgraph/cli/constants.py +206 -0
  86. htmlgraph/cli/core.py +954 -0
  87. htmlgraph/cli/main.py +147 -0
  88. htmlgraph/cli/models.py +475 -0
  89. htmlgraph/cli/templates/__init__.py +1 -0
  90. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  91. htmlgraph/cli/work/__init__.py +239 -0
  92. htmlgraph/cli/work/browse.py +115 -0
  93. htmlgraph/cli/work/features.py +568 -0
  94. htmlgraph/cli/work/orchestration.py +676 -0
  95. htmlgraph/cli/work/report.py +728 -0
  96. htmlgraph/cli/work/sessions.py +466 -0
  97. htmlgraph/cli/work/snapshot.py +559 -0
  98. htmlgraph/cli/work/tracks.py +486 -0
  99. htmlgraph/cli_commands/__init__.py +1 -0
  100. htmlgraph/cli_commands/feature.py +195 -0
  101. htmlgraph/cli_framework.py +115 -0
  102. htmlgraph/collections/__init__.py +18 -0
  103. htmlgraph/collections/base.py +415 -98
  104. htmlgraph/collections/bug.py +53 -0
  105. htmlgraph/collections/chore.py +53 -0
  106. htmlgraph/collections/epic.py +53 -0
  107. htmlgraph/collections/feature.py +12 -26
  108. htmlgraph/collections/insight.py +100 -0
  109. htmlgraph/collections/metric.py +92 -0
  110. htmlgraph/collections/pattern.py +97 -0
  111. htmlgraph/collections/phase.py +53 -0
  112. htmlgraph/collections/session.py +194 -0
  113. htmlgraph/collections/spike.py +56 -16
  114. htmlgraph/collections/task_delegation.py +241 -0
  115. htmlgraph/collections/todo.py +511 -0
  116. htmlgraph/collections/traces.py +487 -0
  117. htmlgraph/config/cost_models.json +56 -0
  118. htmlgraph/config.py +190 -0
  119. htmlgraph/context_analytics.py +344 -0
  120. htmlgraph/converter.py +216 -28
  121. htmlgraph/cost_analysis/__init__.py +5 -0
  122. htmlgraph/cost_analysis/analyzer.py +438 -0
  123. htmlgraph/dashboard.html +2406 -307
  124. htmlgraph/dashboard.html.backup +6592 -0
  125. htmlgraph/dashboard.html.bak +7181 -0
  126. htmlgraph/dashboard.html.bak2 +7231 -0
  127. htmlgraph/dashboard.html.bak3 +7232 -0
  128. htmlgraph/db/__init__.py +38 -0
  129. htmlgraph/db/queries.py +790 -0
  130. htmlgraph/db/schema.py +1788 -0
  131. htmlgraph/decorators.py +317 -0
  132. htmlgraph/dependency_models.py +19 -2
  133. htmlgraph/deploy.py +142 -125
  134. htmlgraph/deployment_models.py +474 -0
  135. htmlgraph/docs/API_REFERENCE.md +841 -0
  136. htmlgraph/docs/HTTP_API.md +750 -0
  137. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  138. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  139. htmlgraph/docs/README.md +532 -0
  140. htmlgraph/docs/__init__.py +77 -0
  141. htmlgraph/docs/docs_version.py +55 -0
  142. htmlgraph/docs/metadata.py +93 -0
  143. htmlgraph/docs/migrations.py +232 -0
  144. htmlgraph/docs/template_engine.py +143 -0
  145. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  146. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  147. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  148. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  149. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  150. htmlgraph/docs/version_check.py +163 -0
  151. htmlgraph/edge_index.py +182 -27
  152. htmlgraph/error_handler.py +544 -0
  153. htmlgraph/event_log.py +100 -52
  154. htmlgraph/event_migration.py +13 -4
  155. htmlgraph/exceptions.py +49 -0
  156. htmlgraph/file_watcher.py +101 -28
  157. htmlgraph/find_api.py +75 -63
  158. htmlgraph/git_events.py +145 -63
  159. htmlgraph/graph.py +1122 -106
  160. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  161. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  162. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  163. htmlgraph/hooks/__init__.py +45 -0
  164. htmlgraph/hooks/bootstrap.py +169 -0
  165. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  166. htmlgraph/hooks/concurrent_sessions.py +208 -0
  167. htmlgraph/hooks/context.py +350 -0
  168. htmlgraph/hooks/drift_handler.py +525 -0
  169. htmlgraph/hooks/event_tracker.py +1314 -0
  170. htmlgraph/hooks/git_commands.py +175 -0
  171. htmlgraph/hooks/hooks-config.example.json +12 -0
  172. htmlgraph/hooks/installer.py +343 -0
  173. htmlgraph/hooks/orchestrator.py +674 -0
  174. htmlgraph/hooks/orchestrator_reflector.py +223 -0
  175. htmlgraph/hooks/post-checkout.sh +28 -0
  176. htmlgraph/hooks/post-commit.sh +24 -0
  177. htmlgraph/hooks/post-merge.sh +26 -0
  178. htmlgraph/hooks/post_tool_use_failure.py +273 -0
  179. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  180. htmlgraph/hooks/posttooluse.py +408 -0
  181. htmlgraph/hooks/pre-commit.sh +94 -0
  182. htmlgraph/hooks/pre-push.sh +28 -0
  183. htmlgraph/hooks/pretooluse.py +819 -0
  184. htmlgraph/hooks/prompt_analyzer.py +637 -0
  185. htmlgraph/hooks/session_handler.py +668 -0
  186. htmlgraph/hooks/session_summary.py +395 -0
  187. htmlgraph/hooks/state_manager.py +504 -0
  188. htmlgraph/hooks/subagent_detection.py +202 -0
  189. htmlgraph/hooks/subagent_stop.py +369 -0
  190. htmlgraph/hooks/task_enforcer.py +255 -0
  191. htmlgraph/hooks/task_validator.py +177 -0
  192. htmlgraph/hooks/validator.py +628 -0
  193. htmlgraph/ids.py +41 -27
  194. htmlgraph/index.d.ts +286 -0
  195. htmlgraph/learning.py +767 -0
  196. htmlgraph/mcp_server.py +69 -23
  197. htmlgraph/models.py +1586 -87
  198. htmlgraph/operations/README.md +62 -0
  199. htmlgraph/operations/__init__.py +79 -0
  200. htmlgraph/operations/analytics.py +339 -0
  201. htmlgraph/operations/bootstrap.py +289 -0
  202. htmlgraph/operations/events.py +244 -0
  203. htmlgraph/operations/fastapi_server.py +231 -0
  204. htmlgraph/operations/hooks.py +350 -0
  205. htmlgraph/operations/initialization.py +597 -0
  206. htmlgraph/operations/initialization.py.backup +228 -0
  207. htmlgraph/operations/server.py +303 -0
  208. htmlgraph/orchestration/__init__.py +58 -0
  209. htmlgraph/orchestration/claude_launcher.py +179 -0
  210. htmlgraph/orchestration/command_builder.py +72 -0
  211. htmlgraph/orchestration/headless_spawner.py +281 -0
  212. htmlgraph/orchestration/live_events.py +377 -0
  213. htmlgraph/orchestration/model_selection.py +327 -0
  214. htmlgraph/orchestration/plugin_manager.py +140 -0
  215. htmlgraph/orchestration/prompts.py +137 -0
  216. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  217. htmlgraph/orchestration/spawners/__init__.py +16 -0
  218. htmlgraph/orchestration/spawners/base.py +194 -0
  219. htmlgraph/orchestration/spawners/claude.py +173 -0
  220. htmlgraph/orchestration/spawners/codex.py +435 -0
  221. htmlgraph/orchestration/spawners/copilot.py +294 -0
  222. htmlgraph/orchestration/spawners/gemini.py +471 -0
  223. htmlgraph/orchestration/subprocess_runner.py +36 -0
  224. htmlgraph/orchestration/task_coordination.py +343 -0
  225. htmlgraph/orchestration.md +563 -0
  226. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  227. htmlgraph/orchestrator.py +669 -0
  228. htmlgraph/orchestrator_config.py +357 -0
  229. htmlgraph/orchestrator_mode.py +328 -0
  230. htmlgraph/orchestrator_validator.py +133 -0
  231. htmlgraph/parallel.py +646 -0
  232. htmlgraph/parser.py +160 -35
  233. htmlgraph/path_query.py +608 -0
  234. htmlgraph/pattern_matcher.py +636 -0
  235. htmlgraph/planning.py +147 -52
  236. htmlgraph/pydantic_models.py +476 -0
  237. htmlgraph/quality_gates.py +350 -0
  238. htmlgraph/query_builder.py +109 -72
  239. htmlgraph/query_composer.py +509 -0
  240. htmlgraph/reflection.py +443 -0
  241. htmlgraph/refs.py +344 -0
  242. htmlgraph/repo_hash.py +512 -0
  243. htmlgraph/repositories/__init__.py +292 -0
  244. htmlgraph/repositories/analytics_repository.py +455 -0
  245. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  246. htmlgraph/repositories/feature_repository.py +581 -0
  247. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  248. htmlgraph/repositories/feature_repository_memory.py +607 -0
  249. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  250. htmlgraph/repositories/filter_service.py +620 -0
  251. htmlgraph/repositories/filter_service_standard.py +445 -0
  252. htmlgraph/repositories/shared_cache.py +621 -0
  253. htmlgraph/repositories/shared_cache_memory.py +395 -0
  254. htmlgraph/repositories/track_repository.py +552 -0
  255. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  256. htmlgraph/repositories/track_repository_memory.py +508 -0
  257. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  258. htmlgraph/routing.py +8 -19
  259. htmlgraph/scripts/deploy.py +1 -2
  260. htmlgraph/sdk/__init__.py +398 -0
  261. htmlgraph/sdk/__init__.pyi +14 -0
  262. htmlgraph/sdk/analytics/__init__.py +19 -0
  263. htmlgraph/sdk/analytics/engine.py +155 -0
  264. htmlgraph/sdk/analytics/helpers.py +178 -0
  265. htmlgraph/sdk/analytics/registry.py +109 -0
  266. htmlgraph/sdk/base.py +484 -0
  267. htmlgraph/sdk/constants.py +216 -0
  268. htmlgraph/sdk/core.pyi +308 -0
  269. htmlgraph/sdk/discovery.py +120 -0
  270. htmlgraph/sdk/help/__init__.py +12 -0
  271. htmlgraph/sdk/help/mixin.py +699 -0
  272. htmlgraph/sdk/mixins/__init__.py +15 -0
  273. htmlgraph/sdk/mixins/attribution.py +113 -0
  274. htmlgraph/sdk/mixins/mixin.py +410 -0
  275. htmlgraph/sdk/operations/__init__.py +12 -0
  276. htmlgraph/sdk/operations/mixin.py +427 -0
  277. htmlgraph/sdk/orchestration/__init__.py +17 -0
  278. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  279. htmlgraph/sdk/orchestration/spawner.py +204 -0
  280. htmlgraph/sdk/planning/__init__.py +19 -0
  281. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  282. htmlgraph/sdk/planning/mixin.py +211 -0
  283. htmlgraph/sdk/planning/parallel.py +186 -0
  284. htmlgraph/sdk/planning/queue.py +210 -0
  285. htmlgraph/sdk/planning/recommendations.py +87 -0
  286. htmlgraph/sdk/planning/smart_planning.py +319 -0
  287. htmlgraph/sdk/session/__init__.py +19 -0
  288. htmlgraph/sdk/session/continuity.py +57 -0
  289. htmlgraph/sdk/session/handoff.py +110 -0
  290. htmlgraph/sdk/session/info.py +309 -0
  291. htmlgraph/sdk/session/manager.py +103 -0
  292. htmlgraph/sdk/strategic/__init__.py +26 -0
  293. htmlgraph/sdk/strategic/mixin.py +563 -0
  294. htmlgraph/server.py +685 -180
  295. htmlgraph/services/__init__.py +10 -0
  296. htmlgraph/services/claiming.py +199 -0
  297. htmlgraph/session_hooks.py +300 -0
  298. htmlgraph/session_manager.py +1392 -175
  299. htmlgraph/session_registry.py +587 -0
  300. htmlgraph/session_state.py +436 -0
  301. htmlgraph/session_warning.py +201 -0
  302. htmlgraph/sessions/__init__.py +23 -0
  303. htmlgraph/sessions/handoff.py +756 -0
  304. htmlgraph/setup.py +34 -17
  305. htmlgraph/spike_index.py +143 -0
  306. htmlgraph/sync_docs.py +12 -15
  307. htmlgraph/system_prompts.py +450 -0
  308. htmlgraph/templates/AGENTS.md.template +366 -0
  309. htmlgraph/templates/CLAUDE.md.template +97 -0
  310. htmlgraph/templates/GEMINI.md.template +87 -0
  311. htmlgraph/templates/orchestration-view.html +350 -0
  312. htmlgraph/track_builder.py +146 -15
  313. htmlgraph/track_manager.py +69 -21
  314. htmlgraph/transcript.py +890 -0
  315. htmlgraph/transcript_analytics.py +699 -0
  316. htmlgraph/types.py +323 -0
  317. htmlgraph/validation.py +115 -0
  318. htmlgraph/watch.py +8 -5
  319. htmlgraph/work_type_utils.py +3 -2
  320. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
  321. htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
  322. htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
  323. htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
  324. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
  325. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  326. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  327. htmlgraph/cli.py +0 -2688
  328. htmlgraph/sdk.py +0 -709
  329. htmlgraph-0.9.3.dist-info/RECORD +0 -61
  330. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  331. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
@@ -0,0 +1,509 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Graph Query Composer for HtmlGraph.
5
+
6
+ Extends QueryBuilder capabilities with graph traversal, enabling queries
7
+ that combine attribute filtering WITH edge traversal.
8
+
9
+ Example:
10
+ from htmlgraph import HtmlGraph
11
+
12
+ graph = HtmlGraph("features/")
13
+
14
+ # Find blocked features whose blockers are high-priority
15
+ results = graph.query_composer() \\
16
+ .where("status", "blocked") \\
17
+ .traverse("blocked_by", direction="outgoing") \\
18
+ .where("priority", "high") \\
19
+ .execute()
20
+
21
+ # Find all features transitively reachable via depends_on from a root
22
+ results = graph.query_composer() \\
23
+ .reachable_from("feat-001", "depends_on") \\
24
+ .where("status", "todo") \\
25
+ .execute()
26
+ """
27
+
28
+ from collections import deque
29
+ from dataclasses import dataclass, field
30
+ from typing import TYPE_CHECKING, Any, Literal
31
+
32
+ from htmlgraph.query_builder import Condition, LogicalOp, Operator
33
+
34
+ if TYPE_CHECKING:
35
+ from htmlgraph.graph import HtmlGraph
36
+ from htmlgraph.models import Node
37
+
38
+
39
+ # Sentinel object to distinguish "no value passed" from None
40
+ _SENTINEL: Any = object()
41
+
42
+
43
+ @dataclass
44
+ class QueryStage:
45
+ """A single stage in the query execution pipeline."""
46
+
47
+ stage_type: Literal["filter", "traverse", "traverse_recursive", "reachable_from"]
48
+ params: dict[str, Any] = field(default_factory=dict)
49
+
50
+
51
+ class GraphQueryComposer:
52
+ """
53
+ Composes graph traversal with attribute filtering.
54
+
55
+ Wraps QueryBuilder condition logic and EdgeIndex traversal into
56
+ a pipeline of stages. Each stage narrows the working set of nodes.
57
+
58
+ Example:
59
+ composer = GraphQueryComposer(graph)
60
+ results = composer \\
61
+ .where("status", "blocked") \\
62
+ .traverse("blocked_by") \\
63
+ .where("priority", "high") \\
64
+ .execute()
65
+ """
66
+
67
+ def __init__(self, graph: HtmlGraph) -> None:
68
+ self.graph = graph
69
+ self._stages: list[QueryStage] = []
70
+ # Accumulate filter conditions for the current filter group.
71
+ # When a traverse stage is added, pending conditions are flushed
72
+ # into a filter stage.
73
+ self._pending_conditions: list[Condition] = []
74
+
75
+ # ------------------------------------------------------------------
76
+ # Attribute filtering (delegates to QueryBuilder condition logic)
77
+ # ------------------------------------------------------------------
78
+
79
+ def where(self, attr: str, value: Any = _SENTINEL) -> GraphQueryComposer:
80
+ """Start or add an attribute filter on the current node set.
81
+
82
+ Args:
83
+ attr: Attribute name (supports dot notation for nested access).
84
+ value: If provided, shorthand for equality check.
85
+
86
+ Returns:
87
+ Self for chaining.
88
+ """
89
+ if value is not _SENTINEL:
90
+ self._pending_conditions.append(
91
+ Condition(
92
+ attribute=attr,
93
+ operator=Operator.EQ,
94
+ value=value,
95
+ logical_op=LogicalOp.AND,
96
+ )
97
+ )
98
+ else:
99
+ # When no value is given, add a placeholder condition that must
100
+ # be completed by a subsequent operator call. For simplicity
101
+ # we store an IS_NOT_NULL condition which acts as a "has attr"
102
+ # check; callers who want other operators should use and_/or_.
103
+ self._pending_conditions.append(
104
+ Condition(
105
+ attribute=attr,
106
+ operator=Operator.IS_NOT_NULL,
107
+ value=None,
108
+ logical_op=LogicalOp.AND,
109
+ )
110
+ )
111
+ return self
112
+
113
+ def and_(self, attr: str, value: Any = _SENTINEL) -> GraphQueryComposer:
114
+ """Add an AND condition.
115
+
116
+ Args:
117
+ attr: Attribute name.
118
+ value: If provided, shorthand for equality check.
119
+
120
+ Returns:
121
+ Self for chaining.
122
+ """
123
+ if value is not _SENTINEL:
124
+ self._pending_conditions.append(
125
+ Condition(
126
+ attribute=attr,
127
+ operator=Operator.EQ,
128
+ value=value,
129
+ logical_op=LogicalOp.AND,
130
+ )
131
+ )
132
+ else:
133
+ self._pending_conditions.append(
134
+ Condition(
135
+ attribute=attr,
136
+ operator=Operator.IS_NOT_NULL,
137
+ value=None,
138
+ logical_op=LogicalOp.AND,
139
+ )
140
+ )
141
+ return self
142
+
143
+ def or_(self, attr: str, value: Any = _SENTINEL) -> GraphQueryComposer:
144
+ """Add an OR condition.
145
+
146
+ Args:
147
+ attr: Attribute name.
148
+ value: If provided, shorthand for equality check.
149
+
150
+ Returns:
151
+ Self for chaining.
152
+ """
153
+ if value is not _SENTINEL:
154
+ self._pending_conditions.append(
155
+ Condition(
156
+ attribute=attr,
157
+ operator=Operator.EQ,
158
+ value=value,
159
+ logical_op=LogicalOp.OR,
160
+ )
161
+ )
162
+ else:
163
+ self._pending_conditions.append(
164
+ Condition(
165
+ attribute=attr,
166
+ operator=Operator.IS_NOT_NULL,
167
+ value=None,
168
+ logical_op=LogicalOp.OR,
169
+ )
170
+ )
171
+ return self
172
+
173
+ # ------------------------------------------------------------------
174
+ # Relationship traversal
175
+ # ------------------------------------------------------------------
176
+
177
+ def traverse(
178
+ self, relationship: str, direction: str = "outgoing"
179
+ ) -> GraphQueryComposer:
180
+ """Follow edges of given relationship type from current result set.
181
+
182
+ For each node in the working set, collect the nodes reachable
183
+ by one hop via *relationship* in the specified *direction*.
184
+
185
+ Args:
186
+ relationship: Edge relationship type (e.g. ``"blocked_by"``).
187
+ direction: ``"outgoing"``, ``"incoming"``, or ``"both"``.
188
+
189
+ Returns:
190
+ Self for chaining.
191
+ """
192
+ self._flush_conditions()
193
+ self._stages.append(
194
+ QueryStage(
195
+ stage_type="traverse",
196
+ params={"relationship": relationship, "direction": direction},
197
+ )
198
+ )
199
+ return self
200
+
201
+ def traverse_recursive(
202
+ self,
203
+ relationship: str,
204
+ direction: str = "outgoing",
205
+ max_depth: int = 10,
206
+ ) -> GraphQueryComposer:
207
+ """Follow edges recursively (transitive closure) up to *max_depth*.
208
+
209
+ Args:
210
+ relationship: Edge relationship type.
211
+ direction: ``"outgoing"``, ``"incoming"``, or ``"both"``.
212
+ max_depth: Maximum traversal depth.
213
+
214
+ Returns:
215
+ Self for chaining.
216
+ """
217
+ self._flush_conditions()
218
+ self._stages.append(
219
+ QueryStage(
220
+ stage_type="traverse_recursive",
221
+ params={
222
+ "relationship": relationship,
223
+ "direction": direction,
224
+ "max_depth": max_depth,
225
+ },
226
+ )
227
+ )
228
+ return self
229
+
230
+ def reachable_from(
231
+ self,
232
+ node_id: str,
233
+ relationship: str,
234
+ direction: str = "outgoing",
235
+ max_depth: int = 10,
236
+ ) -> GraphQueryComposer:
237
+ """Filter to nodes reachable from *node_id* via *relationship*.
238
+
239
+ This resets the working set to all nodes transitively reachable
240
+ from the given starting node (excluding the starting node itself).
241
+
242
+ Args:
243
+ node_id: Starting node ID.
244
+ relationship: Edge relationship type.
245
+ direction: ``"outgoing"``, ``"incoming"``, or ``"both"``.
246
+ max_depth: Maximum traversal depth.
247
+
248
+ Returns:
249
+ Self for chaining.
250
+ """
251
+ self._flush_conditions()
252
+ self._stages.append(
253
+ QueryStage(
254
+ stage_type="reachable_from",
255
+ params={
256
+ "node_id": node_id,
257
+ "relationship": relationship,
258
+ "direction": direction,
259
+ "max_depth": max_depth,
260
+ },
261
+ )
262
+ )
263
+ return self
264
+
265
+ # ------------------------------------------------------------------
266
+ # Convenience methods
267
+ # ------------------------------------------------------------------
268
+
269
+ def blocked_by_chain(self, feature_id: str) -> GraphQueryComposer:
270
+ """Find all features in the ``blocked_by`` chain from *feature_id*.
271
+
272
+ Equivalent to ``reachable_from(feature_id, "blocked_by", "outgoing")``.
273
+
274
+ Args:
275
+ feature_id: Starting feature node ID.
276
+
277
+ Returns:
278
+ Self for chaining.
279
+ """
280
+ return self.reachable_from(feature_id, "blocked_by", direction="outgoing")
281
+
282
+ def dependency_chain(self, feature_id: str) -> GraphQueryComposer:
283
+ """Find all features in the ``depends_on`` chain from *feature_id*.
284
+
285
+ Equivalent to ``reachable_from(feature_id, "depends_on", "outgoing")``.
286
+
287
+ Args:
288
+ feature_id: Starting feature node ID.
289
+
290
+ Returns:
291
+ Self for chaining.
292
+ """
293
+ return self.reachable_from(feature_id, "depends_on", direction="outgoing")
294
+
295
+ # ------------------------------------------------------------------
296
+ # Execution
297
+ # ------------------------------------------------------------------
298
+
299
+ def execute(self) -> list[Node]:
300
+ """Execute all stages and return matching nodes.
301
+
302
+ Returns:
303
+ List of nodes matching the composed query.
304
+ """
305
+ # Flush any remaining pending conditions
306
+ self._flush_conditions()
307
+
308
+ self.graph._ensure_loaded() # noqa: SLF001
309
+
310
+ # Start with all node IDs
311
+ working_set: set[str] = set(self.graph._nodes.keys()) # noqa: SLF001
312
+
313
+ for stage in self._stages:
314
+ if stage.stage_type == "filter":
315
+ working_set = self._apply_filter(working_set, stage)
316
+ elif stage.stage_type == "traverse":
317
+ working_set = self._apply_traverse(working_set, stage)
318
+ elif stage.stage_type == "traverse_recursive":
319
+ working_set = self._apply_traverse_recursive(working_set, stage)
320
+ elif stage.stage_type == "reachable_from":
321
+ working_set = self._apply_reachable_from(working_set, stage)
322
+
323
+ # Resolve IDs to Node objects, preserving only nodes that still exist
324
+ nodes = self.graph._nodes # noqa: SLF001
325
+ return [nodes[nid] for nid in working_set if nid in nodes]
326
+
327
+ def count(self) -> int:
328
+ """Execute and return count of matches.
329
+
330
+ Returns:
331
+ Number of matching nodes.
332
+ """
333
+ return len(self.execute())
334
+
335
+ def first(self) -> Node | None:
336
+ """Execute and return first match or ``None``.
337
+
338
+ Returns:
339
+ First matching node or ``None``.
340
+ """
341
+ results = self.execute()
342
+ return results[0] if results else None
343
+
344
+ def ids(self) -> list[str]:
345
+ """Execute and return just node IDs.
346
+
347
+ Returns:
348
+ List of matching node IDs.
349
+ """
350
+ return [node.id for node in self.execute()]
351
+
352
+ # ------------------------------------------------------------------
353
+ # Internal helpers
354
+ # ------------------------------------------------------------------
355
+
356
+ def _flush_conditions(self) -> None:
357
+ """Flush accumulated conditions into a filter stage."""
358
+ if self._pending_conditions:
359
+ self._stages.append(
360
+ QueryStage(
361
+ stage_type="filter",
362
+ params={"conditions": list(self._pending_conditions)},
363
+ )
364
+ )
365
+ self._pending_conditions = []
366
+
367
+ def _apply_filter(self, working_set: set[str], stage: QueryStage) -> set[str]:
368
+ """Apply attribute filter conditions to the working set."""
369
+ conditions: list[Condition] = stage.params["conditions"]
370
+ nodes = self.graph._nodes # noqa: SLF001
371
+ result: set[str] = set()
372
+
373
+ for nid in working_set:
374
+ node = nodes.get(nid)
375
+ if node is None:
376
+ continue
377
+ if self._evaluate_conditions(node, conditions):
378
+ result.add(nid)
379
+
380
+ return result
381
+
382
+ def _apply_traverse(self, working_set: set[str], stage: QueryStage) -> set[str]:
383
+ """Follow one hop of edges from working set."""
384
+ relationship: str = stage.params["relationship"]
385
+ direction: str = stage.params["direction"]
386
+ edge_index = self.graph._edge_index # noqa: SLF001
387
+ result: set[str] = set()
388
+
389
+ for nid in working_set:
390
+ if direction in ("outgoing", "both"):
391
+ for ref in edge_index.get_outgoing(nid, relationship):
392
+ result.add(ref.target_id)
393
+ if direction in ("incoming", "both"):
394
+ for ref in edge_index.get_incoming(nid, relationship):
395
+ result.add(ref.source_id)
396
+
397
+ return result
398
+
399
+ def _apply_traverse_recursive(
400
+ self, working_set: set[str], stage: QueryStage
401
+ ) -> set[str]:
402
+ """Follow edges recursively from working set with depth limit."""
403
+ relationship: str = stage.params["relationship"]
404
+ direction: str = stage.params["direction"]
405
+ max_depth: int = stage.params["max_depth"]
406
+ edge_index = self.graph._edge_index # noqa: SLF001
407
+
408
+ result: set[str] = set()
409
+ # BFS from every node in working set
410
+ queue: deque[tuple[str, int]] = deque()
411
+ for nid in working_set:
412
+ queue.append((nid, 0))
413
+
414
+ visited: set[str] = set(working_set)
415
+
416
+ while queue:
417
+ current, depth = queue.popleft()
418
+ if depth >= max_depth:
419
+ continue
420
+
421
+ neighbors: set[str] = set()
422
+ if direction in ("outgoing", "both"):
423
+ for ref in edge_index.get_outgoing(current, relationship):
424
+ neighbors.add(ref.target_id)
425
+ if direction in ("incoming", "both"):
426
+ for ref in edge_index.get_incoming(current, relationship):
427
+ neighbors.add(ref.source_id)
428
+
429
+ for neighbor in neighbors:
430
+ if neighbor not in visited:
431
+ visited.add(neighbor)
432
+ result.add(neighbor)
433
+ queue.append((neighbor, depth + 1))
434
+
435
+ return result
436
+
437
+ def _apply_reachable_from(
438
+ self, working_set: set[str], stage: QueryStage
439
+ ) -> set[str]:
440
+ """Compute nodes reachable from a specific node, intersected with working set."""
441
+ node_id: str = stage.params["node_id"]
442
+ relationship: str = stage.params["relationship"]
443
+ direction: str = stage.params["direction"]
444
+ max_depth: int = stage.params["max_depth"]
445
+ edge_index = self.graph._edge_index # noqa: SLF001
446
+
447
+ reachable: set[str] = set()
448
+ visited: set[str] = {node_id}
449
+ queue: deque[tuple[str, int]] = deque([(node_id, 0)])
450
+
451
+ while queue:
452
+ current, depth = queue.popleft()
453
+ if depth >= max_depth:
454
+ continue
455
+
456
+ neighbors: set[str] = set()
457
+ if direction in ("outgoing", "both"):
458
+ for ref in edge_index.get_outgoing(current, relationship):
459
+ neighbors.add(ref.target_id)
460
+ if direction in ("incoming", "both"):
461
+ for ref in edge_index.get_incoming(current, relationship):
462
+ neighbors.add(ref.source_id)
463
+
464
+ for neighbor in neighbors:
465
+ if neighbor not in visited:
466
+ visited.add(neighbor)
467
+ reachable.add(neighbor)
468
+ queue.append((neighbor, depth + 1))
469
+
470
+ # Intersect with the current working set
471
+ return working_set & reachable
472
+
473
+ @staticmethod
474
+ def _evaluate_conditions(node: Node, conditions: list[Condition]) -> bool:
475
+ """Evaluate a list of conditions against a node.
476
+
477
+ Reuses the ``Condition.evaluate`` method from ``query_builder.py``
478
+ so condition evaluation logic is not duplicated.
479
+
480
+ Args:
481
+ node: Node to evaluate.
482
+ conditions: Conditions to check.
483
+
484
+ Returns:
485
+ ``True`` if the node satisfies the combined conditions.
486
+ """
487
+ if not conditions:
488
+ return True
489
+
490
+ result: bool | None = None
491
+
492
+ for condition in conditions:
493
+ condition_result = condition.evaluate(node)
494
+
495
+ # Handle NOT operator
496
+ if condition.logical_op == LogicalOp.NOT:
497
+ condition_result = not condition_result
498
+
499
+ if result is None:
500
+ result = condition_result
501
+ elif condition.logical_op == LogicalOp.AND:
502
+ result = result and condition_result
503
+ elif condition.logical_op == LogicalOp.OR:
504
+ result = result or condition_result
505
+ elif condition.logical_op == LogicalOp.NOT:
506
+ # NOT combined with previous result (AND NOT)
507
+ result = result and condition_result
508
+
509
+ return result if result is not None else True