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/cli.py ADDED
@@ -0,0 +1,725 @@
1
+ """code-graph CLI.
2
+
3
+ python -m codegraph.cli index <project_root> --db out/x.db [--scip index.scip] [--gates config/x.gates.json]
4
+ python -m codegraph.cli reaches <spec>... --db out/x.db [--min-confidence resolved] [--json] [--no-paths]
5
+ python -m codegraph.cli siblings <symbol> --db ...
6
+ python -m codegraph.cli writers <table> --db ...
7
+ python -m codegraph.cli impact <Class::method> --db ...
8
+ python -m codegraph.cli stats --db ...
9
+ python -m codegraph.cli detect <project_root>
10
+ python -m codegraph.cli link --backend out/api.db --frontend out/web.db --db out/combined.db [--report out/x]
11
+ python -m codegraph.cli downstream <spec> --db out/combined.db (forward: page -> ... -> routes/tables)
12
+ python -m codegraph.cli api-calls <all|unmatched|substring|glob*> --db out/combined.db
13
+ python -m codegraph.cli plan list|load|validate|check|baseline [<name>] --db out/combined.db [--verify] [--summary] [--json]
14
+ python -m codegraph.cli routes --db ... [--writes [TABLE]] [--reaches SPEC...] [--missing NAME] [--unguarded]
15
+ python -m codegraph.cli search <name> --db ... [--kind route]
16
+ python -m codegraph.cli channels [PATTERN] --db ... (who can join, which events publish, which client code listens)
17
+ python -m codegraph.cli tests <spec> --db ... (tests covering a symbol / route / table: direct + transitive)
18
+ python -m codegraph.cli bridges [PATTERN] --db ... [--protocol capacitor] [--unmatched] (web / native bridge calls)
19
+ python -m codegraph.cli external [PATTERN] --db ... [--protocol P] [--source S] [--tls-off] (databases, caches, brokers, mail ... and who reaches them)
20
+ python -m codegraph.cli tools [PATTERN] --db ... [--framework F] [--agent A] [--unmatched] (LLM / MCP tools: handler, tables, agents, checks)
21
+ python -m codegraph.cli protocols [PATTERN] --db ... [--protocol P] [--side send|receive] [--unmatched] (every protocol endpoint: senders, receivers, checks)
22
+ python -m codegraph.cli platforms [summary|divergence] --db ... [--target ios] (platform-specific code, gaps between variants)
23
+ reaches / impact / downstream / path / routes / search take --platform TARGET: only code built for that target
24
+ spec forms: table.column | connection:<name> (glob *) | env:<KEY*> | config:<a.b> | Class.method or Class::method (either
25
+ separator, every language) | Class
26
+ | src/app.ts#listOrders (TS / JS symbol in one file; path suffix ok)
27
+ """
28
+ from __future__ import annotations
29
+
30
+ import argparse
31
+ import json
32
+ import os
33
+ import sys
34
+
35
+ from .core.store import GraphStore
36
+ from . import query as Q
37
+ from .bridges import PROTOCOLS as BRIDGE_PROTOCOLS
38
+
39
+
40
+ def main(argv=None):
41
+ from . import __version__
42
+ ap = argparse.ArgumentParser(prog="cg")
43
+ ap.add_argument("--version", action="version", version=f"cg {__version__}")
44
+ sub = ap.add_subparsers(dest="cmd", required=True)
45
+ p = sub.add_parser("index", help="detect languages / frameworks under ROOT and build the graph DB (stats JSON on stdout, coverage on stderr)"); p.add_argument("root"); p.add_argument("--db", required=True); p.add_argument("--name"); p.add_argument("--scip", action="append"); p.add_argument("--gates", help="gate scenarios JSON (e.g. examples/bookstore.gates.json)")
46
+ p.add_argument("--python-root", action="append", metavar="DIR",
47
+ help="Python source root, relative to ROOT (repeatable); replaces detection and python.source_roots in .cg.yaml")
48
+ p.add_argument("--no-apps", action="store_true",
49
+ help="index ROOT as one project although its .cg.yaml lists monorepo apps")
50
+ p.add_argument("--include-generated", action="store_true",
51
+ help="also index generated, copied and vendored files (labelled attrs.generated); default: excluded and listed by `cg coverage`")
52
+ p = sub.add_parser("detect", help="the languages and frameworks detected under ROOT, without indexing"); p.add_argument("root")
53
+ p = sub.add_parser("config", help="project config: `show` the effective configuration and where each value comes from, "
54
+ "`validate` a .cg.yaml")
55
+ p.add_argument("action", choices=["show", "validate"]); p.add_argument("root", nargs="?", default=".", help="indexed root (or, for validate, a config file)")
56
+ p.add_argument("--python-root", action="append", metavar="DIR", help="as for index")
57
+ p.add_argument("--gates", help="as for index"); p.add_argument("--auth-pattern", help="as for routes")
58
+ p.add_argument("--plans-dir", help="as for plan / serve"); p.add_argument("--presets", help="as for serve")
59
+ p.add_argument("--no-apps", action="store_true",
60
+ help="index ROOT as one project although its .cg.yaml lists monorepo apps")
61
+ p.add_argument("--include-generated", action="store_true", help="as for index")
62
+ p.add_argument("--json", action="store_true", help="the effective configuration as JSON")
63
+ p = sub.add_parser("starters", help="starter queries derived from the graph (unguarded write routes, most-reached tables, ...)")
64
+ p.add_argument("--db", required=True); p.add_argument("--json", action="store_true")
65
+ p = sub.add_parser("doctor", help="what this installation can index: tool versions, extractor dependencies, exact or "
66
+ "heuristic mode per language and why, and what to install")
67
+ p.add_argument("root", nargs="?", help="project root: also check project conditions and list only its languages")
68
+ p.add_argument("--scip", action="append", metavar="FILE",
69
+ help="check a SCIP index: documents, occurrences with a usable position, definitions (repeatable)")
70
+ p.add_argument("--json", action="store_true")
71
+ p = sub.add_parser("clean", help="remove cache entries: one project's (ROOT), stale ones (--stale) or all (--all); "
72
+ "--db also deletes a graph DB")
73
+ p.add_argument("root", nargs="?", help="project root: remove the cache entries of this project and of every "
74
+ "project indexed below it")
75
+ p.add_argument("--all", action="store_true", help="empty the whole cache root (keeps the extractors)")
76
+ p.add_argument("--extractors", action="store_true", help="with --all: also remove the extractor installs")
77
+ p.add_argument("--stale", action="store_true",
78
+ help="only entries of older cache versions / layouts and orphaned .tmp / .lock files")
79
+ p.add_argument("--db", help="also delete this graph DB and its -wal / -shm files")
80
+ p.add_argument("--dry-run", action="store_true", help="list what would be removed, delete nothing")
81
+ p.add_argument("--json", action="store_true")
82
+ p = sub.add_parser("setup", help="install the Node / PHP / Dart extractor dependencies (into the user cache; "
83
+ "otherwise done on the first index)")
84
+ p.add_argument("languages", nargs="*", metavar="LANG",
85
+ help="typescript, php, dart (default: every one whose toolchain is installed)")
86
+ p.add_argument("--quiet", action="store_true")
87
+ p.add_argument("--prune", action="store_true",
88
+ help="remove extractor installs this version does not use (left by updates that changed a lock "
89
+ "file) instead of installing")
90
+ p.add_argument("--dry-run", action="store_true", help="with --prune: list what would be removed")
91
+ p = sub.add_parser("coverage", help="which source files / languages the index covers: exact, heuristic, skipped (indexer missing) or unsupported")
92
+ p.add_argument("--db", required=True); p.add_argument("--json", action="store_true")
93
+ p.add_argument("--details", action="store_true", help="the full report: file lists (the first 5 per bucket), fix hints, "
94
+ "syntax error lines, Python source roots (default: a short summary)")
95
+ p.add_argument("--all-files", action="store_true", help="the full report with every file per bucket, excluded files too")
96
+ p = sub.add_parser("link", help="combine a backend and a frontend graph and match client HTTP calls to backend routes")
97
+ p.add_argument("--backend", required=True); p.add_argument("--frontend", required=True); p.add_argument("--db", required=True)
98
+ p.add_argument("--backend-name", default="backend"); p.add_argument("--frontend-name", default="frontend")
99
+ p.add_argument("--report", help="write <prefix>.json/.md match report")
100
+ p = sub.add_parser("path", help="one shortest evidence chain from SRC to DST (exit status 1 when there is none)"); p.add_argument("src"); p.add_argument("dst"); p.add_argument("--db", required=True)
101
+ p.add_argument("--min-confidence", default="heuristic", choices=["heuristic", "resolved", "exact"])
102
+ p.add_argument("--platform", help="only code built for this target (windows, linux, macos, ios, android, web; see docs/platforms.md)")
103
+ p = sub.add_parser("platforms", help="platform-specific code: targets, tagged symbols per target, and divergence "
104
+ "(variants missing a target, API differences, references to code missing on a target)")
105
+ p.add_argument("action", nargs="?", default="summary", choices=["summary", "divergence"])
106
+ p.add_argument("--db", required=True); p.add_argument("--target", help="divergence: only findings that affect this target")
107
+ p.add_argument("--kind", choices=["variants", "api_surface", "missing_callee", "missing_callee_tests"],
108
+ help="divergence: one finding kind (missing_callee_tests: from test code on the project-default platforms)")
109
+ p.add_argument("--max-items", type=int, default=40); p.add_argument("--json", action="store_true")
110
+ p = sub.add_parser("resolutions", help="every place a concept (e.g. timezone) is resolved, with fallback chains and divergence")
111
+ p.add_argument("concept"); p.add_argument("--db", required=True); p.add_argument("--within", help="substring filter on the owning function fqn (e.g. Report)")
112
+ p.add_argument("--no-client", action="store_true"); p.add_argument("--json", action="store_true")
113
+ p = sub.add_parser("serve", help="local web UI over a graph DB")
114
+ p.add_argument("--db", required=True); p.add_argument("--port", type=int, default=8177); p.add_argument("--host", default="127.0.0.1"); p.add_argument("--plans-dir")
115
+ p.add_argument("--presets", help="JSON list of canned queries for the starter cards (default: viz.presets in .cg.yaml, then the sample presets that resolve, then the starter queries)")
116
+ p = sub.add_parser("viz-export", help="self-contained HTML view of one query (opens from disk, no server)")
117
+ p.add_argument("mode", choices=["reaches", "impact", "downstream", "path"]); p.add_argument("specs", nargs="+")
118
+ p.add_argument("--db", required=True); p.add_argument("-o", "--out", required=True)
119
+ p.add_argument("--sinks", help="downstream sink kinds, comma separated (e.g. table,column)")
120
+ p.add_argument("--min-confidence", default="heuristic", choices=["heuristic", "resolved", "exact"])
121
+ p = sub.add_parser("plan", help="planned-change layer: plans/<name>.yaml overlaid on the graph (list/load/validate/check/baseline)")
122
+ p.add_argument("action", choices=["list", "load", "validate", "check", "baseline"]); p.add_argument("name", nargs="?")
123
+ p.add_argument("--db"); p.add_argument("--plans-dir"); p.add_argument("--verify", action="store_true", help="after implement + re-index: planned nodes/edges must now exist")
124
+ p.add_argument("--json", action="store_true"); p.add_argument("-o", "--out", help="also write the report to this file")
125
+ p.add_argument("--max-items", type=int, default=60)
126
+ p.add_argument("--summary", action="store_true", help="compact report: counts per section and check plus the top --max-items items")
127
+ p = sub.add_parser("routes", help="routes with their middleware / guards / auth; scope by what they write or reach, filter by missing guards")
128
+ p.add_argument("--db", required=True)
129
+ p.add_argument("--writes", nargs="?", const="*", metavar="TABLE", help="routes reaching a DB write (any table, or TABLE)")
130
+ p.add_argument("--reaches", nargs="+", metavar="SPEC", help="routes reaching any of these nodes (table, column, connection:, env:, Class.method)")
131
+ p.add_argument("--missing", metavar="NAME", help="keep routes with no guard whose name contains NAME (e.g. auth:api, ApiKeyGuard)")
132
+ p.add_argument("--unguarded", action="store_true", help="keep routes with no auth guard (framework presets, the auth name pattern, .cg.yaml auth.extra_patterns and --auth-pattern)")
133
+ p.add_argument("--auth-pattern", help="extra regex for guard names that count as auth")
134
+ p.add_argument("--min-confidence", default="heuristic", choices=["heuristic", "resolved", "exact"])
135
+ p.add_argument("--max-items", type=int, default=200); p.add_argument("--no-paths", action="store_true"); p.add_argument("--json", action="store_true")
136
+ p.add_argument("--platform", help="only code built for this target (windows, linux, macos, ios, android, web; see docs/platforms.md)")
137
+ p = sub.add_parser("search", help="nodes by name / FQN substring, plus route middleware / guard / auth names")
138
+ p.add_argument("name"); p.add_argument("--db", required=True); p.add_argument("--kind"); p.add_argument("--limit", type=int, default=30)
139
+ p.add_argument("--json", action="store_true"); p.add_argument("--platform", help="only code built for this target (windows, linux, macos, ios, android, web; see docs/platforms.md)")
140
+ p = sub.add_parser("channels", help="broadcast channels: who can join (auth callback + checks), which events publish on it, which client code / pages listen")
141
+ p.add_argument("pattern", nargs="?", help="channel pattern or concrete name (orders.{id}, orders.42, orders.*); omit to list all")
142
+ p.add_argument("--db", required=True); p.add_argument("--json", action="store_true"); p.add_argument("--no-source", action="store_true")
143
+ p = sub.add_parser("bridges", help="web / native bridge calls (Capacitor plugins, React Native / Expo modules, Flutter platform "
144
+ "channels): JS / Dart senders, native receivers per platform, methods missing on a platform")
145
+ p.add_argument("pattern", nargs="?", help="endpoint name, substring or glob (Echo#echo, Echo, samples.flutter.dev/*); omit to list all")
146
+ p.add_argument("--db", required=True); p.add_argument("--json", action="store_true")
147
+ p.add_argument("--protocol", choices=sorted(BRIDGE_PROTOCOLS))
148
+ p.add_argument("--unmatched", action="store_true", help="only endpoints with a check: missing on a platform, no receiver, no sender, external")
149
+ p = sub.add_parser("protocols", help="protocol endpoints (HTTP, Pusher channels, Nest messages, jobs, events, bridges, MQTT / "
150
+ "Socket.IO ...): summary per protocol, or senders, receivers, guards, matches and checks per endpoint")
151
+ p.add_argument("pattern", nargs="?", help="endpoint name, id, substring or glob (`orders.*`, `http:GET /api/*`); omit for the summary")
152
+ p.add_argument("--db", required=True); p.add_argument("--json", action="store_true")
153
+ p.add_argument("--protocol", help="one protocol (http, pusher, nest-rpc, bull, laravel-queue, socketio, mqtt, capacitor, ...)")
154
+ p.add_argument("--side", choices=["send", "receive"]); p.add_argument("--max-items", type=int, default=60)
155
+ p.add_argument("--unmatched", action="store_true", help="only endpoints with a check (no_receiver, no_sender, ambiguous, "
156
+ "schema_mismatch, unguarded) or an external peer")
157
+ p = sub.add_parser("external", help="external systems (databases, caches, brokers, mail, directories, file transfer, object "
158
+ "stores, third-party HTTP hosts): users, entry points, address and credential sources")
159
+ p.add_argument("pattern", nargs="?", help="external id, substring or glob (`external:postgres:*`, `redis`)")
160
+ p.add_argument("--db", required=True); p.add_argument("--json", action="store_true")
161
+ p.add_argument("--protocol", help="postgres, mysql, redis, smtp, amqp, mongodb, ldap, ssh, ftp, s3, https, ...")
162
+ p.add_argument("--source", help="address or credential source: literal, env, env-example, compose, config")
163
+ p.add_argument("--tls-off", action="store_true", help="only systems known to be reached without TLS")
164
+ p.add_argument("--max-items", type=int, default=60)
165
+ p = sub.add_parser("tools", help="LLM tools and MCP tools / resources / prompts: handler, tables it reaches, agents "
166
+ "offering it, callers, checks; agents, dynamic dispatch, model calls")
167
+ p.add_argument("pattern", nargs="?", help="tool name, substring or glob")
168
+ p.add_argument("--db", required=True); p.add_argument("--json", action="store_true")
169
+ p.add_argument("--framework", help="mcp, openai, anthropic, langchain, openai-agents, llamaindex, custom")
170
+ p.add_argument("--agent", help="only tools this agent offers (name or glob)")
171
+ p.add_argument("--unmatched", action="store_true", help="only tools with a check (no_receiver, no_sender, name_collision)")
172
+ p.add_argument("--max-items", type=int, default=60)
173
+ p = sub.add_parser("tests", help="tests covering a symbol / route / table: direct (test code calls it) and transitive (through app code)")
174
+ p.add_argument("spec", help="Class.method or Class::method (either separator, any language), Class, route:VERB /uri, `VERB /path`, /path, table.column ...")
175
+ p.add_argument("--db", required=True); p.add_argument("--json", action="store_true"); p.add_argument("--no-paths", action="store_true")
176
+ p.add_argument("--min-confidence", default="heuristic", choices=["heuristic", "resolved", "exact"])
177
+ p.add_argument("--max-depth", type=int, default=3, help="transitive tests at most N hops from the target (0: any depth)")
178
+ p.add_argument("--unit-only", action="store_true", help="leave out UI / snapshot / screenshot tests")
179
+ p.add_argument("--exclude-root", action="append", default=[], help="a symbol transitive paths must not run through (repeatable)")
180
+ p.add_argument("--through-roots", action="store_true", help="keep paths through app entry points (@main, App.body, MainActivity)")
181
+ p = sub.add_parser("parity", help="port gap report: types, functions, enum cases and constants of --db with no "
182
+ "counterpart in --against (e.g. an iOS app and its Android port)")
183
+ p.add_argument("--db", required=True, help="source graph"); p.add_argument("--against", required=True, help="target graph")
184
+ p.add_argument("--map", help="JSON file {\"source name\": \"target name\"} for renames")
185
+ p.add_argument("--no-fuzzy", action="store_true", help="no fuzzy (shortened / plural word) name matches")
186
+ p.add_argument("--strip-prefix", action="append", default=[], help="a name prefix one side adds (`Vault` in "
187
+ "VaultAddEditState for AddEditState), ignored when matching (repeatable)")
188
+ p.add_argument("--structure", action="store_true", help="also pair symbols whose names differ by what they use: "
189
+ "shared strings, localization keys, endpoints and already-paired callees, plus rename rules learned "
190
+ "from the pairs found (#93); matches carry their evidence")
191
+ p.add_argument("--no-learn", action="store_true", help="with --structure: no learned rename rules")
192
+ p.add_argument("--write-map", help="with --structure: write the inferred pairs as a --map JSON file to review")
193
+ p.add_argument("--json", action="store_true"); p.add_argument("--max-items", type=int, default=200)
194
+ p = sub.add_parser("lint", help="heuristic lints (#88): `lint async-state` flags stale async results, state with "
195
+ "lifecycle and async writers, incomplete cache keys and unsuppressed echoes")
196
+ p.add_argument("check", choices=["async-state"]); p.add_argument("--db", required=True)
197
+ p.add_argument("--rules", help="comma-separated subset: stale-async-result,two-writers,incomplete-cache-key,"
198
+ "echo-suppression (default: all)")
199
+ p.add_argument("--json", action="store_true"); p.add_argument("--tests", action="store_true", help="include test code")
200
+ p = sub.add_parser("roundtrip", help="heuristic: writes of a stored property `Type.prop` through a lossy transform, "
201
+ "read back to seed UI state (#88)")
202
+ p.add_argument("spec"); p.add_argument("--db", required=True); p.add_argument("--json", action="store_true")
203
+ p.add_argument("--tests", action="store_true", help="include test code's reads and writes")
204
+ p = sub.add_parser("viz-plan", help="self-contained HTML overlay of a plan on the real graph")
205
+ p.add_argument("name"); p.add_argument("--db", required=True); p.add_argument("-o", "--out", required=True); p.add_argument("--plans-dir")
206
+ helps = {"reaches": "everything that depends on the targets, grouped by entry classification",
207
+ "siblings": "code related to a symbol: class hierarchy, the same method in sibling classes, shared resources, co-callers",
208
+ "writers": "code that writes a table (or column), or a stored property `Type.prop`",
209
+ "readers": "code that reads a stored property `Type.prop` (READS_PROP)",
210
+ "impact": "callers of a method up to their entry points (reverse walk), overrides listed apart",
211
+ "stats": "node / edge counts of a graph DB",
212
+ "node": "one node's details and its incoming / outgoing edges",
213
+ "downstream": "forward dependencies of a symbol or page (calls, HTTP, routes, services, tables)",
214
+ "api-calls": "client HTTP calls with call sites, request keys and the matched route"}
215
+ for name in ("reaches", "siblings", "writers", "readers", "impact", "stats", "node", "downstream", "api-calls"):
216
+ p = sub.add_parser(name, help=helps[name])
217
+ if name == "reaches":
218
+ p.add_argument("specs", nargs="+")
219
+ elif name == "api-calls":
220
+ p.add_argument("spec", help="all | unmatched | a substring | a * glob ('GET /v1/*/orders*', '*useOrders*')")
221
+ elif name != "stats":
222
+ p.add_argument("spec")
223
+ p.add_argument("--db", required=True)
224
+ p.add_argument("--json", action="store_true")
225
+ p.add_argument("--min-confidence", default="heuristic", choices=["heuristic", "resolved", "exact"])
226
+ p.add_argument("--no-paths", action="store_true")
227
+ p.add_argument("--max-depth", type=int, default=30)
228
+ p.add_argument("--gate", default="auto", help="gate scenario for live/gated split (default: the one indexed; 'none' to disable)")
229
+ if name in ("reaches", "impact", "downstream"):
230
+ p.add_argument("--platform", help="only code built for this target (windows, linux, macos, ios, android, web; see docs/platforms.md)")
231
+ if name == "impact":
232
+ p.add_argument("--plans-dir", help="also list external clients from the snapshot files in this directory (e.g. examples/plans)")
233
+ a = ap.parse_args(argv)
234
+
235
+ if a.cmd == "index":
236
+ try:
237
+ from .indexer import index_project
238
+ except (SyntaxError, ImportError) as ex: # a plugin this interpreter cannot load: name it, point to doctor
239
+ from .doctor import _describe
240
+ print(f"cg index: cannot load the indexer on Python {sys.version.split()[0]}: {_describe(ex)}\n"
241
+ "run `cg doctor` for details; upgrading cg (`uv tool upgrade cg-code-graph`) usually fixes it", file=sys.stderr)
242
+ return 2
243
+ from .config import ConfigError, load as load_config
244
+ try:
245
+ cfg = load_config(a.root) if not a.no_apps else {}
246
+ if cfg.get("apps"): # monorepo: index each app, link each frontend / backend pair
247
+ from .apps import index_apps, render as render_apps
248
+ summary = index_apps(a.root, a.db, cfg, a.scip, python_roots=a.python_root,
249
+ include_generated=a.include_generated)
250
+ print(json.dumps({k: v for k, v in summary.items()}, indent=2, default=str))
251
+ print(render_apps(summary), file=sys.stderr)
252
+ return
253
+ st = index_project(a.root, a.db, a.name, a.scip, gates=a.gates, python_roots=a.python_root,
254
+ include_generated=a.include_generated)
255
+ except ConfigError as ex:
256
+ print(f"cg index: {ex}", file=sys.stderr)
257
+ return 2
258
+ print(json.dumps(st, indent=2, default=str))
259
+ from .coverage import render_summary
260
+ print(render_summary({"": st.get("coverage")}, db=a.db), file=sys.stderr) # stdout stays pure JSON
261
+ return
262
+ if a.cmd == "doctor":
263
+ from .doctor import render, report
264
+ r = report(a.root, scip=a.scip)
265
+ print(json.dumps(r, indent=2) if a.json else render(r))
266
+ broken = bool((r.get("modules") or {}).get("failed"))
267
+ return 1 if broken or any(x.get("warning") for x in r.get("scip") or ()) else 0
268
+ if a.cmd == "clean":
269
+ from .core import cache as C
270
+ try:
271
+ pl = C.plan(a.root, all_=a.all, extractors=a.extractors, stale=a.stale, db=a.db)
272
+ except C.CacheError as ex:
273
+ print(f"cg clean: {ex}", file=sys.stderr)
274
+ return 2
275
+ errors = [] if a.dry_run else C.execute(pl)
276
+ if a.json:
277
+ print(json.dumps({"root": str(pl.root), "dry_run": a.dry_run, "bytes": pl.bytes, "projects": pl.projects,
278
+ "removed" if not a.dry_run else "would_remove":
279
+ [{"path": str(e.path), "kind": e.kind, "bytes": e.size,
280
+ **({"stale": e.stale} if e.stale else {})} for e in pl.remove]
281
+ + [{"path": str(x), "kind": "db", "bytes": C._size(x)} for x in pl.db_files],
282
+ "skipped": [{"path": str(x), "reason": r} for x, r in pl.skipped],
283
+ "errors": [{"path": str(x), "error": r} for x, r in errors]}, indent=2))
284
+ else:
285
+ verb = "would remove" if a.dry_run else "removed"
286
+ for e in pl.remove:
287
+ print(f" {C.human(e.size):>9} {e.kind:<13} {e.path}" + (f" ({e.stale})" if e.stale else ""))
288
+ for x in pl.db_files:
289
+ print(f" {C.human(C._size(x)):>9} {'db':<13} {x}")
290
+ for x, r in pl.skipped:
291
+ print(f" skipped {x} ({r})")
292
+ for x, r in errors:
293
+ print(f" error {x}: {r}", file=sys.stderr)
294
+ n = len(pl.remove) + len(pl.db_files) - len(errors)
295
+ print(f"cg clean: {verb} {n} entr{'y' if n == 1 else 'ies'}, {C.human(pl.bytes)} (cache root {pl.root})"
296
+ + (f"; projects: {', '.join(pl.projects)}" if pl.projects else ""))
297
+ return 1 if errors else 0
298
+ if a.cmd == "setup" and a.prune:
299
+ from .core import cache as _cache, extractors as _ex
300
+ gone = _ex.prune(dry_run=a.dry_run)
301
+ verb = "would remove" if a.dry_run else "removed"
302
+ for pth, size in gone:
303
+ print(f"{verb} {pth} ({_cache.human(size)})")
304
+ print(f"cg setup --prune: {verb} {len(gone)} unused extractor install(s), "
305
+ f"{_cache.human(sum(sz for _, sz in gone))}; kept {', '.join(_ex.cache_dir(x).name for x in _ex.SPECS)}")
306
+ return 0
307
+ if a.cmd == "setup":
308
+ from .doctor import setup
309
+ bad = [x for x in a.languages if x not in ("typescript", "php", "dart")]
310
+ if bad:
311
+ print(f"cg setup: unknown language {bad[0]!r} (typescript, php, dart)", file=sys.stderr)
312
+ return 2
313
+ return setup(a.languages or None, quiet=a.quiet)
314
+ if a.cmd == "coverage":
315
+ from .core.store import GraphStore as _GS
316
+ from .coverage import for_graph, render, render_summary
317
+ if not os.path.isfile(a.db) or os.path.getsize(a.db) == 0:
318
+ print(f"no graph at {a.db}: run `cg index` first", file=sys.stderr)
319
+ return 2
320
+ covs = for_graph(_GS(a.db))
321
+ if a.json:
322
+ print(json.dumps(covs, indent=2))
323
+ else:
324
+ print(render(covs, all_files=a.all_files) if a.details or a.all_files else render_summary(covs))
325
+ return
326
+ if a.cmd == "link":
327
+ from .link import link, write_match_report
328
+ res = link(a.backend, a.frontend, a.db, a.backend_name, a.frontend_name)
329
+ if a.report:
330
+ write_match_report(res, a.report)
331
+ print(json.dumps(res["stats"], indent=2))
332
+ return
333
+ if a.cmd == "config":
334
+ return config_cmd(a)
335
+ if a.cmd == "starters":
336
+ from .starters import for_graph, render as render_starters
337
+ rows = for_graph(GraphStore(a.db))
338
+ print(json.dumps(rows, indent=1) if a.json else render_starters(rows))
339
+ return
340
+ if getattr(a, "plans_dir", None) is None and a.cmd in ("plan", "serve", "viz-plan", "impact") and getattr(a, "db", None):
341
+ from .plans import resolve_plans_dir
342
+ a.plans_dir = resolve_plans_dir(None, a.db) # plans.dir of the indexed project's .cg.yaml
343
+ if a.cmd == "detect":
344
+ from .core.detect import detect
345
+ print(json.dumps(detect(a.root), indent=2))
346
+ return
347
+ if a.cmd == "viz-export":
348
+ from .viz.server import export_html
349
+ print(export_html(a.db, a.out, a.mode, a.specs, a.min_confidence, a.sinks.split(",") if a.sinks else None))
350
+ return
351
+ if a.cmd == "plan":
352
+ return plan_cmd(a)
353
+ if a.cmd == "viz-plan":
354
+ from .viz.server import export_plan_html
355
+ print(export_plan_html(a.db, a.out, a.name, plans_root=a.plans_dir))
356
+ return
357
+ if a.cmd == "parity":
358
+ from . import parity as PA
359
+ mapping = json.loads(open(a.map, encoding="utf-8").read()) if a.map else None
360
+ res = PA.parity(a.db, a.against, mapping=mapping, fuzzy=not a.no_fuzzy, strip_prefixes=a.strip_prefix,
361
+ structure=a.structure, learn=not a.no_learn)
362
+ if a.write_map:
363
+ with open(a.write_map, "w", encoding="utf-8") as fh:
364
+ json.dump(PA.rename_map(res), fh, indent=1, sort_keys=True)
365
+ print(json.dumps(res, indent=1, default=str) if a.json else PA.render(res, max_items=a.max_items))
366
+ return
367
+ if a.cmd == "serve":
368
+ from .viz.server import serve
369
+ serve(a.db, a.host, a.port, plans=a.plans_dir, presets=a.presets)
370
+ return
371
+ if getattr(a, "platform", None):
372
+ from .platforms import resolve_platform
373
+ try:
374
+ a.platform = resolve_platform(a.platform)
375
+ except ValueError as ex:
376
+ print(f"cg {a.cmd}: {ex}", file=sys.stderr)
377
+ return 2
378
+ st = GraphStore(a.db)
379
+ if a.cmd == "platforms":
380
+ from . import platforms as PF
381
+ if a.action == "summary":
382
+ s = PF.summary(st)
383
+ print(json.dumps(s, indent=1, default=str) if a.json else PF.render_summary(s))
384
+ return
385
+ tgt = None
386
+ if a.target:
387
+ try:
388
+ tgt = PF.resolve_platform(a.target)
389
+ except ValueError as ex:
390
+ print(f"cg platforms: {ex}", file=sys.stderr)
391
+ return 2
392
+ res = PF.divergence(st, kind=a.kind, target=tgt)
393
+ print(json.dumps(res, indent=1, default=str) if a.json else PF.render_divergence(res, limit=a.max_items))
394
+ return
395
+ if a.cmd == "resolutions":
396
+ from .concepts import resolutions, render_resolutions
397
+ res = resolutions(st, a.concept, within=a.within, client=not a.no_client)
398
+ print(json.dumps(res, indent=1, default=str) if a.json else render_resolutions(res))
399
+ return
400
+ if a.cmd == "routes":
401
+ from . import routes as R
402
+ res = R.routes_report(st, writes=a.writes, reaches=a.reaches, missing=a.missing, unguarded=a.unguarded,
403
+ auth_pattern=a.auth_pattern, min_conf=a.min_confidence, platform=a.platform)
404
+ if a.json:
405
+ res["completeness"] = R.route_completeness(st)
406
+ print(json.dumps(res, indent=1, default=str) if a.json else R.render_routes(res, st, max_items=a.max_items, paths=not a.no_paths))
407
+ return
408
+ if a.cmd == "channels":
409
+ from .realtime import channels, render_channels
410
+ res = channels(st, a.pattern, with_source=not a.no_source)
411
+ print(json.dumps(res, indent=1, default=str) if a.json else render_channels(res))
412
+ return
413
+ if a.cmd == "bridges":
414
+ from .bridges import bridges, render_bridges
415
+ res = bridges(st, a.pattern, protocol=a.protocol, unmatched=a.unmatched)
416
+ print(json.dumps(res, indent=1, default=str) if a.json else render_bridges(res))
417
+ return
418
+ if a.cmd == "external":
419
+ from .external import external, render_external
420
+ res = external(st, a.pattern, protocol=a.protocol, source=a.source, tls_off=a.tls_off,
421
+ max_items=max(a.max_items, 200) if a.json else a.max_items)
422
+ print(json.dumps(res, indent=1, default=str) if a.json else render_external(res, max_items=a.max_items))
423
+ return
424
+ if a.cmd == "tools":
425
+ from .aitools import render_tools, tools
426
+ res = tools(st, a.pattern, framework=a.framework, unmatched=a.unmatched, agent=a.agent,
427
+ max_items=max(a.max_items, 200) if a.json else a.max_items)
428
+ print(json.dumps(res, indent=1, default=str) if a.json else render_tools(res, max_items=a.max_items))
429
+ return
430
+ if a.cmd == "protocols":
431
+ from .protocols.view import protocols, render_protocols
432
+ res = protocols(st, a.pattern, protocol=a.protocol, side=a.side, unmatched=a.unmatched, max_items=max(a.max_items, 200) if a.json else a.max_items)
433
+ print(json.dumps(res, indent=1, default=str) if a.json else render_protocols(res, max_items=a.max_items))
434
+ return
435
+ if a.cmd == "tests":
436
+ res = Q.tests_covering(st, a.spec, min_conf=a.min_confidence, near_depth=a.max_depth or None,
437
+ unit_only=a.unit_only, exclude_roots=a.exclude_root, through_roots=a.through_roots)
438
+ res["completeness"] = _completeness(st, res.get("targets"))
439
+ print(json.dumps(res, indent=1, default=str) if a.json else Q.render_tests_covering(res, show_paths=not a.no_paths))
440
+ _note(res["completeness"], a.json)
441
+ return
442
+ if a.cmd == "search":
443
+ res = Q.search(st, a.name, kind=a.kind, limit=a.limit, platform=a.platform)
444
+ print(json.dumps(res, indent=1, default=str) if a.json else Q.render_search(res))
445
+ return
446
+ if a.cmd == "reaches":
447
+ res = Q.reaches(st, a.specs, min_conf=a.min_confidence, max_depth=a.max_depth, gate=None if a.gate == "none" else a.gate,
448
+ platform=a.platform)
449
+ res["completeness"] = _completeness(st, [x for t in res["targets"].values() for x in t] + [i["id"] for i in res["items"]])
450
+ print(json.dumps(res, indent=1, default=str) if a.json else Q.render_reaches(res, show_paths=not a.no_paths))
451
+ _note(res["completeness"], a.json)
452
+ elif a.cmd == "impact":
453
+ res = Q.impact(st, a.spec, min_conf=a.min_confidence, platform=a.platform)
454
+ if a.json:
455
+ res["completeness"] = _completeness(st, list(res["targets"]) + [c["id"] for c in res["callers"]])
456
+ print(json.dumps(res, indent=1, default=str)); return
457
+ if res.get("platform"):
458
+ from .platforms import render_filter
459
+ print(render_filter(res["platform"]))
460
+ if res["platform"].get("targets_not_built"):
461
+ print(f"not built for {a.platform}: {', '.join(res['platform']['targets_not_built'][:6])}")
462
+ print(f"targets: {res['targets'][:5]}")
463
+ if not res["targets"]:
464
+ print(f"no method matches {a.spec!r}; try `search` with part of the name"); return
465
+ if a.platform and res["targets"] and set(res["platform"]["targets_not_built"]) == set(res["targets"]):
466
+ print(f"{a.spec} is not built for {a.platform}: nothing calls it there (`cg platforms divergence --target "
467
+ f"{a.platform}` lists references to it that would not build)"); return
468
+ for line in Q.override_lines(res):
469
+ print(line)
470
+ if not res["callers"] and not res["entry_points"]:
471
+ print(Q.explain_no_callers(st, a.spec, res["targets"], a.min_confidence)); return
472
+ print(f"callers (transitive): {len(res['callers'])}")
473
+ for c in res["callers"]:
474
+ loc = f" @ {c['file']}:{c['line']}" if Q.NATIVE_FILE_RE.search(c.get("file") or "") else ""
475
+ print(f" d={c['depth']} [{c['module'] or c['kind']}] {c['fqn']}{loc}{Q.caller_label(c)}")
476
+ print(f"entry points: {len(res['entry_points'])}")
477
+ for e in res["entry_points"]:
478
+ native = Q.NATIVE_FILE_RE.search(e.get("file") or "")
479
+ nm = f"{e.get('fqn') or e['name']} @ {e['file']}:{e['line']}" if native else e["name"]
480
+ print(f" {e['entry_kind']:16} {nm} conf={e['path_confidence']}{Q.CANDIDATE_LABEL if e.get('candidate') else ''}"
481
+ f"{Q.generated_label(e)}{Q.platform_label(e)}")
482
+ if not a.no_paths:
483
+ print(f" path: {Q.fmt_path(e['path'])}")
484
+ if any(x.get("candidate") for x in res["callers"] + res["entry_points"]):
485
+ print(Q.CANDIDATE_NOTE)
486
+ if a.plans_dir:
487
+ from . import plans as P
488
+ hits = P.snapshot_clients(st, [e["id"] for e in res["entry_points"] if e["id"].startswith("route:")], a.plans_dir)
489
+ print(f"external clients (snapshot, not indexed): {len(hits)}")
490
+ for h in hits:
491
+ ev = f"{h['repo']}@{h['commit']}:{h['file']}" + (f":{h['line']}" if h.get("line") else "")
492
+ print(f" {h['method']} {h['path']} -> {h['route'].split(':', 1)[1]} @ {ev}"
493
+ + (f" sends {', '.join(h['sends'])}" if h["sends"] else "") + f" [{h['snapshot']}]")
494
+ _note(_completeness(st, list(res["targets"]) + [c["id"] for c in res["callers"]]), False)
495
+ elif a.cmd == "lint":
496
+ from . import lint_async as LA
497
+ res = LA.lint(st, include_tests=a.tests, rules=a.rules.split(",") if a.rules else None)
498
+ print(json.dumps(res, indent=1) if a.json else LA.render(res))
499
+ elif a.cmd == "roundtrip":
500
+ from . import roundtrip as RT
501
+ res = RT.roundtrip(st, a.spec, include_tests=a.tests)
502
+ print(RT.to_json(res) if a.json else RT.render(res))
503
+ elif a.cmd in ("writers", "readers"):
504
+ rows = Q.writers(st, a.spec) if a.cmd == "writers" else Q.readers(st, a.spec)
505
+ if a.json:
506
+ print(json.dumps(rows, indent=1)); return
507
+ if not rows:
508
+ print(Q.explain_no_writers(st, a.spec, a.cmd)); return
509
+ for r in rows:
510
+ ek = ",".join(sorted(r["entry_kinds"]))
511
+ ex = {k: v for k, v in (r.get("attrs") or {}).items() if k in ("receiver", "accessor", "storage")}
512
+ extra = (" " + " ".join(f"{k}={v}" for k, v in ex.items())) if ex else ""
513
+ kind = f"{r['kind']}({r['attrs']['orig']})" if r.get("test") else r['kind']
514
+ print(f"[{r['module']}] {r['fqn']} {kind} {r['dst']} @{r['file']}:{r['line']} ({r['confidence']}){extra} entries: {ek}")
515
+ verb = "write" if a.cmd == "writers" else "read"
516
+ nt = sum(1 for r in rows if r.get("test"))
517
+ print(f"{len(rows)} {verb} edges, {len({r['src'] for r in rows})} {a.cmd}" + (f" ({nt} from test code)" if nt else ""))
518
+ elif a.cmd == "siblings":
519
+ res = Q.siblings(st, a.spec)
520
+ print(json.dumps(res, indent=1, default=str) if a.json else Q.render_siblings(st, a.spec, res))
521
+ elif a.cmd == "node":
522
+ ids = list(dict.fromkeys(Q.resolve_targets(st, a.spec)))[:20]
523
+ docs = [node_doc(st, nid) for nid in ids]
524
+ if a.json:
525
+ print(json.dumps(docs, indent=1, default=str))
526
+ elif not docs:
527
+ print(f"no node matches {a.spec!r}; try `search` with part of the name")
528
+ else:
529
+ print("\n\n".join(render_node(d) for d in docs))
530
+ elif a.cmd == "path":
531
+ p = Q.path_between(st, a.src, a.dst, min_conf=a.min_confidence, platform=a.platform)
532
+ if a.platform:
533
+ from .platforms import filter_info, render_filter
534
+ print(render_filter(filter_info(st, a.platform)))
535
+ print(Q.fmt_path(p) if p else (f"on {a.platform}: " if a.platform else "") + Q.explain_no_path(st, a.src, a.dst, a.min_confidence))
536
+ for n in Q.path_notes(st, p) if p else []:
537
+ print(f"note: {n}")
538
+ if not p:
539
+ raise SystemExit(1) # scripts can tell "no path" apart from a path
540
+ elif a.cmd == "downstream":
541
+ res = Q.downstream(st, a.spec, min_conf=a.min_confidence, max_depth=a.max_depth, gate=None if a.gate == "none" else a.gate,
542
+ platform=a.platform)
543
+ print(json.dumps(res, indent=1, default=str) if a.json else Q.render_downstream(res, show_paths=not a.no_paths))
544
+ elif a.cmd == "api-calls":
545
+ rows = Q.api_calls(st, a.spec)
546
+ print(json.dumps(rows, indent=1, default=str) if a.json else Q.render_api_calls(rows))
547
+ elif a.cmd == "stats":
548
+ doc = stats_doc(st)
549
+ print(json.dumps(doc, indent=1, default=str) if a.json else render_stats(st, doc))
550
+
551
+ def node_doc(st, nid: str) -> dict:
552
+ """One node with its attrs parsed and its outgoing / incoming edges (`cg node --json`, #75)."""
553
+ n = dict(st.node(nid))
554
+ try:
555
+ n["attrs"] = json.loads(n.get("attrs") or "{}")
556
+ except ValueError:
557
+ pass
558
+
559
+ def edges(col, other):
560
+ out = []
561
+ for e in st.q(f"SELECT kind, {other}, file, line, confidence, attrs FROM edges WHERE {col}=? ORDER BY kind, line, {other}", (nid,)):
562
+ d = {"kind": e["kind"], other: e[other], "file": e["file"], "line": e["line"], "confidence": e["confidence"]}
563
+ if e["attrs"] and e["attrs"] != "{}":
564
+ d["attrs"] = json.loads(e["attrs"])
565
+ out.append(d)
566
+ return out
567
+ return {"node": n, "out": edges("src", "dst"), "in": edges("dst", "src")}
568
+
569
+
570
+ def render_node(d: dict) -> str:
571
+ """`cg node` text: the node's location, kind and attrs, then its edges; the same edge from one site (one row per
572
+ gate scenario or accessor) is listed once with a count."""
573
+ n, at = d["node"], d["node"].get("attrs") if isinstance(d["node"].get("attrs"), dict) else {}
574
+ span = f"{n['file']}:{n['line']}" + (f"-{n['end_line']}" if n.get("end_line") and n["end_line"] != n["line"] else "")
575
+ out = [f"{n['id']} ({n['kind']}{', ' + n['lang'] if n.get('lang') else ''})",
576
+ f" at: {span}" if n.get("file") else " at: (no source location)"]
577
+ if n.get("fqn") and n["fqn"] != n.get("name"):
578
+ out.append(f" fqn: {n['fqn']}")
579
+ if n.get("module"):
580
+ out.append(f" module: {n['module']}")
581
+ if n.get("entry_kind"):
582
+ out.append(f" entry: {n['entry_kind']}")
583
+ if "platforms" in at:
584
+ unk = at.get("platform_unknown") or []
585
+ ps = ", ".join(p + ("?" if p in unk else "") for p in at["platforms"]) or "none of the targets"
586
+ out.append(f" platforms: {ps}" + (f" ({at['platform_expr']})" if at.get("platform_expr") else ""))
587
+ rest = {k: v for k, v in at.items() if k not in ("platforms", "platform_unknown", "platform_expr", "platform_at", "platforms_other")}
588
+ if rest:
589
+ out.append(" attrs: " + ", ".join(f"{k}={v if isinstance(v, str) else json.dumps(v, default=str, ensure_ascii=False)}"
590
+ for k, v in rest.items()))
591
+ if n.get("doc"):
592
+ out.append(" doc: " + " ".join(str(n["doc"]).split())[:200])
593
+ for key, arrow, other in (("out", "->", "dst"), ("in", "<-", "src")):
594
+ es = d[key]
595
+ out.append(f" {'outgoing' if key == 'out' else 'incoming'} ({len(es)}):" if es else f" {'outgoing' if key == 'out' else 'incoming'}: none")
596
+ seen: dict = {}
597
+ for e in es:
598
+ k = (e["kind"], e[other], e["file"], e["line"], e["confidence"])
599
+ seen[k] = seen.get(k, 0) + 1
600
+ for (kind, o, f, line, conf), c in seen.items():
601
+ out.append(f" {arrow} {kind} {o} @{f}:{line} {conf}" + (f" ×{c}" if c > 1 else ""))
602
+ return "\n".join(out)
603
+
604
+
605
+ def stats_doc(st) -> dict:
606
+ """`cg stats --json`: one document, the index stats plus node / edge counts (#75)."""
607
+ m = st.meta()
608
+ nodes = {r["kind"]: r["c"] for r in st.q("SELECT kind, COUNT(*) c FROM nodes GROUP BY kind ORDER BY c DESC")}
609
+ edges: dict = {}
610
+ for r in st.q("SELECT kind, confidence, COUNT(*) c FROM edges GROUP BY kind, confidence ORDER BY kind, confidence"):
611
+ edges.setdefault(r["kind"], {})[r["confidence"]] = r["c"]
612
+ return {"project": m.get("project"), "root": m.get("root"), "indexed_at": m.get("indexed_at"),
613
+ "nodes": sum(nodes.values()), "edges": sum(sum(v.values()) for v in edges.values()),
614
+ "nodes_by_kind": nodes, "edges_by_kind": edges, "stats": m.get("stats")}
615
+
616
+
617
+ def render_stats(st, doc: dict) -> str:
618
+ s = doc.get("stats") or {}
619
+ det = s.get("detected") or {}
620
+ out = [f"project {doc.get('project') or '?'} root {doc.get('root') or '?'} indexed {doc.get('indexed_at') or '?'}"
621
+ + (f" in {s['index_seconds']} s" if s.get("index_seconds") is not None else "")]
622
+ langs = ", ".join(det.get("languages") or {}) or ", ".join((s.get("presets") or {}).get("languages") or []) or "none"
623
+ fws = ", ".join(sorted((s.get("presets") or {}).get("frameworks") or det.get("frameworks") or {})) or "none"
624
+ out.append(f"languages: {langs} | frameworks: {fws} | nodes {doc['nodes']} | edges {doc['edges']}")
625
+ from .coverage import for_graph, summary_line
626
+ for repo, cov in for_graph(st).items():
627
+ out.append(summary_line(cov, repo or None))
628
+ out.append("nodes by kind:")
629
+ out += [f" {k:16} {c}" for k, c in doc["nodes_by_kind"].items()]
630
+ out.append("edges by kind / confidence:")
631
+ out += [f" {k:22} {conf:10} {c}" for k, v in doc["edges_by_kind"].items() for conf, c in v.items()]
632
+ out.append("full index stats (plugins, presets, platforms, coverage): cg stats --json")
633
+ return "\n".join(out)
634
+
635
+
636
+ def _completeness(st, ids) -> dict:
637
+ from .coverage import completeness_for
638
+ return completeness_for(st, ids or [])
639
+
640
+
641
+ def _note(comp: dict, as_json: bool) -> None:
642
+ """The scoped `coverage note:` line after a partial text answer (nothing for complete answers or --json)."""
643
+ from .coverage import answer_note
644
+ if not as_json and not comp.get("complete"):
645
+ n = answer_note(comp)
646
+ if n:
647
+ print(n)
648
+
649
+
650
+ def config_cmd(a) -> int:
651
+ from .config import ConfigError, effective, find, load_file, render_effective, unknown_key_warnings
652
+ if a.action == "validate":
653
+ path = a.root if os.path.isfile(a.root) else find(a.root)
654
+ if path is None:
655
+ print(f"no .cg.yaml / .cg.yml in {a.root}: nothing to validate (the file is optional)")
656
+ return 0
657
+ try:
658
+ cfg = load_file(path)
659
+ except ConfigError as ex:
660
+ print(f"invalid: {ex}", file=sys.stderr)
661
+ return 2
662
+ keys = [k for k in cfg if k not in ("file", "ignored_keys")]
663
+ print(f"ok: {path} ({', '.join(keys) or 'no keys'})")
664
+ for w in unknown_key_warnings(cfg):
665
+ print(f"warning: {w}")
666
+ return 0
667
+ try:
668
+ eff = effective(a.root, a.python_root, a.gates, a.auth_pattern, a.plans_dir, a.presets, a.include_generated)
669
+ except ConfigError as ex:
670
+ print(f"cg config: {ex}", file=sys.stderr)
671
+ return 2
672
+ print(json.dumps(eff, indent=1, default=str) if a.json else render_effective(eff))
673
+ return 0
674
+
675
+
676
+ def plan_cmd(a) -> int:
677
+ from . import plans as P
678
+ if a.action == "list":
679
+ rows = P.list_plans(a.plans_dir)
680
+ if a.json:
681
+ print(json.dumps(rows, indent=1)); return 0
682
+ for r in rows:
683
+ if r.get("error"):
684
+ print(f"{r['name']}: ERROR {r['error']}"); continue
685
+ c = r["counts"]
686
+ print(f"{r['name']} [{r['status']}] {r['title']} (+{c['add_nodes']} nodes, ~{c['modify']} modified, +{c['add_edges']} edges, "
687
+ f"{c['forbid']} forbidden, {c['require']} required; schema errors {r['schema_errors']}) {r['file']}")
688
+ return 0
689
+ if not a.name:
690
+ raise SystemExit("plan name (or path) required")
691
+ plan = P.load_plan(a.name, a.plans_dir)
692
+ if a.action == "load":
693
+ if a.json:
694
+ print(json.dumps({k: v for k, v in plan.items() if not k.startswith("_") or k == "_schema_errors"}, indent=1, default=str))
695
+ else:
696
+ print(P.render_load(plan))
697
+ return 0
698
+ if not a.db:
699
+ raise SystemExit("--db required")
700
+ st = GraphStore(a.db)
701
+ if a.action == "validate":
702
+ v = P.validate(st, plan)
703
+ print(json.dumps(v, indent=1, default=str) if a.json else P.render_validate(v))
704
+ return 0 if v["ok"] else 1
705
+ if a.action == "baseline":
706
+ b = P.make_baseline(st, plan)
707
+ P.baseline_path(plan).write_text(json.dumps(b, indent=1) + "\n")
708
+ print(f"baseline for {len(b['targets'])} modified targets -> {P.baseline_path(plan)}")
709
+ return 0
710
+ res = P.check(st, plan, verify=a.verify, baseline=P.load_baseline(plan) if a.verify else None)
711
+ if a.json:
712
+ txt = json.dumps(res, indent=1, default=str)
713
+ elif a.summary:
714
+ txt = P.render_check_summary(res, max_items=a.max_items if a.max_items != 60 else 5)
715
+ else:
716
+ txt = P.render_check(res, max_items=a.max_items)
717
+ print(txt)
718
+ if a.out:
719
+ from pathlib import Path
720
+ Path(a.out).write_text(txt + "\n")
721
+ return 0
722
+
723
+
724
+ if __name__ == "__main__":
725
+ sys.exit(main() or 0)