dev-guru 0.1.0__tar.gz → 0.2.0__tar.gz

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 (40) hide show
  1. dev_guru-0.2.0/CHANGELOG.md +93 -0
  2. {dev_guru-0.1.0 → dev_guru-0.2.0}/PKG-INFO +20 -6
  3. {dev_guru-0.1.0 → dev_guru-0.2.0}/README.md +17 -4
  4. {dev_guru-0.1.0 → dev_guru-0.2.0}/pyproject.toml +2 -1
  5. dev_guru-0.2.0/src/dev_guru/cli.py +347 -0
  6. dev_guru-0.2.0/src/dev_guru/client/inprocess.py +277 -0
  7. {dev_guru-0.1.0 → dev_guru-0.2.0}/src/dev_guru/client/negotiation.py +7 -3
  8. {dev_guru-0.1.0 → dev_guru-0.2.0}/src/dev_guru/client/port.py +53 -1
  9. dev_guru-0.2.0/src/dev_guru/diagram/__init__.py +308 -0
  10. {dev_guru-0.1.0 → dev_guru-0.2.0}/src/dev_guru/errors.py +2 -0
  11. dev_guru-0.2.0/src/dev_guru/present/__init__.py +333 -0
  12. {dev_guru-0.1.0 → dev_guru-0.2.0}/src/dev_guru/reports/assets/explorer.html +1 -0
  13. {dev_guru-0.1.0 → dev_guru-0.2.0}/src/dev_guru/reports/assets/explorer.js +315 -93
  14. dev_guru-0.2.0/src/dev_guru/reports/pages.py +89 -0
  15. dev_guru-0.2.0/src/dev_guru/reports/profiles.py +49 -0
  16. {dev_guru-0.1.0 → dev_guru-0.2.0}/src/dev_guru/sdk.py +1 -1
  17. dev_guru-0.2.0/src/dev_guru/service.py +88 -0
  18. {dev_guru-0.1.0 → dev_guru-0.2.0}/src/dev_guru/version.py +1 -1
  19. {dev_guru-0.1.0 → dev_guru-0.2.0}/tests/test_client.py +15 -5
  20. dev_guru-0.2.0/tests/test_diagram.py +125 -0
  21. dev_guru-0.2.0/tests/test_facade_features.py +109 -0
  22. {dev_guru-0.1.0 → dev_guru-0.2.0}/tests/test_negotiation.py +7 -2
  23. {dev_guru-0.1.0 → dev_guru-0.2.0}/tests/test_packaging.py +10 -1
  24. dev_guru-0.2.0/tests/test_present.py +119 -0
  25. {dev_guru-0.1.0 → dev_guru-0.2.0}/tests/test_reports.py +36 -0
  26. dev_guru-0.1.0/CHANGELOG.md +0 -49
  27. dev_guru-0.1.0/src/dev_guru/cli.py +0 -149
  28. dev_guru-0.1.0/src/dev_guru/client/inprocess.py +0 -122
  29. dev_guru-0.1.0/src/dev_guru/service.py +0 -53
  30. {dev_guru-0.1.0 → dev_guru-0.2.0}/.gitignore +0 -0
  31. {dev_guru-0.1.0 → dev_guru-0.2.0}/LICENSE +0 -0
  32. {dev_guru-0.1.0 → dev_guru-0.2.0}/src/dev_guru/__init__.py +0 -0
  33. {dev_guru-0.1.0 → dev_guru-0.2.0}/src/dev_guru/__main__.py +0 -0
  34. {dev_guru-0.1.0 → dev_guru-0.2.0}/src/dev_guru/client/__init__.py +0 -0
  35. {dev_guru-0.1.0 → dev_guru-0.2.0}/src/dev_guru/reports/__init__.py +0 -0
  36. {dev_guru-0.1.0 → dev_guru-0.2.0}/src/dev_guru/reports/assets/explorer.css +0 -0
  37. {dev_guru-0.1.0 → dev_guru-0.2.0}/src/dev_guru/reports/model.py +0 -0
  38. {dev_guru-0.1.0 → dev_guru-0.2.0}/src/dev_guru/reports/render.py +0 -0
  39. {dev_guru-0.1.0 → dev_guru-0.2.0}/tests/conftest.py +0 -0
  40. {dev_guru-0.1.0 → dev_guru-0.2.0}/tests/test_cli.py +0 -0
