codemap-core 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.
Files changed (52) hide show
  1. codemap/__init__.py +7 -0
  2. codemap/cli/__init__.py +3 -0
  3. codemap/cli/_common.py +90 -0
  4. codemap/cli/commands/__init__.py +3 -0
  5. codemap/cli/commands/callees.py +102 -0
  6. codemap/cli/commands/callers.py +107 -0
  7. codemap/cli/commands/config.py +78 -0
  8. codemap/cli/commands/diagnostics.py +142 -0
  9. codemap/cli/commands/doctor.py +158 -0
  10. codemap/cli/commands/get.py +93 -0
  11. codemap/cli/commands/index.py +725 -0
  12. codemap/cli/commands/routes.py +104 -0
  13. codemap/cli/commands/search.py +78 -0
  14. codemap/cli/commands/trace.py +179 -0
  15. codemap/cli/main.py +140 -0
  16. codemap/cli/renderers/__init__.py +3 -0
  17. codemap/cli/renderers/json.py +32 -0
  18. codemap/cli/renderers/text.py +24 -0
  19. codemap/config/__init__.py +31 -0
  20. codemap/config/loader.py +96 -0
  21. codemap/config/schema.py +122 -0
  22. codemap/core/__init__.py +7 -0
  23. codemap/core/bridge/__init__.py +8 -0
  24. codemap/core/bridge/base.py +38 -0
  25. codemap/core/bridge/http_route.py +374 -0
  26. codemap/core/bridge/python_cross_module.py +120 -0
  27. codemap/core/bridge/registry.py +117 -0
  28. codemap/core/graph.py +183 -0
  29. codemap/core/models.py +299 -0
  30. codemap/core/store.py +78 -0
  31. codemap/core/symbol.py +314 -0
  32. codemap/diagnostics/__init__.py +3 -0
  33. codemap/diagnostics/exit_codes.py +30 -0
  34. codemap/diagnostics/logging.py +65 -0
  35. codemap/diagnostics/progress.py +68 -0
  36. codemap/indexers/__init__.py +9 -0
  37. codemap/indexers/_example_lang.py +135 -0
  38. codemap/indexers/base.py +77 -0
  39. codemap/indexers/python.py +577 -0
  40. codemap/indexers/registry.py +104 -0
  41. codemap/io/__init__.py +8 -0
  42. codemap/io/atomic.py +97 -0
  43. codemap/io/base.py +12 -0
  44. codemap/io/json_store.py +433 -0
  45. codemap/io/lock.py +87 -0
  46. codemap/io/manifest.py +90 -0
  47. codemap/mcp/__init__.py +3 -0
  48. codemap_core-0.1.0.dist-info/METADATA +480 -0
  49. codemap_core-0.1.0.dist-info/RECORD +52 -0
  50. codemap_core-0.1.0.dist-info/WHEEL +4 -0
  51. codemap_core-0.1.0.dist-info/entry_points.txt +10 -0
  52. codemap_core-0.1.0.dist-info/licenses/LICENSE +21 -0
