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.
- cg_code_graph-0.10.1.dist-info/METADATA +678 -0
- cg_code_graph-0.10.1.dist-info/RECORD +174 -0
- cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
- cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
- cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
- cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
- codegraph/__init__.py +2 -0
- codegraph/aitools.py +129 -0
- codegraph/apps.py +76 -0
- codegraph/blindspots.py +428 -0
- codegraph/bridges.py +1701 -0
- codegraph/cli.py +725 -0
- codegraph/concepts.py +362 -0
- codegraph/config.py +559 -0
- codegraph/core/__init__.py +0 -0
- codegraph/core/cache.py +375 -0
- codegraph/core/detect.py +80 -0
- codegraph/core/extractors.py +187 -0
- codegraph/core/fsutil.py +61 -0
- codegraph/core/generated.py +575 -0
- codegraph/core/model.py +174 -0
- codegraph/core/paths.py +175 -0
- codegraph/core/plugin.py +160 -0
- codegraph/core/store.py +80 -0
- codegraph/core/syntax_errors.py +132 -0
- codegraph/coverage.py +928 -0
- codegraph/doctor.py +453 -0
- codegraph/external.py +613 -0
- codegraph/indexer.py +336 -0
- codegraph/link.py +434 -0
- codegraph/lint_async.py +524 -0
- codegraph/mcp_server.py +1303 -0
- codegraph/parity.py +473 -0
- codegraph/parity_structure.py +307 -0
- codegraph/payload.py +321 -0
- codegraph/plans.py +1285 -0
- codegraph/platform_scan.py +643 -0
- codegraph/platforms.py +1369 -0
- codegraph/plugins/__init__.py +0 -0
- codegraph/plugins/cfamily/__init__.py +0 -0
- codegraph/plugins/cfamily/plugin.py +930 -0
- codegraph/plugins/cfamily/syntax.py +881 -0
- codegraph/plugins/dart/__init__.py +0 -0
- codegraph/plugins/dart/bridges.py +345 -0
- codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
- codegraph/plugins/dart/extractor/pubspec.lock +149 -0
- codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
- codegraph/plugins/dart/http.py +904 -0
- codegraph/plugins/dart/models.py +308 -0
- codegraph/plugins/dart/plugin.py +625 -0
- codegraph/plugins/dart/program.py +907 -0
- codegraph/plugins/django/__init__.py +0 -0
- codegraph/plugins/django/extras.py +378 -0
- codegraph/plugins/django/models.py +508 -0
- codegraph/plugins/django/plugin.py +728 -0
- codegraph/plugins/django/schemas.py +339 -0
- codegraph/plugins/django/shapes.py +216 -0
- codegraph/plugins/django/urls.py +603 -0
- codegraph/plugins/express/__init__.py +0 -0
- codegraph/plugins/express/plugin.py +428 -0
- codegraph/plugins/flutter/__init__.py +0 -0
- codegraph/plugins/flutter/plugin.py +538 -0
- codegraph/plugins/kotlin/__init__.py +0 -0
- codegraph/plugins/kotlin/exact.py +457 -0
- codegraph/plugins/kotlin/plugin.py +1961 -0
- codegraph/plugins/kotlin/reparse.py +234 -0
- codegraph/plugins/laravel/__init__.py +0 -0
- codegraph/plugins/laravel/broadcast.py +351 -0
- codegraph/plugins/laravel/plugin.py +863 -0
- codegraph/plugins/laravel/tests.py +262 -0
- codegraph/plugins/laravel/values.py +728 -0
- codegraph/plugins/native/__init__.py +0 -0
- codegraph/plugins/native/gates.py +286 -0
- codegraph/plugins/native/runner.py +183 -0
- codegraph/plugins/native/scipread.py +194 -0
- codegraph/plugins/native/ts.py +54 -0
- codegraph/plugins/nest/__init__.py +0 -0
- codegraph/plugins/nest/plugin.py +654 -0
- codegraph/plugins/nextjs/__init__.py +0 -0
- codegraph/plugins/nextjs/plugin.py +336 -0
- codegraph/plugins/nuxt/__init__.py +0 -0
- codegraph/plugins/nuxt/plugin.py +308 -0
- codegraph/plugins/php/__init__.py +0 -0
- codegraph/plugins/php/extractor/composer.json +5 -0
- codegraph/plugins/php/extractor/composer.lock +76 -0
- codegraph/plugins/php/extractor/extract.php +743 -0
- codegraph/plugins/php/gating.py +573 -0
- codegraph/plugins/php/plugin.py +668 -0
- codegraph/plugins/php/strings.py +197 -0
- codegraph/plugins/python/__init__.py +0 -0
- codegraph/plugins/python/aitools.py +664 -0
- codegraph/plugins/python/external.py +245 -0
- codegraph/plugins/python/fields.py +107 -0
- codegraph/plugins/python/plugin.py +1733 -0
- codegraph/plugins/python/refs.py +485 -0
- codegraph/plugins/python/roots.py +412 -0
- codegraph/plugins/python/socketio.py +210 -0
- codegraph/plugins/python/subproc.py +864 -0
- codegraph/plugins/python/tests.py +1040 -0
- codegraph/plugins/python/values.py +179 -0
- codegraph/plugins/pyweb/__init__.py +0 -0
- codegraph/plugins/pyweb/plugin.py +1334 -0
- codegraph/plugins/pyweb/values.py +68 -0
- codegraph/plugins/rust/__init__.py +0 -0
- codegraph/plugins/rust/cargo.py +226 -0
- codegraph/plugins/rust/plugin.py +980 -0
- codegraph/plugins/rust/syntax.py +678 -0
- codegraph/plugins/scip/__init__.py +0 -0
- codegraph/plugins/scip/importer.py +129 -0
- codegraph/plugins/scip/scip.proto +962 -0
- codegraph/plugins/scip/scip_pb2.py +97 -0
- codegraph/plugins/stubs/__init__.py +0 -0
- codegraph/plugins/stubs/plugins.py +38 -0
- codegraph/plugins/swift/__init__.py +0 -0
- codegraph/plugins/swift/baseurl.py +109 -0
- codegraph/plugins/swift/exact.py +415 -0
- codegraph/plugins/swift/indexstore.py +209 -0
- codegraph/plugins/swift/packages.py +174 -0
- codegraph/plugins/swift/plugin.py +2890 -0
- codegraph/plugins/ts/__init__.py +0 -0
- codegraph/plugins/ts/baseurl.py +185 -0
- codegraph/plugins/ts/extractor/extract.mjs +2652 -0
- codegraph/plugins/ts/extractor/fw.mjs +685 -0
- codegraph/plugins/ts/extractor/package-lock.json +205 -0
- codegraph/plugins/ts/extractor/package.json +9 -0
- codegraph/plugins/ts/plugin.py +480 -0
- codegraph/plugins/tsweb/__init__.py +0 -0
- codegraph/plugins/tsweb/common.py +290 -0
- codegraph/plugins/tsweb/data.py +276 -0
- codegraph/presets/__init__.py +146 -0
- codegraph/presets/c_cpp.yaml +9 -0
- codegraph/presets/common.yaml +66 -0
- codegraph/presets/dart.yaml +9 -0
- codegraph/presets/django-ninja.yaml +15 -0
- codegraph/presets/django.yaml +25 -0
- codegraph/presets/djangorestframework.yaml +17 -0
- codegraph/presets/express.yaml +17 -0
- codegraph/presets/kotlin.yaml +11 -0
- codegraph/presets/laravel.yaml +40 -0
- codegraph/presets/nest.yaml +11 -0
- codegraph/presets/nextjs.yaml +15 -0
- codegraph/presets/nuxt.yaml +9 -0
- codegraph/presets/php.yaml +5 -0
- codegraph/presets/python.yaml +10 -0
- codegraph/presets/rust.yaml +5 -0
- codegraph/presets/swift.yaml +10 -0
- codegraph/presets/typescript.yaml +13 -0
- codegraph/process_runs.py +328 -0
- codegraph/protocols/__init__.py +299 -0
- codegraph/protocols/builtin.py +67 -0
- codegraph/protocols/matchers.py +144 -0
- codegraph/protocols/view.py +334 -0
- codegraph/query.py +2089 -0
- codegraph/realtime.py +260 -0
- codegraph/roundtrip.py +346 -0
- codegraph/routes.py +442 -0
- codegraph/starters.py +218 -0
- codegraph/tests_index.py +117 -0
- codegraph/viz/__init__.py +0 -0
- codegraph/viz/graph.py +369 -0
- codegraph/viz/server.py +198 -0
- codegraph/viz/static/app.css +148 -0
- codegraph/viz/static/app.js +1082 -0
- codegraph/viz/static/index.html +81 -0
- codegraph/viz/static/layered.js +237 -0
- codegraph/viz/static/vendor/VERSIONS.txt +4 -0
- codegraph/viz/static/vendor/cose-base.js +3214 -0
- codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
- codegraph/viz/static/vendor/cytoscape.min.js +31 -0
- codegraph/viz/static/vendor/layout-base.js +5230 -0
- codegraph/viz/tools/package-lock.json +303 -0
- codegraph/viz/tools/package.json +7 -0
- codegraph/viz/tools/shoot.mjs +165 -0
- 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)
|