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.
- codemap/__init__.py +7 -0
- codemap/cli/__init__.py +3 -0
- codemap/cli/_common.py +90 -0
- codemap/cli/commands/__init__.py +3 -0
- codemap/cli/commands/callees.py +102 -0
- codemap/cli/commands/callers.py +107 -0
- codemap/cli/commands/config.py +78 -0
- codemap/cli/commands/diagnostics.py +142 -0
- codemap/cli/commands/doctor.py +158 -0
- codemap/cli/commands/get.py +93 -0
- codemap/cli/commands/index.py +725 -0
- codemap/cli/commands/routes.py +104 -0
- codemap/cli/commands/search.py +78 -0
- codemap/cli/commands/trace.py +179 -0
- codemap/cli/main.py +140 -0
- codemap/cli/renderers/__init__.py +3 -0
- codemap/cli/renderers/json.py +32 -0
- codemap/cli/renderers/text.py +24 -0
- codemap/config/__init__.py +31 -0
- codemap/config/loader.py +96 -0
- codemap/config/schema.py +122 -0
- codemap/core/__init__.py +7 -0
- codemap/core/bridge/__init__.py +8 -0
- codemap/core/bridge/base.py +38 -0
- codemap/core/bridge/http_route.py +374 -0
- codemap/core/bridge/python_cross_module.py +120 -0
- codemap/core/bridge/registry.py +117 -0
- codemap/core/graph.py +183 -0
- codemap/core/models.py +299 -0
- codemap/core/store.py +78 -0
- codemap/core/symbol.py +314 -0
- codemap/diagnostics/__init__.py +3 -0
- codemap/diagnostics/exit_codes.py +30 -0
- codemap/diagnostics/logging.py +65 -0
- codemap/diagnostics/progress.py +68 -0
- codemap/indexers/__init__.py +9 -0
- codemap/indexers/_example_lang.py +135 -0
- codemap/indexers/base.py +77 -0
- codemap/indexers/python.py +577 -0
- codemap/indexers/registry.py +104 -0
- codemap/io/__init__.py +8 -0
- codemap/io/atomic.py +97 -0
- codemap/io/base.py +12 -0
- codemap/io/json_store.py +433 -0
- codemap/io/lock.py +87 -0
- codemap/io/manifest.py +90 -0
- codemap/mcp/__init__.py +3 -0
- codemap_core-0.1.0.dist-info/METADATA +480 -0
- codemap_core-0.1.0.dist-info/RECORD +52 -0
- codemap_core-0.1.0.dist-info/WHEEL +4 -0
- codemap_core-0.1.0.dist-info/entry_points.txt +10 -0
- codemap_core-0.1.0.dist-info/licenses/LICENSE +21 -0
codemap/__init__.py
ADDED
codemap/cli/__init__.py
ADDED
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,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"]
|