cg-code-graph 0.10.1__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 (174) hide show
  1. cg_code_graph-0.10.1.dist-info/METADATA +678 -0
  2. cg_code_graph-0.10.1.dist-info/RECORD +174 -0
  3. cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
  4. cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
  5. cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
  6. cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
  7. codegraph/__init__.py +2 -0
  8. codegraph/aitools.py +129 -0
  9. codegraph/apps.py +76 -0
  10. codegraph/blindspots.py +428 -0
  11. codegraph/bridges.py +1701 -0
  12. codegraph/cli.py +725 -0
  13. codegraph/concepts.py +362 -0
  14. codegraph/config.py +559 -0
  15. codegraph/core/__init__.py +0 -0
  16. codegraph/core/cache.py +375 -0
  17. codegraph/core/detect.py +80 -0
  18. codegraph/core/extractors.py +187 -0
  19. codegraph/core/fsutil.py +61 -0
  20. codegraph/core/generated.py +575 -0
  21. codegraph/core/model.py +174 -0
  22. codegraph/core/paths.py +175 -0
  23. codegraph/core/plugin.py +160 -0
  24. codegraph/core/store.py +80 -0
  25. codegraph/core/syntax_errors.py +132 -0
  26. codegraph/coverage.py +928 -0
  27. codegraph/doctor.py +453 -0
  28. codegraph/external.py +613 -0
  29. codegraph/indexer.py +336 -0
  30. codegraph/link.py +434 -0
  31. codegraph/lint_async.py +524 -0
  32. codegraph/mcp_server.py +1303 -0
  33. codegraph/parity.py +473 -0
  34. codegraph/parity_structure.py +307 -0
  35. codegraph/payload.py +321 -0
  36. codegraph/plans.py +1285 -0
  37. codegraph/platform_scan.py +643 -0
  38. codegraph/platforms.py +1369 -0
  39. codegraph/plugins/__init__.py +0 -0
  40. codegraph/plugins/cfamily/__init__.py +0 -0
  41. codegraph/plugins/cfamily/plugin.py +930 -0
  42. codegraph/plugins/cfamily/syntax.py +881 -0
  43. codegraph/plugins/dart/__init__.py +0 -0
  44. codegraph/plugins/dart/bridges.py +345 -0
  45. codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
  46. codegraph/plugins/dart/extractor/pubspec.lock +149 -0
  47. codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
  48. codegraph/plugins/dart/http.py +904 -0
  49. codegraph/plugins/dart/models.py +308 -0
  50. codegraph/plugins/dart/plugin.py +625 -0
  51. codegraph/plugins/dart/program.py +907 -0
  52. codegraph/plugins/django/__init__.py +0 -0
  53. codegraph/plugins/django/extras.py +378 -0
  54. codegraph/plugins/django/models.py +508 -0
  55. codegraph/plugins/django/plugin.py +728 -0
  56. codegraph/plugins/django/schemas.py +339 -0
  57. codegraph/plugins/django/shapes.py +216 -0
  58. codegraph/plugins/django/urls.py +603 -0
  59. codegraph/plugins/express/__init__.py +0 -0
  60. codegraph/plugins/express/plugin.py +428 -0
  61. codegraph/plugins/flutter/__init__.py +0 -0
  62. codegraph/plugins/flutter/plugin.py +538 -0
  63. codegraph/plugins/kotlin/__init__.py +0 -0
  64. codegraph/plugins/kotlin/exact.py +457 -0
  65. codegraph/plugins/kotlin/plugin.py +1961 -0
  66. codegraph/plugins/kotlin/reparse.py +234 -0
  67. codegraph/plugins/laravel/__init__.py +0 -0
  68. codegraph/plugins/laravel/broadcast.py +351 -0
  69. codegraph/plugins/laravel/plugin.py +863 -0
  70. codegraph/plugins/laravel/tests.py +262 -0
  71. codegraph/plugins/laravel/values.py +728 -0
  72. codegraph/plugins/native/__init__.py +0 -0
  73. codegraph/plugins/native/gates.py +286 -0
  74. codegraph/plugins/native/runner.py +183 -0
  75. codegraph/plugins/native/scipread.py +194 -0
  76. codegraph/plugins/native/ts.py +54 -0
  77. codegraph/plugins/nest/__init__.py +0 -0
  78. codegraph/plugins/nest/plugin.py +654 -0
  79. codegraph/plugins/nextjs/__init__.py +0 -0
  80. codegraph/plugins/nextjs/plugin.py +336 -0
  81. codegraph/plugins/nuxt/__init__.py +0 -0
  82. codegraph/plugins/nuxt/plugin.py +308 -0
  83. codegraph/plugins/php/__init__.py +0 -0
  84. codegraph/plugins/php/extractor/composer.json +5 -0
  85. codegraph/plugins/php/extractor/composer.lock +76 -0
  86. codegraph/plugins/php/extractor/extract.php +743 -0
  87. codegraph/plugins/php/gating.py +573 -0
  88. codegraph/plugins/php/plugin.py +668 -0
  89. codegraph/plugins/php/strings.py +197 -0
  90. codegraph/plugins/python/__init__.py +0 -0
  91. codegraph/plugins/python/aitools.py +664 -0
  92. codegraph/plugins/python/external.py +245 -0
  93. codegraph/plugins/python/fields.py +107 -0
  94. codegraph/plugins/python/plugin.py +1733 -0
  95. codegraph/plugins/python/refs.py +485 -0
  96. codegraph/plugins/python/roots.py +412 -0
  97. codegraph/plugins/python/socketio.py +210 -0
  98. codegraph/plugins/python/subproc.py +864 -0
  99. codegraph/plugins/python/tests.py +1040 -0
  100. codegraph/plugins/python/values.py +179 -0
  101. codegraph/plugins/pyweb/__init__.py +0 -0
  102. codegraph/plugins/pyweb/plugin.py +1334 -0
  103. codegraph/plugins/pyweb/values.py +68 -0
  104. codegraph/plugins/rust/__init__.py +0 -0
  105. codegraph/plugins/rust/cargo.py +226 -0
  106. codegraph/plugins/rust/plugin.py +980 -0
  107. codegraph/plugins/rust/syntax.py +678 -0
  108. codegraph/plugins/scip/__init__.py +0 -0
  109. codegraph/plugins/scip/importer.py +129 -0
  110. codegraph/plugins/scip/scip.proto +962 -0
  111. codegraph/plugins/scip/scip_pb2.py +97 -0
  112. codegraph/plugins/stubs/__init__.py +0 -0
  113. codegraph/plugins/stubs/plugins.py +38 -0
  114. codegraph/plugins/swift/__init__.py +0 -0
  115. codegraph/plugins/swift/baseurl.py +109 -0
  116. codegraph/plugins/swift/exact.py +415 -0
  117. codegraph/plugins/swift/indexstore.py +209 -0
  118. codegraph/plugins/swift/packages.py +174 -0
  119. codegraph/plugins/swift/plugin.py +2890 -0
  120. codegraph/plugins/ts/__init__.py +0 -0
  121. codegraph/plugins/ts/baseurl.py +185 -0
  122. codegraph/plugins/ts/extractor/extract.mjs +2652 -0
  123. codegraph/plugins/ts/extractor/fw.mjs +685 -0
  124. codegraph/plugins/ts/extractor/package-lock.json +205 -0
  125. codegraph/plugins/ts/extractor/package.json +9 -0
  126. codegraph/plugins/ts/plugin.py +480 -0
  127. codegraph/plugins/tsweb/__init__.py +0 -0
  128. codegraph/plugins/tsweb/common.py +290 -0
  129. codegraph/plugins/tsweb/data.py +276 -0
  130. codegraph/presets/__init__.py +146 -0
  131. codegraph/presets/c_cpp.yaml +9 -0
  132. codegraph/presets/common.yaml +66 -0
  133. codegraph/presets/dart.yaml +9 -0
  134. codegraph/presets/django-ninja.yaml +15 -0
  135. codegraph/presets/django.yaml +25 -0
  136. codegraph/presets/djangorestframework.yaml +17 -0
  137. codegraph/presets/express.yaml +17 -0
  138. codegraph/presets/kotlin.yaml +11 -0
  139. codegraph/presets/laravel.yaml +40 -0
  140. codegraph/presets/nest.yaml +11 -0
  141. codegraph/presets/nextjs.yaml +15 -0
  142. codegraph/presets/nuxt.yaml +9 -0
  143. codegraph/presets/php.yaml +5 -0
  144. codegraph/presets/python.yaml +10 -0
  145. codegraph/presets/rust.yaml +5 -0
  146. codegraph/presets/swift.yaml +10 -0
  147. codegraph/presets/typescript.yaml +13 -0
  148. codegraph/process_runs.py +328 -0
  149. codegraph/protocols/__init__.py +299 -0
  150. codegraph/protocols/builtin.py +67 -0
  151. codegraph/protocols/matchers.py +144 -0
  152. codegraph/protocols/view.py +334 -0
  153. codegraph/query.py +2089 -0
  154. codegraph/realtime.py +260 -0
  155. codegraph/roundtrip.py +346 -0
  156. codegraph/routes.py +442 -0
  157. codegraph/starters.py +218 -0
  158. codegraph/tests_index.py +117 -0
  159. codegraph/viz/__init__.py +0 -0
  160. codegraph/viz/graph.py +369 -0
  161. codegraph/viz/server.py +198 -0
  162. codegraph/viz/static/app.css +148 -0
  163. codegraph/viz/static/app.js +1082 -0
  164. codegraph/viz/static/index.html +81 -0
  165. codegraph/viz/static/layered.js +237 -0
  166. codegraph/viz/static/vendor/VERSIONS.txt +4 -0
  167. codegraph/viz/static/vendor/cose-base.js +3214 -0
  168. codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
  169. codegraph/viz/static/vendor/cytoscape.min.js +31 -0
  170. codegraph/viz/static/vendor/layout-base.js +5230 -0
  171. codegraph/viz/tools/package-lock.json +303 -0
  172. codegraph/viz/tools/package.json +7 -0
  173. codegraph/viz/tools/shoot.mjs +165 -0
  174. codegraph/xcode.py +251 -0
