frontend-perception-engine 0.1.0__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.
- frontend_perception_engine-0.1.0.dist-info/METADATA +235 -0
- frontend_perception_engine-0.1.0.dist-info/RECORD +50 -0
- frontend_perception_engine-0.1.0.dist-info/WHEEL +5 -0
- frontend_perception_engine-0.1.0.dist-info/entry_points.txt +2 -0
- frontend_perception_engine-0.1.0.dist-info/top_level.txt +1 -0
- navigation/__init__.py +1 -0
- navigation/browser_use/__init__.py +17 -0
- navigation/browser_use/agent_runner.py +165 -0
- navigation/browser_use/hints.py +155 -0
- navigation/browser_use/integration.py +46 -0
- navigation/browser_use/llm.py +70 -0
- navigation/codeGraph/__init__.py +4 -0
- navigation/codeGraph/crg_impl.py +170 -0
- navigation/codeGraph/factory.py +24 -0
- navigation/codeGraph/interface.py +78 -0
- navigation/codeGraph/null_impl.py +47 -0
- navigation/mcp/__init__.py +4 -0
- navigation/mcp/__main__.py +4 -0
- navigation/mcp/diff.py +28 -0
- navigation/mcp/envelope.py +50 -0
- navigation/mcp/handlers.py +791 -0
- navigation/mcp/instructions.py +36 -0
- navigation/mcp/resources.py +82 -0
- navigation/mcp/scan_registry.py +47 -0
- navigation/mcp/server.py +229 -0
- navigation/mcp/session_store.py +87 -0
- navigation/mcp/tools.py +332 -0
- navigation/perception/__init__.py +76 -0
- navigation/perception/artifacts.py +19 -0
- navigation/perception/auth_gate.py +57 -0
- navigation/perception/budget.py +55 -0
- navigation/perception/cdp_hub.py +122 -0
- navigation/perception/dev_insights.py +625 -0
- navigation/perception/exploration.py +99 -0
- navigation/perception/feature_flags.py +83 -0
- navigation/perception/file_upload.py +84 -0
- navigation/perception/flow_graph.py +121 -0
- navigation/perception/form_probe.py +148 -0
- navigation/perception/iframe_context.py +76 -0
- navigation/perception/observation.py +97 -0
- navigation/perception/preflight.py +91 -0
- navigation/perception/rich_editors.py +90 -0
- navigation/perception/route_guards.py +136 -0
- navigation/perception/runner.py +138 -0
- navigation/perception/scan.py +74 -0
- navigation/perception/scripted_actions.py +67 -0
- navigation/perception/state_manager.py +114 -0
- navigation/perception/verification.py +117 -0
- navigation/perception/virtual_scroll.py +84 -0
- navigation/perception/websocket_observer.py +79 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_bedrock_config() -> dict[str, Any]:
|
|
8
|
+
return {
|
|
9
|
+
"model": os.getenv("BEDROCK_MODEL", "amazon.nova-pro-v1:0"),
|
|
10
|
+
"region": os.getenv("AWS_REGION") or os.getenv("AWS_DEFAULT_REGION", "us-east-1"),
|
|
11
|
+
"aws_access_key_id": os.getenv("AWS_ACCESS_KEY_ID"),
|
|
12
|
+
"aws_secret_access_key": os.getenv("AWS_SECRET_ACCESS_KEY"),
|
|
13
|
+
"aws_session_token": os.getenv("AWS_SESSION_TOKEN"),
|
|
14
|
+
"aws_sso_auth": os.getenv("AWS_SSO_AUTH", "").lower() in {"1", "true", "yes"},
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_bedrock_llm(
|
|
19
|
+
model: str | None = None,
|
|
20
|
+
region: str | None = None,
|
|
21
|
+
temperature: float = 0.2,
|
|
22
|
+
):
|
|
23
|
+
"""Create Browser Use ChatAWSBedrock for Nova / other Bedrock models."""
|
|
24
|
+
cfg = get_bedrock_config()
|
|
25
|
+
model = model or cfg["model"]
|
|
26
|
+
region = region or cfg["region"]
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
from browser_use.llm import ChatAWSBedrock
|
|
30
|
+
except ImportError as exc:
|
|
31
|
+
raise RuntimeError("browser-use is not installed") from exc
|
|
32
|
+
|
|
33
|
+
kwargs: dict[str, Any] = {
|
|
34
|
+
"model": model,
|
|
35
|
+
"aws_region": region,
|
|
36
|
+
"temperature": temperature,
|
|
37
|
+
"aws_sso_auth": cfg["aws_sso_auth"],
|
|
38
|
+
}
|
|
39
|
+
if cfg["aws_access_key_id"] and cfg["aws_secret_access_key"]:
|
|
40
|
+
kwargs["aws_access_key_id"] = cfg["aws_access_key_id"]
|
|
41
|
+
kwargs["aws_secret_access_key"] = cfg["aws_secret_access_key"]
|
|
42
|
+
if cfg["aws_session_token"]:
|
|
43
|
+
kwargs["aws_session_token"] = cfg["aws_session_token"]
|
|
44
|
+
else:
|
|
45
|
+
# Fall back to boto3 default credential chain (~/.aws/credentials, SSO, etc.)
|
|
46
|
+
try:
|
|
47
|
+
import boto3
|
|
48
|
+
|
|
49
|
+
session = boto3.Session(region_name=region)
|
|
50
|
+
if session.get_credentials() is not None:
|
|
51
|
+
kwargs["session"] = session
|
|
52
|
+
kwargs["aws_sso_auth"] = True
|
|
53
|
+
except ImportError:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
return ChatAWSBedrock(**kwargs)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def credentials_available() -> bool:
|
|
60
|
+
cfg = get_bedrock_config()
|
|
61
|
+
if cfg["aws_sso_auth"]:
|
|
62
|
+
return True
|
|
63
|
+
if cfg["aws_access_key_id"] and cfg["aws_secret_access_key"]:
|
|
64
|
+
return True
|
|
65
|
+
try:
|
|
66
|
+
import boto3
|
|
67
|
+
|
|
68
|
+
return boto3.Session().get_credentials() is not None
|
|
69
|
+
except Exception:
|
|
70
|
+
return False
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
|
|
6
|
+
from .interface import GraphQueryResult, ICodeGraph
|
|
7
|
+
|
|
8
|
+
CRGError = Exception
|
|
9
|
+
_build_or_update_graph: Callable[..., dict[str, Any]] | None = None
|
|
10
|
+
_query_graph: Callable[..., dict[str, Any]] | None = None
|
|
11
|
+
_semantic_search_nodes: Callable[..., dict[str, Any]] | None = None
|
|
12
|
+
_get_impact_radius: Callable[..., dict[str, Any]] | None = None
|
|
13
|
+
_list_graph_stats: Callable[..., dict[str, Any]] | None = None
|
|
14
|
+
_traverse_graph_func: Callable[..., dict[str, Any]] | None = None
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from code_review_graph.tools.build import build_or_update_graph as _build_or_update_graph
|
|
18
|
+
from code_review_graph.tools.query import (
|
|
19
|
+
get_impact_radius as _get_impact_radius,
|
|
20
|
+
list_graph_stats as _list_graph_stats,
|
|
21
|
+
query_graph as _query_graph,
|
|
22
|
+
semantic_search_nodes as _semantic_search_nodes,
|
|
23
|
+
traverse_graph_func as _traverse_graph_func,
|
|
24
|
+
)
|
|
25
|
+
except Exception as exc: # pragma: no cover - defensive import
|
|
26
|
+
CRGError = type(exc)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CRGCodeGraph(ICodeGraph):
|
|
30
|
+
"""CRG-backed code graph adapter hidden behind ICodeGraph."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, repo_root: str | Path) -> None:
|
|
33
|
+
self.repo_root = str(Path(repo_root))
|
|
34
|
+
|
|
35
|
+
def _ok(self, summary: str, payload: dict[str, Any]) -> GraphQueryResult:
|
|
36
|
+
return GraphQueryResult(ok=True, source="crg", summary=summary, payload=payload)
|
|
37
|
+
|
|
38
|
+
def _err(self, action: str, exc: Exception) -> GraphQueryResult:
|
|
39
|
+
return GraphQueryResult(
|
|
40
|
+
ok=False,
|
|
41
|
+
source="crg",
|
|
42
|
+
summary=f"CRG {action} failed.",
|
|
43
|
+
error=str(exc),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _ensure_available() -> None:
|
|
48
|
+
required = [
|
|
49
|
+
_build_or_update_graph,
|
|
50
|
+
_query_graph,
|
|
51
|
+
_semantic_search_nodes,
|
|
52
|
+
_get_impact_radius,
|
|
53
|
+
_list_graph_stats,
|
|
54
|
+
_traverse_graph_func,
|
|
55
|
+
]
|
|
56
|
+
if any(fn is None for fn in required):
|
|
57
|
+
raise RuntimeError("code-review-graph is not available")
|
|
58
|
+
|
|
59
|
+
def initialize(self) -> GraphQueryResult:
|
|
60
|
+
try:
|
|
61
|
+
self._ensure_available()
|
|
62
|
+
payload = _build_or_update_graph(repo_root=self.repo_root, full_rebuild=False, postprocess="minimal")
|
|
63
|
+
return self._ok(payload.get("summary", "Graph initialized"), payload)
|
|
64
|
+
except Exception as exc:
|
|
65
|
+
return self._err("initialize", exc)
|
|
66
|
+
|
|
67
|
+
def refresh(self) -> GraphQueryResult:
|
|
68
|
+
try:
|
|
69
|
+
self._ensure_available()
|
|
70
|
+
payload = _build_or_update_graph(repo_root=self.repo_root, full_rebuild=False)
|
|
71
|
+
return self._ok(payload.get("summary", "Graph refreshed"), payload)
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
return self._err("refresh", exc)
|
|
74
|
+
|
|
75
|
+
def rebuild(self) -> GraphQueryResult:
|
|
76
|
+
try:
|
|
77
|
+
self._ensure_available()
|
|
78
|
+
payload = _build_or_update_graph(repo_root=self.repo_root, full_rebuild=True)
|
|
79
|
+
return self._ok(payload.get("summary", "Graph rebuilt"), payload)
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
return self._err("rebuild", exc)
|
|
82
|
+
|
|
83
|
+
def search(self, query: str, *, kind: str | None = None, limit: int = 20) -> GraphQueryResult:
|
|
84
|
+
try:
|
|
85
|
+
self._ensure_available()
|
|
86
|
+
payload = _semantic_search_nodes(query=query, kind=kind, limit=limit, repo_root=self.repo_root)
|
|
87
|
+
return self._ok(payload.get("summary", f"Search for {query}"), payload)
|
|
88
|
+
except Exception as exc:
|
|
89
|
+
return self._err("search", exc)
|
|
90
|
+
|
|
91
|
+
def shortest_path(self, query: str, *, depth: int = 3, mode: str = "bfs") -> GraphQueryResult:
|
|
92
|
+
try:
|
|
93
|
+
self._ensure_available()
|
|
94
|
+
payload = _traverse_graph_func(query=query, depth=depth, mode=mode, repo_root=self.repo_root)
|
|
95
|
+
return self._ok(payload.get("summary", f"Traversal for {query}"), payload)
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
return self._err("shortest_path", exc)
|
|
98
|
+
|
|
99
|
+
def get_neighbors(self, target: str, *, relation: str = "callees_of") -> GraphQueryResult:
|
|
100
|
+
try:
|
|
101
|
+
self._ensure_available()
|
|
102
|
+
payload = _query_graph(pattern=relation, target=target, repo_root=self.repo_root)
|
|
103
|
+
return self._ok(payload.get("summary", f"{relation} for {target}"), payload)
|
|
104
|
+
except Exception as exc:
|
|
105
|
+
return self._err("get_neighbors", exc)
|
|
106
|
+
|
|
107
|
+
def get_component(self, name: str) -> GraphQueryResult:
|
|
108
|
+
return self.search(name, kind="Class", limit=10)
|
|
109
|
+
|
|
110
|
+
def get_file(self, file_path: str) -> GraphQueryResult:
|
|
111
|
+
try:
|
|
112
|
+
self._ensure_available()
|
|
113
|
+
payload = _query_graph(pattern="file_summary", target=file_path, repo_root=self.repo_root)
|
|
114
|
+
return self._ok(payload.get("summary", f"File summary for {file_path}"), payload)
|
|
115
|
+
except Exception as exc:
|
|
116
|
+
return self._err("get_file", exc)
|
|
117
|
+
|
|
118
|
+
def get_route(self, changed_files: list[str] | None = None, *, max_depth: int = 2) -> GraphQueryResult:
|
|
119
|
+
try:
|
|
120
|
+
self._ensure_available()
|
|
121
|
+
payload = _get_impact_radius(
|
|
122
|
+
changed_files=changed_files,
|
|
123
|
+
max_depth=max_depth,
|
|
124
|
+
repo_root=self.repo_root,
|
|
125
|
+
)
|
|
126
|
+
return self._ok(payload.get("summary", "Impact route"), payload)
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
return self._err("get_route", exc)
|
|
129
|
+
|
|
130
|
+
def query(self, query_type: str, **kwargs: Any) -> GraphQueryResult:
|
|
131
|
+
# Public, graph-agnostic API contract for current + future methods.
|
|
132
|
+
if query_type == "search":
|
|
133
|
+
return self.search(kwargs["query"], kind=kwargs.get("kind"), limit=kwargs.get("limit", 20))
|
|
134
|
+
if query_type == "neighbors":
|
|
135
|
+
return self.get_neighbors(kwargs["target"], relation=kwargs.get("relation", "callees_of"))
|
|
136
|
+
if query_type == "component":
|
|
137
|
+
return self.get_component(kwargs["name"])
|
|
138
|
+
if query_type == "file":
|
|
139
|
+
return self.get_file(kwargs["file_path"])
|
|
140
|
+
if query_type == "route":
|
|
141
|
+
return self.get_route(kwargs.get("changed_files"), max_depth=kwargs.get("max_depth", 2))
|
|
142
|
+
if query_type == "shortest_path":
|
|
143
|
+
return self.shortest_path(kwargs["query"], depth=kwargs.get("depth", 3), mode=kwargs.get("mode", "bfs"))
|
|
144
|
+
if query_type == "stats":
|
|
145
|
+
try:
|
|
146
|
+
self._ensure_available()
|
|
147
|
+
payload = _list_graph_stats(repo_root=self.repo_root)
|
|
148
|
+
return self._ok(payload.get("summary", "Graph stats"), payload)
|
|
149
|
+
except Exception as exc:
|
|
150
|
+
return self._err("stats", exc)
|
|
151
|
+
|
|
152
|
+
# Future API shape examples (defer to search/traversal today):
|
|
153
|
+
if query_type in {
|
|
154
|
+
"find_navigation_hint",
|
|
155
|
+
"find_relevant_components",
|
|
156
|
+
"find_likely_route",
|
|
157
|
+
"find_related_files",
|
|
158
|
+
"find_button_candidates",
|
|
159
|
+
"find_component_hierarchy",
|
|
160
|
+
"find_entry_point",
|
|
161
|
+
}:
|
|
162
|
+
prompt = " ".join(str(v) for v in kwargs.values())
|
|
163
|
+
return self.search(prompt, limit=10)
|
|
164
|
+
|
|
165
|
+
return GraphQueryResult(
|
|
166
|
+
ok=False,
|
|
167
|
+
source="crg",
|
|
168
|
+
summary=f"Unknown query type: {query_type}",
|
|
169
|
+
error="unknown_query_type",
|
|
170
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .crg_impl import CRGCodeGraph
|
|
6
|
+
from .interface import ICodeGraph
|
|
7
|
+
from .null_impl import NullCodeGraph
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_code_graph(repo_root: str | Path, enabled: bool = True) -> ICodeGraph:
|
|
11
|
+
if not enabled:
|
|
12
|
+
return NullCodeGraph()
|
|
13
|
+
graph = CRGCodeGraph(repo_root=repo_root)
|
|
14
|
+
init = graph.initialize()
|
|
15
|
+
if not init.ok:
|
|
16
|
+
return NullCodeGraph()
|
|
17
|
+
# First-time sandbox: incremental init may succeed with 0 nodes — force full build.
|
|
18
|
+
stats = graph.query("stats")
|
|
19
|
+
node_count = (stats.payload or {}).get("node_count", 0)
|
|
20
|
+
if node_count == 0:
|
|
21
|
+
rebuild = graph.rebuild()
|
|
22
|
+
if not rebuild.ok:
|
|
23
|
+
return NullCodeGraph()
|
|
24
|
+
return graph
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(slots=True)
|
|
9
|
+
class GraphQueryResult:
|
|
10
|
+
ok: bool
|
|
11
|
+
source: str
|
|
12
|
+
summary: str
|
|
13
|
+
payload: dict[str, Any] = field(default_factory=dict)
|
|
14
|
+
error: str | None = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ICodeGraph(ABC):
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def initialize(self) -> GraphQueryResult:
|
|
20
|
+
raise NotImplementedError
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def refresh(self) -> GraphQueryResult:
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def rebuild(self) -> GraphQueryResult:
|
|
28
|
+
raise NotImplementedError
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def search(self, query: str, *, kind: str | None = None, limit: int = 20) -> GraphQueryResult:
|
|
32
|
+
raise NotImplementedError
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def shortest_path(self, query: str, *, depth: int = 3, mode: str = "bfs") -> GraphQueryResult:
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def get_neighbors(self, target: str, *, relation: str = "callees_of") -> GraphQueryResult:
|
|
40
|
+
raise NotImplementedError
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def get_component(self, name: str) -> GraphQueryResult:
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def get_file(self, file_path: str) -> GraphQueryResult:
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def get_route(self, changed_files: list[str] | None = None, *, max_depth: int = 2) -> GraphQueryResult:
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def query(self, query_type: str, **kwargs: Any) -> GraphQueryResult:
|
|
56
|
+
raise NotImplementedError
|
|
57
|
+
|
|
58
|
+
# Future-facing methods (designed now, can remain no-op in implementations)
|
|
59
|
+
def find_navigation_hint(self, goal: str, **kwargs: Any) -> GraphQueryResult:
|
|
60
|
+
return self.query("find_navigation_hint", goal=goal, **kwargs)
|
|
61
|
+
|
|
62
|
+
def find_relevant_components(self, intent: str, **kwargs: Any) -> GraphQueryResult:
|
|
63
|
+
return self.query("find_relevant_components", intent=intent, **kwargs)
|
|
64
|
+
|
|
65
|
+
def find_likely_route(self, intent: str, **kwargs: Any) -> GraphQueryResult:
|
|
66
|
+
return self.query("find_likely_route", intent=intent, **kwargs)
|
|
67
|
+
|
|
68
|
+
def find_related_files(self, symbol: str, **kwargs: Any) -> GraphQueryResult:
|
|
69
|
+
return self.query("find_related_files", symbol=symbol, **kwargs)
|
|
70
|
+
|
|
71
|
+
def find_button_candidates(self, label_hint: str, **kwargs: Any) -> GraphQueryResult:
|
|
72
|
+
return self.query("find_button_candidates", label_hint=label_hint, **kwargs)
|
|
73
|
+
|
|
74
|
+
def find_component_hierarchy(self, component: str, **kwargs: Any) -> GraphQueryResult:
|
|
75
|
+
return self.query("find_component_hierarchy", component=component, **kwargs)
|
|
76
|
+
|
|
77
|
+
def find_entry_point(self, feature: str, **kwargs: Any) -> GraphQueryResult:
|
|
78
|
+
return self.query("find_entry_point", feature=feature, **kwargs)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .interface import GraphQueryResult, ICodeGraph
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NullCodeGraph(ICodeGraph):
|
|
9
|
+
"""Safe fallback when CRG is unavailable."""
|
|
10
|
+
|
|
11
|
+
def _disabled(self, action: str) -> GraphQueryResult:
|
|
12
|
+
return GraphQueryResult(
|
|
13
|
+
ok=False,
|
|
14
|
+
source="null",
|
|
15
|
+
summary=f"Code graph unavailable: {action} skipped.",
|
|
16
|
+
error="code_graph_unavailable",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def initialize(self) -> GraphQueryResult:
|
|
20
|
+
return self._disabled("initialize")
|
|
21
|
+
|
|
22
|
+
def refresh(self) -> GraphQueryResult:
|
|
23
|
+
return self._disabled("refresh")
|
|
24
|
+
|
|
25
|
+
def rebuild(self) -> GraphQueryResult:
|
|
26
|
+
return self._disabled("rebuild")
|
|
27
|
+
|
|
28
|
+
def search(self, query: str, *, kind: str | None = None, limit: int = 20) -> GraphQueryResult:
|
|
29
|
+
return self._disabled("search")
|
|
30
|
+
|
|
31
|
+
def shortest_path(self, query: str, *, depth: int = 3, mode: str = "bfs") -> GraphQueryResult:
|
|
32
|
+
return self._disabled("shortest_path")
|
|
33
|
+
|
|
34
|
+
def get_neighbors(self, target: str, *, relation: str = "callees_of") -> GraphQueryResult:
|
|
35
|
+
return self._disabled("get_neighbors")
|
|
36
|
+
|
|
37
|
+
def get_component(self, name: str) -> GraphQueryResult:
|
|
38
|
+
return self._disabled("get_component")
|
|
39
|
+
|
|
40
|
+
def get_file(self, file_path: str) -> GraphQueryResult:
|
|
41
|
+
return self._disabled("get_file")
|
|
42
|
+
|
|
43
|
+
def get_route(self, changed_files: list[str] | None = None, *, max_depth: int = 2) -> GraphQueryResult:
|
|
44
|
+
return self._disabled("get_route")
|
|
45
|
+
|
|
46
|
+
def query(self, query_type: str, **kwargs: Any) -> GraphQueryResult:
|
|
47
|
+
return self._disabled(query_type)
|
navigation/mcp/diff.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Compare two observation snapshots by scan_id."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _blocking_issues(obs: dict[str, Any]) -> list[str]:
|
|
8
|
+
di = obs.get("dev_insights") or {}
|
|
9
|
+
summary = di.get("summary") or {}
|
|
10
|
+
return list(summary.get("blocking_issues") or [])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def diff_observations(before: dict[str, Any], after: dict[str, Any]) -> dict[str, Any]:
|
|
14
|
+
b_block = set(_blocking_issues(before))
|
|
15
|
+
a_block = set(_blocking_issues(after))
|
|
16
|
+
b_dom = str(before.get("dom_text") or "")
|
|
17
|
+
a_dom = str(after.get("dom_text") or "")
|
|
18
|
+
excerpt_len = 500
|
|
19
|
+
return {
|
|
20
|
+
"url_before": before.get("url") or "",
|
|
21
|
+
"url_after": after.get("url") or "",
|
|
22
|
+
"url_changed": (before.get("url") or "") != (after.get("url") or ""),
|
|
23
|
+
"dom_text_changed": b_dom != a_dom,
|
|
24
|
+
"new_blocking_issues": sorted(a_block - b_block),
|
|
25
|
+
"removed_blocking_issues": sorted(b_block - a_block),
|
|
26
|
+
"dom_excerpt_before": b_dom[:excerpt_len],
|
|
27
|
+
"dom_excerpt_after": a_dom[:excerpt_len],
|
|
28
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Standard MCP tool response envelope (contract v1.0)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
CONTRACT_VERSION = "1.0"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def make_envelope(
|
|
11
|
+
tool: str,
|
|
12
|
+
*,
|
|
13
|
+
ok: bool = True,
|
|
14
|
+
session_id: str | None = None,
|
|
15
|
+
run_id: str | None = None,
|
|
16
|
+
scan_id: str | None = None,
|
|
17
|
+
url: str = "",
|
|
18
|
+
error: str | None = None,
|
|
19
|
+
degraded: list[str] | None = None,
|
|
20
|
+
data: dict[str, Any] | None = None,
|
|
21
|
+
) -> dict[str, Any]:
|
|
22
|
+
return {
|
|
23
|
+
"contract_version": CONTRACT_VERSION,
|
|
24
|
+
"tool": tool,
|
|
25
|
+
"ok": ok,
|
|
26
|
+
"session_id": session_id,
|
|
27
|
+
"run_id": run_id,
|
|
28
|
+
"scan_id": scan_id,
|
|
29
|
+
"url": url,
|
|
30
|
+
"error": error,
|
|
31
|
+
"degraded": list(degraded or []),
|
|
32
|
+
"data": data or {},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def envelope_json(**kwargs: Any) -> str:
|
|
37
|
+
return json.dumps(make_envelope(**kwargs), indent=2, default=str)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def agent_summary_from_observation(obs_dict: dict[str, Any]) -> dict[str, Any]:
|
|
41
|
+
"""Compact summary for host agent reasoning (no planning hints)."""
|
|
42
|
+
di = obs_dict.get("dev_insights") or {}
|
|
43
|
+
summary = di.get("summary") or {}
|
|
44
|
+
page_meta = di.get("page_meta")
|
|
45
|
+
return {
|
|
46
|
+
"blocking": summary.get("blocking_issues") or [],
|
|
47
|
+
"advisory": summary.get("advisory_issues") or [],
|
|
48
|
+
"page_meta": page_meta,
|
|
49
|
+
"degraded": list(obs_dict.get("degraded") or []) + list(di.get("degraded") or []),
|
|
50
|
+
}
|