codmap 0.0.15__tar.gz → 0.0.16__tar.gz
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.
- {codmap-0.0.15 → codmap-0.0.16}/PKG-INFO +3 -2
- {codmap-0.0.15 → codmap-0.0.16}/README.md +2 -1
- {codmap-0.0.15 → codmap-0.0.16}/codemap/cli.py +10 -2
- {codmap-0.0.15 → codmap-0.0.16}/codemap/model.py +101 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/query.py +122 -10
- {codmap-0.0.15 → codmap-0.0.16}/codemap/serve/audit.py +17 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/serve/impact.py +54 -1
- {codmap-0.0.15 → codmap-0.0.16}/codemap/serve/mcp_server.py +39 -16
- {codmap-0.0.15 → codmap-0.0.16}/codemap/serve/session.py +20 -5
- {codmap-0.0.15 → codmap-0.0.16}/codmap.egg-info/PKG-INFO +3 -2
- {codmap-0.0.15 → codmap-0.0.16}/codmap.egg-info/SOURCES.txt +2 -0
- {codmap-0.0.15 → codmap-0.0.16}/pyproject.toml +1 -1
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_m1_query.py +33 -12
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c28_limit_envelope.py +14 -1
- codmap-0.0.16/tests/test_r1c39_edge_resolution.py +215 -0
- codmap-0.0.16/tests/test_r1c40_flow_reach.py +291 -0
- {codmap-0.0.15 → codmap-0.0.16}/LICENSE +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/__init__.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/apidiff.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/arch.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/diagnostics.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/extract/__init__.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/extract/attrflow.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/extract/behavior.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/extract/dataflow.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/extract/dispatch.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/extract/griffe_extractor.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/extract/gsource.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/extract/roots.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/extract/union.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/freshness.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/incremental.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/integrations/__init__.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/integrations/base.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/integrations/cocoindex.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/integrations/gate.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/integrations/gitnexus.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/integrations/registry.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/integrations/transport.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/provenance.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/scope.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/serve/__init__.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/serve/_scip_pb2.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/serve/api_surface.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/serve/apidiff.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/serve/architecture.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/serve/check.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/serve/ctags.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/serve/limits.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/serve/livingdocs.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/serve/mermaid.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/serve/pack.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/serve/rag.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/serve/review.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/serve/scip.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/serve/semantic.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/serve/server.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/serve/subsystems.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/serve/vault.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/store.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/tomlio.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codemap/watch.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codmap.egg-info/dependency_links.txt +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codmap.egg-info/entry_points.txt +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codmap.egg-info/requires.txt +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/codmap.egg-info/top_level.txt +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/setup.cfg +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_epistemic.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_gitnexus_router.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_impact_depth.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_integrations.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_issue3_serve_freshness.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_livingdocs.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_m0_api_surface.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_m11_argcontract.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_m12_dataflow.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_m15_review.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_m16_architecture.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_m17_mcp.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_m18_freshness.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_m1_5_semantics.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_m2_views.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_m32_watch.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_m3_serve.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_m4_behavior.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_m5_deep.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_m6_repo_scope.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_m7_dispatch.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_m9_family.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c13_callgraph_accuracy.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c13_grep_vs_graph.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c16_semantic.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c20_attribute_edges.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c21_flat_layout.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c22_source_visible_refs.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c23_hard_python.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c24_test_mapping.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c25_provenance.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c26_deep_union.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c27_config_honesty.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c29_lazy_imports.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c2_ctags.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c30_local_import_calls.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c30f1_reexport_calls.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c30f2_check_scope.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c31_path_origin.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c32_report_json.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c33_query_signature.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c34_signature_kinds.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c35_contract_location.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c36_target_resolution.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c37_rule_mutation.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c38_tool_drift.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c3_arch_contract.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c3_dogfood.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c41_scope_membership.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c42_deep_tier_stability.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c43_incremental_splice.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c44_absent_answers.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c45_repeat_union.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c46_shadowed_definitions.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c47_incremental_resample.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c48_type_checking_imports.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c49_type_only_cycles.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c4_complexity.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c5_apidiff.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c6_pack.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c7_edge_vocab.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c8_deadcode.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_r1c9_incremental.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_scip_export.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_scope.py +0 -0
- {codmap-0.0.15 → codmap-0.0.16}/tests/test_subsystems.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codmap
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.16
|
|
4
4
|
Summary: Static analyzer that turns a package's source into a queryable code graph.
|
|
5
5
|
Author-email: kogriv <kogriv@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -177,7 +177,8 @@ codemap query analyze_zones --graph graph.json
|
|
|
177
177
|
codemap report architecture --graph graph.json # layers, coupling, god-objects, cycles
|
|
178
178
|
codemap report dependencies --graph graph.json
|
|
179
179
|
codemap report dead-code --graph graph.json
|
|
180
|
-
codemap report impact --symbol MyClass --graph graph.json
|
|
180
|
+
codemap report impact --symbol MyClass --graph graph.json # who references it, and which
|
|
181
|
+
# flows it lands on — at which step
|
|
181
182
|
codemap report api-surface --graph graph.json --format json # every kind has a json form
|
|
182
183
|
|
|
183
184
|
# change-set review straight from a diff → risk-sorted dossier
|
|
@@ -141,7 +141,8 @@ codemap query analyze_zones --graph graph.json
|
|
|
141
141
|
codemap report architecture --graph graph.json # layers, coupling, god-objects, cycles
|
|
142
142
|
codemap report dependencies --graph graph.json
|
|
143
143
|
codemap report dead-code --graph graph.json
|
|
144
|
-
codemap report impact --symbol MyClass --graph graph.json
|
|
144
|
+
codemap report impact --symbol MyClass --graph graph.json # who references it, and which
|
|
145
|
+
# flows it lands on — at which step
|
|
145
146
|
codemap report api-surface --graph graph.json --format json # every kind has a json form
|
|
146
147
|
|
|
147
148
|
# change-set review straight from a diff → risk-sorted dossier
|
|
@@ -507,7 +507,8 @@ def _cmd_report(args) -> int:
|
|
|
507
507
|
if args.kind == "impact":
|
|
508
508
|
if not args.symbol:
|
|
509
509
|
raise SystemExit("error: report impact needs --symbol <name>")
|
|
510
|
-
print(render_impact(Query(graph), args.symbol, depth=args.depth
|
|
510
|
+
print(render_impact(Query(graph), args.symbol, depth=args.depth,
|
|
511
|
+
flow_depth=args.flow_depth), end="")
|
|
511
512
|
return 0
|
|
512
513
|
if args.kind in ("communities", "flows"):
|
|
513
514
|
from codemap.serve.subsystems import render_communities, render_flows
|
|
@@ -542,7 +543,11 @@ def _report_json(graph, args) -> dict:
|
|
|
542
543
|
ids = q.impact_targets(args.symbol)
|
|
543
544
|
return {"kind": "impact", "target": graph.target, "symbol": args.symbol,
|
|
544
545
|
"matched": ids,
|
|
545
|
-
|
|
546
|
+
# R1-C40: the flows the change lands on ride next to the blast radius,
|
|
547
|
+
# not inside it — two answers with two different partialities.
|
|
548
|
+
"reports": [{"id": sid, **q.impact(sid, depth=args.depth),
|
|
549
|
+
"flows": q.flows_to(sid, max_depth=args.flow_depth)}
|
|
550
|
+
for sid in ids]}
|
|
546
551
|
if args.kind == "communities":
|
|
547
552
|
return {"kind": "communities", "target": graph.target,
|
|
548
553
|
"communities": Query(graph).communities()}
|
|
@@ -962,6 +967,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
962
967
|
r.add_argument("--symbol", help="Symbol for `report impact` (short or full name).")
|
|
963
968
|
r.add_argument("--depth", type=int, default=2,
|
|
964
969
|
help="report impact: transitive BFS depth (default 2).")
|
|
970
|
+
r.add_argument("--flow-depth", type=int, default=5,
|
|
971
|
+
help="report impact: how many call steps a flow may take to reach "
|
|
972
|
+
"the symbol (default 5, same as `report flows`).")
|
|
965
973
|
r.add_argument("--min-confidence", choices=["low", "medium", "high"], default=None,
|
|
966
974
|
help="report dead-code: only show candidates at/above this confidence.")
|
|
967
975
|
r.add_argument("--format", choices=["markdown", "json"], default="markdown")
|
|
@@ -97,6 +97,107 @@ EDGE_TYPES = frozenset({
|
|
|
97
97
|
"accesses", # function → attribute node it reads/writes (extras.access, R1-C20)
|
|
98
98
|
})
|
|
99
99
|
|
|
100
|
+
# Closed vocabulary of ``extras.resolution`` (R1-C39), keyed by (edge type, value).
|
|
101
|
+
#
|
|
102
|
+
# The route was already on the edge before this table existed — `calls` alone carries six
|
|
103
|
+
# distinguishable values — but five modules emitted them and nothing enumerated them, so a
|
|
104
|
+
# new or mistyped value shipped in silence. The guard is the R1-C7 shape: a pair absent
|
|
105
|
+
# here fails, and a row that stops appearing in any build fails too.
|
|
106
|
+
#
|
|
107
|
+
# ``means`` is the honest part. On `calls`/`accesses` the value says **how the target was
|
|
108
|
+
# found** (route); on `references` three of four values say **what kind of site** it was
|
|
109
|
+
# (an annotation, a name used as a value, a doc mention) and only `imported` is a route.
|
|
110
|
+
# One field, two questions — stated rather than papered over (design D4).
|
|
111
|
+
#
|
|
112
|
+
# ``confidence`` is a *grade*, never a probability: `exact` (the target came from a binding
|
|
113
|
+
# read in the source), `inferred` (a type-inference engine produced it — precise but a
|
|
114
|
+
# sample: one deep build misses a live edge ~1 time in 4, R1-C42), `heuristic` (a name
|
|
115
|
+
# match, not a binding — an honest over-approximation). A computed float would read as a
|
|
116
|
+
# precision that is not there. It is derived, never stored: writing it on the edge would be
|
|
117
|
+
# a second source of truth for a pure function of the first (design D2/D3).
|
|
118
|
+
RESOLUTIONS: dict[tuple[str, str], dict[str, str]] = {
|
|
119
|
+
("calls", "self"): {"means": "route", "confidence": "exact",
|
|
120
|
+
"how": "receiver is `self`; target is a member of the enclosing class"},
|
|
121
|
+
("calls", "module"): {"means": "route", "confidence": "exact",
|
|
122
|
+
"how": "the name is defined at this module's level"},
|
|
123
|
+
("calls", "imported"): {"means": "route", "confidence": "exact",
|
|
124
|
+
"how": "the name is bound by an import statement in this file"},
|
|
125
|
+
("calls", "deep"): {"means": "route", "confidence": "inferred",
|
|
126
|
+
"how": "receiver type inferred by jedi (deep tier only)"},
|
|
127
|
+
("calls", "registry"): {"means": "route", "confidence": "exact",
|
|
128
|
+
"how": "a literal registry key resolved to the registered impl (M7)"},
|
|
129
|
+
("calls", "registry-candidate"): {
|
|
130
|
+
"means": "route", "confidence": "heuristic",
|
|
131
|
+
"how": "a factory/getter call fanned out to every member of the family — an "
|
|
132
|
+
"honest over-approximation, not a resolved target"},
|
|
133
|
+
("accesses", "self"): {"means": "route", "confidence": "exact",
|
|
134
|
+
"how": "`self.attr` inside the class that declares the attribute"},
|
|
135
|
+
("accesses", "class"): {"means": "route", "confidence": "exact",
|
|
136
|
+
"how": "`Class.attr` through an import or module member"},
|
|
137
|
+
("accesses", "construct"): {"means": "route", "confidence": "exact",
|
|
138
|
+
"how": "`Class(...).attr` — the constructor names the owner"},
|
|
139
|
+
("accesses", "deep"): {"means": "route", "confidence": "inferred",
|
|
140
|
+
"how": "owner type inferred by jedi (deep tier only)"},
|
|
141
|
+
("references", "annotation"): {"means": "site", "confidence": "exact",
|
|
142
|
+
"how": "the symbol appears in a type annotation"},
|
|
143
|
+
("references", "name"): {"means": "site", "confidence": "exact",
|
|
144
|
+
"how": "the symbol is named as a value (dict entry, default=…)"},
|
|
145
|
+
("references", "doc"): {"means": "site", "confidence": "exact",
|
|
146
|
+
"how": "a documentation file names the symbol"},
|
|
147
|
+
("references", "imported"): {"means": "route", "confidence": "exact",
|
|
148
|
+
"how": "a consumer root names an imported core symbol"},
|
|
149
|
+
("reads", "string-key"): {"means": "route", "confidence": "exact",
|
|
150
|
+
"how": "a literal subscript key — the column set is an "
|
|
151
|
+
"over-set, dict access lands here too"},
|
|
152
|
+
("writes", "string-key"): {"means": "route", "confidence": "exact",
|
|
153
|
+
"how": "a literal subscript key (see `reads`)"},
|
|
154
|
+
("imports", "flat"): {"means": "route", "confidence": "exact",
|
|
155
|
+
"how": "sibling import under a flat layout (R1-C21)"},
|
|
156
|
+
("export", "flat"): {"means": "route", "confidence": "exact",
|
|
157
|
+
"how": "re-export inferred under a flat layout (R1-C21)"},
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#: Edge types that carry no ``resolution``: pure syntax, with no route to record.
|
|
161
|
+
UNRESOLVED_EDGE_TYPES = frozenset({"contains", "inherits", "decorated_by", "implements"})
|
|
162
|
+
|
|
163
|
+
#: Grades, strongest first — the order `min_confidence` filters by.
|
|
164
|
+
CONFIDENCE_ORDER = ("exact", "inferred", "heuristic")
|
|
165
|
+
|
|
166
|
+
#: A value this build's table does not know. Reachable only from an artifact built by
|
|
167
|
+
#: another version of the tool — never from a graph this build produced (the guard test
|
|
168
|
+
#: is what keeps that true), so it is reported, not raised, when a graph is merely read.
|
|
169
|
+
UNKNOWN_CONFIDENCE = "unknown"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def resolution_of(edge, *, strict: bool = True) -> dict[str, str] | None:
|
|
173
|
+
"""The table row for ``edge``, or ``None`` when it carries no ``resolution``.
|
|
174
|
+
|
|
175
|
+
``None`` is a legitimate answer (``contains`` has no route), and it is distinct from
|
|
176
|
+
a value the table does not know. Under ``strict`` that one raises — a silent unknown
|
|
177
|
+
is how an open vocabulary pretends to be closed — and this is the form the guard test
|
|
178
|
+
and the extractor use. Reading a *foreign* graph passes ``strict=False`` and gets an
|
|
179
|
+
``unknown`` grade instead: refusing to open an artifact from another version is a
|
|
180
|
+
worse answer than saying which part of it this build cannot grade.
|
|
181
|
+
"""
|
|
182
|
+
value = edge.extras.get("resolution")
|
|
183
|
+
if value is None:
|
|
184
|
+
return None
|
|
185
|
+
row = RESOLUTIONS.get((edge.type, value))
|
|
186
|
+
if row is None:
|
|
187
|
+
if not strict:
|
|
188
|
+
return {"means": "route", "confidence": UNKNOWN_CONFIDENCE,
|
|
189
|
+
"how": f"{value!r} is not in this build's table"}
|
|
190
|
+
raise KeyError(f"unknown resolution {value!r} on a {edge.type!r} edge — add it to "
|
|
191
|
+
f"RESOLUTIONS (codemap/model.py) with its meaning and grade")
|
|
192
|
+
return row
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def confidence_of(edge, *, strict: bool = True) -> str | None:
|
|
196
|
+
"""Grade of ``edge``: exact | inferred | heuristic, or ``None`` when it has no route."""
|
|
197
|
+
row = resolution_of(edge, strict=strict)
|
|
198
|
+
return row["confidence"] if row else None
|
|
199
|
+
|
|
200
|
+
|
|
100
201
|
# The subset of EDGE_TYPES an incremental build splices from the old graph for modules
|
|
101
202
|
# it did not recompute (``incremental.py``) — and therefore the classes a deep+incremental
|
|
102
203
|
# graph answers from an *earlier* build's jedi sample (R1-C43). Lives with the vocabulary
|
|
@@ -13,7 +13,19 @@ import re
|
|
|
13
13
|
|
|
14
14
|
import networkx as nx
|
|
15
15
|
|
|
16
|
-
from codemap.model import Graph, Node
|
|
16
|
+
from codemap.model import CONFIDENCE_ORDER, Graph, Node, confidence_of
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _grade_rank(grade: str | None) -> int:
|
|
20
|
+
"""Route grades ordered strongest-first (R1-C39); an unknown one sorts last."""
|
|
21
|
+
return CONFIDENCE_ORDER.index(grade) if grade in CONFIDENCE_ORDER else len(CONFIDENCE_ORDER)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _check_grade(min_confidence: str | None) -> None:
|
|
25
|
+
if min_confidence is not None and min_confidence not in CONFIDENCE_ORDER:
|
|
26
|
+
raise ValueError(f"min_confidence must be one of {CONFIDENCE_ORDER}, "
|
|
27
|
+
f"got {min_confidence!r}")
|
|
28
|
+
|
|
17
29
|
|
|
18
30
|
# Dead-code confidence, most-certain first (R1-C8). "high" = no inbound edge of any
|
|
19
31
|
# kind and no decorator/registry hook; "medium" = an implicit-use hook (decorator /
|
|
@@ -162,7 +174,16 @@ class Query:
|
|
|
162
174
|
self._call_in: dict[str, list[tuple[str, dict]]] = {}
|
|
163
175
|
for e in graph.edges:
|
|
164
176
|
if e.type == "calls":
|
|
165
|
-
|
|
177
|
+
# R1-C39: the grade of the route rides on the graph edge, so `callers` /
|
|
178
|
+
# `callees` can answer "only what a binding found". Two edges may collapse
|
|
179
|
+
# onto one pair (a call resolved exactly at one site, fanned out from a
|
|
180
|
+
# registry at another) — the *strongest* wins: the pair is genuinely
|
|
181
|
+
# connected by a binding, whatever else also points that way.
|
|
182
|
+
grade = confidence_of(e, strict=False)
|
|
183
|
+
prev = self._calls.edges.get((e.source, e.target), {}).get("confidence")
|
|
184
|
+
if prev is not None and _grade_rank(prev) < _grade_rank(grade):
|
|
185
|
+
grade = prev
|
|
186
|
+
self._calls.add_edge(e.source, e.target, confidence=grade)
|
|
166
187
|
self._call_in.setdefault(e.target, []).append((e.source, e.extras))
|
|
167
188
|
# implements edges (M9/F4): concrete impl -> Protocol (structural typing,
|
|
168
189
|
# synthesised via the registry family since it's never inherited).
|
|
@@ -412,17 +433,53 @@ class Query:
|
|
|
412
433
|
|
|
413
434
|
# -- call graph (M4, best-effort — see gaps/ CM-09) ----------------------
|
|
414
435
|
|
|
415
|
-
def callers(self, symbol_id: str) -> list[str]:
|
|
416
|
-
"""Functions that statically call ``symbol_id`` (resolved calls only).
|
|
417
|
-
if symbol_id not in self._calls:
|
|
418
|
-
return []
|
|
419
|
-
return sorted(self._calls.predecessors(symbol_id))
|
|
436
|
+
def callers(self, symbol_id: str, *, min_confidence: str | None = None) -> list[str]:
|
|
437
|
+
"""Functions that statically call ``symbol_id`` (resolved calls only).
|
|
420
438
|
|
|
421
|
-
|
|
422
|
-
|
|
439
|
+
``min_confidence`` (R1-C39) keeps only edges whose route grades at least that
|
|
440
|
+
strong — ``"exact"`` asks for calls a *binding* found and drops the honest
|
|
441
|
+
over-approximations (a factory fanned out across a registry family). Default:
|
|
442
|
+
every resolved edge, whatever found it.
|
|
443
|
+
"""
|
|
444
|
+
_check_grade(min_confidence) # before the membership test: a bad argument must
|
|
445
|
+
if symbol_id not in self._calls: # not pass silently just because the symbol is
|
|
446
|
+
return [] # absent from the call graph
|
|
447
|
+
return sorted(src for src in self._calls.predecessors(symbol_id)
|
|
448
|
+
if self._passes(src, symbol_id, min_confidence))
|
|
449
|
+
|
|
450
|
+
def callees(self, symbol_id: str, *, min_confidence: str | None = None) -> list[str]:
|
|
451
|
+
"""Internal symbols ``symbol_id`` statically calls (see :meth:`callers`)."""
|
|
452
|
+
_check_grade(min_confidence)
|
|
423
453
|
if symbol_id not in self._calls:
|
|
424
454
|
return []
|
|
425
|
-
return sorted(self._calls.successors(symbol_id)
|
|
455
|
+
return sorted(tgt for tgt in self._calls.successors(symbol_id)
|
|
456
|
+
if self._passes(symbol_id, tgt, min_confidence))
|
|
457
|
+
|
|
458
|
+
def _passes(self, source: str, target: str, min_confidence: str | None) -> bool:
|
|
459
|
+
if min_confidence is None:
|
|
460
|
+
return True
|
|
461
|
+
grade = self._calls.edges[source, target].get("confidence")
|
|
462
|
+
return _grade_rank(grade) <= _grade_rank(min_confidence)
|
|
463
|
+
|
|
464
|
+
def confidence_map(self) -> dict[str, dict[str, int]]:
|
|
465
|
+
"""Edge counts by route grade, and by (edge type, resolution) pair — R1-C39.
|
|
466
|
+
|
|
467
|
+
The answer to "how much of this graph was found by a binding, and how much by a
|
|
468
|
+
name match", which no field of the artifact stated before: the route was on every
|
|
469
|
+
edge, but nothing said what a route is worth. Edges with no route at all
|
|
470
|
+
(``contains``, ``inherits``, …) are counted under ``none`` rather than dropped —
|
|
471
|
+
absence is a number here too (R1-C28).
|
|
472
|
+
"""
|
|
473
|
+
by_grade: dict[str, int] = {}
|
|
474
|
+
by_pair: dict[str, int] = {}
|
|
475
|
+
for e in self.graph.edges:
|
|
476
|
+
grade = confidence_of(e, strict=False) or "none"
|
|
477
|
+
by_grade[grade] = by_grade.get(grade, 0) + 1
|
|
478
|
+
if e.extras.get("resolution") is not None:
|
|
479
|
+
key = f"{e.type}:{e.extras['resolution']}"
|
|
480
|
+
by_pair[key] = by_pair.get(key, 0) + 1
|
|
481
|
+
return {"by_grade": dict(sorted(by_grade.items())),
|
|
482
|
+
"by_pair": dict(sorted(by_pair.items()))}
|
|
426
483
|
|
|
427
484
|
def call_contract(self, symbol_id: str) -> list[dict]:
|
|
428
485
|
"""Per-caller argument contract of calls into ``symbol_id`` (+ members) — F7.
|
|
@@ -815,6 +872,61 @@ class Query:
|
|
|
815
872
|
return {"entry": entry, "edges": edges, "reached": len(seen) - 1,
|
|
816
873
|
"max_depth": max((e["distance"] for e in edges), default=0)}
|
|
817
874
|
|
|
875
|
+
def flows_to(self, symbol_id: str, *, max_depth: int = 5,
|
|
876
|
+
root: str = "core") -> dict:
|
|
877
|
+
"""Which flows a change to ``symbol_id`` lands on, and at which step (R1-C40).
|
|
878
|
+
|
|
879
|
+
The join of the two ends codemap already had: :meth:`impact` walks inbound
|
|
880
|
+
("who references"), :meth:`flow` walks outbound ("what a call sets in
|
|
881
|
+
motion"), and neither answers the question asked *before* a change — "what
|
|
882
|
+
stops working, and where in the scenario". A flow here is what ``flows``
|
|
883
|
+
already calls one: an entry point of ``root`` (:meth:`entry_points`).
|
|
884
|
+
``first_step`` is the shortest distance in ``calls`` edges from that entry
|
|
885
|
+
to the symbol **or one of its members** — shortest because "first" is what
|
|
886
|
+
was asked, and a BFS gives the minimum by construction.
|
|
887
|
+
|
|
888
|
+
Computed as one reverse BFS from the targets rather than a forward walk per
|
|
889
|
+
entry: the distance is the same measured from either end, and the cost drops
|
|
890
|
+
from O(entries × graph) to O(graph). The equivalence is what the acceptance
|
|
891
|
+
test checks against :meth:`flow`.
|
|
892
|
+
|
|
893
|
+
Says "reached", never "broken" (design D3): the graph knows the symbol lies
|
|
894
|
+
on the path, not whether the edit breaks it. Three separate partialities are
|
|
895
|
+
named rather than folded into the list — ``non_call_refs`` (a reference that
|
|
896
|
+
arrives by import / inheritance / decoration / attribute access cannot appear
|
|
897
|
+
in a flow at all), ``beyond_depth`` (entries that do reach, further than
|
|
898
|
+
``max_depth`` — counted, not silently dropped), and the standing note that
|
|
899
|
+
call resolution is a lower bound. ``in_call_graph: False`` is the honest
|
|
900
|
+
"nothing to say about flows" for a symbol the call layer never modelled — not
|
|
901
|
+
the same answer as an empty list (R1-C44).
|
|
902
|
+
"""
|
|
903
|
+
targets = {i for i in self._member_ids(symbol_id) if i in self._calls}
|
|
904
|
+
entries = self.entry_points(root)
|
|
905
|
+
refs = self.references_to(symbol_id)
|
|
906
|
+
out = {
|
|
907
|
+
"symbol": symbol_id, "root": root, "max_depth": max_depth,
|
|
908
|
+
"in_call_graph": bool(targets), "entry_points": len(entries),
|
|
909
|
+
"flows": [], "beyond_depth": 0,
|
|
910
|
+
"non_call_refs": sum(1 for r in refs if r["type"] != "calls"),
|
|
911
|
+
}
|
|
912
|
+
if not targets:
|
|
913
|
+
return out
|
|
914
|
+
dist = {t: 0 for t in targets}
|
|
915
|
+
frontier, step = set(targets), 0
|
|
916
|
+
while frontier:
|
|
917
|
+
nxt: set[str] = set()
|
|
918
|
+
for node in sorted(frontier):
|
|
919
|
+
for pred in self._calls.predecessors(node):
|
|
920
|
+
if pred not in dist:
|
|
921
|
+
dist[pred] = step + 1
|
|
922
|
+
nxt.add(pred)
|
|
923
|
+
frontier, step = nxt, step + 1
|
|
924
|
+
reaching = sorted((dist[e], e) for e in entries if e in dist)
|
|
925
|
+
out["flows"] = [{"entry": e, "first_step": d}
|
|
926
|
+
for d, e in reaching if d <= max_depth]
|
|
927
|
+
out["beyond_depth"] = sum(1 for d, _ in reaching if d > max_depth)
|
|
928
|
+
return out
|
|
929
|
+
|
|
818
930
|
# -- relevance ranking (R1-C6) -------------------------------------------
|
|
819
931
|
|
|
820
932
|
def _expand_seeds(self, seeds) -> set[str]:
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from codemap.diagnostics import diagnostics, render_lines
|
|
6
|
+
from codemap.model import CONFIDENCE_ORDER, RESOLUTIONS, UNKNOWN_CONFIDENCE
|
|
6
7
|
from codemap.query import Query
|
|
7
8
|
from codemap.tomlio import read_toml
|
|
8
9
|
|
|
@@ -250,6 +251,22 @@ def render_behavior(query: Query) -> str:
|
|
|
250
251
|
by_res[e.extras.get("resolution", "?")] = by_res.get(e.extras.get("resolution", "?"), 0) + 1
|
|
251
252
|
calls_edges = sum(by_res.values())
|
|
252
253
|
lines.append(f"_Emitted {calls_edges} `calls` edges (deduped caller→callee)._")
|
|
254
|
+
# R1-C39: the route was already on every edge; what a route is worth was not.
|
|
255
|
+
grades: dict[str, int] = {}
|
|
256
|
+
for value, count in by_res.items():
|
|
257
|
+
row = RESOLUTIONS.get(("calls", value))
|
|
258
|
+
grade = row["confidence"] if row else UNKNOWN_CONFIDENCE
|
|
259
|
+
grades[grade] = grades.get(grade, 0) + count
|
|
260
|
+
if grades:
|
|
261
|
+
lines.append("")
|
|
262
|
+
lines.append("- by route grade: " + ", ".join(
|
|
263
|
+
f"**{g}** {grades[g]}" for g in CONFIDENCE_ORDER if g in grades)
|
|
264
|
+
+ (f", unknown {grades[UNKNOWN_CONFIDENCE]}"
|
|
265
|
+
if UNKNOWN_CONFIDENCE in grades else ""))
|
|
266
|
+
lines.append("")
|
|
267
|
+
lines.append("_`exact` — a binding read from the source found the target; "
|
|
268
|
+
"`inferred` — a type-inference engine did (precise, but one deep "
|
|
269
|
+
"build is a sample); `heuristic` — a name match, not a binding._")
|
|
253
270
|
bridged = by_res.get("registry", 0) + by_res.get("registry-candidate", 0)
|
|
254
271
|
if bridged:
|
|
255
272
|
lines.append("")
|
|
@@ -11,7 +11,57 @@ from __future__ import annotations
|
|
|
11
11
|
from codemap.query import Query
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
_FLOW_ROWS = 40
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _flow_section(rep: dict) -> list[str]:
|
|
18
|
+
"""R1-C40: which scenarios the change lands on, and at which step.
|
|
19
|
+
|
|
20
|
+
The three partialities of `Query.flows_to` are rendered as words, not left for the
|
|
21
|
+
reader to infer from a short list: an empty flow list next to twenty references is
|
|
22
|
+
the exact shape that reads as "nothing will break" when it means "the flow layer
|
|
23
|
+
cannot see this kind of edge".
|
|
24
|
+
"""
|
|
25
|
+
lines = [f"### Flows reached ({len(rep['flows'])} of {rep['entry_points']} "
|
|
26
|
+
f"entry point(s) in root `{rep['root']}`)", ""]
|
|
27
|
+
if not rep["in_call_graph"]:
|
|
28
|
+
lines += ["_The call layer never modelled this symbol — no resolved call reaches "
|
|
29
|
+
"it or leaves it — so there is nothing to say about flows here. This is "
|
|
30
|
+
"not 'no flow reaches it'._", ""]
|
|
31
|
+
return lines
|
|
32
|
+
if rep["flows"]:
|
|
33
|
+
lines.append("_`step` — where in the flow the change first lands: 1 means the "
|
|
34
|
+
"entry point calls it directly, 0 that the entry point is the "
|
|
35
|
+
"symbol itself (or one of its members), so the flow starts inside "
|
|
36
|
+
"the change._")
|
|
37
|
+
lines.append("")
|
|
38
|
+
for f in rep["flows"][:_FLOW_ROWS]:
|
|
39
|
+
lines.append(f"- `{f['entry']}` — step {f['first_step']}")
|
|
40
|
+
if len(rep["flows"]) > _FLOW_ROWS:
|
|
41
|
+
lines.append(f"- _… {len(rep['flows']) - _FLOW_ROWS} more_")
|
|
42
|
+
else:
|
|
43
|
+
lines.append(f"_No entry point reaches it within {rep['max_depth']} step(s)._")
|
|
44
|
+
lines.append("")
|
|
45
|
+
notes = []
|
|
46
|
+
if rep["beyond_depth"]:
|
|
47
|
+
notes.append(f"{rep['beyond_depth']} further entry point(s) reach it **beyond** "
|
|
48
|
+
f"{rep['max_depth']} steps — counted, not listed.")
|
|
49
|
+
if rep["non_call_refs"]:
|
|
50
|
+
notes.append(f"{rep['non_call_refs']} direct reference(s) arrive by an edge that "
|
|
51
|
+
"is not a call (import / inheritance / decoration / attribute) and "
|
|
52
|
+
"cannot appear in a flow at all.")
|
|
53
|
+
notes.append("Flows follow resolved `calls` edges only — a lower bound both ways: an "
|
|
54
|
+
"unresolved caller also leaves a real internal looking like an entry "
|
|
55
|
+
"point, so the denominator is an upper bound. *Reached*, not *broken*: "
|
|
56
|
+
"the graph knows the symbol is on the path, not whether the change "
|
|
57
|
+
"breaks it.")
|
|
58
|
+
lines.append("_" + " ".join(notes) + "_")
|
|
59
|
+
lines.append("")
|
|
60
|
+
return lines
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def render_impact(query: Query, symbol: str, *, depth: int = 2,
|
|
64
|
+
flow_depth: int = 5) -> str:
|
|
15
65
|
"""Markdown blast-radius for the symbol matching ``symbol`` (short or full)."""
|
|
16
66
|
ids = query.impact_targets(symbol) # F23: short name / full id / re-export
|
|
17
67
|
lines = [f"# Impact — `{symbol}`", ""]
|
|
@@ -64,6 +114,9 @@ def render_impact(query: Query, symbol: str, *, depth: int = 2) -> str:
|
|
|
64
114
|
lines.append(f"- _+{len(indirect)} transitive (distance >1)_")
|
|
65
115
|
lines.append("")
|
|
66
116
|
|
|
117
|
+
# R1-C40: from "who references" to "what stops working, and where".
|
|
118
|
+
lines += _flow_section(query.flows_to(sid, max_depth=flow_depth))
|
|
119
|
+
|
|
67
120
|
# F7: argument contract of the call-sites — what a signature change touches.
|
|
68
121
|
contract = query.call_contract(sid)
|
|
69
122
|
if contract:
|
|
@@ -55,15 +55,27 @@ def _compact_impact(env: dict, limit: int) -> dict:
|
|
|
55
55
|
for e in result.get("impact", []):
|
|
56
56
|
refs = e.get("refs", [])
|
|
57
57
|
shown = min(limit, len(refs))
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
entry = {**e, "refs": refs[:limit],
|
|
59
|
+
"refs_shown": shown, "refs_total": len(refs)}
|
|
60
|
+
# R1-C40: the flows section is a second computed list, cut here as well — in its
|
|
61
|
+
# own vocabulary, so a reader never has to guess which list the envelope's summed
|
|
62
|
+
# block describes (it is the refs one).
|
|
63
|
+
flows = e.get("flows")
|
|
64
|
+
if isinstance(flows, dict):
|
|
65
|
+
listed = flows.get("flows", [])
|
|
66
|
+
entry["flows"] = {**flows, "flows": listed[:limit],
|
|
67
|
+
"flows_shown": min(limit, len(listed)),
|
|
68
|
+
"flows_total": len(listed)}
|
|
69
|
+
entries.append(entry)
|
|
60
70
|
shown_total += shown
|
|
61
71
|
refs_total += len(refs)
|
|
62
72
|
result["impact"] = entries
|
|
63
73
|
env["result"] = result
|
|
64
74
|
env["limit"] = limit_block(limit, shown_total, refs_total,
|
|
65
|
-
note="applied per impact entry, not
|
|
66
|
-
"the by_root counts are complete
|
|
75
|
+
note="counts the refs list, applied per impact entry, not "
|
|
76
|
+
"across the answer; the by_root counts are complete "
|
|
77
|
+
"regardless, and the flows list carries its own "
|
|
78
|
+
"flows_shown / flows_total")
|
|
67
79
|
return env
|
|
68
80
|
|
|
69
81
|
|
|
@@ -139,14 +151,20 @@ def build_mcp_server(session: "Session", name: str = "codemap") -> Any:
|
|
|
139
151
|
return op("resolve", {"name": name})
|
|
140
152
|
|
|
141
153
|
@server.tool()
|
|
142
|
-
def callers(symbol: str) -> dict:
|
|
143
|
-
"""Functions that statically call `symbol` (resolved calls only).
|
|
144
|
-
|
|
154
|
+
def callers(symbol: str, min_confidence: str | None = None) -> dict:
|
|
155
|
+
"""Functions that statically call `symbol` (resolved calls only).
|
|
156
|
+
`min_confidence` keeps only edges whose route grades at least that strong:
|
|
157
|
+
`exact` (a binding in the source found the target), `inferred` (a type-inference
|
|
158
|
+
engine did — precise, but one deep build samples ~3 of 4 live edges), `heuristic`
|
|
159
|
+
(a name match, e.g. a factory fanned out across a registry family). Omit for
|
|
160
|
+
every resolved edge. `stats` reports the whole graph's grade mix."""
|
|
161
|
+
return op("callers", {"symbol": symbol, "min_confidence": min_confidence})
|
|
145
162
|
|
|
146
163
|
@server.tool()
|
|
147
|
-
def callees(symbol: str) -> dict:
|
|
148
|
-
"""Internal symbols that `symbol` statically calls.
|
|
149
|
-
|
|
164
|
+
def callees(symbol: str, min_confidence: str | None = None) -> dict:
|
|
165
|
+
"""Internal symbols that `symbol` statically calls. `min_confidence` as in
|
|
166
|
+
`callers`: exact | inferred | heuristic."""
|
|
167
|
+
return op("callees", {"symbol": symbol, "min_confidence": min_confidence})
|
|
150
168
|
|
|
151
169
|
@server.tool()
|
|
152
170
|
def tests(symbol: str, depth: int = 3, cap: int = 25) -> dict:
|
|
@@ -164,13 +182,18 @@ def build_mcp_server(session: "Session", name: str = "codemap") -> Any:
|
|
|
164
182
|
return op("covers", {"test": test, "depth": depth, "cap": cap})
|
|
165
183
|
|
|
166
184
|
@server.tool()
|
|
167
|
-
def impact(symbol: str, depth: int = 2, limit: int = 40, full: bool = False
|
|
185
|
+
def impact(symbol: str, depth: int = 2, limit: int = 40, full: bool = False,
|
|
186
|
+
flow_depth: int = 5) -> dict:
|
|
168
187
|
"""Blast radius of changing `symbol`: inbound references up to `depth`, counted
|
|
169
|
-
by provenance root (core/tests/docs/…)
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
188
|
+
by provenance root (core/tests/docs/…), plus `flows` — which entry points reach
|
|
189
|
+
it and at which step (`first_step`), within `flow_depth`. Compact by default
|
|
190
|
+
(F22): omits the duplicate markdown and caps each list at `limit` (by_root counts
|
|
191
|
+
stay complete; entries carry refs_shown/refs_total and flows_shown/flows_total).
|
|
192
|
+
Flows follow resolved `calls` edges only: `non_call_refs` says how many
|
|
193
|
+
references cannot appear there, `beyond_depth` how many entries reach further,
|
|
194
|
+
and `in_call_graph: false` means the call layer never modelled the symbol — not
|
|
195
|
+
that no flow reaches it. Pass full=true for the entire payload with markdown."""
|
|
196
|
+
env = op("impact", {"symbol": symbol, "depth": depth, "flow_depth": flow_depth})
|
|
174
197
|
return env if full else _compact_impact(env, limit)
|
|
175
198
|
|
|
176
199
|
@server.tool()
|
|
@@ -110,7 +110,10 @@ _UNLIMITED_BY_DESIGN = {
|
|
|
110
110
|
"pack": "budget is a token budget the caller sets on purpose, and the result is "
|
|
111
111
|
"explicitly a *pack* — self-describing by construction",
|
|
112
112
|
"impact": "depth bounds the walk, it does not slice a computed list; the reach is "
|
|
113
|
-
"already echoed back as by_distance / max_distance"
|
|
113
|
+
"already echoed back as by_distance / max_distance, and flow_depth "
|
|
114
|
+
"likewise bounds the flow walk while counting what lies past it "
|
|
115
|
+
"(beyond_depth). Both lists come back whole here; the transport cut "
|
|
116
|
+
"lives in _compact_impact and declares itself there (R1-C40)",
|
|
114
117
|
"flows": "depth likewise bounds the walk, and the answer carries its own depth",
|
|
115
118
|
}
|
|
116
119
|
|
|
@@ -367,6 +370,9 @@ class Session:
|
|
|
367
370
|
"edges": len(self.graph.edges),
|
|
368
371
|
"node_kinds": dict(Counter(n.kind for n in self.graph.nodes.values())),
|
|
369
372
|
"edge_types": dict(Counter(e.type for e in self.graph.edges)),
|
|
373
|
+
# R1-C39: how much of this graph a binding found, and how much a name match.
|
|
374
|
+
# The route was always on the edge; what it is worth was never stated.
|
|
375
|
+
"confidence": self.query.confidence_map(),
|
|
370
376
|
}
|
|
371
377
|
# M18 + #3: age of the graph WE SERVE (not the on-disk file), with an explicit
|
|
372
378
|
# stale flag when the artifact was rebuilt after we loaded it.
|
|
@@ -442,13 +448,20 @@ class Session:
|
|
|
442
448
|
def _op_impact(self, args) -> dict:
|
|
443
449
|
sym = args["symbol"]
|
|
444
450
|
depth = int(args.get("depth", 2))
|
|
451
|
+
flow_depth = int(args.get("flow_depth", 5))
|
|
445
452
|
ids = self.query.impact_targets(sym) # F23: accept full id / re-export too
|
|
446
453
|
if not ids: # R1-C44: this op resolves on its own, so it records on its own
|
|
447
454
|
self._resolution = _not_found(sym)
|
|
448
455
|
return {
|
|
449
456
|
"symbol": sym,
|
|
450
|
-
|
|
451
|
-
"
|
|
457
|
+
# R1-C40: blast radius and the flows it lands on are two answers kept side by
|
|
458
|
+
# side, not merged — "who references" and "where in a scenario the change
|
|
459
|
+
# first bites" have different partialities, and each states its own.
|
|
460
|
+
"impact": [{**self.query.impact(sid, depth=depth),
|
|
461
|
+
"flows": self.query.flows_to(sid, max_depth=flow_depth)}
|
|
462
|
+
for sid in ids],
|
|
463
|
+
"markdown": render_impact(self.query, sym, depth=depth,
|
|
464
|
+
flow_depth=flow_depth),
|
|
452
465
|
}
|
|
453
466
|
|
|
454
467
|
def _op_resolve(self, args) -> dict | None:
|
|
@@ -505,10 +518,12 @@ class Session:
|
|
|
505
518
|
return res
|
|
506
519
|
|
|
507
520
|
def _op_callers(self, args) -> list:
|
|
508
|
-
return self.query.callers(self._canon(args["symbol"])
|
|
521
|
+
return self.query.callers(self._canon(args["symbol"]),
|
|
522
|
+
min_confidence=args.get("min_confidence"))
|
|
509
523
|
|
|
510
524
|
def _op_callees(self, args) -> list:
|
|
511
|
-
return self.query.callees(self._canon(args["symbol"])
|
|
525
|
+
return self.query.callees(self._canon(args["symbol"]),
|
|
526
|
+
min_confidence=args.get("min_confidence"))
|
|
512
527
|
|
|
513
528
|
def _op_implementers(self, args) -> list:
|
|
514
529
|
return self.query.implementers(self._canon(args["protocol"]))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codmap
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.16
|
|
4
4
|
Summary: Static analyzer that turns a package's source into a queryable code graph.
|
|
5
5
|
Author-email: kogriv <kogriv@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -177,7 +177,8 @@ codemap query analyze_zones --graph graph.json
|
|
|
177
177
|
codemap report architecture --graph graph.json # layers, coupling, god-objects, cycles
|
|
178
178
|
codemap report dependencies --graph graph.json
|
|
179
179
|
codemap report dead-code --graph graph.json
|
|
180
|
-
codemap report impact --symbol MyClass --graph graph.json
|
|
180
|
+
codemap report impact --symbol MyClass --graph graph.json # who references it, and which
|
|
181
|
+
# flows it lands on — at which step
|
|
181
182
|
codemap report api-surface --graph graph.json --format json # every kind has a json form
|
|
182
183
|
|
|
183
184
|
# change-set review straight from a diff → risk-sorted dossier
|
|
@@ -107,8 +107,10 @@ tests/test_r1c35_contract_location.py
|
|
|
107
107
|
tests/test_r1c36_target_resolution.py
|
|
108
108
|
tests/test_r1c37_rule_mutation.py
|
|
109
109
|
tests/test_r1c38_tool_drift.py
|
|
110
|
+
tests/test_r1c39_edge_resolution.py
|
|
110
111
|
tests/test_r1c3_arch_contract.py
|
|
111
112
|
tests/test_r1c3_dogfood.py
|
|
113
|
+
tests/test_r1c40_flow_reach.py
|
|
112
114
|
tests/test_r1c41_scope_membership.py
|
|
113
115
|
tests/test_r1c42_deep_tier_stability.py
|
|
114
116
|
tests/test_r1c43_incremental_splice.py
|
|
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
|
|
|
9
9
|
# version. README says this plainly, since `pip install codmap` / `import codemap` is a
|
|
10
10
|
# seam a reader will otherwise hit by surprise.
|
|
11
11
|
name = "codmap"
|
|
12
|
-
version = "0.0.
|
|
12
|
+
version = "0.0.16"
|
|
13
13
|
description = "Static analyzer that turns a package's source into a queryable code graph."
|
|
14
14
|
readme = "README.md"
|
|
15
15
|
# M20/D1: measured, not assumed. Each release of the declared range runs the full suite in
|