codmap 0.0.15__tar.gz → 0.0.17__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.17}/PKG-INFO +3 -2
- {codmap-0.0.15 → codmap-0.0.17}/README.md +2 -1
- {codmap-0.0.15 → codmap-0.0.17}/codemap/apidiff.py +40 -4
- {codmap-0.0.15 → codmap-0.0.17}/codemap/cli.py +10 -2
- {codmap-0.0.15 → codmap-0.0.17}/codemap/model.py +101 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/query.py +193 -16
- {codmap-0.0.15 → codmap-0.0.17}/codemap/serve/apidiff.py +13 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/serve/audit.py +17 -0
- codmap-0.0.17/codemap/serve/impact.py +160 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/serve/mcp_server.py +39 -16
- {codmap-0.0.15 → codmap-0.0.17}/codemap/serve/session.py +52 -5
- {codmap-0.0.15 → codmap-0.0.17}/codmap.egg-info/PKG-INFO +3 -2
- {codmap-0.0.15 → codmap-0.0.17}/codmap.egg-info/SOURCES.txt +3 -0
- {codmap-0.0.15 → codmap-0.0.17}/pyproject.toml +1 -1
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_m1_query.py +33 -12
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c28_limit_envelope.py +14 -1
- codmap-0.0.17/tests/test_r1c39_edge_resolution.py +215 -0
- codmap-0.0.17/tests/test_r1c40_flow_reach.py +296 -0
- codmap-0.0.17/tests/test_r1c50_flow_entry_points.py +285 -0
- codmap-0.0.15/codemap/serve/impact.py +0 -86
- {codmap-0.0.15 → codmap-0.0.17}/LICENSE +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/__init__.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/arch.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/diagnostics.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/extract/__init__.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/extract/attrflow.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/extract/behavior.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/extract/dataflow.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/extract/dispatch.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/extract/griffe_extractor.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/extract/gsource.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/extract/roots.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/extract/union.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/freshness.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/incremental.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/integrations/__init__.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/integrations/base.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/integrations/cocoindex.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/integrations/gate.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/integrations/gitnexus.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/integrations/registry.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/integrations/transport.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/provenance.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/scope.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/serve/__init__.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/serve/_scip_pb2.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/serve/api_surface.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/serve/architecture.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/serve/check.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/serve/ctags.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/serve/limits.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/serve/livingdocs.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/serve/mermaid.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/serve/pack.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/serve/rag.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/serve/review.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/serve/scip.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/serve/semantic.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/serve/server.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/serve/subsystems.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/serve/vault.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/store.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/tomlio.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codemap/watch.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codmap.egg-info/dependency_links.txt +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codmap.egg-info/entry_points.txt +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codmap.egg-info/requires.txt +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/codmap.egg-info/top_level.txt +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/setup.cfg +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_epistemic.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_gitnexus_router.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_impact_depth.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_integrations.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_issue3_serve_freshness.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_livingdocs.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_m0_api_surface.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_m11_argcontract.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_m12_dataflow.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_m15_review.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_m16_architecture.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_m17_mcp.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_m18_freshness.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_m1_5_semantics.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_m2_views.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_m32_watch.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_m3_serve.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_m4_behavior.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_m5_deep.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_m6_repo_scope.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_m7_dispatch.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_m9_family.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c13_callgraph_accuracy.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c13_grep_vs_graph.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c16_semantic.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c20_attribute_edges.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c21_flat_layout.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c22_source_visible_refs.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c23_hard_python.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c24_test_mapping.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c25_provenance.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c26_deep_union.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c27_config_honesty.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c29_lazy_imports.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c2_ctags.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c30_local_import_calls.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c30f1_reexport_calls.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c30f2_check_scope.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c31_path_origin.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c32_report_json.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c33_query_signature.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c34_signature_kinds.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c35_contract_location.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c36_target_resolution.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c37_rule_mutation.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c38_tool_drift.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c3_arch_contract.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c3_dogfood.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c41_scope_membership.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c42_deep_tier_stability.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c43_incremental_splice.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c44_absent_answers.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c45_repeat_union.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c46_shadowed_definitions.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c47_incremental_resample.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c48_type_checking_imports.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c49_type_only_cycles.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c4_complexity.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c5_apidiff.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c6_pack.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c7_edge_vocab.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c8_deadcode.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_r1c9_incremental.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_scip_export.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/tests/test_scope.py +0 -0
- {codmap-0.0.15 → codmap-0.0.17}/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.17
|
|
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
|
|
@@ -45,6 +45,8 @@ class ApiDiff:
|
|
|
45
45
|
added: list[str] = field(default_factory=list) # new public symbols
|
|
46
46
|
removed: list[str] = field(default_factory=list) # deleted public symbols (each breaking)
|
|
47
47
|
changes: list[Change] = field(default_factory=list) # per-symbol classified changes
|
|
48
|
+
root: str | None = "core" # the provenance root compared (R1-C52)
|
|
49
|
+
excluded: dict[str, int] = field(default_factory=dict) # public symbols left unjudged, by root
|
|
48
50
|
|
|
49
51
|
@property
|
|
50
52
|
def breaking(self) -> list[Change]:
|
|
@@ -52,6 +54,8 @@ class ApiDiff:
|
|
|
52
54
|
|
|
53
55
|
def to_dict(self) -> dict:
|
|
54
56
|
return {
|
|
57
|
+
"root": self.root,
|
|
58
|
+
"excluded": dict(self.excluded),
|
|
55
59
|
"added": sorted(self.added),
|
|
56
60
|
"removed": sorted(self.removed),
|
|
57
61
|
"changes": [
|
|
@@ -184,11 +188,43 @@ def _is_public(node: Node) -> bool:
|
|
|
184
188
|
return node.visibility == "public"
|
|
185
189
|
|
|
186
190
|
|
|
187
|
-
def
|
|
188
|
-
"""
|
|
191
|
+
def _root_of(node: Node) -> str:
|
|
192
|
+
"""Provenance root of a node; an untagged graph is all ``core`` by construction."""
|
|
193
|
+
return node.extras.get("root", "core")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def diff_api(old: Graph, new: Graph, *, root: str | None = "core") -> ApiDiff:
|
|
197
|
+
"""Diff the public API surface of two graphs (old → new).
|
|
198
|
+
|
|
199
|
+
``root`` restricts the comparison to one provenance root, default ``core`` — the
|
|
200
|
+
package. R1-C52: this used to filter on visibility alone, so on a repo-scoped graph
|
|
201
|
+
(built with ``--consumer``) every public *test* function counted as added API. The
|
|
202
|
+
dogfood target measured it on their own release gate: **40 of 47** "added public
|
|
203
|
+
symbols" were test functions, which means `--exit-code` fired on test churn rather
|
|
204
|
+
than on the package. A public function in `tests/` is not this package's API.
|
|
205
|
+
|
|
206
|
+
Pass ``root=None`` to compare every root, and read ``excluded`` for what a root
|
|
207
|
+
filter left out — a gate must name what it did not judge (R1-C30-f2).
|
|
208
|
+
"""
|
|
189
209
|
diff = ApiDiff()
|
|
190
|
-
|
|
191
|
-
|
|
210
|
+
|
|
211
|
+
def _pick(graph: Graph) -> tuple[dict, dict[str, int]]:
|
|
212
|
+
keep, skipped = {}, {}
|
|
213
|
+
for i, n in graph.nodes.items():
|
|
214
|
+
if n.kind not in ("function", "class", "attribute"):
|
|
215
|
+
continue
|
|
216
|
+
r = _root_of(n)
|
|
217
|
+
if root is not None and r != root:
|
|
218
|
+
if _is_public(n):
|
|
219
|
+
skipped[r] = skipped.get(r, 0) + 1
|
|
220
|
+
continue
|
|
221
|
+
keep[i] = n
|
|
222
|
+
return keep, skipped
|
|
223
|
+
|
|
224
|
+
old_nodes, _ = _pick(old)
|
|
225
|
+
new_nodes, skipped = _pick(new)
|
|
226
|
+
diff.root = root
|
|
227
|
+
diff.excluded = dict(sorted(skipped.items()))
|
|
192
228
|
|
|
193
229
|
for sid, n in new_nodes.items():
|
|
194
230
|
if sid not in old_nodes and _is_public(n):
|
|
@@ -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.
|
|
@@ -777,18 +834,75 @@ class Query:
|
|
|
777
834
|
return out
|
|
778
835
|
|
|
779
836
|
def entry_points(self, root: str = "core") -> list[str]:
|
|
780
|
-
"""Call-forest roots: functions that call out
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
837
|
+
"""Call-forest roots: functions that call out and are called by nothing *in
|
|
838
|
+
their own root* (resolved calls only).
|
|
839
|
+
|
|
840
|
+
Where behaviour starts — public API / mains / not-yet-triggered. Restricted to
|
|
841
|
+
one provenance ``root`` (default core).
|
|
842
|
+
|
|
843
|
+
R1-C50: "called by nothing" used to mean in-degree zero **across the whole
|
|
844
|
+
graph**, which on a library means the public API is never an entry point — the
|
|
845
|
+
one function a user enters the package through is called from `tests`,
|
|
846
|
+
`examples` and `scripts`, and those 43 calls were what disqualified it. A call
|
|
847
|
+
from a consumer root is a *use*, not an internal caller; being used is not
|
|
848
|
+
evidence of being reachable-from-elsewhere. Reported by the dogfood target as
|
|
849
|
+
[issue #19](https://github.com/kogriv/codemap/issues/19). On a single-package
|
|
850
|
+
graph every node is `core`, so this is the same set as before by construction.
|
|
851
|
+
|
|
852
|
+
Best-effort in **both** directions: an unresolved caller leaves a real internal
|
|
853
|
+
looking like an entry point (the set is an over-estimate), and resolving one
|
|
854
|
+
*removes* an entry point (it is an under-estimate of heads whose chain the
|
|
855
|
+
resolver cannot close). The second direction is what #19 measured.
|
|
785
856
|
"""
|
|
786
857
|
return sorted(
|
|
787
858
|
n for n in self._calls.nodes
|
|
788
859
|
if self.root_of(n) == root
|
|
789
|
-
and self._calls.out_degree(n) > 0
|
|
860
|
+
and self._calls.out_degree(n) > 0
|
|
861
|
+
and not any(self.root_of(p) == root
|
|
862
|
+
for p in self._calls.predecessors(n))
|
|
790
863
|
)
|
|
791
864
|
|
|
865
|
+
def external_callers(self, symbol_id: str, root: str = "core") -> dict[str, int]:
|
|
866
|
+
"""Resolved callers of ``symbol_id`` living **outside** ``root``, by root.
|
|
867
|
+
|
|
868
|
+
R1-C50/D8: on an entry point this is the evidence that the head is public —
|
|
869
|
+
`{"tests": 43}` says the package is entered here, which is exactly what the
|
|
870
|
+
in-degree rule used to read as a disqualification.
|
|
871
|
+
"""
|
|
872
|
+
out: dict[str, int] = {}
|
|
873
|
+
if symbol_id in self._calls:
|
|
874
|
+
for p in self._calls.predecessors(symbol_id):
|
|
875
|
+
r = self.root_of(p)
|
|
876
|
+
if r != root:
|
|
877
|
+
out[r] = out.get(r, 0) + 1
|
|
878
|
+
return dict(sorted(out.items()))
|
|
879
|
+
|
|
880
|
+
def caller_grades(self, symbol_id: str) -> dict[str, int]:
|
|
881
|
+
"""Histogram of the route grades of the resolved calls *into* ``symbol_id``.
|
|
882
|
+
|
|
883
|
+
R1-C51: what `callers(min_confidence=…)` would drop, so a filtered answer can
|
|
884
|
+
say so instead of coming back as a bare empty list. Reported by the dogfood
|
|
885
|
+
target with the measurement that makes it matter: of the 25 symbols their
|
|
886
|
+
registry fan-out reaches, **13 have no `exact` caller at all** — every strategy
|
|
887
|
+
method — so `min_confidence="exact"` answers `[]` about symbols that are called
|
|
888
|
+
on every run, through an object a factory returned for a string key.
|
|
889
|
+
"""
|
|
890
|
+
out: dict[str, int] = {}
|
|
891
|
+
if symbol_id in self._calls:
|
|
892
|
+
for p in self._calls.predecessors(symbol_id):
|
|
893
|
+
g = self._calls.edges[p, symbol_id].get("confidence") or "unknown"
|
|
894
|
+
out[g] = out.get(g, 0) + 1
|
|
895
|
+
return dict(sorted(out.items(), key=lambda kv: _grade_rank(kv[0])))
|
|
896
|
+
|
|
897
|
+
def callee_grades(self, symbol_id: str) -> dict[str, int]:
|
|
898
|
+
"""Histogram of the route grades of the resolved calls *out of* ``symbol_id``."""
|
|
899
|
+
out: dict[str, int] = {}
|
|
900
|
+
if symbol_id in self._calls:
|
|
901
|
+
for t in self._calls.successors(symbol_id):
|
|
902
|
+
g = self._calls.edges[symbol_id, t].get("confidence") or "unknown"
|
|
903
|
+
out[g] = out.get(g, 0) + 1
|
|
904
|
+
return dict(sorted(out.items(), key=lambda kv: _grade_rank(kv[0])))
|
|
905
|
+
|
|
792
906
|
def flow(self, entry: str, *, max_depth: int = 5) -> dict:
|
|
793
907
|
"""Forward call-flow from ``entry`` along ``calls`` edges, bounded by depth.
|
|
794
908
|
|
|
@@ -815,6 +929,69 @@ class Query:
|
|
|
815
929
|
return {"entry": entry, "edges": edges, "reached": len(seen) - 1,
|
|
816
930
|
"max_depth": max((e["distance"] for e in edges), default=0)}
|
|
817
931
|
|
|
932
|
+
def flows_to(self, symbol_id: str, *, max_depth: int = 5,
|
|
933
|
+
root: str = "core") -> dict:
|
|
934
|
+
"""Which flows a change to ``symbol_id`` lands on, and at which step (R1-C40).
|
|
935
|
+
|
|
936
|
+
The join of the two ends codemap already had: :meth:`impact` walks inbound
|
|
937
|
+
("who references"), :meth:`flow` walks outbound ("what a call sets in
|
|
938
|
+
motion"), and neither answers the question asked *before* a change — "what
|
|
939
|
+
stops working, and where in the scenario". A flow here is what ``flows``
|
|
940
|
+
already calls one: an entry point of ``root`` (:meth:`entry_points`).
|
|
941
|
+
``first_step`` is the shortest distance in ``calls`` edges from that entry
|
|
942
|
+
to the symbol **or one of its members** — shortest because "first" is what
|
|
943
|
+
was asked, and a BFS gives the minimum by construction.
|
|
944
|
+
|
|
945
|
+
Computed as one reverse BFS from the targets rather than a forward walk per
|
|
946
|
+
entry: the distance is the same measured from either end, and the cost drops
|
|
947
|
+
from O(entries × graph) to O(graph). The equivalence is what the acceptance
|
|
948
|
+
test checks against :meth:`flow`.
|
|
949
|
+
|
|
950
|
+
Says "reached", never "broken" (design D3): the graph knows the symbol lies
|
|
951
|
+
on the path, not whether the edit breaks it. Three separate partialities are
|
|
952
|
+
named rather than folded into the list — ``non_call_refs`` (a reference that
|
|
953
|
+
arrives by import / inheritance / decoration / attribute access cannot appear
|
|
954
|
+
in a flow at all), ``beyond_depth`` (entries that do reach, further than
|
|
955
|
+
``max_depth`` — counted, not silently dropped), and the standing note that
|
|
956
|
+
call resolution is a lower bound. ``in_call_graph: False`` is the honest
|
|
957
|
+
"nothing to say about flows" for a symbol the call layer never modelled — not
|
|
958
|
+
the same answer as an empty list (R1-C44).
|
|
959
|
+
"""
|
|
960
|
+
targets = {i for i in self._member_ids(symbol_id) if i in self._calls}
|
|
961
|
+
entries = self.entry_points(root)
|
|
962
|
+
refs = self.references_to(symbol_id)
|
|
963
|
+
out = {
|
|
964
|
+
"symbol": symbol_id, "root": root, "max_depth": max_depth,
|
|
965
|
+
"in_call_graph": bool(targets), "entry_points": len(entries),
|
|
966
|
+
"flows": [], "beyond_depth": 0, "nearest_beyond": None,
|
|
967
|
+
"inbound_calls": sum(len(self._calls.pred[t]) for t in targets),
|
|
968
|
+
"non_call_refs": sum(1 for r in refs if r["type"] != "calls"),
|
|
969
|
+
}
|
|
970
|
+
if not targets:
|
|
971
|
+
return out
|
|
972
|
+
dist = {t: 0 for t in targets}
|
|
973
|
+
frontier, step = set(targets), 0
|
|
974
|
+
while frontier:
|
|
975
|
+
nxt: set[str] = set()
|
|
976
|
+
for node in sorted(frontier):
|
|
977
|
+
for pred in self._calls.predecessors(node):
|
|
978
|
+
if pred not in dist:
|
|
979
|
+
dist[pred] = step + 1
|
|
980
|
+
nxt.add(pred)
|
|
981
|
+
frontier, step = nxt, step + 1
|
|
982
|
+
reaching = sorted((dist[e], e) for e in entries if e in dist)
|
|
983
|
+
out["flows"] = [{"entry": e, "first_step": d,
|
|
984
|
+
"external_callers": self.external_callers(e, root)}
|
|
985
|
+
for d, e in reaching if d <= max_depth]
|
|
986
|
+
beyond = [d for d, _ in reaching if d > max_depth]
|
|
987
|
+
# R1-C50/D9: "0 flows" and "0 flows, and the nearest head is one step past the
|
|
988
|
+
# bound" are different answers, and the second one is actionable. Naming only
|
|
989
|
+
# the count made the headline read as unreachable — measured on a deep graph
|
|
990
|
+
# where four real heads sat at step 6 under a bound of 5.
|
|
991
|
+
out["beyond_depth"] = len(beyond)
|
|
992
|
+
out["nearest_beyond"] = min(beyond) if beyond else None
|
|
993
|
+
return out
|
|
994
|
+
|
|
818
995
|
# -- relevance ranking (R1-C6) -------------------------------------------
|
|
819
996
|
|
|
820
997
|
def _expand_seeds(self, seeds) -> set[str]:
|
|
@@ -44,6 +44,19 @@ def render_apidiff(old: Graph, new: Graph) -> str:
|
|
|
44
44
|
out.append(f"{verdict} {s['added']} added, {s['removed']} removed, "
|
|
45
45
|
f"{s['changed_symbols']} changed.")
|
|
46
46
|
out.append("")
|
|
47
|
+
# R1-C52: a gate must name what it did not judge. The verdict covers one root — the
|
|
48
|
+
# package — and on a repo-scoped graph that leaves the public symbols of `tests`,
|
|
49
|
+
# `examples` and friends outside it. Silence here is what let 40 test functions read
|
|
50
|
+
# as 40 additions to the API.
|
|
51
|
+
if d.get("root") is not None:
|
|
52
|
+
excluded = d.get("excluded") or {}
|
|
53
|
+
out.append(f"_Compared: public symbols of root `{d['root']}` — the package._"
|
|
54
|
+
if not excluded else
|
|
55
|
+
f"_Compared: public symbols of root `{d['root']}` — the package. "
|
|
56
|
+
"**Not judged:** "
|
|
57
|
+
+ ", ".join(f"{n} public symbol(s) in `{r}`" for r, n in excluded.items())
|
|
58
|
+
+ " — a consumer root is not this package's API._")
|
|
59
|
+
out.append("")
|
|
47
60
|
# R1-C25/D4: a verdict about the code is only a verdict about the code when both
|
|
48
61
|
# graphs came from the same tool. Say so above the verdict, not in a footnote.
|
|
49
62
|
prov = d.get("provenance") or {}
|
|
@@ -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("")
|