dev-guru 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.
- dev_guru/__init__.py +21 -0
- dev_guru/__main__.py +3 -0
- dev_guru/cli.py +149 -0
- dev_guru/client/__init__.py +22 -0
- dev_guru/client/inprocess.py +122 -0
- dev_guru/client/negotiation.py +69 -0
- dev_guru/client/port.py +87 -0
- dev_guru/errors.py +44 -0
- dev_guru/reports/__init__.py +1 -0
- dev_guru/reports/assets/explorer.css +218 -0
- dev_guru/reports/assets/explorer.html +75 -0
- dev_guru/reports/assets/explorer.js +822 -0
- dev_guru/reports/model.py +264 -0
- dev_guru/reports/render.py +137 -0
- dev_guru/sdk.py +56 -0
- dev_guru/service.py +53 -0
- dev_guru/version.py +3 -0
- dev_guru-0.1.0.dist-info/METADATA +113 -0
- dev_guru-0.1.0.dist-info/RECORD +22 -0
- dev_guru-0.1.0.dist-info/WHEEL +4 -0
- dev_guru-0.1.0.dist-info/entry_points.txt +2 -0
- dev_guru-0.1.0.dist-info/licenses/LICENSE +201 -0
dev_guru/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Dev Guru: a local-first developer product on the Ananke Plexus harness."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from dev_guru.version import __version__
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from dev_guru.sdk import GuruClient
|
|
11
|
+
|
|
12
|
+
__all__ = ["GuruClient", "__version__"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def __getattr__(name: str) -> Any:
|
|
16
|
+
# Lazy, so `import dev_guru` stays cheap and submodules never import the package's SDK back (no cycle).
|
|
17
|
+
if name == "GuruClient":
|
|
18
|
+
from dev_guru.sdk import GuruClient
|
|
19
|
+
|
|
20
|
+
return GuruClient
|
|
21
|
+
raise AttributeError(f"module 'dev_guru' has no attribute {name!r}")
|
dev_guru/__main__.py
ADDED
dev_guru/cli.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""The ``dev`` command. CLI and SDK share one service layer (dev_guru.service)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import platform
|
|
7
|
+
import webbrowser
|
|
8
|
+
from importlib import metadata
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Annotated
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
|
|
15
|
+
from dev_guru import errors, service
|
|
16
|
+
from dev_guru.client import TRANSPORTS, connect
|
|
17
|
+
from dev_guru.version import __version__
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(
|
|
20
|
+
name="dev",
|
|
21
|
+
help="Dev Guru: understand, improve and verify your software, on the Ananke Plexus harness.",
|
|
22
|
+
no_args_is_help=True,
|
|
23
|
+
add_completion=False,
|
|
24
|
+
)
|
|
25
|
+
console = Console()
|
|
26
|
+
err = Console(stderr=True)
|
|
27
|
+
TransportOpt = Annotated[str, typer.Option(help=f"Harness transport: {', '.join(TRANSPORTS)}")]
|
|
28
|
+
JsonOpt = Annotated[bool, typer.Option("--json", help="Machine-readable output")]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _version(value: bool) -> None:
|
|
32
|
+
if value:
|
|
33
|
+
console.print(f"dev-guru {__version__}")
|
|
34
|
+
raise typer.Exit()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.callback()
|
|
38
|
+
def main(
|
|
39
|
+
version: Annotated[
|
|
40
|
+
bool,
|
|
41
|
+
typer.Option(
|
|
42
|
+
"--version", callback=_version, is_eager=True, help="Show the version and exit"
|
|
43
|
+
),
|
|
44
|
+
] = False,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Dev Guru: understand, improve and verify your software, on the Ananke Plexus harness."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _fail(exc: errors.GuruError, as_json: bool, code: int) -> None:
|
|
50
|
+
if as_json:
|
|
51
|
+
console.print_json(json.dumps({"ok": False, "error": exc.to_dict()}))
|
|
52
|
+
else:
|
|
53
|
+
err.print(f"[red]✗ {exc.code}: {exc.message}[/red]")
|
|
54
|
+
if exc.hint:
|
|
55
|
+
err.print(f" [dim]{exc.hint}[/dim]")
|
|
56
|
+
raise typer.Exit(code) from exc
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _optional(distribution: str) -> str | None:
|
|
60
|
+
try:
|
|
61
|
+
return metadata.version(distribution)
|
|
62
|
+
except metadata.PackageNotFoundError:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@app.command()
|
|
67
|
+
def doctor(transport: TransportOpt = "in-process", as_json: JsonOpt = False) -> None:
|
|
68
|
+
"""Check the Ananke harness: installed version, supported range and the capabilities Dev Guru needs.
|
|
69
|
+
|
|
70
|
+
Exits 1 when the harness is missing or incompatible, 2 for an unknown transport.
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
info = connect(transport).info()
|
|
74
|
+
except errors.GuruError as exc:
|
|
75
|
+
_fail(exc, as_json, 2)
|
|
76
|
+
return
|
|
77
|
+
dex = _optional("dex-trove")
|
|
78
|
+
report = {
|
|
79
|
+
"ok": info.compatible,
|
|
80
|
+
"dev_guru": __version__,
|
|
81
|
+
"python": platform.python_version(),
|
|
82
|
+
"harness": info.model_dump(),
|
|
83
|
+
"optional": {"dex-trove": dex},
|
|
84
|
+
}
|
|
85
|
+
if as_json:
|
|
86
|
+
console.print_json(json.dumps(report))
|
|
87
|
+
else:
|
|
88
|
+
mark = "[green]✓[/green]" if info.compatible else "[red]✗[/red]"
|
|
89
|
+
console.print(
|
|
90
|
+
f"dev-guru {__version__} · Python {report['python']} · transport {info.transport}"
|
|
91
|
+
)
|
|
92
|
+
console.print(
|
|
93
|
+
f"{mark} ananke-plexus {info.ananke_version or 'not installed'} (supported {info.supported_range})"
|
|
94
|
+
)
|
|
95
|
+
for cap, present in info.capabilities.items():
|
|
96
|
+
console.print(f" {'✓' if present else '✗'} {cap}")
|
|
97
|
+
for reason in info.reasons:
|
|
98
|
+
console.print(f" [red]{reason}[/red]")
|
|
99
|
+
console.print(f" [dim]dex-trove: {dex or 'not installed (optional)'}[/dim]")
|
|
100
|
+
if not info.compatible:
|
|
101
|
+
raise typer.Exit(1)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@app.command()
|
|
105
|
+
def explore(
|
|
106
|
+
workspace: Annotated[Path, typer.Argument(help="Project to explore")] = Path("."),
|
|
107
|
+
output: Annotated[
|
|
108
|
+
Path, typer.Option("--output", "-o", help="Report file (a manifest is written next to it)")
|
|
109
|
+
] = Path("dev-guru-explorer.html"),
|
|
110
|
+
name: Annotated[str | None, typer.Option(help="Project name shown in the report")] = None,
|
|
111
|
+
open_browser: Annotated[
|
|
112
|
+
bool, typer.Option("--open", help="Open the report when it's written")
|
|
113
|
+
] = False,
|
|
114
|
+
transport: TransportOpt = "in-process",
|
|
115
|
+
as_json: JsonOpt = False,
|
|
116
|
+
) -> None:
|
|
117
|
+
"""Explore a codebase: Ananke builds the graph, Dev Guru writes an interactive, offline HTML explorer.
|
|
118
|
+
|
|
119
|
+
The report is one file with no network access: architecture constellation, dependency flow, impact rings,
|
|
120
|
+
call graph, hotspots, a guided tour, search and a source inspector.
|
|
121
|
+
"""
|
|
122
|
+
try:
|
|
123
|
+
result = service.explore(workspace, client=connect(transport), name=name)
|
|
124
|
+
page, sidecar = service.export_html(result, output)
|
|
125
|
+
except errors.GuruError as exc:
|
|
126
|
+
_fail(exc, as_json, 1)
|
|
127
|
+
return
|
|
128
|
+
project = result.model["project"]
|
|
129
|
+
summary = {
|
|
130
|
+
"ok": True,
|
|
131
|
+
"report": str(page),
|
|
132
|
+
"manifest": str(sidecar),
|
|
133
|
+
"input_digest": result.manifest["input_digest"],
|
|
134
|
+
"project": project,
|
|
135
|
+
"insights": {k: len(v) for k, v in result.model["insights"].items()},
|
|
136
|
+
}
|
|
137
|
+
if as_json:
|
|
138
|
+
console.print_json(json.dumps(summary))
|
|
139
|
+
else:
|
|
140
|
+
console.print(
|
|
141
|
+
f"[green]✓[/green] {project['files']} files · {project['symbols']} symbols · "
|
|
142
|
+
f"{project['imports']} imports · {project['calls']} calls → {page}"
|
|
143
|
+
)
|
|
144
|
+
cycles = len(result.model["insights"]["cycles"])
|
|
145
|
+
if cycles:
|
|
146
|
+
console.print(f" [yellow]{cycles} import cycle(s) found; see the Flow view[/yellow]")
|
|
147
|
+
console.print(f" [dim]manifest {sidecar} · {result.manifest['input_digest'][:19]}…[/dim]")
|
|
148
|
+
if open_browser:
|
|
149
|
+
webbrowser.open(page.resolve().as_uri())
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""The Ananke client port. Use :func:`connect` to get a client for one explicit transport."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dev_guru import errors
|
|
6
|
+
from dev_guru.client.inprocess import InProcessAnanke
|
|
7
|
+
from dev_guru.client.port import AnankeClient, GraphData, GraphSummary, HarnessInfo
|
|
8
|
+
|
|
9
|
+
TRANSPORTS = ("in-process",) # planned: "process" (isolated executable), "gateway" (local HTTP)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def connect(transport: str = "in-process") -> AnankeClient:
|
|
13
|
+
"""A client for exactly ``transport``. An unknown or unavailable transport is an error, not a fallback."""
|
|
14
|
+
if transport == "in-process":
|
|
15
|
+
return InProcessAnanke()
|
|
16
|
+
raise errors.GuruError(
|
|
17
|
+
errors.UNSUPPORTED_TRANSPORT,
|
|
18
|
+
f"transport {transport!r} is not supported; supported: {', '.join(TRANSPORTS)}",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
__all__ = ["TRANSPORTS", "AnankeClient", "GraphData", "GraphSummary", "HarnessInfo", "connect"]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""In-process transport: calls the installed ``ananke-plexus`` package through its public facade."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import Counter
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import dev_guru.client.negotiation as negotiation
|
|
9
|
+
from dev_guru import errors
|
|
10
|
+
from dev_guru.client.port import GraphData, GraphEdgeData, GraphNodeData, GraphSummary, HarnessInfo
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class InProcessAnanke:
|
|
14
|
+
transport = "in-process"
|
|
15
|
+
|
|
16
|
+
def info(self) -> HarnessInfo:
|
|
17
|
+
version = negotiation.installed_version()
|
|
18
|
+
if version is None:
|
|
19
|
+
return HarnessInfo(
|
|
20
|
+
transport=self.transport,
|
|
21
|
+
ananke_version=None,
|
|
22
|
+
supported_range=negotiation.SUPPORTED_ANANKE,
|
|
23
|
+
compatible=False,
|
|
24
|
+
reasons=["ananke-plexus is not installed in this environment"],
|
|
25
|
+
)
|
|
26
|
+
caps = negotiation.probe()
|
|
27
|
+
missing = [c for c in negotiation.REQUIRED if not caps.get(c)]
|
|
28
|
+
reasons: list[str] = []
|
|
29
|
+
if not negotiation.supported(version):
|
|
30
|
+
reasons.append(
|
|
31
|
+
f"ananke-plexus {version} is outside the supported range {negotiation.SUPPORTED_ANANKE}"
|
|
32
|
+
)
|
|
33
|
+
if missing:
|
|
34
|
+
reasons.append(f"required harness capabilities are missing: {', '.join(missing)}")
|
|
35
|
+
return HarnessInfo(
|
|
36
|
+
transport=self.transport,
|
|
37
|
+
ananke_version=version,
|
|
38
|
+
supported_range=negotiation.SUPPORTED_ANANKE,
|
|
39
|
+
compatible=not reasons,
|
|
40
|
+
capabilities=caps,
|
|
41
|
+
missing_required=missing,
|
|
42
|
+
reasons=reasons,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def require_compatible(self) -> HarnessInfo:
|
|
46
|
+
info = self.info()
|
|
47
|
+
if info.ananke_version is None:
|
|
48
|
+
raise errors.GuruError(
|
|
49
|
+
errors.HARNESS_MISSING,
|
|
50
|
+
"Ananke Plexus is not installed",
|
|
51
|
+
hint="pip install 'ananke-plexus" + negotiation.SUPPORTED_ANANKE + "'",
|
|
52
|
+
)
|
|
53
|
+
if info.missing_required:
|
|
54
|
+
raise errors.GuruError(errors.CAPABILITY_MISSING, "; ".join(info.reasons))
|
|
55
|
+
if not info.compatible:
|
|
56
|
+
raise errors.GuruError(
|
|
57
|
+
errors.HARNESS_INCOMPATIBLE,
|
|
58
|
+
"; ".join(info.reasons),
|
|
59
|
+
hint=f"install ananke-plexus{negotiation.SUPPORTED_ANANKE}",
|
|
60
|
+
)
|
|
61
|
+
return info
|
|
62
|
+
|
|
63
|
+
def graph_data(self, workspace: Path) -> GraphData:
|
|
64
|
+
"""Ask Ananke to build the workspace graph and return the snapshot it stored."""
|
|
65
|
+
info = self.require_compatible()
|
|
66
|
+
root = workspace.expanduser().resolve()
|
|
67
|
+
if not root.is_dir():
|
|
68
|
+
raise errors.GuruError(errors.WORKSPACE_INVALID, f"workspace {root} is not a directory")
|
|
69
|
+
from ananke.plexus.api import Ananke
|
|
70
|
+
from ananke.plexus.graph.service import load_graph
|
|
71
|
+
|
|
72
|
+
result = Ananke.open(root).graph_build()
|
|
73
|
+
if not result.ok:
|
|
74
|
+
raise errors.GuruError(
|
|
75
|
+
errors.HARNESS_FAILED, f"Ananke could not build the graph: {result.summary}"
|
|
76
|
+
)
|
|
77
|
+
snapshot = load_graph(root)
|
|
78
|
+
if (
|
|
79
|
+
snapshot is None
|
|
80
|
+
): # a "successful" build with nothing stored is a harness fault, not an empty graph
|
|
81
|
+
raise errors.GuruError(
|
|
82
|
+
errors.HARNESS_FAILED, "Ananke reported a graph build but stored no snapshot"
|
|
83
|
+
)
|
|
84
|
+
return GraphData(
|
|
85
|
+
snapshot_id=snapshot.snapshot_id,
|
|
86
|
+
provider=snapshot.provider,
|
|
87
|
+
ananke_version=info.ananke_version,
|
|
88
|
+
nodes=[
|
|
89
|
+
GraphNodeData(
|
|
90
|
+
id=n.node_id, kind=n.kind, name=n.name, path=n.path, metadata=dict(n.metadata)
|
|
91
|
+
)
|
|
92
|
+
for n in snapshot.nodes
|
|
93
|
+
],
|
|
94
|
+
edges=[
|
|
95
|
+
GraphEdgeData(
|
|
96
|
+
source=e.source,
|
|
97
|
+
target=e.target,
|
|
98
|
+
kind=e.kind,
|
|
99
|
+
origin=e.origin,
|
|
100
|
+
confidence=e.confidence,
|
|
101
|
+
location=e.source_location,
|
|
102
|
+
)
|
|
103
|
+
for e in snapshot.edges
|
|
104
|
+
],
|
|
105
|
+
source=str(result.details.get("path", "")),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def graph_summary(self, workspace: Path) -> GraphSummary:
|
|
109
|
+
"""Ask Ananke to build the workspace graph, then summarise the snapshot it stored."""
|
|
110
|
+
data = self.graph_data(workspace)
|
|
111
|
+
return GraphSummary(
|
|
112
|
+
snapshot_id=data.snapshot_id,
|
|
113
|
+
provider=data.provider,
|
|
114
|
+
nodes=len(data.nodes),
|
|
115
|
+
edges=len(data.edges),
|
|
116
|
+
nodes_by_kind=dict(sorted(Counter(n.kind for n in data.nodes).items())),
|
|
117
|
+
edges_by_kind=dict(sorted(Counter(e.kind for e in data.edges).items())),
|
|
118
|
+
source=data.source,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
__all__ = ["InProcessAnanke"]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Which Ananke Plexus this Dev Guru supports, and what the installed one can do.
|
|
2
|
+
|
|
3
|
+
Negotiation is explicit: Dev Guru reads the installed harness version, checks it against ``SUPPORTED_ANANKE`` and
|
|
4
|
+
probes the specific interfaces it calls. An out-of-range or incomplete harness is an error with a stable code,
|
|
5
|
+
never a silent downgrade.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib
|
|
11
|
+
import re
|
|
12
|
+
from importlib import metadata
|
|
13
|
+
|
|
14
|
+
SUPPORTED_ANANKE = ">=0.3.12,<0.4" # keep in step with pyproject.toml (tests/test_packaging.py)
|
|
15
|
+
_MIN = (0, 3, 12)
|
|
16
|
+
_BELOW = (0, 4, 0)
|
|
17
|
+
|
|
18
|
+
# Capability id -> (module, attribute path). Required ones are what this release of Dev Guru calls.
|
|
19
|
+
CAPABILITIES: dict[str, tuple[str, str]] = {
|
|
20
|
+
"graph.build": ("ananke.plexus.api", "Ananke.graph_build"),
|
|
21
|
+
"graph.load": ("ananke.plexus.graph.service", "load_graph"),
|
|
22
|
+
"graph.impact": ("ananke.plexus.api", "Ananke.graph_impact"),
|
|
23
|
+
"run.status": ("ananke.plexus.api", "Ananke.run_status"),
|
|
24
|
+
"spec.create": ("ananke.plexus.api", "Ananke.create_spec"),
|
|
25
|
+
"registry.open": ("ananke.plexus.registry.registry", "Registry.open"),
|
|
26
|
+
}
|
|
27
|
+
REQUIRED = ("graph.build", "graph.load")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def parse_version(text: str) -> tuple[int, int, int] | None:
|
|
31
|
+
m = re.match(r"^(\d+)\.(\d+)\.(\d+)", text.strip())
|
|
32
|
+
return (int(m.group(1)), int(m.group(2)), int(m.group(3))) if m else None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def supported(version: str) -> bool:
|
|
36
|
+
parsed = parse_version(version)
|
|
37
|
+
return parsed is not None and _MIN <= parsed < _BELOW
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def installed_version(distribution: str = "ananke-plexus") -> str | None:
|
|
41
|
+
try:
|
|
42
|
+
return metadata.version(distribution)
|
|
43
|
+
except metadata.PackageNotFoundError:
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def probe(capabilities: dict[str, tuple[str, str]] = CAPABILITIES) -> dict[str, bool]:
|
|
48
|
+
"""Which capabilities the importable harness exposes (imports only; nothing runs)."""
|
|
49
|
+
found: dict[str, bool] = {}
|
|
50
|
+
for cap, (module, attr_path) in capabilities.items():
|
|
51
|
+
try:
|
|
52
|
+
obj: object = importlib.import_module(module)
|
|
53
|
+
for part in attr_path.split("."):
|
|
54
|
+
obj = getattr(obj, part)
|
|
55
|
+
found[cap] = callable(obj)
|
|
56
|
+
except (ImportError, AttributeError):
|
|
57
|
+
found[cap] = False
|
|
58
|
+
return found
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
"CAPABILITIES",
|
|
63
|
+
"REQUIRED",
|
|
64
|
+
"SUPPORTED_ANANKE",
|
|
65
|
+
"installed_version",
|
|
66
|
+
"parse_version",
|
|
67
|
+
"probe",
|
|
68
|
+
"supported",
|
|
69
|
+
]
|
dev_guru/client/port.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""The one port between Dev Guru and the Ananke harness.
|
|
2
|
+
|
|
3
|
+
Dev Guru never analyses code itself: every domain operation goes through an :class:`AnankeClient`. Today there is
|
|
4
|
+
one transport (``in-process``: the installed ``ananke-plexus`` package). An isolated executable and a local HTTP
|
|
5
|
+
gateway are planned. Exactly one transport is chosen per run and recorded; a failing transport is reported, never
|
|
6
|
+
silently swapped for another (docs/decisions/0001-ananke-client-port.md).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Protocol, runtime_checkable
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HarnessInfo(BaseModel):
|
|
18
|
+
"""What negotiation found: the transport, harness version, support verdict and capabilities."""
|
|
19
|
+
|
|
20
|
+
transport: str
|
|
21
|
+
ananke_version: str | None
|
|
22
|
+
supported_range: str
|
|
23
|
+
compatible: bool
|
|
24
|
+
capabilities: dict[str, bool] = Field(default_factory=dict)
|
|
25
|
+
missing_required: list[str] = Field(default_factory=list)
|
|
26
|
+
reasons: list[str] = Field(default_factory=list)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GraphSummary(BaseModel):
|
|
30
|
+
"""A bounded projection of an Ananke graph snapshot. Ananke keeps the snapshot itself."""
|
|
31
|
+
|
|
32
|
+
snapshot_id: str
|
|
33
|
+
provider: str
|
|
34
|
+
nodes: int
|
|
35
|
+
edges: int
|
|
36
|
+
nodes_by_kind: dict[str, int] = Field(default_factory=dict)
|
|
37
|
+
edges_by_kind: dict[str, int] = Field(default_factory=dict)
|
|
38
|
+
source: str # where Ananke stored the snapshot (Ananke-owned workspace state)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class GraphNodeData(BaseModel):
|
|
42
|
+
id: str
|
|
43
|
+
kind: str
|
|
44
|
+
name: str
|
|
45
|
+
path: str = ""
|
|
46
|
+
metadata: dict[str, object] = Field(default_factory=dict)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class GraphEdgeData(BaseModel):
|
|
50
|
+
source: str
|
|
51
|
+
target: str
|
|
52
|
+
kind: str
|
|
53
|
+
origin: str = "extracted"
|
|
54
|
+
confidence: float = 1.0
|
|
55
|
+
location: str = ""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class GraphData(BaseModel):
|
|
59
|
+
"""The graph Ananke built, as plain data. Dev Guru projects it; it never adds analysis of its own."""
|
|
60
|
+
|
|
61
|
+
snapshot_id: str
|
|
62
|
+
provider: str
|
|
63
|
+
ananke_version: str | None
|
|
64
|
+
nodes: list[GraphNodeData] = Field(default_factory=list)
|
|
65
|
+
edges: list[GraphEdgeData] = Field(default_factory=list)
|
|
66
|
+
source: str = ""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@runtime_checkable
|
|
70
|
+
class AnankeClient(Protocol):
|
|
71
|
+
transport: str
|
|
72
|
+
|
|
73
|
+
def info(self) -> HarnessInfo: ...
|
|
74
|
+
|
|
75
|
+
def graph_summary(self, workspace: Path) -> GraphSummary: ...
|
|
76
|
+
|
|
77
|
+
def graph_data(self, workspace: Path) -> GraphData: ...
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
__all__ = [
|
|
81
|
+
"AnankeClient",
|
|
82
|
+
"GraphData",
|
|
83
|
+
"GraphEdgeData",
|
|
84
|
+
"GraphNodeData",
|
|
85
|
+
"GraphSummary",
|
|
86
|
+
"HarnessInfo",
|
|
87
|
+
]
|
dev_guru/errors.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Errors with stable codes, so the CLI, the SDK and later the web UI report failures the same way."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
# Stable codes: callers and tests branch on these, never on message text.
|
|
6
|
+
HARNESS_MISSING = "HARNESS_MISSING"
|
|
7
|
+
HARNESS_INCOMPATIBLE = "HARNESS_INCOMPATIBLE"
|
|
8
|
+
CAPABILITY_MISSING = "CAPABILITY_MISSING"
|
|
9
|
+
UNSUPPORTED_TRANSPORT = "UNSUPPORTED_TRANSPORT"
|
|
10
|
+
WORKSPACE_INVALID = "WORKSPACE_INVALID"
|
|
11
|
+
HARNESS_FAILED = "HARNESS_FAILED"
|
|
12
|
+
CODES = frozenset(
|
|
13
|
+
{
|
|
14
|
+
HARNESS_MISSING,
|
|
15
|
+
HARNESS_INCOMPATIBLE,
|
|
16
|
+
CAPABILITY_MISSING,
|
|
17
|
+
UNSUPPORTED_TRANSPORT,
|
|
18
|
+
WORKSPACE_INVALID,
|
|
19
|
+
HARNESS_FAILED,
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GuruError(Exception):
|
|
25
|
+
"""A failure Dev Guru can explain: a stable ``code``, a message, an optional hint and run id."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self, code: str, message: str, *, hint: str = "", run_id: str | None = None
|
|
29
|
+
) -> None:
|
|
30
|
+
if code not in CODES:
|
|
31
|
+
raise ValueError(f"unknown error code {code!r}")
|
|
32
|
+
super().__init__(message)
|
|
33
|
+
self.code = code
|
|
34
|
+
self.message = message
|
|
35
|
+
self.hint = hint
|
|
36
|
+
self.run_id = run_id
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> dict[str, str | None]:
|
|
39
|
+
return {
|
|
40
|
+
"code": self.code,
|
|
41
|
+
"message": self.message,
|
|
42
|
+
"hint": self.hint,
|
|
43
|
+
"run_id": self.run_id,
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Report view models and renderers. Presentation only: the analysis comes from Ananke."""
|