@@ -0,0 +1,93 @@
1
+ # Changelog
2
+
3
+ All notable changes to Dev Guru are documented here.
4
+
5
+ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) — [SemVer](https://semver.org/).
6
+
7
+ ---
8
+
9
+ ## [Unreleased]
10
+
11
+ ---
12
+
13
+ ## [0.2.0] — 2026-11-03
14
+
15
+ ### Added
16
+
17
+ - **Runs through the Ananke app facade** (`ananke-app/1`, needs Ananke 0.3.13):
18
+ - `dev explore` builds the graph in an idempotent Ananke run, records the run in the report manifest
19
+ (`ananke_run`), and registers the HTML back into that run as an artifact with its inputs;
20
+ - a failed build is a failed run, with its id kept as evidence.
21
+ - **`dev sources`**: source-neutral capability discovery (internal folders, private Git, troves) with an offline
22
+ picker page. Works with Dex switched off.
23
+ - **`dev learning review`**: an offline review page for an Ananke learning candidate, showing the gate status,
24
+ evaluation, review, provenance and a diff per destination.
25
+
26
+ - **`dev diagram`**: an animated SVG of the relationships a document states (arrow lines, Mermaid flowcharts, From/To
27
+ tables). Every edge cites its file and line, and an edited edge that fails its citation is drawn as a hypothesis.
28
+ Guide: [Diagrams from documents](docs/guides/diagrams.md).
29
+ - **`dev present`**: Markdown slides become one offline, accessible HTML deck:
30
+ - `lantern` and `paper` themes, responsive columns, step-by-step lists, speaker notes, keyboard navigation;
31
+ - bar charts as accessible SVG;
32
+ - embedded local images that require alt text; raw HTML is shown as text;
33
+ - `--check` verifies a deck. Guide: [Presentations](docs/guides/presentations.md).
34
+ - **Publication profiles** for `dev explore` (`--profile public`): removes source, docstrings, string defaults in
35
+ signatures and the Git revision, so a report is safe to publish. The manifest records the profile.
36
+
37
+ ---
38
+
39
+ ## [0.1.1] — 2026-11-02
40
+
41
+ ### Changed
42
+
43
+ - **The constellation is a 3D force-directed graph.** Imports are springs, files repel each other and packages pull
44
+ their files together. Drag to orbit, scroll to zoom, drag a file to pull the graph, double-click to fly to a file;
45
+ it turns slowly when idle. Shaded bodies, depth fog, a star field and pulses along lit imports, with labels that
46
+ never overlap. Drawn on a plain canvas inside the report: no WebGL, no library, no network. With reduced motion it
47
+ settles before the first frame and stays still.
48
+ - Docs: the client-port design is now a plain concept page, [How Dev Guru talks to Ananke](docs/concepts/ananke-client.md).
49
+ PyPI links on the README, the docs home and the site.
50
+
51
+ ### Fixed
52
+
53
+ - `.trivyignore` added, so the release workflow's Trivy gate can run.
54
+
55
+ ---
56
+
57
+ ## [0.1.0] — 2026-11-02
58
+
59
+ The first release.
60
+
61
+ ### Added
62
+
63
+ - **`dev explore`**: Ananke builds the code graph, and Dev Guru writes one interactive, offline HTML explorer plus a
64
+ manifest.
65
+ - Six views: constellation, flow (package or file level), impact rings, calls, hotspots and a narrated tour.
66
+ - Around them: search, a command palette, a source inspector, a light and dark theme, and URL-shareable state.
67
+ - Explicit downloads of the view state and of analysis requests.
68
+ - **Report safety.**
69
+ - Everything is inlined, and the page sets a CSP with `connect-src 'none'`.
70
+ - JSON is embedded with `<`, `>`, `&` and U+2028/U+2029 escaped; untrusted text is set only with
71
+ `textContent`.
72
+ - Templates are filled in a single pass, so source code containing the template's placeholders stays data.
73
+ - A manifest records the renderer, the Dev Guru and Ananke versions, the input digest and the git revision.
74
+ - **View model `dev-guru/explorer@1`**: packages, files, symbols with source snippets, resolved import, call and
75
+ inheritance edges, insights (most depended-on, largest, entry points, import cycles) and stated limitations.
76
+ Deterministic.
77
+ - **SDK** `GuruClient(workspace).explore()` / `.export()`, sharing one service layer with the CLI.
78
+ - **`dev doctor`**: version and capability negotiation with Ananke (`ananke-plexus>=0.3.12,<0.4`) over one explicit
79
+ transport, with stable error codes.
80
+ - **Docs site** with a live demo (Dev Guru exploring its own source) and the "Lantern" theme.
81
+ - **Release gates:**
82
+ - acceptance on the built wheel, including real-Chrome report journeys and a dogfood run on Dev Guru's own source;
83
+ - a distribution scan (gitleaks, pip-audit, GuardDog, Bandit);
84
+ - Trivy and build provenance.
85
+
86
+ ### Changed
87
+
88
+ - The command is **`dev`** (it was `devguru` in the unreleased scaffold).
89
+
90
+ ### Fixed
91
+
92
+ - Import cycles in Dev Guru itself, found by exploring it: `dev_guru` now exposes `GuruClient` lazily, and
93
+ `client.inprocess` imports `negotiation` directly.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dev-guru
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Dev Guru: understand, improve, verify and learn from your software. A local-first developer product on the Ananke Plexus harness: Python SDK and CLI first, offline HTML reports, later a local web UI.
5
5
  Project-URL: Homepage, https://github.com/Neo-Pragnya/Dev-Guru
6
6
  Project-URL: Repository, https://github.com/Neo-Pragnya/Dev-Guru
@@ -22,7 +22,8 @@ Classifier: Programming Language :: Python :: 3.14
22
22
  Classifier: Topic :: Software Development :: Quality Assurance
23
23
  Classifier: Typing :: Typed
24
24
  Requires-Python: >=3.11
25
- Requires-Dist: ananke-plexus<0.4,>=0.3.12
25
+ Requires-Dist: ananke-plexus<0.4,>=0.3.13
26
+ Requires-Dist: markdown-it-py<5,>=3
26
27
  Requires-Dist: pydantic<3,>=2.11
27
28
  Requires-Dist: rich>=13
28
29
  Requires-Dist: typer<1,>=0.16
@@ -42,10 +43,13 @@ Description-Content-Type: text/markdown
42
43
 
43
44
  # Dev Guru
44
45
 
46
+ [![PyPI](https://img.shields.io/pypi/v/dev-guru)](https://pypi.org/project/dev-guru/)
47
+ [![Python](https://img.shields.io/pypi/pyversions/dev-guru)](https://pypi.org/project/dev-guru/)
48
+
45
49
  **See your code. Understand it. Change it with confidence.**
46
50
 
47
51
  Point Dev Guru at a Python project and get **one offline HTML explorer**:
48
- - a living constellation of packages and files;
52
+ - a 3D constellation of files you can orbit, where imports pull and packages cluster;
49
53
  - the flow of dependencies;
50
54
  - the blast radius of a change;
51
55
  - who calls what, and where the hotspots are;
@@ -61,14 +65,14 @@ dev explore path/to/project -o explorer.html --open
61
65
 
62
66
  ![Dev Guru running dev doctor and dev explore](docs/assets/dev-explore.svg)
63
67
 
64
- **[Documentation and a live demo →](https://neo-pragnya.github.io/Dev-Guru/)**: the site's hero runs the real
68
+ **[Documentation and a live demo →](https://neo-pragnya.github.io/Dev-Guru/)** · **[Dev Guru on PyPI →](https://pypi.org/project/dev-guru/)**: the site's hero runs the real
65
69
  report of Dev Guru exploring its own source.
66
70
 
67
71
  ## What's in a report
68
72
 
69
73
  | View | Question it answers |
70
74
  | --- | --- |
71
- | ✦ Constellation | What's here? Files clustered by package; hover to light up imports |
75
+ | ✦ Constellation | What's here? A 3D force-directed graph: drag to orbit, hover to light up imports |
72
76
  | ⇶ Flow | What depends on what? Entry side to foundations; cycles glow rose |
73
77
  | ◎ Impact | What breaks if this changes? Blast-radius rings over importers and callers |
74
78
  | ƒ Calls | Who calls in, and what goes out? A symbol-level ego network |
@@ -79,6 +83,16 @@ Plus search (`/`), a command palette (`Ctrl`/`⌘` `K`), a source inspector, a l
79
83
  URLs. Reports are **offline and view-only**, treat source as text, and carry a manifest with the input digest and the
80
84
  Git revision. See [report safety](docs/concepts/report-safety.md).
81
85
 
86
+ ## More than the explorer
87
+
88
+ ```bash
89
+ dev present talk.md # Markdown slides → one offline, accessible HTML deck
90
+ dev diagram docs/arch.md # an animated SVG of what a document states, every edge cited
91
+ dev explore . --profile public # a report safe to publish: no source, docstrings or Git revision
92
+ dev sources # capabilities from your folders, private Git and Dex Trove
93
+ dev learning review NAME # review what Ananke learned before it becomes reusable
94
+ ```
95
+
82
96
  ## From Python
83
97
 
84
98
  ```python
@@ -98,7 +112,7 @@ with GuruClient(workspace="path/to/project") as guru:
98
112
  | [Dex Trove](https://github.com/Neo-Pragnya/Dex-Trove) | Optional capability memory: curated skills, agents, instructions and workflows |
99
113
 
100
114
  Dev Guru never analyses code itself: every domain operation goes through one client port to Ananke, and Dex is never
101
- required. See [Architecture](docs/concepts/architecture.md) and [ADR-0001](docs/decisions/0001-ananke-client-port.md).
115
+ required. See [Architecture](docs/concepts/architecture.md) and [How Dev Guru talks to Ananke](docs/concepts/ananke-client.md).
102
116
 
103
117
  ## Quality gates
104
118
 
@@ -1,9 +1,12 @@
1
1
  # Dev Guru
2
2
 
3
+ [![PyPI](https://img.shields.io/pypi/v/dev-guru)](https://pypi.org/project/dev-guru/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/dev-guru)](https://pypi.org/project/dev-guru/)
5
+
3
6
  **See your code. Understand it. Change it with confidence.**
4
7
 
5
8
  Point Dev Guru at a Python project and get **one offline HTML explorer**:
6
- - a living constellation of packages and files;
9
+ - a 3D constellation of files you can orbit, where imports pull and packages cluster;
7
10
  - the flow of dependencies;
8
11
  - the blast radius of a change;
9
12
  - who calls what, and where the hotspots are;
@@ -19,14 +22,14 @@ dev explore path/to/project -o explorer.html --open
19
22
 
20
23
  ![Dev Guru running dev doctor and dev explore](docs/assets/dev-explore.svg)
21
24
 
22
- **[Documentation and a live demo →](https://neo-pragnya.github.io/Dev-Guru/)**: the site's hero runs the real
25
+ **[Documentation and a live demo →](https://neo-pragnya.github.io/Dev-Guru/)** · **[Dev Guru on PyPI →](https://pypi.org/project/dev-guru/)**: the site's hero runs the real
23
26
  report of Dev Guru exploring its own source.
24
27
 
25
28
  ## What's in a report
26
29
 
27
30
  | View | Question it answers |
28
31
  | --- | --- |
29
- | ✦ Constellation | What's here? Files clustered by package; hover to light up imports |
32
+ | ✦ Constellation | What's here? A 3D force-directed graph: drag to orbit, hover to light up imports |
30
33
  | ⇶ Flow | What depends on what? Entry side to foundations; cycles glow rose |
31
34
  | ◎ Impact | What breaks if this changes? Blast-radius rings over importers and callers |
32
35
  | ƒ Calls | Who calls in, and what goes out? A symbol-level ego network |
@@ -37,6 +40,16 @@ Plus search (`/`), a command palette (`Ctrl`/`⌘` `K`), a source inspector, a l
37
40
  URLs. Reports are **offline and view-only**, treat source as text, and carry a manifest with the input digest and the
38
41
  Git revision. See [report safety](docs/concepts/report-safety.md).
39
42
 
43
+ ## More than the explorer
44
+
45
+ ```bash
46
+ dev present talk.md # Markdown slides → one offline, accessible HTML deck
47
+ dev diagram docs/arch.md # an animated SVG of what a document states, every edge cited
48
+ dev explore . --profile public # a report safe to publish: no source, docstrings or Git revision
49
+ dev sources # capabilities from your folders, private Git and Dex Trove
50
+ dev learning review NAME # review what Ananke learned before it becomes reusable
51
+ ```
52
+
40
53
  ## From Python
41
54
 
42
55
  ```python
@@ -56,7 +69,7 @@ with GuruClient(workspace="path/to/project") as guru:
56
69
  | [Dex Trove](https://github.com/Neo-Pragnya/Dex-Trove) | Optional capability memory: curated skills, agents, instructions and workflows |
57
70
 
58
71
  Dev Guru never analyses code itself: every domain operation goes through one client port to Ananke, and Dex is never
59
- required. See [Architecture](docs/concepts/architecture.md) and [ADR-0001](docs/decisions/0001-ananke-client-port.md).
72
+ required. See [Architecture](docs/concepts/architecture.md) and [How Dev Guru talks to Ananke](docs/concepts/ananke-client.md).
60
73
 
61
74
  ## Quality gates
62
75
 
@@ -28,7 +28,8 @@ classifiers = [
28
28
  # Dev Guru is a client of Ananke: the harness owns analysis, execution and evidence (docs/architecture.md).
29
29
  # Keep this range in step with SUPPORTED_ANANKE in src/dev_guru/client/negotiation.py (tests/test_packaging.py).
30
30
  dependencies = [
31
- "ananke-plexus>=0.3.12,<0.4",
31
+ "ananke-plexus>=0.3.13,<0.4",
32
+ "markdown-it-py>=3,<5",
32
33
  "pydantic>=2.11,<3",
33
34
  "rich>=13",
34
35
  "typer>=0.16,<1",
@@ -0,0 +1,347 @@
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
+ from rich.markup import escape
15
+
16
+ from dev_guru import errors, service
17
+ from dev_guru.client import TRANSPORTS, connect
18
+ from dev_guru.version import __version__
19
+
20
+ app = typer.Typer(
21
+ name="dev",
22
+ help="Dev Guru: understand, improve and verify your software, on the Ananke Plexus harness.",
23
+ no_args_is_help=True,
24
+ add_completion=False,
25
+ )
26
+ console = Console()
27
+ err = Console(stderr=True)
28
+ TransportOpt = Annotated[str, typer.Option(help=f"Harness transport: {', '.join(TRANSPORTS)}")]
29
+ JsonOpt = Annotated[bool, typer.Option("--json", help="Machine-readable output")]
30
+
31
+
32
+ def _version(value: bool) -> None:
33
+ if value:
34
+ console.print(f"dev-guru {__version__}")
35
+ raise typer.Exit()
36
+
37
+
38
+ @app.callback()
39
+ def main(
40
+ version: Annotated[
41
+ bool,
42
+ typer.Option(
43
+ "--version", callback=_version, is_eager=True, help="Show the version and exit"
44
+ ),
45
+ ] = False,
46
+ ) -> None:
47
+ """Dev Guru: understand, improve and verify your software, on the Ananke Plexus harness."""
48
+
49
+
50
+ def _fail(exc: errors.GuruError, as_json: bool, code: int) -> None:
51
+ if as_json:
52
+ console.print_json(json.dumps({"ok": False, "error": exc.to_dict()}))
53
+ else:
54
+ err.print(f"[red]✗ {exc.code}: {exc.message}[/red]")
55
+ if exc.hint:
56
+ err.print(f" [dim]{exc.hint}[/dim]")
57
+ raise typer.Exit(code) from exc
58
+
59
+
60
+ def _optional(distribution: str) -> str | None:
61
+ try:
62
+ return metadata.version(distribution)
63
+ except metadata.PackageNotFoundError:
64
+ return None
65
+
66
+
67
+ @app.command()
68
+ def doctor(transport: TransportOpt = "in-process", as_json: JsonOpt = False) -> None:
69
+ """Check the Ananke harness: installed version, supported range and the capabilities Dev Guru needs.
70
+
71
+ Exits 1 when the harness is missing or incompatible, 2 for an unknown transport.
72
+ """
73
+ try:
74
+ info = connect(transport).info()
75
+ except errors.GuruError as exc:
76
+ _fail(exc, as_json, 2)
77
+ return
78
+ dex = _optional("dex-trove")
79
+ report = {
80
+ "ok": info.compatible,
81
+ "dev_guru": __version__,
82
+ "python": platform.python_version(),
83
+ "harness": info.model_dump(),
84
+ "optional": {"dex-trove": dex},
85
+ }
86
+ if as_json:
87
+ console.print_json(json.dumps(report))
88
+ else:
89
+ mark = "[green]✓[/green]" if info.compatible else "[red]✗[/red]"
90
+ console.print(
91
+ f"dev-guru {__version__} · Python {report['python']} · transport {info.transport}"
92
+ )
93
+ console.print(
94
+ f"{mark} ananke-plexus {info.ananke_version or 'not installed'} (supported {info.supported_range})"
95
+ )
96
+ for cap, present in info.capabilities.items():
97
+ console.print(f" {'✓' if present else '✗'} {cap}")
98
+ for reason in info.reasons:
99
+ console.print(f" [red]{reason}[/red]")
100
+ console.print(f" [dim]dex-trove: {dex or 'not installed (optional)'}[/dim]")
101
+ if not info.compatible:
102
+ raise typer.Exit(1)
103
+
104
+
105
+ @app.command()
106
+ def explore(
107
+ workspace: Annotated[Path, typer.Argument(help="Project to explore")] = Path("."),
108
+ output: Annotated[
109
+ Path, typer.Option("--output", "-o", help="Report file (a manifest is written next to it)")
110
+ ] = Path("dev-guru-explorer.html"),
111
+ name: Annotated[str | None, typer.Option(help="Project name shown in the report")] = None,
112
+ open_browser: Annotated[
113
+ bool, typer.Option("--open", help="Open the report when it's written")
114
+ ] = False,
115
+ transport: TransportOpt = "in-process",
116
+ profile: Annotated[
117
+ str,
118
+ typer.Option(
119
+ help="private (everything) or public (no source, docstrings or Git revision: safe to publish)"
120
+ ),
121
+ ] = "private",
122
+ as_json: JsonOpt = False,
123
+ ) -> None:
124
+ """Explore a codebase: Ananke builds the graph, Dev Guru writes an interactive, offline HTML explorer.
125
+
126
+ The report is one file with no network access: architecture constellation, dependency flow, impact rings,
127
+ call graph, hotspots, a guided tour, search and a source inspector.
128
+ """
129
+ try:
130
+ client = connect(transport)
131
+ result = service.explore(workspace, client=client, name=name, profile=profile)
132
+ page, sidecar = service.export_html(result, output, client=client)
133
+ except errors.GuruError as exc:
134
+ _fail(exc, as_json, 1)
135
+ return
136
+ project = result.model["project"]
137
+ summary = {
138
+ "ok": True,
139
+ "report": str(page),
140
+ "manifest": str(sidecar),
141
+ "input_digest": result.manifest["input_digest"],
142
+ "project": project,
143
+ "insights": {k: len(v) for k, v in result.model["insights"].items()},
144
+ }
145
+ if as_json:
146
+ console.print_json(json.dumps(summary))
147
+ else:
148
+ console.print(
149
+ f"[green]✓[/green] {project['files']} files · {project['symbols']} symbols · "
150
+ f"{project['imports']} imports · {project['calls']} calls → {page}"
151
+ )
152
+ cycles = len(result.model["insights"]["cycles"])
153
+ if cycles:
154
+ console.print(f" [yellow]{cycles} import cycle(s) found; see the Flow view[/yellow]")
155
+ console.print(f" [dim]manifest {sidecar} · {result.manifest['input_digest'][:19]}…[/dim]")
156
+ if open_browser:
157
+ webbrowser.open(page.resolve().as_uri())
158
+
159
+
160
+ @app.command()
161
+ def present(
162
+ slides: Annotated[
163
+ Path, typer.Argument(help="Markdown slides (separate slides with a line of ---)")
164
+ ],
165
+ output: Annotated[Path | None, typer.Option("--output", "-o", help="Deck file")] = None,
166
+ theme: Annotated[
167
+ str | None, typer.Option(help="lantern (dark) or paper (light); default from front matter")
168
+ ] = None,
169
+ check_only: Annotated[
170
+ bool, typer.Option("--check", help="Check an existing HTML deck instead of building")
171
+ ] = False,
172
+ open_browser: Annotated[
173
+ bool, typer.Option("--open", help="Open the deck when it's written")
174
+ ] = False,
175
+ ) -> None:
176
+ """Turn Markdown slides into one offline, accessible HTML deck (arrow keys, N for notes, print to PDF).
177
+
178
+ Raw HTML is shown as text, images must be local with alt text, charts come from ```chart JSON blocks, and the
179
+ page can't make network requests. --check verifies a built deck.
180
+ """
181
+ from dev_guru import present as deck
182
+
183
+ if check_only:
184
+ problems = deck.check(slides.read_text(encoding="utf-8"))
185
+ for p in problems:
186
+ err.print(f"[red]✗[/red] {p}")
187
+ if problems:
188
+ raise typer.Exit(1)
189
+ console.print(f"[green]✓[/green] {slides}: offline and accessible checks pass")
190
+ return
191
+ try:
192
+ page = deck.build(slides.read_text(encoding="utf-8"), slides.parent, theme=theme)
193
+ except (deck.DeckError, OSError) as exc:
194
+ err.print(
195
+ f"[red]✗ {escape(str(exc))}[/red]"
196
+ ) # the message quotes Markdown like ![alt](src)
197
+ raise typer.Exit(2) from exc
198
+ problems = deck.check(page)
199
+ if (
200
+ problems
201
+ ): # a bug in the renderer, not the author's input: never write a deck that fails its own check
202
+ err.print("[red]✗ built deck failed its checks: " + "; ".join(problems) + "[/red]")
203
+ raise typer.Exit(1)
204
+ out = output or slides.with_suffix(".html")
205
+ out.write_text(page, encoding="utf-8")
206
+ n = page.count('class="slide"')
207
+ console.print(f"[green]✓[/green] {n} slides → {out}")
208
+ if open_browser:
209
+ webbrowser.open(out.resolve().as_uri())
210
+
211
+
212
+ @app.command()
213
+ def sources(
214
+ query: Annotated[str, typer.Argument(help="Words to match (optional)")] = "",
215
+ workspace: Annotated[
216
+ Path, typer.Option("--workspace", "-w", help="Project whose sources to read")
217
+ ] = Path("."),
218
+ html_out: Annotated[
219
+ Path | None, typer.Option("--html", help="Also write an offline picker page")
220
+ ] = None,
221
+ transport: TransportOpt = "in-process",
222
+ as_json: JsonOpt = False,
223
+ ) -> None:
224
+ """Browse capabilities from every source the project configures (internal folders, private Git, troves).
225
+
226
+ Source-neutral: Dev Guru asks Ananke's provider port; Dex Trove is just one optional source.
227
+ """
228
+ from dev_guru.reports import pages
229
+
230
+ try:
231
+ found = connect(transport).discover_capabilities(workspace, query)
232
+ except errors.GuruError as exc:
233
+ _fail(exc, as_json, 1)
234
+ return
235
+ rows = [c.model_dump() for c in found]
236
+ if html_out is not None:
237
+ html_out.write_text(pages.sources_page(rows), encoding="utf-8")
238
+ if as_json:
239
+ console.print_json(json.dumps(rows))
240
+ return
241
+ for c in found:
242
+ console.print(f"{c.identity}@{c.version} [dim]{escape(c.summary[:80])}[/dim]")
243
+ console.print(f"{len(found)} capabilities" + (f" · picker: {html_out}" if html_out else ""))
244
+
245
+
246
+ learning_app = typer.Typer(
247
+ help="Review what Ananke learned before it becomes reusable memory.", no_args_is_help=True
248
+ )
249
+ app.add_typer(learning_app, name="learning")
250
+
251
+
252
+ @learning_app.command("review")
253
+ def learning_review(
254
+ name: Annotated[str, typer.Argument(help="Candidate name (see `ananke learning list`)")],
255
+ workspace: Annotated[Path, typer.Option("--workspace", "-w")] = Path("."),
256
+ output: Annotated[
257
+ Path | None, typer.Option("--output", "-o", help="Review page (default <name>-review.html)")
258
+ ] = None,
259
+ open_browser: Annotated[bool, typer.Option("--open")] = False,
260
+ transport: TransportOpt = "in-process",
261
+ as_json: JsonOpt = False,
262
+ ) -> None:
263
+ """A review page for a learning candidate: gate status, evaluation, review, provenance and per-destination diff.
264
+
265
+ Reviewing changes nothing: approve with `ananke learning review` and promote with `ananke learning promote`.
266
+ """
267
+ from dev_guru.reports import pages
268
+
269
+ try:
270
+ cand = connect(transport).learning_candidate(workspace, name)
271
+ except errors.GuruError as exc:
272
+ _fail(exc, as_json, 1)
273
+ return
274
+ out = output or Path(f"{name}-review.html")
275
+ out.write_text(pages.review_page(cand.model_dump()), encoding="utf-8")
276
+ if as_json:
277
+ console.print_json(json.dumps(cand.model_dump() | {"page": str(out)}))
278
+ else:
279
+ state = (
280
+ "[green]ready to promote[/green]"
281
+ if not cand.problems
282
+ else "[yellow]" + escape("; ".join(cand.problems)) + "[/yellow]"
283
+ )
284
+ console.print(f"{name}: {state} → {out}")
285
+ if open_browser:
286
+ webbrowser.open(out.resolve().as_uri())
287
+
288
+
289
+ @app.command()
290
+ def diagram(
291
+ document: Annotated[Path, typer.Argument(help="An approved Markdown document")],
292
+ output: Annotated[
293
+ Path | None, typer.Option("--output", "-o", help="SVG file (default <document>.svg)")
294
+ ] = None,
295
+ model_out: Annotated[
296
+ Path | None, typer.Option("--model", help="Also write the editable model (JSON)")
297
+ ] = None,
298
+ from_model: Annotated[
299
+ Path | None, typer.Option("--from-model", help="Render an edited model instead")
300
+ ] = None,
301
+ static: Annotated[bool, typer.Option("--static", help="No animation")] = False,
302
+ as_json: JsonOpt = False,
303
+ ) -> None:
304
+ """An animated SVG of the relationships a document states, every one cited (file and line).
305
+
306
+ Reads arrow lines (A -> B: label), Mermaid flowcharts and From/To tables, never prose, so nothing is invented.
307
+ Edited relationships whose citation doesn't hold up are drawn as hypotheses (dashed, "?").
308
+ """
309
+ from dev_guru import diagram as dg
310
+
311
+ try:
312
+ text = document.read_text(encoding="utf-8")
313
+ model = (
314
+ json.loads(from_model.read_text(encoding="utf-8"))
315
+ if from_model
316
+ else dg.extract(text, document.name)
317
+ )
318
+ model = dg.verify(model, text)
319
+ except (dg.DiagramError, OSError, ValueError) as exc:
320
+ err.print(f"[red]✗ {escape(str(exc))}[/red]")
321
+ raise typer.Exit(2) from exc
322
+ svg = dg.render(model, animate=not static)
323
+ problems = dg.svg_problems(svg)
324
+ if problems: # never write an unsafe SVG, whatever the input
325
+ err.print("[red]✗ " + "; ".join(problems) + "[/red]")
326
+ raise typer.Exit(1)
327
+ out = output or document.with_suffix(".svg")
328
+ out.write_text(svg, encoding="utf-8")
329
+ if model_out:
330
+ model_out.write_text(
331
+ json.dumps(model, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
332
+ )
333
+ hyp = sum(1 for r in model["relations"] if r["status"] != "cited")
334
+ summary = {
335
+ "svg": str(out),
336
+ "entities": len(model["entities"]),
337
+ "relations": len(model["relations"]),
338
+ "hypotheses": hyp,
339
+ }
340
+ if as_json:
341
+ console.print_json(json.dumps(summary))
342
+ else:
343
+ console.print(
344
+ f"[green]✓[/green] {len(model['relations']) - hyp} cited relationships, {summary['entities']} entities"
345
+ + (f", [yellow]{hyp} hypotheses[/yellow]" if hyp else "")
346
+ + f" → {out}"
347
+ )