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.
Files changed (55) hide show
  1. codemap/__init__.py +10 -0
  2. codemap/apidiff.py +208 -0
  3. codemap/arch.py +190 -0
  4. codemap/cli.py +718 -0
  5. codemap/diagnostics.py +256 -0
  6. codemap/extract/__init__.py +10 -0
  7. codemap/extract/attrflow.py +230 -0
  8. codemap/extract/behavior.py +771 -0
  9. codemap/extract/dataflow.py +97 -0
  10. codemap/extract/dispatch.py +248 -0
  11. codemap/extract/griffe_extractor.py +496 -0
  12. codemap/extract/gsource.py +83 -0
  13. codemap/extract/roots.py +427 -0
  14. codemap/freshness.py +94 -0
  15. codemap/incremental.py +195 -0
  16. codemap/integrations/__init__.py +51 -0
  17. codemap/integrations/base.py +196 -0
  18. codemap/integrations/cocoindex.py +78 -0
  19. codemap/integrations/gate.py +58 -0
  20. codemap/integrations/gitnexus.py +93 -0
  21. codemap/integrations/registry.py +69 -0
  22. codemap/integrations/transport.py +46 -0
  23. codemap/model.py +178 -0
  24. codemap/provenance.py +248 -0
  25. codemap/query.py +1164 -0
  26. codemap/scope.py +212 -0
  27. codemap/serve/__init__.py +26 -0
  28. codemap/serve/_scip_pb2.py +100 -0
  29. codemap/serve/api_surface.py +60 -0
  30. codemap/serve/apidiff.py +83 -0
  31. codemap/serve/architecture.py +101 -0
  32. codemap/serve/audit.py +176 -0
  33. codemap/serve/check.py +80 -0
  34. codemap/serve/ctags.py +203 -0
  35. codemap/serve/impact.py +84 -0
  36. codemap/serve/livingdocs.py +174 -0
  37. codemap/serve/mcp_server.py +278 -0
  38. codemap/serve/mermaid.py +120 -0
  39. codemap/serve/pack.py +93 -0
  40. codemap/serve/rag.py +142 -0
  41. codemap/serve/review.py +197 -0
  42. codemap/serve/scip.py +183 -0
  43. codemap/serve/semantic.py +71 -0
  44. codemap/serve/server.py +43 -0
  45. codemap/serve/session.py +482 -0
  46. codemap/serve/subsystems.py +85 -0
  47. codemap/serve/vault.py +156 -0
  48. codemap/store.py +28 -0
  49. codemap/tomlio.py +59 -0
  50. codmap-0.0.3.dist-info/METADATA +245 -0
  51. codmap-0.0.3.dist-info/RECORD +55 -0
  52. codmap-0.0.3.dist-info/WHEEL +5 -0
  53. codmap-0.0.3.dist-info/entry_points.txt +2 -0
  54. codmap-0.0.3.dist-info/licenses/LICENSE +21 -0
  55. 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
@@ -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"