codegraph/indexer.py ADDED
@@ -0,0 +1,336 @@
1
+ """Orchestrates detection -> language plugins (+ framework sub-plugins) -> entry tagging -> SQLite."""
2
+ from __future__ import annotations
3
+
4
+ import copy
5
+ import json
6
+ import time
7
+ from collections import defaultdict, deque
8
+ from pathlib import Path
9
+
10
+ from .core.detect import detect
11
+ from .core.model import ENTRY_KINDS, PROPAGATING
12
+ from .core.plugin import GraphBuilder, Project, gc_paused
13
+ from .core.store import GraphStore
14
+ from .plugins.laravel.plugin import LaravelPlugin
15
+ from .plugins.php.plugin import PhpPlugin
16
+ from .plugins.stubs.plugins import SCIP_PLUGINS
17
+ from .plugins.ts.plugin import TypeScriptPlugin
18
+ from .plugins.nuxt.plugin import NuxtPlugin
19
+ from .plugins.rust.plugin import RustPlugin
20
+ from .plugins.cfamily.plugin import CFamilyPlugin
21
+ from .plugins.python.plugin import PythonPlugin
22
+ from .plugins.django.plugin import DjangoPlugin
23
+ from .plugins.pyweb.plugin import FastAPIPlugin, FlaskPlugin
24
+ from .plugins.dart.plugin import DartPlugin
25
+ from .plugins.flutter.plugin import FlutterPlugin
26
+ from .plugins.nest.plugin import NestPlugin
27
+ from .plugins.nextjs.plugin import NextPlugin
28
+ from .plugins.express.plugin import ExpressPlugin
29
+ from .plugins.kotlin.plugin import KotlinPlugin
30
+ from .plugins.swift.plugin import SwiftPlugin
31
+
32
+ LANGUAGE_PLUGINS = [PhpPlugin(), TypeScriptPlugin(), PythonPlugin(), DartPlugin(), RustPlugin(), CFamilyPlugin(), KotlinPlugin(), SwiftPlugin(),
33
+ *SCIP_PLUGINS]
34
+ FRAMEWORK_PLUGINS = [LaravelPlugin(), NuxtPlugin(), DjangoPlugin(), FastAPIPlugin(), FlaskPlugin(), FlutterPlugin(), NestPlugin(), NextPlugin(), ExpressPlugin()]
35
+
36
+
37
+ def _crash_site(ex: BaseException) -> str | None:
38
+ """Innermost codegraph frame of an exception (file:line function), for plugin crash reports."""
39
+ import traceback
40
+ frames = [f for f in traceback.extract_tb(ex.__traceback__) if "codegraph" in f.filename]
41
+ if not frames:
42
+ return None
43
+ f = frames[-1]
44
+ return f"{Path(f.filename).name}:{f.lineno} {f.name}"
45
+
46
+
47
+ def tag_entries(builder: GraphBuilder, skip_gate: str | None = None) -> list[tuple]:
48
+ """Forward closure from every entry node over propagating edges (optionally ignoring edges
49
+ that are dead under gate scenario `skip_gate`). Returns rows (node_id, entry_kind, entry_count, sample_entry)."""
50
+ with gc_paused():
51
+ return _tag_entries(builder, skip_gate)
52
+
53
+
54
+ def _tag_entries(builder: GraphBuilder, skip_gate: str | None) -> list[tuple]:
55
+ prop = set(PROPAGATING)
56
+ fwd = defaultdict(list)
57
+ for e in builder.edges.values():
58
+ if e.kind in prop and not (skip_gate and e.gate == skip_gate):
59
+ fwd[e.src].append(e.dst)
60
+ counts = defaultdict(lambda: defaultdict(int))
61
+ sample = {}
62
+ for n in builder.nodes.values():
63
+ if not n.entry_kind:
64
+ continue
65
+ seen, dq = {n.id}, deque([n.id])
66
+ while dq:
67
+ x = dq.popleft()
68
+ for y in fwd.get(x, ()):
69
+ if y not in seen:
70
+ seen.add(y)
71
+ dq.append(y)
72
+ for x in seen:
73
+ counts[x][n.entry_kind] += 1
74
+ sample.setdefault((x, n.entry_kind), n.id)
75
+ return [(nid, k, c, sample[(nid, k)]) for nid, ks in counts.items() for k, c in ks.items()]
76
+
77
+
78
+ def setup(project: Project) -> dict:
79
+ """Which language plugins, framework plugins and presets apply to `project` (detection, then .cg.yaml
80
+ frameworks.add / remove). Fills project.options["presets"] (the applied preset names)."""
81
+ from . import presets
82
+ from .config import FRAMEWORK_PLUGIN_NAMES
83
+ cfg = project.options.get("config") or {}
84
+ fwc = cfg.get("frameworks") or {}
85
+ add, remove = set(fwc.get("add") or []), set(fwc.get("remove") or [])
86
+ if not project.detected:
87
+ project.detected = detect(project.root)
88
+ # fresh plugin instances per run: state a plugin keeps on itself (caches, gate predicates, file lists) never
89
+ # carries over to the next project indexed in the same process (MCP `index`, test suites)
90
+ langs = [lp for lp in copy.deepcopy(LANGUAGE_PLUGINS) if lp.detect(project)]
91
+ all_fws = copy.deepcopy(FRAMEWORK_PLUGINS)
92
+ detected = [f for f in all_fws if f.detect(project)]
93
+ fws = [f for f in all_fws if (f in detected or f.name in add) and f.name not in remove]
94
+ found = {f.name: "detected" for f in detected}
95
+ for k in (project.detected.get("frameworks") or {}): # sub-frameworks with a preset (DRF, django-ninja)
96
+ k = presets.FRAMEWORK_ALIASES.get(k, k)
97
+ if k in presets.frameworks() and k not in FRAMEWORK_PLUGIN_NAMES:
98
+ found.setdefault(k, "detected")
99
+ names = {k: v for k, v in found.items() if k not in remove}
100
+ for k in sorted(add):
101
+ names.setdefault(k, ".cg.yaml")
102
+ applied = presets.select([lp.name for lp in langs], list(names))
103
+ project.options["presets"] = applied
104
+ return {"language_plugins": langs, "framework_plugins": fws, "languages": [lp.name for lp in langs],
105
+ "frameworks": names, "removed": sorted(remove & set(found)), "presets": applied}
106
+
107
+
108
+ def scip_warnings(plugins: dict) -> list[str]:
109
+ """Unusable SCIP indexes (no readable positions, no definitions, none matched): the generic importer's
110
+ (`scip:<path>`) and the exact layers' (`<lang>.scip.warning`)."""
111
+ out = []
112
+ for k, v in plugins.items():
113
+ if not isinstance(v, dict):
114
+ continue
115
+ w = v.get("warning") if k.startswith("scip:") else (v.get("scip") or {}).get("warning") \
116
+ if isinstance(v.get("scip"), dict) else None
117
+ if w:
118
+ out.append(w if k.startswith("scip:") else f"{k}: {w}")
119
+ return out
120
+
121
+
122
+ def index_project(root: str | Path, db_path: str | Path, name: str | None = None, scip: list[str] | None = None,
123
+ gates: str | None = None, python_roots: list[str] | None = None, include_generated: bool = False) -> dict:
124
+ """Index `root` into a new graph DB. The project config file (.cg.yaml at the root) is read automatically;
125
+ python_roots (`cg index --python-root`) overrides its python.source_roots and `gates` its gates. Generated, copied
126
+ and vendored files (codegraph/core/generated.py) stay out of the graph unless include_generated
127
+ (`--include-generated`, .cg.yaml generated.include) indexes them with attrs.generated. Raises
128
+ config.ConfigError for an invalid config file or root."""
129
+ from .config import ConfigError, load as load_config, norm_root, resolve_path
130
+ t0 = time.time()
131
+ project = Project(root=Path(root).resolve(), name=name or Path(root).name)
132
+ project.options["config"] = cfg = load_config(project.root)
133
+ if scip:
134
+ project.options["scip"] = list(scip) # a language plugin may consume its own index (Kotlin: scip-java)
135
+ if python_roots:
136
+ project.options["python_roots"] = list(dict.fromkeys(norm_root(r, "--python-root") for r in python_roots))
137
+ gates_from = "flag" if gates else None
138
+ if not gates and cfg.get("gates"):
139
+ gates, gates_from = str(resolve_path(project.root, cfg["gates"])), cfg["file"]
140
+ if not Path(gates).is_file():
141
+ raise ConfigError(f"{cfg['file']}: gates: {cfg['gates']!r} does not exist")
142
+ if gates:
143
+ project.options["gates"] = json.loads(Path(gates).read_text())["scenarios"]
144
+ project.detected = detect(project.root)
145
+ # one file scan up front: coverage counts + the generated / copied / vendored classification every walk consults
146
+ from .core.generated import Classifier, apply as apply_generated
147
+ from .core.paths import rules as path_rules
148
+ from .coverage import scan_tree
149
+ t_scan = time.time()
150
+ clf = Classifier(project.root, cfg, include=include_generated)
151
+ scanned = scan_tree(project.root, path_rules(project, "common", "scan_skip_dirs", generated=False), classifier=clf)
152
+ project.options["generated"] = clf
153
+ if clf.include:
154
+ project.options["dart_keep_generated"] = True
155
+ scan_seconds = round(time.time() - t_scan, 2)
156
+ builder = GraphBuilder()
157
+ stats = {"detected": project.detected, "plugins": {}}
158
+ if cfg:
159
+ stats["config"] = {k: v for k, v in cfg.items()}
160
+ if python_roots:
161
+ stats["python_roots_flag"] = project.options["python_roots"]
162
+ if include_generated:
163
+ stats["include_generated_flag"] = True
164
+ if gates_from:
165
+ stats["gates_from"] = gates_from
166
+ plan = setup(project)
167
+ stats["presets"] = {"applied": plan["presets"], "languages": plan["languages"], "frameworks": plan["frameworks"]}
168
+ if plan["removed"]:
169
+ stats["presets"]["frameworks_removed"] = plan["removed"]
170
+ file_reports: dict = {} # language -> per-file outcome (coverage file completeness)
171
+ frameworks = plan["framework_plugins"]
172
+ for lp in plan["language_plugins"]:
173
+ fws = [f for f in frameworks if f.language == lp.name]
174
+ lp.program = None
175
+ # one missing indexer / toolchain never fails the whole index: that language is skipped with a note
176
+ missing = lp.prerequisite_problem(project) if hasattr(lp, "prerequisite_problem") else None
177
+ if missing:
178
+ stats["plugins"][lp.name] = {"status": "skipped", "reason": missing}
179
+ continue
180
+ n0, e0 = len(builder.nodes), len(builder.edges)
181
+ lp.file_report = None
182
+ try:
183
+ st = lp.index(project, builder, fws)
184
+ except Exception as ex: # noqa: BLE001
185
+ stats["plugins"][lp.name] = {"status": "skipped", "reason": f"{type(ex).__name__}: {str(ex)[:300]}",
186
+ "partial_nodes": len(builder.nodes) - n0, "partial_edges": len(builder.edges) - e0}
187
+ continue
188
+ stats["plugins"][lp.name] = st
189
+ if isinstance(st, dict) and st.get("status") in ("skipped", "error", "stub"):
190
+ continue
191
+ if getattr(lp, "file_report", None) is not None:
192
+ file_reports[lp.name] = lp.file_report
193
+ lp.file_report = None
194
+ ctx = getattr(lp, "program", None)
195
+ for fw in fws:
196
+ try:
197
+ stats["plugins"][f"{lp.name}/{fw.name}"] = fw.contribute(project, builder, ctx)
198
+ except Exception as ex: # noqa: BLE001
199
+ stats["plugins"][f"{lp.name}/{fw.name}"] = {"status": "skipped", "reason": f"{type(ex).__name__}: {str(ex)[:300]}",
200
+ "at": _crash_site(ex)}
201
+ if fws and hasattr(lp, "after_frameworks"):
202
+ lp.after_frameworks(builder, st)
203
+ if hasattr(lp, "link_test_requests"):
204
+ lp.link_test_requests(builder, st)
205
+ for fw in frameworks:
206
+ if f"{fw.language}/{fw.name}" not in stats["plugins"]:
207
+ stats["plugins"][f"{fw.language}/{fw.name}"] = {"status": "detected; language plugin not active"}
208
+ if scip:
209
+ from .plugins.scip.importer import import_scip
210
+ for s in scip:
211
+ if s in (project.options.get("scip_consumed") or []):
212
+ stats["plugins"][f"scip:{s}"] = {"status": "imported by the Kotlin plugin (exact layer)"}
213
+ continue
214
+ stats["plugins"][f"scip:{s}"] = import_scip(s, builder)
215
+ # generated / copied / vendored files: out of the graph (default) or labelled (attrs.generated)
216
+ stats["generated"] = apply_generated(builder, clf)
217
+ # web / native bridges: native receivers of Capacitor / React Native / Flutter calls (platform marks on their files)
218
+ from . import bridges as bridges_mod
219
+ t_br = time.time()
220
+ br = bridges_mod.apply(project, builder, scanned)
221
+ br_seconds = time.time() - t_br
222
+ # platform-specific code: platform tags on nodes / edges, variant implementations linked, divergence
223
+ from .platforms import apply as apply_platforms
224
+ t_pl = time.time()
225
+ pl = apply_platforms(project, builder)
226
+ if pl:
227
+ pl["seconds"] = round(time.time() - t_pl, 2)
228
+ stats["platforms"] = pl
229
+ # external systems (#40): env address keys, Laravel connections, .env.example / docker-compose -> external:<protocol>:<target>
230
+ from .external import attach as attach_external
231
+ if (ex := attach_external(builder, project.root)):
232
+ stats["external"] = ex
233
+ # tests that run the project's programs in a subprocess, outside Python (Rust, Node, PHP artisan, Dart; #60)
234
+ from .process_runs import apply as apply_process_runs
235
+ if (pr := apply_process_runs(project, builder)):
236
+ stats["process_runs"] = pr
237
+ # test code (tests/, *.spec.ts ...) never feeds the application graph: its edges become TEST_* kinds
238
+ from .tests_index import isolate_tests
239
+ stats["tests"] = isolate_tests(builder)
240
+ t_br = time.time()
241
+ br = bridges_mod.finalize(project, builder, br, (pl or {}).get("targets") or bridges_mod.project_targets(project, builder))
242
+ if br:
243
+ br["seconds"] = round(br_seconds + time.time() - t_br, 2)
244
+ stats["bridges"] = br
245
+ from .tests_index import link_local_channels
246
+ if (ch := link_local_channels(builder)):
247
+ stats["channels_linked"] = ch
248
+ # protocol endpoints (#31): MATCHES_ENDPOINT between senders and receivers of one graph (wildcards, templates)
249
+ from .protocols import apply as apply_protocols
250
+ if (pr := apply_protocols(builder, ((cfg or {}).get("protocols") or {}).get("external") or [])):
251
+ stats["protocols"] = pr
252
+ # dangling edge targets -> placeholder nodes so every edge resolves
253
+ for e in list(builder.edges.values()):
254
+ for nid in (e.src, e.dst):
255
+ if nid not in builder.nodes:
256
+ kind, key = nid.split(":", 1)
257
+ builder.add_node(kind, key, attrs={"placeholder": True})
258
+ # files parsed with syntax errors (#73): their error spans and the declarations the graph lost there
259
+ from .core.syntax_errors import summarize as summarize_syntax_errors
260
+ raw_err = {lang: rep["syntax_errors"] for lang, rep in file_reports.items() if rep.get("syntax_errors")}
261
+ if raw_err:
262
+ want = {f for raw in raw_err.values() for f in raw}
263
+ names_by_file: dict = {}
264
+ for n in builder.nodes.values():
265
+ if n.file in want and n.name:
266
+ names_by_file.setdefault(n.file, set()).add(n.name.split(".")[-1].split("(")[0])
267
+ for lang, raw in raw_err.items():
268
+ file_reports[lang]["syntax_errors"] = summarize_syntax_errors(
269
+ lang, project.root, raw, names_by_file, file_reports[lang].get("parse_failed"))
270
+ # completeness: one file scan for coverage + the blind-spot detectors (patterns no plugin models)
271
+ from .coverage import compute
272
+ from .blindspots import detect as detect_blind_spots
273
+ t_cov = time.time()
274
+ progs = {lp.name: lp.program for lp in plan["language_plugins"] if getattr(lp, "program", None) is not None
275
+ and lp.name in stats["plugins"] and "status" not in (stats["plugins"][lp.name] or {})}
276
+ bspots = detect_blind_spots(project.root, scanned.paths, builder, progs)
277
+ t_tag = time.time()
278
+ rows = tag_entries(builder)
279
+ stats["entry_tagging_seconds"] = round(time.time() - t_tag, 2)
280
+ store = GraphStore.create(db_path)
281
+ with gc_paused():
282
+ store.write(builder.nodes.values(), builder.edges.values())
283
+ store.db.executemany("INSERT INTO node_entry VALUES (?,?,?,?)", rows)
284
+ scen = [g["name"] for g in (project.options.get("gates") or [])[:1]]
285
+ for sc in scen:
286
+ live = tag_entries(builder, skip_gate=sc)
287
+ store.db.executemany("INSERT INTO node_entry_live VALUES (?,?,?,?,?)", [(sc, *r) for r in live])
288
+ stats["gated_edges"] = {sc: sum(1 for e in builder.edges.values() if e.gate == sc)}
289
+ for lp in plan["language_plugins"]:
290
+ store.db.executemany("INSERT OR REPLACE INTO gate_predicates VALUES (?,?,?,?)", getattr(lp, "gate_predicates", []) or [])
291
+ store.db.commit()
292
+ stats["coverage"] = compute(project.root, {k: v for k, v in stats["plugins"].items() if "/" not in k and not k.startswith("scip:")},
293
+ scip_imported=bool(scip), reports=file_reports, scanned=scanned, blind_spots=bspots,
294
+ warnings=scip_warnings(stats["plugins"]))
295
+ # detected.languages: the root marker files (detect), plus every language a plugin indexed source files of: an
296
+ # Xcode app has no root Package.swift, a monorepo no root tsconfig.json (#75)
297
+ dl = stats["detected"].setdefault("languages", {})
298
+ for e in stats["coverage"].get("languages") or []:
299
+ pst = stats["plugins"].get(e["language"])
300
+ if e["files"] and e["language"] not in dl and isinstance(pst, dict) and pst.get("status") not in ("skipped", "error", "stub"):
301
+ dl[e["language"]] = ["source files (" + ", ".join(f"{x} {n}" for x, n in sorted(e["by_ext"].items())) + ")"]
302
+ gsum = clf.summary()
303
+ if gsum["files"] or gsum.get("build_dirs"):
304
+ stats["coverage"]["generated"] = gsum
305
+ # starter queries for the visual view / MCP, derived from this graph (each resolves to existing nodes)
306
+ from .starters import generate as gen_starters
307
+ t_st = time.time()
308
+ store.set_meta(project=project.name, root=str(project.root), stats=stats) # presets / config for the route guards
309
+ gst = GraphStore(db_path)
310
+ try:
311
+ rep: dict = {}
312
+ stats["starters"] = gen_starters(gst, report=rep)
313
+ if rep.get("skipped"):
314
+ stats["starters_skipped"] = rep["skipped"] # over the time budget
315
+ except Exception as ex: # noqa: BLE001 (never fails an index)
316
+ stats["starters"], stats["starters_error"] = [], f"{type(ex).__name__}: {str(ex)[:200]}"
317
+ finally:
318
+ gst.db.close()
319
+ stats["starters_seconds"] = round(time.time() - t_st, 2)
320
+ stats["coverage"]["setup"] = {"frameworks": sorted(plan["frameworks"]), "presets": plan["presets"],
321
+ "config": cfg.get("file")}
322
+ if pl: # per-target coverage: what each declared target builds, conditions left unevaluated
323
+ stats["coverage"]["platforms"] = {k: pl[k] for k in ("targets", "per_target", "unevaluated_conditions",
324
+ "unevaluated_samples", "conditions") if k in pl}
325
+ from .coverage import value_counts
326
+ vc = value_counts(builder)
327
+ if vc: # enum cases / constants per language and the references resolved to them (#84)
328
+ stats["coverage"]["values"] = vc
329
+ stats["completeness_seconds"] = round(t_tag - t_cov, 2)
330
+ stats["scan_seconds"] = scan_seconds
331
+ stats["nodes"] = len(builder.nodes)
332
+ stats["edges"] = len(builder.edges)
333
+ stats["index_seconds"] = round(time.time() - t0, 2)
334
+ store.set_meta(project=project.name, root=str(project.root), stats=stats, indexed_at=time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime()))
335
+ store.db.close()
336
+ return stats