codemap/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """CodeMap — language-neutral code index for AI agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ __all__ = ["__version__"]
@@ -0,0 +1,3 @@
1
+ """Typer CLI surface. Only argument parsing and output rendering live here."""
2
+
3
+ from __future__ import annotations
codemap/cli/_common.py ADDED
@@ -0,0 +1,90 @@
1
+ """Shared helpers for the query commands (search / get / callers / callees / trace).
2
+
3
+ These keep error UX consistent: a missing ``.codemap/`` is exit 66, a
4
+ corrupt index is exit 65, and a malformed SymbolID is exit 64. Friendly
5
+ messages go to stderr; the commands themselves only emit data on stdout.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path, PurePosixPath
11
+
12
+ import typer
13
+
14
+ from codemap.cli.renderers import text
15
+ from codemap.core.symbol import SymbolID, SymbolParseError
16
+ from codemap.diagnostics.exit_codes import ExitCode
17
+ from codemap.io.json_store import IntegrityError, JsonStore
18
+ from codemap.io.manifest import SchemaVersionError
19
+
20
+ CODEMAP_DIR = ".codemap"
21
+ _SNIPPET_CONTEXT = 0
22
+
23
+
24
+ def open_store_or_exit(project_path: Path) -> JsonStore:
25
+ """Open ``<project>/.codemap/`` read-only; exit gracefully on failure."""
26
+ codemap_dir = project_path / CODEMAP_DIR
27
+ if not codemap_dir.exists():
28
+ text.console(stderr=True).print(
29
+ f"[red]No `.codemap/` directory found at {project_path}[/red]. "
30
+ "Run `codemap index` first."
31
+ )
32
+ raise typer.Exit(code=int(ExitCode.NO_INPUT))
33
+ try:
34
+ return JsonStore.open(codemap_dir, mode="r")
35
+ except (SchemaVersionError, IntegrityError) as exc:
36
+ text.console(stderr=True).print(
37
+ f"[red]Index corrupted:[/red] {exc}\nTry `codemap index --rebuild`."
38
+ )
39
+ raise typer.Exit(code=int(ExitCode.DATA_ERROR)) from exc
40
+
41
+
42
+ def parse_symbol_id_or_exit(raw: str) -> SymbolID:
43
+ """Parse a SCIP SymbolID; exit 64 with a readable message on failure."""
44
+ try:
45
+ return SymbolID.parse(raw)
46
+ except SymbolParseError as exc:
47
+ text.console(stderr=True).print(
48
+ f"[red]Invalid SymbolID:[/red] {exc}\n"
49
+ "Expected format: '<scheme> <manager> <package> <version> <descriptors>'"
50
+ )
51
+ raise typer.Exit(code=int(ExitCode.USAGE_ERROR)) from exc
52
+
53
+
54
+ def read_snippet(
55
+ project_root: Path,
56
+ file: PurePosixPath,
57
+ line: int,
58
+ *,
59
+ context: int = _SNIPPET_CONTEXT,
60
+ ) -> str | None:
61
+ """Read ``project_root/file`` and return the line ``line`` (1-based).
62
+
63
+ With ``context > 0`` returns the surrounding ``context`` lines joined with
64
+ newlines. Returns ``None`` if the file is missing or the line is out of
65
+ range — callers render symbols without snippets in that case.
66
+ """
67
+ src = project_root / str(file)
68
+ try:
69
+ lines = src.read_text(encoding="utf-8").splitlines()
70
+ except (OSError, UnicodeDecodeError):
71
+ return None
72
+ if line < 1 or line > len(lines):
73
+ return None
74
+ start = max(line - 1 - context, 0)
75
+ end = min(line + context, len(lines))
76
+ return "\n".join(lines[start:end])
77
+
78
+
79
+ def format_location(file: PurePosixPath, line: int) -> str:
80
+ """Render a ``file:line`` cursor."""
81
+ return f"{file}:{line}"
82
+
83
+
84
+ __all__ = [
85
+ "CODEMAP_DIR",
86
+ "format_location",
87
+ "open_store_or_exit",
88
+ "parse_symbol_id_or_exit",
89
+ "read_snippet",
90
+ ]
@@ -0,0 +1,3 @@
1
+ """CLI command modules. Each command is one file."""
2
+
3
+ from __future__ import annotations
@@ -0,0 +1,102 @@
1
+ """``codemap callees <symbol-id>`` — list downstream callees."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ from codemap.cli._common import (
11
+ format_location,
12
+ open_store_or_exit,
13
+ parse_symbol_id_or_exit,
14
+ )
15
+ from codemap.cli.renderers import json as json_renderer
16
+ from codemap.cli.renderers import text
17
+
18
+
19
+ def register(app: typer.Typer) -> None:
20
+ @app.command("callees")
21
+ def callees(
22
+ ctx: typer.Context,
23
+ symbol_id: Annotated[str, typer.Argument(help="Source SymbolID.")],
24
+ depth: Annotated[
25
+ int,
26
+ typer.Option("--depth", "-d", min=1, help="How many hops downstream."),
27
+ ] = 1,
28
+ project: Annotated[
29
+ Path,
30
+ typer.Option(
31
+ "--project",
32
+ "-p",
33
+ file_okay=False,
34
+ dir_okay=True,
35
+ help="Project root containing `.codemap/`.",
36
+ ),
37
+ ] = Path("."),
38
+ ) -> None:
39
+ """Print every edge whose ``source`` matches ``symbol_id`` (depth-limited)."""
40
+ as_json: bool = ctx.obj["json_output"]
41
+ sid = parse_symbol_id_or_exit(symbol_id)
42
+ with open_store_or_exit(project) as store:
43
+ edges = store.callees(sid, depth=depth)
44
+ results = []
45
+ for edge in edges:
46
+ target_sym = store.get(edge.target)
47
+ results.append((edge, target_sym))
48
+
49
+ if as_json:
50
+ json_renderer.emit(
51
+ "callees",
52
+ {
53
+ "source": str(sid),
54
+ "depth": depth,
55
+ "results": [
56
+ {
57
+ "symbol": str(edge.target),
58
+ "kind": (sym.kind if sym else "external"),
59
+ "file": (str(sym.file) if sym else None),
60
+ "line": (
61
+ edge.location.start_line
62
+ if edge.location
63
+ else (sym.range.start_line if sym else None)
64
+ ),
65
+ "edge": {
66
+ "kind": edge.kind,
67
+ "confidence": edge.confidence,
68
+ },
69
+ }
70
+ for edge, sym in results
71
+ ],
72
+ },
73
+ )
74
+ return
75
+
76
+ cons = text.console()
77
+ if not results:
78
+ cons.print(f"[yellow]No callees for[/yellow] {symbol_id!r}")
79
+ return
80
+ rows = []
81
+ for edge, sym in results:
82
+ line = (
83
+ edge.location.start_line if edge.location else (sym.range.start_line if sym else 0)
84
+ )
85
+ rows.append(
86
+ [
87
+ edge.kind,
88
+ edge.confidence,
89
+ (format_location(sym.file, line) if sym is not None else "(external)"),
90
+ str(edge.target),
91
+ ]
92
+ )
93
+ cons.print(
94
+ text.table(
95
+ f"Callees of {symbol_id}",
96
+ ["edge", "confidence", "location", "target"],
97
+ rows,
98
+ )
99
+ )
100
+
101
+
102
+ __all__ = ["register"]
@@ -0,0 +1,107 @@
1
+ """``codemap callers <symbol-id>`` — list upstream callers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ from codemap.cli._common import (
11
+ format_location,
12
+ open_store_or_exit,
13
+ parse_symbol_id_or_exit,
14
+ read_snippet,
15
+ )
16
+ from codemap.cli.renderers import json as json_renderer
17
+ from codemap.cli.renderers import text
18
+
19
+
20
+ def register(app: typer.Typer) -> None:
21
+ @app.command("callers")
22
+ def callers(
23
+ ctx: typer.Context,
24
+ symbol_id: Annotated[str, typer.Argument(help="Target SymbolID.")],
25
+ depth: Annotated[
26
+ int,
27
+ typer.Option("--depth", "-d", min=1, help="How many hops upstream."),
28
+ ] = 1,
29
+ project: Annotated[
30
+ Path,
31
+ typer.Option(
32
+ "--project",
33
+ "-p",
34
+ file_okay=False,
35
+ dir_okay=True,
36
+ help="Project root containing `.codemap/`.",
37
+ ),
38
+ ] = Path("."),
39
+ ) -> None:
40
+ """Print every edge whose ``target`` matches ``symbol_id`` (depth-limited)."""
41
+ as_json: bool = ctx.obj["json_output"]
42
+ sid = parse_symbol_id_or_exit(symbol_id)
43
+ with open_store_or_exit(project) as store:
44
+ edges = store.callers(sid, depth=depth)
45
+ manifest = store.manifest()
46
+ results = []
47
+ for edge in edges:
48
+ source_sym = store.get(edge.source)
49
+ results.append((edge, source_sym))
50
+
51
+ if as_json:
52
+ json_renderer.emit(
53
+ "callers",
54
+ {
55
+ "target": str(sid),
56
+ "depth": depth,
57
+ "results": [
58
+ {
59
+ "symbol": str(edge.source),
60
+ "kind": (sym.kind if sym else "unknown"),
61
+ "file": (str(sym.file) if sym else None),
62
+ "line": (
63
+ edge.location.start_line
64
+ if edge.location
65
+ else (sym.range.start_line if sym else None)
66
+ ),
67
+ "edge": {
68
+ "kind": edge.kind,
69
+ "confidence": edge.confidence,
70
+ },
71
+ }
72
+ for edge, sym in results
73
+ ],
74
+ },
75
+ )
76
+ return
77
+
78
+ cons = text.console()
79
+ if not results:
80
+ cons.print(f"[yellow]No callers for[/yellow] {symbol_id!r}")
81
+ return
82
+ rows = []
83
+ for edge, sym in results:
84
+ line = (
85
+ edge.location.start_line if edge.location else (sym.range.start_line if sym else 0)
86
+ )
87
+ rows.append(
88
+ [
89
+ edge.kind,
90
+ edge.confidence,
91
+ (format_location(sym.file, line) if sym is not None else "(external)"),
92
+ str(edge.source),
93
+ ]
94
+ )
95
+ cons.print(
96
+ text.table(
97
+ f"Callers of {symbol_id}",
98
+ ["edge", "confidence", "location", "source"],
99
+ rows,
100
+ )
101
+ )
102
+ # Suppress unused warning by referencing manifest for snippet support
103
+ _ = manifest
104
+ _ = read_snippet # snippets in callers add too much noise — kept for future
105
+
106
+
107
+ __all__ = ["register"]
@@ -0,0 +1,78 @@
1
+ """``codemap config show`` — print the merged effective configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ from codemap.cli.renderers import json as json_renderer
11
+ from codemap.cli.renderers import text
12
+ from codemap.config import ConfigError, dump_config, load_config
13
+ from codemap.config.loader import project_config_path, user_config_path
14
+ from codemap.diagnostics.exit_codes import ExitCode
15
+
16
+
17
+ def register(app: typer.Typer) -> None:
18
+ config_app = typer.Typer(
19
+ name="config",
20
+ help="Inspect and edit configuration files.",
21
+ no_args_is_help=True,
22
+ )
23
+ app.add_typer(config_app, name="config")
24
+
25
+ @config_app.command("show")
26
+ def show(
27
+ ctx: typer.Context,
28
+ project: Annotated[
29
+ Path,
30
+ typer.Option(
31
+ "--project",
32
+ "-p",
33
+ file_okay=False,
34
+ dir_okay=True,
35
+ help="Project root containing `.codemap/`.",
36
+ ),
37
+ ] = Path("."),
38
+ ) -> None:
39
+ """Print the merged configuration that ``index`` would use."""
40
+ as_json: bool = ctx.obj["json_output"]
41
+ codemap_dir = project / ".codemap"
42
+ try:
43
+ config = load_config(codemap_dir if codemap_dir.exists() else None)
44
+ except ConfigError as exc:
45
+ text.console(stderr=True).print(f"[red]Config error:[/red] {exc}")
46
+ raise typer.Exit(code=int(ExitCode.CONFIG_ERROR)) from exc
47
+
48
+ sources = {
49
+ "defaults": "(built-in)",
50
+ "user": str(user_config_path()),
51
+ "project": str(project_config_path(codemap_dir)),
52
+ }
53
+
54
+ if as_json:
55
+ json_renderer.emit(
56
+ "config show",
57
+ {
58
+ "config": config.model_dump(mode="json"),
59
+ "sources": sources,
60
+ },
61
+ )
62
+ return
63
+
64
+ cons = text.console()
65
+ cons.print("[bold]Config sources (overlaid in order):[/bold]")
66
+ for label, p in sources.items():
67
+ mark = ""
68
+ if (label == "user" and not user_config_path().exists()) or (
69
+ label == "project" and not project_config_path(codemap_dir).exists()
70
+ ):
71
+ mark = " [dim](missing)[/dim]"
72
+ cons.print(f" • {label}: {p}{mark}")
73
+ cons.print()
74
+ cons.print("[bold]Effective configuration:[/bold]")
75
+ cons.print(dump_config(config))
76
+
77
+
78
+ __all__ = ["register"]
@@ -0,0 +1,142 @@
1
+ """``codemap diagnostics`` — surface indexer / bridge diagnostics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ from codemap.cli._common import format_location, open_store_or_exit
11
+ from codemap.cli.renderers import json as json_renderer
12
+ from codemap.cli.renderers import text
13
+
14
+ _SEVERITY_COLORS = {
15
+ "error": "red",
16
+ "warning": "yellow",
17
+ "info": "cyan",
18
+ }
19
+
20
+
21
+ def register(app: typer.Typer) -> None:
22
+ @app.command("diagnostics")
23
+ def diagnostics(
24
+ ctx: typer.Context,
25
+ severity: Annotated[
26
+ str | None,
27
+ typer.Option(
28
+ "--severity",
29
+ "-s",
30
+ help="Filter by severity: error / warning / info.",
31
+ ),
32
+ ] = None,
33
+ producer: Annotated[
34
+ str | None,
35
+ typer.Option(
36
+ "--producer",
37
+ help="Filter by producer (indexer or bridge name).",
38
+ ),
39
+ ] = None,
40
+ code: Annotated[
41
+ str | None,
42
+ typer.Option(
43
+ "--code",
44
+ help="Filter by diagnostic code (e.g. ROUTE001).",
45
+ ),
46
+ ] = None,
47
+ limit: Annotated[
48
+ int,
49
+ typer.Option("--limit", "-n", help="Maximum entries to show.", min=1),
50
+ ] = 50,
51
+ project: Annotated[
52
+ Path,
53
+ typer.Option(
54
+ "--project",
55
+ "-p",
56
+ file_okay=False,
57
+ dir_okay=True,
58
+ help="Project root containing `.codemap/`.",
59
+ ),
60
+ ] = Path("."),
61
+ ) -> None:
62
+ """List diagnostics collected during the last `codemap index`."""
63
+ as_json: bool = ctx.obj["json_output"]
64
+ severity_filter = severity.lower() if severity else None
65
+ with open_store_or_exit(project) as store:
66
+ all_diag = list(store.iter_diagnostics())
67
+
68
+ filtered = [
69
+ d
70
+ for d in all_diag
71
+ if (severity_filter is None or d.severity == severity_filter)
72
+ and (producer is None or d.producer == producer)
73
+ and (code is None or d.code == code)
74
+ ]
75
+ shown = filtered[:limit]
76
+
77
+ if as_json:
78
+ json_renderer.emit(
79
+ "diagnostics",
80
+ {
81
+ "total": len(all_diag),
82
+ "matched": len(filtered),
83
+ "shown": len(shown),
84
+ "filters": {
85
+ "severity": severity_filter,
86
+ "producer": producer,
87
+ "code": code,
88
+ },
89
+ "results": [
90
+ {
91
+ "severity": d.severity,
92
+ "code": d.code,
93
+ "producer": d.producer,
94
+ "file": str(d.file),
95
+ "line": d.range.start_line if d.range else None,
96
+ "message": d.message,
97
+ }
98
+ for d in shown
99
+ ],
100
+ },
101
+ )
102
+ return
103
+
104
+ cons = text.console()
105
+ if not all_diag:
106
+ cons.print("[green]No diagnostics recorded.[/green]")
107
+ return
108
+ if not filtered:
109
+ cons.print(
110
+ f"[yellow]No diagnostics matched the filter.[/yellow] "
111
+ f"(total recorded: {len(all_diag)})"
112
+ )
113
+ return
114
+
115
+ cons.print(
116
+ f"[bold]Showing {len(shown)} of {len(filtered)} matched ({len(all_diag)} total)[/bold]"
117
+ )
118
+ rows = []
119
+ for d in shown:
120
+ color = _SEVERITY_COLORS.get(d.severity, "white")
121
+ location = (
122
+ format_location(d.file, d.range.start_line) if d.range is not None else str(d.file)
123
+ )
124
+ rows.append(
125
+ [
126
+ f"[{color}]{d.severity}[/{color}]",
127
+ d.code,
128
+ d.producer,
129
+ location,
130
+ d.message,
131
+ ]
132
+ )
133
+ cons.print(
134
+ text.table(
135
+ "Diagnostics",
136
+ ["severity", "code", "producer", "location", "message"],
137
+ rows,
138
+ )
139
+ )
140
+
141
+
142
+ __all__ = ["register"]
@@ -0,0 +1,158 @@
1
+ """``codemap doctor`` — health check for the local index and plugin set."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from pathlib import Path
7
+ from typing import Annotated, Any
8
+
9
+ import typer
10
+
11
+ from codemap import __version__
12
+ from codemap.cli.renderers import json as json_renderer
13
+ from codemap.cli.renderers import text
14
+ from codemap.core.bridge.registry import get_registry as get_bridges
15
+ from codemap.diagnostics.exit_codes import ExitCode
16
+ from codemap.indexers.registry import get_registry as get_indexers
17
+ from codemap.io.json_store import IntegrityError, JsonStore
18
+ from codemap.io.manifest import SchemaVersionError
19
+
20
+ CODEMAP_DIR = ".codemap"
21
+
22
+
23
+ def register(app: typer.Typer) -> None:
24
+ @app.command("doctor")
25
+ def doctor(
26
+ ctx: typer.Context,
27
+ path: Annotated[
28
+ Path,
29
+ typer.Argument(
30
+ exists=False,
31
+ file_okay=False,
32
+ dir_okay=True,
33
+ help="Project root to diagnose (defaults to CWD).",
34
+ ),
35
+ ] = Path("."),
36
+ ) -> None:
37
+ """Report on registered plugins and the local `.codemap/` state."""
38
+ as_json: bool = ctx.obj["json_output"]
39
+ codemap_dir = path / CODEMAP_DIR
40
+
41
+ indexer_rows = [
42
+ [ix.name, ix.version, ", ".join(ix.languages), ", ".join(ix.file_patterns)]
43
+ for ix in get_indexers().all().values()
44
+ ]
45
+ bridge_rows = [
46
+ [b.name, b.version, ", ".join(b.requires) or "-"] for b in get_bridges().all().values()
47
+ ]
48
+
49
+ index_status, manifest_info, issue = _inspect_index(codemap_dir)
50
+
51
+ payload = {
52
+ "codemap_version": __version__,
53
+ "project_root": str(path.resolve()),
54
+ "indexers": [
55
+ {
56
+ "name": r[0],
57
+ "version": r[1],
58
+ "languages": r[2].split(", ") if r[2] else [],
59
+ "file_patterns": r[3].split(", ") if r[3] else [],
60
+ }
61
+ for r in indexer_rows
62
+ ],
63
+ "bridges": [
64
+ {"name": r[0], "version": r[1], "requires": r[2].split(", ") if r[2] != "-" else []}
65
+ for r in bridge_rows
66
+ ],
67
+ "index": {
68
+ "status": index_status,
69
+ "info": manifest_info,
70
+ "issue": issue,
71
+ },
72
+ }
73
+
74
+ if as_json:
75
+ json_renderer.emit("doctor", payload)
76
+ else:
77
+ _render_text(payload, indexer_rows, bridge_rows)
78
+
79
+ # Non-zero exit if index is in a recoverable bad state, so CI can act.
80
+ if index_status == "corrupted":
81
+ raise typer.Exit(code=int(ExitCode.DATA_ERROR))
82
+
83
+
84
+ def _inspect_index(codemap_dir: Path) -> tuple[str, dict[str, object], str | None]:
85
+ if not codemap_dir.exists():
86
+ return "absent", {}, None
87
+ try:
88
+ with JsonStore.open(codemap_dir, mode="r") as store:
89
+ m = store.manifest()
90
+ return (
91
+ "ok",
92
+ {
93
+ "schema_version": m.schema_version,
94
+ "codemap_version": m.codemap_version,
95
+ "storage_backend": m.storage_backend,
96
+ "symbol_count": m.stats.symbol_count,
97
+ "edge_count": m.stats.edge_count,
98
+ "indexer_count": len(m.indexers),
99
+ "bridge_count": len(m.bridges),
100
+ "updated_at": m.updated_at.isoformat(),
101
+ },
102
+ None,
103
+ )
104
+ except (SchemaVersionError, IntegrityError, OSError, ValueError) as exc:
105
+ return "corrupted", {}, str(exc)
106
+
107
+
108
+ def _render_text(
109
+ payload: Mapping[str, Any],
110
+ indexer_rows: list[list[str]],
111
+ bridge_rows: list[list[str]],
112
+ ) -> None:
113
+ cons = text.console()
114
+ cons.print(f"[bold]CodeMap[/bold] {payload['codemap_version']}")
115
+ cons.print(f"project_root: {payload['project_root']}")
116
+ cons.print()
117
+ if indexer_rows:
118
+ cons.print(
119
+ text.table(
120
+ "Registered indexers",
121
+ ["name", "version", "languages", "file_patterns"],
122
+ indexer_rows,
123
+ )
124
+ )
125
+ else:
126
+ cons.print("[yellow]No indexers registered[/yellow]")
127
+ if bridge_rows:
128
+ cons.print(
129
+ text.table(
130
+ "Registered bridges",
131
+ ["name", "version", "requires"],
132
+ bridge_rows,
133
+ )
134
+ )
135
+ cons.print()
136
+ idx = payload["index"]
137
+ assert isinstance(idx, dict)
138
+ status = idx["status"]
139
+ if status == "absent":
140
+ cons.print(
141
+ "[yellow]No `.codemap/` directory found.[/yellow] Run `codemap index` to build one."
142
+ )
143
+ elif status == "corrupted":
144
+ cons.print(f"[red]Index corrupted:[/red] {idx['issue']}")
145
+ cons.print("Run `codemap index --rebuild` to regenerate.")
146
+ else:
147
+ info = idx["info"]
148
+ assert isinstance(info, dict)
149
+ cons.print(
150
+ text.table(
151
+ "Index status",
152
+ ["field", "value"],
153
+ [[k, v] for k, v in info.items()],
154
+ )
155
+ )
156
+
157
+
158
+ __all__ = ["register"]