codmap 0.0.3__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 +10 -0
- codemap/apidiff.py +208 -0
- codemap/arch.py +190 -0
- codemap/cli.py +718 -0
- codemap/diagnostics.py +256 -0
- codemap/extract/__init__.py +10 -0
- codemap/extract/attrflow.py +230 -0
- codemap/extract/behavior.py +771 -0
- codemap/extract/dataflow.py +97 -0
- codemap/extract/dispatch.py +248 -0
- codemap/extract/griffe_extractor.py +496 -0
- codemap/extract/gsource.py +83 -0
- codemap/extract/roots.py +427 -0
- codemap/freshness.py +94 -0
- codemap/incremental.py +195 -0
- codemap/integrations/__init__.py +51 -0
- codemap/integrations/base.py +196 -0
- codemap/integrations/cocoindex.py +78 -0
- codemap/integrations/gate.py +58 -0
- codemap/integrations/gitnexus.py +93 -0
- codemap/integrations/registry.py +69 -0
- codemap/integrations/transport.py +46 -0
- codemap/model.py +178 -0
- codemap/provenance.py +248 -0
- codemap/query.py +1164 -0
- codemap/scope.py +212 -0
- codemap/serve/__init__.py +26 -0
- codemap/serve/_scip_pb2.py +100 -0
- codemap/serve/api_surface.py +60 -0
- codemap/serve/apidiff.py +83 -0
- codemap/serve/architecture.py +101 -0
- codemap/serve/audit.py +176 -0
- codemap/serve/check.py +80 -0
- codemap/serve/ctags.py +203 -0
- codemap/serve/impact.py +84 -0
- codemap/serve/livingdocs.py +174 -0
- codemap/serve/mcp_server.py +278 -0
- codemap/serve/mermaid.py +120 -0
- codemap/serve/pack.py +93 -0
- codemap/serve/rag.py +142 -0
- codemap/serve/review.py +197 -0
- codemap/serve/scip.py +183 -0
- codemap/serve/semantic.py +71 -0
- codemap/serve/server.py +43 -0
- codemap/serve/session.py +482 -0
- codemap/serve/subsystems.py +85 -0
- codemap/serve/vault.py +156 -0
- codemap/store.py +28 -0
- codemap/tomlio.py +59 -0
- codmap-0.0.3.dist-info/METADATA +245 -0
- codmap-0.0.3.dist-info/RECORD +55 -0
- codmap-0.0.3.dist-info/WHEEL +5 -0
- codmap-0.0.3.dist-info/entry_points.txt +2 -0
- codmap-0.0.3.dist-info/licenses/LICENSE +21 -0
- codmap-0.0.3.dist-info/top_level.txt +1 -0
codemap/scope.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Input scope manifest — M19.A (design: ``docs/design/scope.md``).
|
|
2
|
+
|
|
3
|
+
A deterministic identity of the **input** that produced a graph. codemap is already
|
|
4
|
+
deterministic on its *output* (canonical ``graph.json``); this is the symmetric thing
|
|
5
|
+
for the *input*: resolve a scope (build args / spec) to a **sorted file list**,
|
|
6
|
+
content-hash each file (sha-256), build a **profile**, and compute a **scope_id**.
|
|
7
|
+
|
|
8
|
+
Operates **in place over the real tree** (the live path — enables watch/incremental
|
|
9
|
+
downstream). When the root is a git repo, enumeration prefers ``git ls-files`` (the
|
|
10
|
+
gitignore-correct set — no venv/build/cache, for free) and records git provenance;
|
|
11
|
+
identity (``scope_id``) is always our sha-256, independent of git and of dirty state.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
import os
|
|
18
|
+
import subprocess
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
# What codemap actually consumes as input (source + docs it indexes as references).
|
|
22
|
+
DEFAULT_INCLUDE = ("*.py", "*.md")
|
|
23
|
+
# fs-mode default excludes (git mode gets these for free via .gitignore).
|
|
24
|
+
DEFAULT_EXCLUDE_DIRS = frozenset({
|
|
25
|
+
"__pycache__", ".git", ".venv", "venv", "node_modules", "build", "dist",
|
|
26
|
+
".eggs", ".mypy_cache", ".pytest_cache", ".ruff_cache", ".tox",
|
|
27
|
+
})
|
|
28
|
+
_LARGEST_N = 10
|
|
29
|
+
# consumer dir name → role; anything else keeps its own basename as the role.
|
|
30
|
+
_KNOWN_ROLES = frozenset({"tests", "examples", "research", "scripts", "docs", "benchmarks"})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _git(root: Path, *args: str) -> str | None:
|
|
34
|
+
"""Run ``git -C root …``; return stdout (stripped) or None on any failure."""
|
|
35
|
+
try:
|
|
36
|
+
out = subprocess.run(("git", "-C", str(root), *args),
|
|
37
|
+
capture_output=True, text=True, check=True)
|
|
38
|
+
except (OSError, subprocess.CalledProcessError):
|
|
39
|
+
return None
|
|
40
|
+
return out.stdout
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _match_include(name: str, include: tuple[str, ...]) -> bool:
|
|
44
|
+
from fnmatch import fnmatch
|
|
45
|
+
return any(fnmatch(name, pat) for pat in include)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _role_of(rel: str, roots: list[tuple[str, str]]) -> str:
|
|
49
|
+
"""Assign a relative path to a role by its longest matching root prefix."""
|
|
50
|
+
parts = rel.split("/")
|
|
51
|
+
best_role, best_len = "core", -1
|
|
52
|
+
for rpath, role in roots:
|
|
53
|
+
rp = rpath.split("/") if rpath else []
|
|
54
|
+
if parts[:len(rp)] == rp and len(rp) > best_len:
|
|
55
|
+
best_role, best_len = role, len(rp)
|
|
56
|
+
return best_role
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _roots_spec(root: Path, core: Path, consumers, docs) -> list[tuple[str, str]]:
|
|
60
|
+
"""[(rel-path-from-root, role)] for core + consumers + docs (longest-match order)."""
|
|
61
|
+
out: list[tuple[str, str]] = [(_rel(root, core), "core")]
|
|
62
|
+
for c in consumers:
|
|
63
|
+
p = Path(c).resolve()
|
|
64
|
+
out.append((_rel(root, p), p.name if p.name in _KNOWN_ROLES else p.name))
|
|
65
|
+
for d in docs:
|
|
66
|
+
p = Path(d).resolve()
|
|
67
|
+
out.append((_rel(root, p), "docs"))
|
|
68
|
+
return out
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _rel(root: Path, p: Path) -> str:
|
|
72
|
+
try:
|
|
73
|
+
return p.resolve().relative_to(root).as_posix()
|
|
74
|
+
except ValueError:
|
|
75
|
+
return p.resolve().as_posix()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _pick_root(core: Path, consumers, docs) -> tuple[Path, str]:
|
|
79
|
+
"""Choose the scope root (paths are stored relative to it) and enumeration mode.
|
|
80
|
+
|
|
81
|
+
Prefer the git top-level (→ git mode); else the common ancestor of the inputs
|
|
82
|
+
(→ fs mode). Returns (root, mode).
|
|
83
|
+
"""
|
|
84
|
+
core = core.resolve()
|
|
85
|
+
top = _git(core if core.is_dir() else core.parent, "rev-parse", "--show-toplevel")
|
|
86
|
+
if top:
|
|
87
|
+
return Path(top.strip()), "git"
|
|
88
|
+
paths = [core] + [Path(p).resolve() for p in (*consumers, *docs)]
|
|
89
|
+
base = Path(os.path.commonpath([str(p) for p in paths])) if len(paths) > 1 else core.parent
|
|
90
|
+
return base, "fs"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _enumerate_git(root: Path, roots: list[tuple[str, str]]) -> tuple[list[str], dict]:
|
|
94
|
+
"""git ls-files over the scope pathspecs → (rel paths, {path: git_blob})."""
|
|
95
|
+
specs = [rp for rp, _ in roots if rp]
|
|
96
|
+
out = _git(root, "ls-files", "-s", "--", *specs) or ""
|
|
97
|
+
paths, blobs = [], {}
|
|
98
|
+
for line in out.splitlines():
|
|
99
|
+
# "<mode> <blob> <stage>\t<path>"
|
|
100
|
+
meta, _, path = line.partition("\t")
|
|
101
|
+
if not path:
|
|
102
|
+
continue
|
|
103
|
+
cols = meta.split()
|
|
104
|
+
if len(cols) >= 2:
|
|
105
|
+
blobs[path] = cols[1]
|
|
106
|
+
paths.append(path)
|
|
107
|
+
return paths, blobs
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _enumerate_fs(root: Path, roots, include, exclude_dirs) -> list[str]:
|
|
111
|
+
paths = []
|
|
112
|
+
for rp, _role in roots:
|
|
113
|
+
base = (root / rp) if rp else root
|
|
114
|
+
if not base.exists():
|
|
115
|
+
continue
|
|
116
|
+
for dirpath, dirnames, filenames in os.walk(base):
|
|
117
|
+
dirnames[:] = [d for d in dirnames if d not in exclude_dirs]
|
|
118
|
+
for fn in filenames:
|
|
119
|
+
if _match_include(fn, include):
|
|
120
|
+
paths.append(Path(dirpath, fn).resolve().relative_to(root).as_posix())
|
|
121
|
+
return paths
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def resolve_scope(
|
|
125
|
+
core: str | Path,
|
|
126
|
+
*,
|
|
127
|
+
consumers: tuple[str | Path, ...] = (),
|
|
128
|
+
docs: tuple[str | Path, ...] = (),
|
|
129
|
+
include: tuple[str, ...] = DEFAULT_INCLUDE,
|
|
130
|
+
exclude_dirs=DEFAULT_EXCLUDE_DIRS,
|
|
131
|
+
use_git: bool = True,
|
|
132
|
+
) -> dict:
|
|
133
|
+
"""Resolve a scope to ``{scope_id, profile, git, files}`` (design §1)."""
|
|
134
|
+
core = Path(core).resolve()
|
|
135
|
+
root, mode = _pick_root(core, consumers, docs)
|
|
136
|
+
if not use_git:
|
|
137
|
+
mode = "fs"
|
|
138
|
+
roots = _roots_spec(root, core, consumers, docs)
|
|
139
|
+
|
|
140
|
+
if mode == "git":
|
|
141
|
+
rels, blobs = _enumerate_git(root, roots)
|
|
142
|
+
rels = [r for r in rels if _match_include(Path(r).name, include)]
|
|
143
|
+
else:
|
|
144
|
+
rels, blobs = _enumerate_fs(root, roots, include, exclude_dirs), {}
|
|
145
|
+
|
|
146
|
+
files = []
|
|
147
|
+
for rel in sorted(set(rels)):
|
|
148
|
+
fpath = root / rel
|
|
149
|
+
try:
|
|
150
|
+
data = fpath.read_bytes()
|
|
151
|
+
except OSError:
|
|
152
|
+
continue
|
|
153
|
+
rec = {"path": rel, "sha256": hashlib.sha256(data).hexdigest(),
|
|
154
|
+
"bytes": len(data), "role": _role_of(rel, roots),
|
|
155
|
+
"loc": data.count(b"\n") + (1 if data and not data.endswith(b"\n") else 0)}
|
|
156
|
+
if rel in blobs:
|
|
157
|
+
rec["git_blob"] = blobs[rel]
|
|
158
|
+
files.append(rec)
|
|
159
|
+
|
|
160
|
+
scope_id = "sha256:" + hashlib.sha256(
|
|
161
|
+
"\n".join(f"{f['path']}\t{f['sha256']}" for f in files).encode("utf-8")
|
|
162
|
+
).hexdigest()
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
"scope_id": scope_id,
|
|
166
|
+
"root": str(root),
|
|
167
|
+
"profile": _profile(files),
|
|
168
|
+
"git": _git_block(root, roots, mode) if mode == "git" else {"mode": "fs"},
|
|
169
|
+
# files carry loc for the profile; drop it from the persisted per-file record
|
|
170
|
+
"files": [{k: v for k, v in f.items() if k != "loc"} for f in files],
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _profile(files: list[dict]) -> dict:
|
|
175
|
+
by_role: dict[str, dict] = {}
|
|
176
|
+
by_ext: dict[str, dict] = {}
|
|
177
|
+
for f in files:
|
|
178
|
+
for bucket, key in ((by_role, f["role"]), (by_ext, Path(f["path"]).suffix or "—")):
|
|
179
|
+
b = bucket.setdefault(key, {"files": 0, "bytes": 0, "loc": 0})
|
|
180
|
+
b["files"] += 1
|
|
181
|
+
b["bytes"] += f["bytes"]
|
|
182
|
+
b["loc"] += f["loc"]
|
|
183
|
+
largest = sorted(files, key=lambda f: (-f["bytes"], f["path"]))[:_LARGEST_N]
|
|
184
|
+
return {
|
|
185
|
+
"file_count": len(files),
|
|
186
|
+
"total_bytes": sum(f["bytes"] for f in files),
|
|
187
|
+
"loc_total": sum(f["loc"] for f in files),
|
|
188
|
+
"by_role": dict(sorted(by_role.items())),
|
|
189
|
+
"by_ext": dict(sorted(by_ext.items())),
|
|
190
|
+
"largest": [{"path": f["path"], "bytes": f["bytes"]} for f in largest],
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _git_block(root: Path, roots: list[tuple[str, str]], mode: str) -> dict:
|
|
195
|
+
commit = (_git(root, "rev-parse", "HEAD") or "").strip()
|
|
196
|
+
ref = (_git(root, "rev-parse", "--abbrev-ref", "HEAD") or "").strip()
|
|
197
|
+
specs = [rp for rp, _ in roots if rp]
|
|
198
|
+
status = _git(root, "status", "--porcelain", "--", *specs) or ""
|
|
199
|
+
dirty_files = sorted(line[3:].strip() for line in status.splitlines() if line.strip())
|
|
200
|
+
return {"mode": mode, "commit": commit, "ref": ref,
|
|
201
|
+
"dirty": bool(dirty_files), "dirty_files": dirty_files}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def diff_scopes(a: dict, b: dict) -> dict:
|
|
205
|
+
"""Added / removed / changed files between two scope manifests (by path+sha256)."""
|
|
206
|
+
am = {f["path"]: f["sha256"] for f in a.get("files", [])}
|
|
207
|
+
bm = {f["path"]: f["sha256"] for f in b.get("files", [])}
|
|
208
|
+
added = sorted(set(bm) - set(am))
|
|
209
|
+
removed = sorted(set(am) - set(bm))
|
|
210
|
+
changed = sorted(p for p in set(am) & set(bm) if am[p] != bm[p])
|
|
211
|
+
return {"added": added, "removed": removed, "changed": changed,
|
|
212
|
+
"same_scope_id": a.get("scope_id") == b.get("scope_id")}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Serve layer: views/reports over the canonical graph (DESIGN §4)."""
|
|
2
|
+
|
|
3
|
+
from codemap.serve.api_surface import render_api_surface
|
|
4
|
+
from codemap.serve.architecture import build_architecture, render_architecture
|
|
5
|
+
from codemap.serve.audit import render_behavior, render_dead_code, render_dependencies
|
|
6
|
+
from codemap.serve.impact import render_impact
|
|
7
|
+
from codemap.serve.mermaid import render_mermaid
|
|
8
|
+
from codemap.serve.rag import build_chunks, render_rag
|
|
9
|
+
from codemap.serve.session import Session, build_query_result
|
|
10
|
+
from codemap.serve.vault import build_vault
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"Session",
|
|
14
|
+
"build_query_result",
|
|
15
|
+
"render_api_surface",
|
|
16
|
+
"render_dependencies",
|
|
17
|
+
"render_dead_code",
|
|
18
|
+
"render_behavior",
|
|
19
|
+
"render_architecture",
|
|
20
|
+
"build_architecture",
|
|
21
|
+
"render_impact",
|
|
22
|
+
"render_mermaid",
|
|
23
|
+
"render_rag",
|
|
24
|
+
"build_chunks",
|
|
25
|
+
"build_vault",
|
|
26
|
+
]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# vendored SCIP protobuf bindings — generated from sourcegraph/scip scip.proto
|
|
2
|
+
# (regenerate: protoc --python_out=. scip.proto). The runtime-version guard was
|
|
3
|
+
# lowered to 5.26.0 so the optional `scip` extra works on protobuf>=5.26.
|
|
4
|
+
# -*- coding: utf-8 -*-
|
|
5
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
6
|
+
# NO CHECKED-IN PROTOBUF GENCODE
|
|
7
|
+
# source: scip.proto
|
|
8
|
+
# Protobuf Python Version: 7.35.1
|
|
9
|
+
"""Generated protocol buffer code."""
|
|
10
|
+
from google.protobuf import descriptor as _descriptor
|
|
11
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
12
|
+
from google.protobuf import runtime_version as _runtime_version
|
|
13
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
14
|
+
from google.protobuf.internal import builder as _builder
|
|
15
|
+
_runtime_version.ValidateProtobufRuntimeVersion(
|
|
16
|
+
_runtime_version.Domain.PUBLIC,
|
|
17
|
+
5,
|
|
18
|
+
26,
|
|
19
|
+
0,
|
|
20
|
+
'',
|
|
21
|
+
'scip.proto'
|
|
22
|
+
)
|
|
23
|
+
# @@protoc_insertion_point(imports)
|
|
24
|
+
|
|
25
|
+
_sym_db = _symbol_database.Default()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nscip.proto\x12\x04scip\"\x7f\n\x05Index\x12 \n\x08metadata\x18\x01 \x01(\x0b\x32\x0e.scip.Metadata\x12!\n\tdocuments\x18\x02 \x03(\x0b\x32\x0e.scip.Document\x12\x31\n\x10\x65xternal_symbols\x18\x03 \x03(\x0b\x32\x17.scip.SymbolInformation\"\x9f\x01\n\x08Metadata\x12&\n\x07version\x18\x01 \x01(\x0e\x32\x15.scip.ProtocolVersion\x12!\n\ttool_info\x18\x02 \x01(\x0b\x32\x0e.scip.ToolInfo\x12\x14\n\x0cproject_root\x18\x03 \x01(\t\x12\x32\n\x16text_document_encoding\x18\x04 \x01(\x0e\x32\x12.scip.TextEncoding\"<\n\x08ToolInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x11\n\targuments\x18\x03 \x03(\t\"\xc5\x01\n\x08\x44ocument\x12\x10\n\x08language\x18\x04 \x01(\t\x12\x15\n\rrelative_path\x18\x01 \x01(\t\x12%\n\x0boccurrences\x18\x02 \x03(\x0b\x32\x10.scip.Occurrence\x12(\n\x07symbols\x18\x03 \x03(\x0b\x32\x17.scip.SymbolInformation\x12\x0c\n\x04text\x18\x05 \x01(\t\x12\x31\n\x11position_encoding\x18\x06 \x01(\x0e\x32\x16.scip.PositionEncoding\"_\n\x06Symbol\x12\x0e\n\x06scheme\x18\x01 \x01(\t\x12\x1e\n\x07package\x18\x02 \x01(\x0b\x32\r.scip.Package\x12%\n\x0b\x64\x65scriptors\x18\x03 \x03(\x0b\x32\x10.scip.Descriptor\"9\n\x07Package\x12\x0f\n\x07manager\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0f\n\x07version\x18\x03 \x01(\t\"\x82\x02\n\nDescriptor\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x15\n\rdisambiguator\x18\x02 \x01(\t\x12\'\n\x06suffix\x18\x03 \x01(\x0e\x32\x17.scip.Descriptor.Suffix\"\xa5\x01\n\x06Suffix\x12\x15\n\x11UnspecifiedSuffix\x10\x00\x12\r\n\tNamespace\x10\x01\x12\x0f\n\x07Package\x10\x01\x1a\x02\x08\x01\x12\x08\n\x04Type\x10\x02\x12\x08\n\x04Term\x10\x03\x12\n\n\x06Method\x10\x04\x12\x11\n\rTypeParameter\x10\x05\x12\r\n\tParameter\x10\x06\x12\x08\n\x04Meta\x10\x07\x12\t\n\x05Local\x10\x08\x12\t\n\x05Macro\x10\t\x1a\x02\x10\x01\"d\n\tSignature\x12\x10\n\x08language\x18\x04 \x01(\t\x12\x0c\n\x04text\x18\x05 \x01(\t\x12%\n\x0boccurrences\x18\x02 \x03(\x0b\x32\x10.scip.OccurrenceJ\x04\x08\x01\x10\x02J\x04\x08\x03\x10\x04J\x04\x08\x06\x10\x07\"\xf1\x0b\n\x11SymbolInformation\x12\x0e\n\x06symbol\x18\x01 \x01(\t\x12\x15\n\rdocumentation\x18\x03 \x03(\t\x12)\n\rrelationships\x18\x04 \x03(\x0b\x32\x12.scip.Relationship\x12*\n\x04kind\x18\x05 \x01(\x0e\x32\x1c.scip.SymbolInformation.Kind\x12\x14\n\x0c\x64isplay_name\x18\x06 \x01(\t\x12\x30\n\x17signature_documentation\x18\x07 \x01(\x0b\x32\x0f.scip.Signature\x12\x18\n\x10\x65nclosing_symbol\x18\x08 \x01(\t\"\xfb\t\n\x04Kind\x12\x13\n\x0fUnspecifiedKind\x10\x00\x12\x12\n\x0e\x41\x62stractMethod\x10\x42\x12\x0c\n\x08\x41\x63\x63\x65ssor\x10H\x12\t\n\x05\x41rray\x10\x01\x12\r\n\tAssertion\x10\x02\x12\x12\n\x0e\x41ssociatedType\x10\x03\x12\r\n\tAttribute\x10\x04\x12\t\n\x05\x41xiom\x10\x05\x12\x0b\n\x07\x42oolean\x10\x06\x12\t\n\x05\x43lass\x10\x07\x12\x0b\n\x07\x43oncept\x10V\x12\x0c\n\x08\x43onstant\x10\x08\x12\x0f\n\x0b\x43onstructor\x10\t\x12\x0c\n\x08\x43ontract\x10>\x12\x0e\n\nDataFamily\x10\n\x12\x0c\n\x08\x44\x65legate\x10I\x12\x08\n\x04\x45num\x10\x0b\x12\x0e\n\nEnumMember\x10\x0c\x12\t\n\x05\x45rror\x10?\x12\t\n\x05\x45vent\x10\r\x12\r\n\tExtension\x10T\x12\x08\n\x04\x46\x61\x63t\x10\x0e\x12\t\n\x05\x46ield\x10\x0f\x12\x08\n\x04\x46ile\x10\x10\x12\x0c\n\x08\x46unction\x10\x11\x12\n\n\x06Getter\x10\x12\x12\x0b\n\x07Grammar\x10\x13\x12\x0c\n\x08Instance\x10\x14\x12\r\n\tInterface\x10\x15\x12\x07\n\x03Key\x10\x16\x12\x08\n\x04Lang\x10\x17\x12\t\n\x05Lemma\x10\x18\x12\x0b\n\x07Library\x10@\x12\t\n\x05Macro\x10\x19\x12\n\n\x06Method\x10\x1a\x12\x0f\n\x0bMethodAlias\x10J\x12\x12\n\x0eMethodReceiver\x10\x1b\x12\x17\n\x13MethodSpecification\x10\x43\x12\x0b\n\x07Message\x10\x1c\x12\t\n\x05Mixin\x10U\x12\x0c\n\x08Modifier\x10\x41\x12\n\n\x06Module\x10\x1d\x12\r\n\tNamespace\x10\x1e\x12\x08\n\x04Null\x10\x1f\x12\n\n\x06Number\x10 \x12\n\n\x06Object\x10!\x12\x0c\n\x08Operator\x10\"\x12\x0b\n\x07Package\x10#\x12\x11\n\rPackageObject\x10$\x12\r\n\tParameter\x10%\x12\x12\n\x0eParameterLabel\x10&\x12\x0b\n\x07Pattern\x10\'\x12\r\n\tPredicate\x10(\x12\x0c\n\x08Property\x10)\x12\x0c\n\x08Protocol\x10*\x12\x12\n\x0eProtocolMethod\x10\x44\x12\x15\n\x11PureVirtualMethod\x10\x45\x12\x0f\n\x0bQuasiquoter\x10+\x12\x11\n\rSelfParameter\x10,\x12\n\n\x06Setter\x10-\x12\r\n\tSignature\x10.\x12\x12\n\x0eSingletonClass\x10K\x12\x13\n\x0fSingletonMethod\x10L\x12\x14\n\x10StaticDataMember\x10M\x12\x0f\n\x0bStaticEvent\x10N\x12\x0f\n\x0bStaticField\x10O\x12\x10\n\x0cStaticMethod\x10P\x12\x12\n\x0eStaticProperty\x10Q\x12\x12\n\x0eStaticVariable\x10R\x12\n\n\x06String\x10\x30\x12\n\n\x06Struct\x10\x31\x12\r\n\tSubscript\x10/\x12\n\n\x06Tactic\x10\x32\x12\x0b\n\x07Theorem\x10\x33\x12\x11\n\rThisParameter\x10\x34\x12\t\n\x05Trait\x10\x35\x12\x0f\n\x0bTraitMethod\x10\x46\x12\x08\n\x04Type\x10\x36\x12\r\n\tTypeAlias\x10\x37\x12\r\n\tTypeClass\x10\x38\x12\x13\n\x0fTypeClassMethod\x10G\x12\x0e\n\nTypeFamily\x10\x39\x12\x11\n\rTypeParameter\x10:\x12\t\n\x05Union\x10;\x12\t\n\x05Value\x10<\x12\x0c\n\x08Variable\x10=\"\x82\x01\n\x0cRelationship\x12\x0e\n\x06symbol\x18\x01 \x01(\t\x12\x14\n\x0cis_reference\x18\x02 \x01(\x08\x12\x19\n\x11is_implementation\x18\x03 \x01(\x08\x12\x1a\n\x12is_type_definition\x18\x04 \x01(\x08\x12\x15\n\ris_definition\x18\x05 \x01(\x08\"O\n\x0fSingleLineRange\x12\x0c\n\x04line\x18\x01 \x01(\x05\x12\x17\n\x0fstart_character\x18\x02 \x01(\x05\x12\x15\n\rend_character\x18\x03 \x01(\x05\"f\n\x0eMultiLineRange\x12\x12\n\nstart_line\x18\x01 \x01(\x05\x12\x17\n\x0fstart_character\x18\x02 \x01(\x05\x12\x10\n\x08\x65nd_line\x18\x03 \x01(\x05\x12\x15\n\rend_character\x18\x04 \x01(\x05\"\xd8\x03\n\nOccurrence\x12\x11\n\x05range\x18\x01 \x03(\x05\x42\x02\x18\x01\x12\x32\n\x11single_line_range\x18\x08 \x01(\x0b\x32\x15.scip.SingleLineRangeH\x00\x12\x30\n\x10multi_line_range\x18\t \x01(\x0b\x32\x14.scip.MultiLineRangeH\x00\x12\x0e\n\x06symbol\x18\x02 \x01(\t\x12\x14\n\x0csymbol_roles\x18\x03 \x01(\x05\x12\x1e\n\x16override_documentation\x18\x04 \x03(\t\x12%\n\x0bsyntax_kind\x18\x05 \x01(\x0e\x32\x10.scip.SyntaxKind\x12%\n\x0b\x64iagnostics\x18\x06 \x03(\x0b\x32\x10.scip.Diagnostic\x12\x1b\n\x0f\x65nclosing_range\x18\x07 \x03(\x05\x42\x02\x18\x01\x12<\n\x1bsingle_line_enclosing_range\x18\n \x01(\x0b\x32\x15.scip.SingleLineRangeH\x01\x12:\n\x1amulti_line_enclosing_range\x18\x0b \x01(\x0b\x32\x14.scip.MultiLineRangeH\x01\x42\r\n\x0btyped_rangeB\x17\n\x15typed_enclosing_range\"\x80\x01\n\nDiagnostic\x12 \n\x08severity\x18\x01 \x01(\x0e\x32\x0e.scip.Severity\x12\x0c\n\x04\x63ode\x18\x02 \x01(\t\x12\x0f\n\x07message\x18\x03 \x01(\t\x12\x0e\n\x06source\x18\x04 \x01(\t\x12!\n\x04tags\x18\x05 \x03(\x0e\x32\x13.scip.DiagnosticTag*1\n\x0fProtocolVersion\x12\x1e\n\x1aUnspecifiedProtocolVersion\x10\x00*@\n\x0cTextEncoding\x12\x1b\n\x17UnspecifiedTextEncoding\x10\x00\x12\x08\n\x04UTF8\x10\x01\x12\t\n\x05UTF16\x10\x02*\xa4\x01\n\x10PositionEncoding\x12\x1f\n\x1bUnspecifiedPositionEncoding\x10\x00\x12#\n\x1fUTF8CodeUnitOffsetFromLineStart\x10\x01\x12$\n UTF16CodeUnitOffsetFromLineStart\x10\x02\x12$\n UTF32CodeUnitOffsetFromLineStart\x10\x03*\x94\x01\n\nSymbolRole\x12\x19\n\x15UnspecifiedSymbolRole\x10\x00\x12\x0e\n\nDefinition\x10\x01\x12\n\n\x06Import\x10\x02\x12\x0f\n\x0bWriteAccess\x10\x04\x12\x0e\n\nReadAccess\x10\x08\x12\r\n\tGenerated\x10\x10\x12\x08\n\x04Test\x10 \x12\x15\n\x11\x46orwardDefinition\x10@*\xea\x06\n\nSyntaxKind\x12\x19\n\x15UnspecifiedSyntaxKind\x10\x00\x12\x0b\n\x07\x43omment\x10\x01\x12\x18\n\x14PunctuationDelimiter\x10\x02\x12\x16\n\x12PunctuationBracket\x10\x03\x12\x0b\n\x07Keyword\x10\x04\x12\x19\n\x11IdentifierKeyword\x10\x04\x1a\x02\x08\x01\x12\x16\n\x12IdentifierOperator\x10\x05\x12\x0e\n\nIdentifier\x10\x06\x12\x15\n\x11IdentifierBuiltin\x10\x07\x12\x12\n\x0eIdentifierNull\x10\x08\x12\x16\n\x12IdentifierConstant\x10\t\x12\x1b\n\x17IdentifierMutableGlobal\x10\n\x12\x17\n\x13IdentifierParameter\x10\x0b\x12\x13\n\x0fIdentifierLocal\x10\x0c\x12\x16\n\x12IdentifierShadowed\x10\r\x12\x17\n\x13IdentifierNamespace\x10\x0e\x12\x18\n\x10IdentifierModule\x10\x0e\x1a\x02\x08\x01\x12\x16\n\x12IdentifierFunction\x10\x0f\x12 \n\x1cIdentifierFunctionDefinition\x10\x10\x12\x13\n\x0fIdentifierMacro\x10\x11\x12\x1d\n\x19IdentifierMacroDefinition\x10\x12\x12\x12\n\x0eIdentifierType\x10\x13\x12\x19\n\x15IdentifierBuiltinType\x10\x14\x12\x17\n\x13IdentifierAttribute\x10\x15\x12\x0f\n\x0bRegexEscape\x10\x16\x12\x11\n\rRegexRepeated\x10\x17\x12\x11\n\rRegexWildcard\x10\x18\x12\x12\n\x0eRegexDelimiter\x10\x19\x12\r\n\tRegexJoin\x10\x1a\x12\x11\n\rStringLiteral\x10\x1b\x12\x17\n\x13StringLiteralEscape\x10\x1c\x12\x18\n\x14StringLiteralSpecial\x10\x1d\x12\x14\n\x10StringLiteralKey\x10\x1e\x12\x14\n\x10\x43haracterLiteral\x10\x1f\x12\x12\n\x0eNumericLiteral\x10 \x12\x12\n\x0e\x42ooleanLiteral\x10!\x12\x07\n\x03Tag\x10\"\x12\x10\n\x0cTagAttribute\x10#\x12\x10\n\x0cTagDelimiter\x10$\x1a\x02\x10\x01*V\n\x08Severity\x12\x17\n\x13UnspecifiedSeverity\x10\x00\x12\t\n\x05\x45rror\x10\x01\x12\x0b\n\x07Warning\x10\x02\x12\x0f\n\x0bInformation\x10\x03\x12\x08\n\x04Hint\x10\x04*N\n\rDiagnosticTag\x12\x1c\n\x18UnspecifiedDiagnosticTag\x10\x00\x12\x0f\n\x0bUnnecessary\x10\x01\x12\x0e\n\nDeprecated\x10\x02*\xa5\n\n\x08Language\x12\x17\n\x13UnspecifiedLanguage\x10\x00\x12\x08\n\x04\x41\x42\x41P\x10<\x12\x08\n\x04\x41pex\x10`\x12\x07\n\x03\x41PL\x10\x31\x12\x07\n\x03\x41\x64\x61\x10\'\x12\x08\n\x04\x41gda\x10-\x12\x0c\n\x08\x41sciiDoc\x10V\x12\x0c\n\x08\x41ssembly\x10:\x12\x07\n\x03\x41wk\x10\x42\x12\x07\n\x03\x42\x61t\x10\x44\x12\n\n\x06\x42ibTeX\x10Q\x12\x05\n\x01\x43\x10\"\x12\t\n\x05\x43OBOL\x10;\x12\x07\n\x03\x43PP\x10#\x12\x07\n\x03\x43SS\x10\x1a\x12\n\n\x06\x43Sharp\x10\x01\x12\x0b\n\x07\x43lojure\x10\x08\x12\x10\n\x0c\x43offeescript\x10\x15\x12\x0e\n\nCommonLisp\x10\t\x12\x07\n\x03\x43oq\x10/\x12\x08\n\x04\x43UDA\x10\x61\x12\x08\n\x04\x44\x61rt\x10\x03\x12\n\n\x06\x44\x65lphi\x10\x39\x12\x08\n\x04\x44iff\x10X\x12\x0e\n\nDockerfile\x10P\x12\n\n\x06\x44yalog\x10\x32\x12\n\n\x06\x45lixir\x10\x11\x12\n\n\x06\x45rlang\x10\x12\x12\n\n\x06\x46Sharp\x10*\x12\x08\n\x04\x46ish\x10\x41\x12\x08\n\x04\x46low\x10\x18\x12\x0b\n\x07\x46ortran\x10\x38\x12\x0e\n\nGit_Commit\x10[\x12\x0e\n\nGit_Config\x10Y\x12\x0e\n\nGit_Rebase\x10\\\x12\x06\n\x02Go\x10!\x12\x0b\n\x07GraphQL\x10\x62\x12\n\n\x06Groovy\x10\x07\x12\x08\n\x04HTML\x10\x1e\x12\x08\n\x04Hack\x10\x14\x12\x0e\n\nHandlebars\x10Z\x12\x0b\n\x07Haskell\x10,\x12\t\n\x05Idris\x10.\x12\x07\n\x03Ini\x10H\x12\x05\n\x01J\x10\x33\x12\x08\n\x04JSON\x10K\x12\x08\n\x04Java\x10\x06\x12\x0e\n\nJavaScript\x10\x16\x12\x13\n\x0fJavaScriptReact\x10]\x12\x0b\n\x07Jsonnet\x10L\x12\t\n\x05Julia\x10\x37\x12\x0c\n\x08Justfile\x10m\x12\n\n\x06Kotlin\x10\x04\x12\t\n\x05LaTeX\x10S\x12\x08\n\x04Lean\x10\x30\x12\x08\n\x04Less\x10\x1b\x12\x07\n\x03Lua\x10\x0c\x12\x08\n\x04Luau\x10l\x12\x0c\n\x08Makefile\x10O\x12\x0c\n\x08Markdown\x10T\x12\n\n\x06Matlab\x10\x34\x12\n\n\x06Nickel\x10n\x12\x07\n\x03Nix\x10M\x12\t\n\x05OCaml\x10)\x12\x0f\n\x0bObjective_C\x10$\x12\x11\n\rObjective_CPP\x10%\x12\x08\n\x04Odin\x10o\x12\n\n\x06Pascal\x10\x63\x12\x07\n\x03PHP\x10\x13\x12\t\n\x05PLSQL\x10\x46\x12\x08\n\x04Perl\x10\r\x12\x0e\n\nPowerShell\x10\x43\x12\n\n\x06Prolog\x10G\x12\x0c\n\x08Protobuf\x10\x64\x12\n\n\x06Python\x10\x0f\x12\x05\n\x01R\x10\x36\x12\n\n\x06Racket\x10\x0b\x12\x08\n\x04Raku\x10\x0e\x12\t\n\x05Razor\x10>\x12\t\n\x05Repro\x10\x66\x12\x08\n\x04ReST\x10U\x12\x08\n\x04Ruby\x10\x10\x12\x08\n\x04Rust\x10(\x12\x07\n\x03SAS\x10=\x12\x08\n\x04SCSS\x10\x1d\x12\x07\n\x03SML\x10+\x12\x07\n\x03SQL\x10\x45\x12\x08\n\x04Sass\x10\x1c\x12\t\n\x05Scala\x10\x05\x12\n\n\x06Scheme\x10\n\x12\x0f\n\x0bShellScript\x10@\x12\x0b\n\x07Skylark\x10N\x12\t\n\x05Slang\x10k\x12\x0c\n\x08Solidity\x10_\x12\n\n\x06Svelte\x10j\x12\t\n\x05Swift\x10\x02\x12\x07\n\x03Tcl\x10\x65\x12\x08\n\x04TOML\x10I\x12\x07\n\x03TeX\x10R\x12\n\n\x06Thrift\x10g\x12\x0e\n\nTypeScript\x10\x17\x12\x13\n\x0fTypeScriptReact\x10^\x12\x0b\n\x07Verilog\x10h\x12\x08\n\x04VHDL\x10i\x12\x0f\n\x0bVisualBasic\x10?\x12\x07\n\x03Vue\x10\x19\x12\x0b\n\x07Wolfram\x10\x35\x12\x07\n\x03XML\x10\x1f\x12\x07\n\x03XSL\x10 \x12\x08\n\x04YAML\x10J\x12\x07\n\x03Zig\x10&BN\n\x12org.scip_code.scipB\tScipProtoP\x01Z+github.com/scip-code/scip/bindings/go/scip/b\x06proto3')
|
|
31
|
+
|
|
32
|
+
_globals = globals()
|
|
33
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
34
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'scip_pb2', _globals)
|
|
35
|
+
if not _descriptor._USE_C_DESCRIPTORS:
|
|
36
|
+
_globals['DESCRIPTOR']._loaded_options = None
|
|
37
|
+
_globals['DESCRIPTOR']._serialized_options = b'\n\022org.scip_code.scipB\tScipProtoP\001Z+github.com/scip-code/scip/bindings/go/scip/'
|
|
38
|
+
_globals['_SYNTAXKIND']._loaded_options = None
|
|
39
|
+
_globals['_SYNTAXKIND']._serialized_options = b'\020\001'
|
|
40
|
+
_globals['_SYNTAXKIND'].values_by_name["IdentifierKeyword"]._loaded_options = None
|
|
41
|
+
_globals['_SYNTAXKIND'].values_by_name["IdentifierKeyword"]._serialized_options = b'\010\001'
|
|
42
|
+
_globals['_SYNTAXKIND'].values_by_name["IdentifierModule"]._loaded_options = None
|
|
43
|
+
_globals['_SYNTAXKIND'].values_by_name["IdentifierModule"]._serialized_options = b'\010\001'
|
|
44
|
+
_globals['_DESCRIPTOR_SUFFIX']._loaded_options = None
|
|
45
|
+
_globals['_DESCRIPTOR_SUFFIX']._serialized_options = b'\020\001'
|
|
46
|
+
_globals['_DESCRIPTOR_SUFFIX'].values_by_name["Package"]._loaded_options = None
|
|
47
|
+
_globals['_DESCRIPTOR_SUFFIX'].values_by_name["Package"]._serialized_options = b'\010\001'
|
|
48
|
+
_globals['_OCCURRENCE'].fields_by_name['range']._loaded_options = None
|
|
49
|
+
_globals['_OCCURRENCE'].fields_by_name['range']._serialized_options = b'\030\001'
|
|
50
|
+
_globals['_OCCURRENCE'].fields_by_name['enclosing_range']._loaded_options = None
|
|
51
|
+
_globals['_OCCURRENCE'].fields_by_name['enclosing_range']._serialized_options = b'\030\001'
|
|
52
|
+
_globals['_PROTOCOLVERSION']._serialized_start=3540
|
|
53
|
+
_globals['_PROTOCOLVERSION']._serialized_end=3589
|
|
54
|
+
_globals['_TEXTENCODING']._serialized_start=3591
|
|
55
|
+
_globals['_TEXTENCODING']._serialized_end=3655
|
|
56
|
+
_globals['_POSITIONENCODING']._serialized_start=3658
|
|
57
|
+
_globals['_POSITIONENCODING']._serialized_end=3822
|
|
58
|
+
_globals['_SYMBOLROLE']._serialized_start=3825
|
|
59
|
+
_globals['_SYMBOLROLE']._serialized_end=3973
|
|
60
|
+
_globals['_SYNTAXKIND']._serialized_start=3976
|
|
61
|
+
_globals['_SYNTAXKIND']._serialized_end=4850
|
|
62
|
+
_globals['_SEVERITY']._serialized_start=4852
|
|
63
|
+
_globals['_SEVERITY']._serialized_end=4938
|
|
64
|
+
_globals['_DIAGNOSTICTAG']._serialized_start=4940
|
|
65
|
+
_globals['_DIAGNOSTICTAG']._serialized_end=5018
|
|
66
|
+
_globals['_LANGUAGE']._serialized_start=5021
|
|
67
|
+
_globals['_LANGUAGE']._serialized_end=6338
|
|
68
|
+
_globals['_INDEX']._serialized_start=20
|
|
69
|
+
_globals['_INDEX']._serialized_end=147
|
|
70
|
+
_globals['_METADATA']._serialized_start=150
|
|
71
|
+
_globals['_METADATA']._serialized_end=309
|
|
72
|
+
_globals['_TOOLINFO']._serialized_start=311
|
|
73
|
+
_globals['_TOOLINFO']._serialized_end=371
|
|
74
|
+
_globals['_DOCUMENT']._serialized_start=374
|
|
75
|
+
_globals['_DOCUMENT']._serialized_end=571
|
|
76
|
+
_globals['_SYMBOL']._serialized_start=573
|
|
77
|
+
_globals['_SYMBOL']._serialized_end=668
|
|
78
|
+
_globals['_PACKAGE']._serialized_start=670
|
|
79
|
+
_globals['_PACKAGE']._serialized_end=727
|
|
80
|
+
_globals['_DESCRIPTOR']._serialized_start=730
|
|
81
|
+
_globals['_DESCRIPTOR']._serialized_end=988
|
|
82
|
+
_globals['_DESCRIPTOR_SUFFIX']._serialized_start=823
|
|
83
|
+
_globals['_DESCRIPTOR_SUFFIX']._serialized_end=988
|
|
84
|
+
_globals['_SIGNATURE']._serialized_start=990
|
|
85
|
+
_globals['_SIGNATURE']._serialized_end=1090
|
|
86
|
+
_globals['_SYMBOLINFORMATION']._serialized_start=1093
|
|
87
|
+
_globals['_SYMBOLINFORMATION']._serialized_end=2614
|
|
88
|
+
_globals['_SYMBOLINFORMATION_KIND']._serialized_start=1339
|
|
89
|
+
_globals['_SYMBOLINFORMATION_KIND']._serialized_end=2614
|
|
90
|
+
_globals['_RELATIONSHIP']._serialized_start=2617
|
|
91
|
+
_globals['_RELATIONSHIP']._serialized_end=2747
|
|
92
|
+
_globals['_SINGLELINERANGE']._serialized_start=2749
|
|
93
|
+
_globals['_SINGLELINERANGE']._serialized_end=2828
|
|
94
|
+
_globals['_MULTILINERANGE']._serialized_start=2830
|
|
95
|
+
_globals['_MULTILINERANGE']._serialized_end=2932
|
|
96
|
+
_globals['_OCCURRENCE']._serialized_start=2935
|
|
97
|
+
_globals['_OCCURRENCE']._serialized_end=3407
|
|
98
|
+
_globals['_DIAGNOSTIC']._serialized_start=3410
|
|
99
|
+
_globals['_DIAGNOSTIC']._serialized_end=3538
|
|
100
|
+
# @@protoc_insertion_point(module_scope)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""API-surface report — view D (DESIGN §4.1-D), the M0 deliverable.
|
|
2
|
+
|
|
3
|
+
The public surface of the target: public symbols grouped by module, with
|
|
4
|
+
signatures, first docstring line and a deprecated marker. Reads the canonical
|
|
5
|
+
graph; renders Markdown.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
|
|
12
|
+
from codemap.model import Graph
|
|
13
|
+
|
|
14
|
+
_SYMBOL_KINDS = {"class", "function", "attribute"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def render_api_surface(graph: Graph) -> str:
|
|
18
|
+
"""Render the public API surface of ``graph`` as Markdown."""
|
|
19
|
+
by_module: dict[str, list] = defaultdict(list)
|
|
20
|
+
for node in graph.nodes.values():
|
|
21
|
+
if node.visibility != "public" or node.kind not in _SYMBOL_KINDS:
|
|
22
|
+
continue
|
|
23
|
+
module = node.id.rsplit(".", 1)[0]
|
|
24
|
+
by_module[module].append(node)
|
|
25
|
+
|
|
26
|
+
lines = [f"# API surface — `{graph.target}`", ""]
|
|
27
|
+
public_modules = sorted(
|
|
28
|
+
n.id for n in graph.nodes.values() if n.kind == "module" and n.visibility == "public"
|
|
29
|
+
)
|
|
30
|
+
total = sum(len(v) for v in by_module.values())
|
|
31
|
+
lines.append(f"_{total} public symbols across {len(public_modules)} modules._")
|
|
32
|
+
lines.append("")
|
|
33
|
+
|
|
34
|
+
for module in public_modules:
|
|
35
|
+
symbols = sorted(by_module.get(module, []), key=lambda n: n.id)
|
|
36
|
+
if not symbols:
|
|
37
|
+
continue
|
|
38
|
+
lines.append(f"## `{module}`")
|
|
39
|
+
lines.append("")
|
|
40
|
+
for node in symbols:
|
|
41
|
+
name = node.id.rsplit(".", 1)[1]
|
|
42
|
+
head = node.signature or name
|
|
43
|
+
marker = " **⚠ deprecated**" if node.is_deprecated else ""
|
|
44
|
+
lines.append(f"- **`{head}`** ({node.kind}){marker}")
|
|
45
|
+
doc = _first_line(node.docstring)
|
|
46
|
+
if doc:
|
|
47
|
+
lines.append(f" - {doc}")
|
|
48
|
+
lines.append("")
|
|
49
|
+
|
|
50
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _first_line(docstring: str | None) -> str | None:
|
|
54
|
+
if not docstring:
|
|
55
|
+
return None
|
|
56
|
+
for line in docstring.strip().splitlines():
|
|
57
|
+
line = line.strip()
|
|
58
|
+
if line:
|
|
59
|
+
return line
|
|
60
|
+
return None
|
codemap/serve/apidiff.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Render the two-graph API diff (R1-C5) — structured + markdown.
|
|
2
|
+
|
|
3
|
+
The engine lives in ``codemap.apidiff``; this is the presentation shared by the
|
|
4
|
+
``diff`` CLI command and the ``diff`` serve op. Breaking changes lead (that is what
|
|
5
|
+
fails a release gate); removed public symbols are listed with them, since a deleted
|
|
6
|
+
symbol is itself breaking.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from codemap.apidiff import ApiDiff, BREAKING, INFO, WARNING, diff_api
|
|
12
|
+
from codemap.model import Graph
|
|
13
|
+
|
|
14
|
+
_CAP = 40
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_apidiff(old: Graph, new: Graph) -> dict:
|
|
18
|
+
"""Structured API diff: {old_target, new_target, ...ApiDiff.to_dict()}."""
|
|
19
|
+
d = diff_api(old, new).to_dict()
|
|
20
|
+
d["old_target"] = old.target
|
|
21
|
+
d["new_target"] = new.target
|
|
22
|
+
# a removed public symbol is itself a breaking change — fold into the count.
|
|
23
|
+
d["summary"]["breaking_total"] = d["summary"]["breaking"] + d["summary"]["removed"]
|
|
24
|
+
d["ok"] = d["summary"]["breaking_total"] == 0
|
|
25
|
+
# R1-C25/D4: the envelope says whether the pair is a before/after of the code at all.
|
|
26
|
+
from codemap.provenance import comparability
|
|
27
|
+
d["provenance"] = comparability(old.provenance, new.provenance)
|
|
28
|
+
return d
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _bullets(items, cap=_CAP):
|
|
32
|
+
for x in items[:cap]:
|
|
33
|
+
yield f"- `{x}`"
|
|
34
|
+
if len(items) > cap:
|
|
35
|
+
yield f"- _… {len(items) - cap} more_"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def render_apidiff(old: Graph, new: Graph) -> str:
|
|
39
|
+
d = build_apidiff(old, new)
|
|
40
|
+
s = d["summary"]
|
|
41
|
+
out = [f"# API diff — `{old.target}` → `{new.target}`", ""]
|
|
42
|
+
verdict = ("✅ **No breaking changes.**" if d["ok"]
|
|
43
|
+
else f"❌ **{s['breaking_total']} breaking change(s).**")
|
|
44
|
+
out.append(f"{verdict} {s['added']} added, {s['removed']} removed, "
|
|
45
|
+
f"{s['changed_symbols']} changed.")
|
|
46
|
+
out.append("")
|
|
47
|
+
# R1-C25/D4: a verdict about the code is only a verdict about the code when both
|
|
48
|
+
# graphs came from the same tool. Say so above the verdict, not in a footnote.
|
|
49
|
+
prov = d.get("provenance") or {}
|
|
50
|
+
if prov and not prov.get("comparable", True):
|
|
51
|
+
out.append("> ⚠️ These graphs are not directly comparable: "
|
|
52
|
+
+ "; ".join(prov["differences"])
|
|
53
|
+
+ f". Old: {prov['old']} | new: {prov['new']}. "
|
|
54
|
+
"Differences below may be tool changes, not code changes.")
|
|
55
|
+
out.append("")
|
|
56
|
+
|
|
57
|
+
removed = d["removed"]
|
|
58
|
+
if removed:
|
|
59
|
+
out.append(f"## Removed public symbols — breaking ({len(removed)})")
|
|
60
|
+
out.extend(_bullets(removed))
|
|
61
|
+
out.append("")
|
|
62
|
+
|
|
63
|
+
by_sev = {BREAKING: [], WARNING: [], INFO: []}
|
|
64
|
+
for c in d["changes"]:
|
|
65
|
+
by_sev[c["severity"]].append(c)
|
|
66
|
+
labels = {BREAKING: "Breaking signature changes", WARNING: "Warnings (review)",
|
|
67
|
+
INFO: "Compatible changes"}
|
|
68
|
+
for sev in (BREAKING, WARNING, INFO):
|
|
69
|
+
rows = by_sev[sev]
|
|
70
|
+
if not rows:
|
|
71
|
+
continue
|
|
72
|
+
out.append(f"## {labels[sev]} ({len(rows)})")
|
|
73
|
+
for c in rows[:_CAP]:
|
|
74
|
+
out.append(f"- `{c['symbol']}` — {c['detail']}")
|
|
75
|
+
if len(rows) > _CAP:
|
|
76
|
+
out.append(f"- _… {len(rows) - _CAP} more_")
|
|
77
|
+
out.append("")
|
|
78
|
+
|
|
79
|
+
if d["added"]:
|
|
80
|
+
out.append(f"## Added public symbols ({len(d['added'])})")
|
|
81
|
+
out.extend(_bullets(d["added"]))
|
|
82
|
+
out.append("")
|
|
83
|
+
return "\n".join(out).rstrip() + "\n"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Architecture overview — the whole-system shape in one view (M16 / A9).
|
|
2
|
+
|
|
3
|
+
The A9 dogfood found every local view existed (symbol, diff, column) but no
|
|
4
|
+
*global* one: an architect asking "what shape is this system?" had only
|
|
5
|
+
``report dependencies`` (import cycles + in-degree). This synthesises the pieces
|
|
6
|
+
already in the graph — import cycles, **layers** + direction/violations (F18),
|
|
7
|
+
**coupling** Ca/Ce/instability (F19), **god-objects & call-hubs** (F20) — into one
|
|
8
|
+
report. No schema change: pure aggregation over the import graph / calls / contains
|
|
9
|
+
/ provenance.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from codemap.diagnostics import render_lines
|
|
15
|
+
from codemap.query import Query
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_architecture(query: Query) -> dict:
|
|
19
|
+
"""Structured whole-system overview (cycles + layers + coupling + hotspots)."""
|
|
20
|
+
return {
|
|
21
|
+
"target": query.graph.target,
|
|
22
|
+
"cycles": query.import_cycles(),
|
|
23
|
+
"layers": query.layers(),
|
|
24
|
+
"coupling": query.coupling(),
|
|
25
|
+
"hotspots": query.hotspots(),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def render_architecture(query: Query) -> str:
|
|
30
|
+
"""Human markdown for the architecture overview (highest-signal first)."""
|
|
31
|
+
a = build_architecture(query)
|
|
32
|
+
ig = query.import_graph
|
|
33
|
+
core_mods = [m for m in ig.nodes if query.root_of(m) == "core"]
|
|
34
|
+
out = [f"# Architecture overview — `{a['target']}`", ""]
|
|
35
|
+
out.append(f"_{len(core_mods)} core modules, {ig.number_of_edges()} import edges._")
|
|
36
|
+
out.append("")
|
|
37
|
+
# R1-C21: with an empty import graph, "no layer violations" and "acyclic" below are
|
|
38
|
+
# *vacuous*, not clean. Each check states its own consequence (issue #8).
|
|
39
|
+
out.extend(render_lines(query.graph))
|
|
40
|
+
|
|
41
|
+
# -- layers -------------------------------------------------------------
|
|
42
|
+
lay = a["layers"]
|
|
43
|
+
out.append(f"## Layers ({len(lay['layers'])})")
|
|
44
|
+
out.append("")
|
|
45
|
+
for name, mods in lay["layers"].items():
|
|
46
|
+
out.append(f"- **{name}** — {len(mods)} module(s)")
|
|
47
|
+
out.append("")
|
|
48
|
+
out.append("### Inter-layer dependencies")
|
|
49
|
+
out.append("")
|
|
50
|
+
out.extend([f"- {edge} ({n})" for edge, n in lay["edges"].items()] or ["_none._"])
|
|
51
|
+
out.append("")
|
|
52
|
+
if lay["violations"]:
|
|
53
|
+
out.append("### ⚠ Layer violations (mutual dependency)")
|
|
54
|
+
out.append("")
|
|
55
|
+
out.extend(f"- {a} ↔ {b}" for a, b in lay["violations"])
|
|
56
|
+
else:
|
|
57
|
+
out.append("_No layer violations (no mutually-dependent layer pair)._")
|
|
58
|
+
out.append("")
|
|
59
|
+
|
|
60
|
+
# -- cycles -------------------------------------------------------------
|
|
61
|
+
out.append(f"## Import cycles: {len(a['cycles'])}")
|
|
62
|
+
out.append("")
|
|
63
|
+
out.extend([f"- {' → '.join(c)} → {c[0]}" for c in
|
|
64
|
+
sorted(a["cycles"], key=lambda c: (len(c), c))]
|
|
65
|
+
or ["_none — import graph is acyclic._"])
|
|
66
|
+
out.append("")
|
|
67
|
+
|
|
68
|
+
# -- coupling -----------------------------------------------------------
|
|
69
|
+
out.append("## Coupling (top by afferent Ca)")
|
|
70
|
+
out.append("")
|
|
71
|
+
out.append("_Ca = depended-on-by, Ce = depends-on, I = Ce/(Ca+Ce): 0 stable → 1 unstable._")
|
|
72
|
+
out.append("")
|
|
73
|
+
for r in a["coupling"][:12]:
|
|
74
|
+
out.append(f"- `{r['module']}` — Ca {r['ca']}, Ce {r['ce']}, I {r['instability']:.2f}")
|
|
75
|
+
out.append("")
|
|
76
|
+
|
|
77
|
+
# -- hotspots -----------------------------------------------------------
|
|
78
|
+
hs = a["hotspots"]
|
|
79
|
+
out.append(f"## God-object candidates (≥ methods): {len(hs['god_classes'])}")
|
|
80
|
+
out.append("")
|
|
81
|
+
out.append("_methods = concentration of behaviour; ΣCC / maxCC = McCabe complexity across them._")
|
|
82
|
+
out.append("")
|
|
83
|
+
out.extend([f"- `{g['class']}` — {g['methods']} methods, ΣCC {g['total_cc']}, maxCC {g['max_cc']}"
|
|
84
|
+
for g in hs["god_classes"]] or ["_none above threshold._"])
|
|
85
|
+
out.append("")
|
|
86
|
+
complex_fns = hs.get("complex_functions", [])
|
|
87
|
+
out.append(f"## Most complex functions (cyclomatic ≥ threshold): {len(complex_fns)}")
|
|
88
|
+
out.append("")
|
|
89
|
+
out.append("_CC = McCabe cyclomatic; MI = Maintainability Index (0–100, higher is better)._")
|
|
90
|
+
out.append("")
|
|
91
|
+
out.extend([f"- `{f['id']}` — CC {f['cc']}, MI {f['mi']} ({f['sloc']} sloc)"
|
|
92
|
+
for f in complex_fns] or ["_none above threshold._"])
|
|
93
|
+
out.append("")
|
|
94
|
+
out.append("## Call-graph hubs (in+out degree)")
|
|
95
|
+
out.append("")
|
|
96
|
+
out.append("_`pervasive` = logging/util that hubs by nature — expected noise, not risk._")
|
|
97
|
+
out.append("")
|
|
98
|
+
for h in hs["call_hubs"][:12]:
|
|
99
|
+
tag = " _(pervasive)_" if h["pervasive"] else ""
|
|
100
|
+
out.append(f"- `{h['id']}` — {h['degree']}{tag}")
|
|
101
|
+
return "\n".join(out).rstrip() + "\n"
|