codmap 0.0.16__tar.gz → 0.0.18__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.16 → codmap-0.0.18}/PKG-INFO +2 -2
- {codmap-0.0.16 → codmap-0.0.18}/README.md +1 -1
- {codmap-0.0.16 → codmap-0.0.18}/codemap/apidiff.py +40 -4
- {codmap-0.0.16 → codmap-0.0.18}/codemap/query.py +98 -13
- {codmap-0.0.16 → codmap-0.0.18}/codemap/serve/apidiff.py +13 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/serve/impact.py +30 -9
- {codmap-0.0.16 → codmap-0.0.18}/codemap/serve/mermaid.py +13 -1
- {codmap-0.0.16 → codmap-0.0.18}/codemap/serve/session.py +142 -12
- {codmap-0.0.16 → codmap-0.0.18}/codmap.egg-info/PKG-INFO +2 -2
- {codmap-0.0.16 → codmap-0.0.18}/codmap.egg-info/SOURCES.txt +3 -0
- {codmap-0.0.16 → codmap-0.0.18}/pyproject.toml +1 -1
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c40_flow_reach.py +7 -2
- codmap-0.0.18/tests/test_r1c50_flow_entry_points.py +285 -0
- codmap-0.0.18/tests/test_r1c53_narrowing_declared.py +199 -0
- codmap-0.0.18/tests/test_r1c54_deterministic_rendering.py +131 -0
- {codmap-0.0.16 → codmap-0.0.18}/LICENSE +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/__init__.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/arch.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/cli.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/diagnostics.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/extract/__init__.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/extract/attrflow.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/extract/behavior.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/extract/dataflow.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/extract/dispatch.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/extract/griffe_extractor.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/extract/gsource.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/extract/roots.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/extract/union.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/freshness.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/incremental.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/integrations/__init__.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/integrations/base.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/integrations/cocoindex.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/integrations/gate.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/integrations/gitnexus.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/integrations/registry.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/integrations/transport.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/model.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/provenance.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/scope.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/serve/__init__.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/serve/_scip_pb2.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/serve/api_surface.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/serve/architecture.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/serve/audit.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/serve/check.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/serve/ctags.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/serve/limits.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/serve/livingdocs.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/serve/mcp_server.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/serve/pack.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/serve/rag.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/serve/review.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/serve/scip.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/serve/semantic.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/serve/server.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/serve/subsystems.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/serve/vault.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/store.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/tomlio.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codemap/watch.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codmap.egg-info/dependency_links.txt +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codmap.egg-info/entry_points.txt +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codmap.egg-info/requires.txt +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/codmap.egg-info/top_level.txt +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/setup.cfg +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_epistemic.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_gitnexus_router.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_impact_depth.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_integrations.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_issue3_serve_freshness.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_livingdocs.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_m0_api_surface.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_m11_argcontract.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_m12_dataflow.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_m15_review.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_m16_architecture.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_m17_mcp.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_m18_freshness.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_m1_5_semantics.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_m1_query.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_m2_views.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_m32_watch.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_m3_serve.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_m4_behavior.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_m5_deep.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_m6_repo_scope.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_m7_dispatch.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_m9_family.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c13_callgraph_accuracy.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c13_grep_vs_graph.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c16_semantic.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c20_attribute_edges.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c21_flat_layout.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c22_source_visible_refs.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c23_hard_python.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c24_test_mapping.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c25_provenance.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c26_deep_union.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c27_config_honesty.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c28_limit_envelope.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c29_lazy_imports.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c2_ctags.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c30_local_import_calls.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c30f1_reexport_calls.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c30f2_check_scope.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c31_path_origin.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c32_report_json.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c33_query_signature.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c34_signature_kinds.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c35_contract_location.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c36_target_resolution.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c37_rule_mutation.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c38_tool_drift.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c39_edge_resolution.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c3_arch_contract.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c3_dogfood.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c41_scope_membership.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c42_deep_tier_stability.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c43_incremental_splice.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c44_absent_answers.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c45_repeat_union.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c46_shadowed_definitions.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c47_incremental_resample.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c48_type_checking_imports.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c49_type_only_cycles.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c4_complexity.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c5_apidiff.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c6_pack.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c7_edge_vocab.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c8_deadcode.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_r1c9_incremental.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_scip_export.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/tests/test_scope.py +0 -0
- {codmap-0.0.16 → codmap-0.0.18}/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.18
|
|
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
|
|
@@ -44,7 +44,7 @@ and a **SCIP index** for interop with Sourcegraph / Glean and other precise-code
|
|
|
44
44
|
|
|
45
45
|
[](https://github.com/kogriv/codemap/actions/workflows/ci.yml)
|
|
46
46
|
|
|
47
|
-
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **
|
|
47
|
+
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **938 tests with no failures on
|
|
48
48
|
Python 3.11–3.14** ([in CI](docs/ci.md): the full suite including the dogfood pass, a determinism check, a
|
|
49
49
|
wheel smoke test, and ctags/SCIP interop against the real CLIs), warm serve surface with 31 ops (28 exposed
|
|
50
50
|
as MCP tools), and SCIP export. See **[DESIGN.md](DESIGN.md)** (product design &
|
|
@@ -8,7 +8,7 @@ and a **SCIP index** for interop with Sourcegraph / Glean and other precise-code
|
|
|
8
8
|
|
|
9
9
|
[](https://github.com/kogriv/codemap/actions/workflows/ci.yml)
|
|
10
10
|
|
|
11
|
-
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **
|
|
11
|
+
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **938 tests with no failures on
|
|
12
12
|
Python 3.11–3.14** ([in CI](docs/ci.md): the full suite including the dogfood pass, a determinism check, a
|
|
13
13
|
wheel smoke test, and ctags/SCIP interop against the real CLIs), warm serve surface with 31 ops (28 exposed
|
|
14
14
|
as MCP tools), and SCIP export. See **[DESIGN.md](DESIGN.md)** (product design &
|
|
@@ -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):
|
|
@@ -21,6 +21,25 @@ def _grade_rank(grade: str | None) -> int:
|
|
|
21
21
|
return CONFIDENCE_ORDER.index(grade) if grade in CONFIDENCE_ORDER else len(CONFIDENCE_ORDER)
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
def _canonical_cycles(cycles) -> list[list[str]]:
|
|
25
|
+
"""Cycles in a stable form: each rotated to start at its smallest node, then sorted.
|
|
26
|
+
|
|
27
|
+
R1-C54, reported by the lab as [issue #20](https://github.com/kogriv/codemap/issues/20).
|
|
28
|
+
``nx.simple_cycles`` yields a cycle starting wherever its traversal happened to enter
|
|
29
|
+
it, and that traversal follows set-iteration order, i.e. string hashes. The chain is
|
|
30
|
+
the *same cycle* either way — but three consumers print it, and they printed three
|
|
31
|
+
different texts for one graph across eight hash seeds.
|
|
32
|
+
|
|
33
|
+
The sort afterwards is the point of the rotation: ``arch.py`` already sorted cycles by
|
|
34
|
+
``(len, c)`` and that looked like canonicalisation, except the key itself moved with
|
|
35
|
+
the rotation — ``["a","b"]`` and ``["b","a"]`` are one cycle and two keys. Rotating
|
|
36
|
+
first makes the existing sort mean what it appeared to mean.
|
|
37
|
+
"""
|
|
38
|
+
out = [c[i:] + c[:i] for c in (list(x) for x in cycles)
|
|
39
|
+
if (i := c.index(min(c))) >= 0]
|
|
40
|
+
return sorted(out, key=lambda c: (len(c), c))
|
|
41
|
+
|
|
42
|
+
|
|
24
43
|
def _check_grade(min_confidence: str | None) -> None:
|
|
25
44
|
if min_confidence is not None and min_confidence not in CONFIDENCE_ORDER:
|
|
26
45
|
raise ValueError(f"min_confidence must be one of {CONFIDENCE_ORDER}, "
|
|
@@ -834,18 +853,75 @@ class Query:
|
|
|
834
853
|
return out
|
|
835
854
|
|
|
836
855
|
def entry_points(self, root: str = "core") -> list[str]:
|
|
837
|
-
"""Call-forest roots: functions that call out
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
856
|
+
"""Call-forest roots: functions that call out and are called by nothing *in
|
|
857
|
+
their own root* (resolved calls only).
|
|
858
|
+
|
|
859
|
+
Where behaviour starts — public API / mains / not-yet-triggered. Restricted to
|
|
860
|
+
one provenance ``root`` (default core).
|
|
861
|
+
|
|
862
|
+
R1-C50: "called by nothing" used to mean in-degree zero **across the whole
|
|
863
|
+
graph**, which on a library means the public API is never an entry point — the
|
|
864
|
+
one function a user enters the package through is called from `tests`,
|
|
865
|
+
`examples` and `scripts`, and those 43 calls were what disqualified it. A call
|
|
866
|
+
from a consumer root is a *use*, not an internal caller; being used is not
|
|
867
|
+
evidence of being reachable-from-elsewhere. Reported by the dogfood target as
|
|
868
|
+
[issue #19](https://github.com/kogriv/codemap/issues/19). On a single-package
|
|
869
|
+
graph every node is `core`, so this is the same set as before by construction.
|
|
870
|
+
|
|
871
|
+
Best-effort in **both** directions: an unresolved caller leaves a real internal
|
|
872
|
+
looking like an entry point (the set is an over-estimate), and resolving one
|
|
873
|
+
*removes* an entry point (it is an under-estimate of heads whose chain the
|
|
874
|
+
resolver cannot close). The second direction is what #19 measured.
|
|
842
875
|
"""
|
|
843
876
|
return sorted(
|
|
844
877
|
n for n in self._calls.nodes
|
|
845
878
|
if self.root_of(n) == root
|
|
846
|
-
and self._calls.out_degree(n) > 0
|
|
879
|
+
and self._calls.out_degree(n) > 0
|
|
880
|
+
and not any(self.root_of(p) == root
|
|
881
|
+
for p in self._calls.predecessors(n))
|
|
847
882
|
)
|
|
848
883
|
|
|
884
|
+
def external_callers(self, symbol_id: str, root: str = "core") -> dict[str, int]:
|
|
885
|
+
"""Resolved callers of ``symbol_id`` living **outside** ``root``, by root.
|
|
886
|
+
|
|
887
|
+
R1-C50/D8: on an entry point this is the evidence that the head is public —
|
|
888
|
+
`{"tests": 43}` says the package is entered here, which is exactly what the
|
|
889
|
+
in-degree rule used to read as a disqualification.
|
|
890
|
+
"""
|
|
891
|
+
out: dict[str, int] = {}
|
|
892
|
+
if symbol_id in self._calls:
|
|
893
|
+
for p in self._calls.predecessors(symbol_id):
|
|
894
|
+
r = self.root_of(p)
|
|
895
|
+
if r != root:
|
|
896
|
+
out[r] = out.get(r, 0) + 1
|
|
897
|
+
return dict(sorted(out.items()))
|
|
898
|
+
|
|
899
|
+
def caller_grades(self, symbol_id: str) -> dict[str, int]:
|
|
900
|
+
"""Histogram of the route grades of the resolved calls *into* ``symbol_id``.
|
|
901
|
+
|
|
902
|
+
R1-C51: what `callers(min_confidence=…)` would drop, so a filtered answer can
|
|
903
|
+
say so instead of coming back as a bare empty list. Reported by the dogfood
|
|
904
|
+
target with the measurement that makes it matter: of the 25 symbols their
|
|
905
|
+
registry fan-out reaches, **13 have no `exact` caller at all** — every strategy
|
|
906
|
+
method — so `min_confidence="exact"` answers `[]` about symbols that are called
|
|
907
|
+
on every run, through an object a factory returned for a string key.
|
|
908
|
+
"""
|
|
909
|
+
out: dict[str, int] = {}
|
|
910
|
+
if symbol_id in self._calls:
|
|
911
|
+
for p in self._calls.predecessors(symbol_id):
|
|
912
|
+
g = self._calls.edges[p, symbol_id].get("confidence") or "unknown"
|
|
913
|
+
out[g] = out.get(g, 0) + 1
|
|
914
|
+
return dict(sorted(out.items(), key=lambda kv: _grade_rank(kv[0])))
|
|
915
|
+
|
|
916
|
+
def callee_grades(self, symbol_id: str) -> dict[str, int]:
|
|
917
|
+
"""Histogram of the route grades of the resolved calls *out of* ``symbol_id``."""
|
|
918
|
+
out: dict[str, int] = {}
|
|
919
|
+
if symbol_id in self._calls:
|
|
920
|
+
for t in self._calls.successors(symbol_id):
|
|
921
|
+
g = self._calls.edges[symbol_id, t].get("confidence") or "unknown"
|
|
922
|
+
out[g] = out.get(g, 0) + 1
|
|
923
|
+
return dict(sorted(out.items(), key=lambda kv: _grade_rank(kv[0])))
|
|
924
|
+
|
|
849
925
|
def flow(self, entry: str, *, max_depth: int = 5) -> dict:
|
|
850
926
|
"""Forward call-flow from ``entry`` along ``calls`` edges, bounded by depth.
|
|
851
927
|
|
|
@@ -906,7 +982,8 @@ class Query:
|
|
|
906
982
|
out = {
|
|
907
983
|
"symbol": symbol_id, "root": root, "max_depth": max_depth,
|
|
908
984
|
"in_call_graph": bool(targets), "entry_points": len(entries),
|
|
909
|
-
"flows": [], "beyond_depth": 0,
|
|
985
|
+
"flows": [], "beyond_depth": 0, "nearest_beyond": None,
|
|
986
|
+
"inbound_calls": sum(len(self._calls.pred[t]) for t in targets),
|
|
910
987
|
"non_call_refs": sum(1 for r in refs if r["type"] != "calls"),
|
|
911
988
|
}
|
|
912
989
|
if not targets:
|
|
@@ -922,9 +999,16 @@ class Query:
|
|
|
922
999
|
nxt.add(pred)
|
|
923
1000
|
frontier, step = nxt, step + 1
|
|
924
1001
|
reaching = sorted((dist[e], e) for e in entries if e in dist)
|
|
925
|
-
out["flows"] = [{"entry": e, "first_step": d
|
|
1002
|
+
out["flows"] = [{"entry": e, "first_step": d,
|
|
1003
|
+
"external_callers": self.external_callers(e, root)}
|
|
926
1004
|
for d, e in reaching if d <= max_depth]
|
|
927
|
-
|
|
1005
|
+
beyond = [d for d, _ in reaching if d > max_depth]
|
|
1006
|
+
# R1-C50/D9: "0 flows" and "0 flows, and the nearest head is one step past the
|
|
1007
|
+
# bound" are different answers, and the second one is actionable. Naming only
|
|
1008
|
+
# the count made the headline read as unreachable — measured on a deep graph
|
|
1009
|
+
# where four real heads sat at step 6 under a bound of 5.
|
|
1010
|
+
out["beyond_depth"] = len(beyond)
|
|
1011
|
+
out["nearest_beyond"] = min(beyond) if beyond else None
|
|
928
1012
|
return out
|
|
929
1013
|
|
|
930
1014
|
# -- relevance ranking (R1-C6) -------------------------------------------
|
|
@@ -1004,7 +1088,7 @@ class Query:
|
|
|
1004
1088
|
would report someone's fix as their bug. Those cycles are still real coupling
|
|
1005
1089
|
and are returned by :meth:`lazy_import_cycles`.
|
|
1006
1090
|
"""
|
|
1007
|
-
return
|
|
1091
|
+
return _canonical_cycles(nx.simple_cycles(self._imports_eager))
|
|
1008
1092
|
|
|
1009
1093
|
def lazy_import_cycles(self) -> list[list[str]]:
|
|
1010
1094
|
"""Dependency cycles that close **only** through a function-local import.
|
|
@@ -1022,8 +1106,8 @@ class Query:
|
|
|
1022
1106
|
them, or a tree could hide real coupling by adding one.
|
|
1023
1107
|
"""
|
|
1024
1108
|
eager = {frozenset(c) for c in nx.simple_cycles(self._imports_eager)}
|
|
1025
|
-
return
|
|
1026
|
-
|
|
1109
|
+
return _canonical_cycles(c for c in nx.simple_cycles(self._imports_runtime)
|
|
1110
|
+
if frozenset(c) not in eager)
|
|
1027
1111
|
|
|
1028
1112
|
def type_only_import_cycles(self) -> list[list[str]]:
|
|
1029
1113
|
"""Dependency cycles that close **only** with an import under ``if TYPE_CHECKING:``.
|
|
@@ -1035,7 +1119,8 @@ class Query:
|
|
|
1035
1119
|
opt-in rule instead.
|
|
1036
1120
|
"""
|
|
1037
1121
|
runtime = {frozenset(c) for c in nx.simple_cycles(self._imports_runtime)}
|
|
1038
|
-
return
|
|
1122
|
+
return _canonical_cycles(c for c in nx.simple_cycles(self._imports)
|
|
1123
|
+
if frozenset(c) not in runtime)
|
|
1039
1124
|
|
|
1040
1125
|
def import_map(self) -> dict:
|
|
1041
1126
|
"""How much of the import graph each scope contributed (R1-C29).
|
|
@@ -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 {}
|
|
@@ -36,25 +36,46 @@ def _flow_section(rep: dict) -> list[str]:
|
|
|
36
36
|
"the change._")
|
|
37
37
|
lines.append("")
|
|
38
38
|
for f in rep["flows"][:_FLOW_ROWS]:
|
|
39
|
-
|
|
39
|
+
ext = f.get("external_callers") or {}
|
|
40
|
+
tail = (" _(entered from " + ", ".join(f"{r} ×{n}" for r, n in ext.items())
|
|
41
|
+
+ ")_" if ext else "")
|
|
42
|
+
lines.append(f"- `{f['entry']}` — step {f['first_step']}{tail}")
|
|
40
43
|
if len(rep["flows"]) > _FLOW_ROWS:
|
|
41
44
|
lines.append(f"- _… {len(rep['flows']) - _FLOW_ROWS} more_")
|
|
45
|
+
elif rep["nearest_beyond"] is not None:
|
|
46
|
+
# R1-C50/D9: an empty list with a head one step past the bound is not the same
|
|
47
|
+
# answer as an empty list, and it is the one a reader can act on.
|
|
48
|
+
lines.append(f"_No entry point within {rep['max_depth']} step(s) — but "
|
|
49
|
+
f"{rep['beyond_depth']} reach it further out, the nearest at step "
|
|
50
|
+
f"**{rep['nearest_beyond']}**. Re-run with `--flow-depth "
|
|
51
|
+
f"{rep['nearest_beyond']}` to see them._")
|
|
52
|
+
elif rep["inbound_calls"]:
|
|
53
|
+
lines.append(f"_No entry point reaches it, at any depth — yet {rep['inbound_calls']} "
|
|
54
|
+
"resolved call(s) do reach it. Every chain above it is either closed "
|
|
55
|
+
"in a call cycle (no head to start from) or starts outside root "
|
|
56
|
+
f"`{rep['root']}`. Not 'nothing calls it'._")
|
|
42
57
|
else:
|
|
43
|
-
lines.append(f"_No entry point reaches it within {rep['max_depth']} step(s)
|
|
58
|
+
lines.append(f"_No entry point reaches it within {rep['max_depth']} step(s), and "
|
|
59
|
+
"no resolved call reaches it either._")
|
|
44
60
|
lines.append("")
|
|
45
61
|
notes = []
|
|
46
|
-
if rep["beyond_depth"]:
|
|
62
|
+
if rep["beyond_depth"] and rep["flows"]:
|
|
47
63
|
notes.append(f"{rep['beyond_depth']} further entry point(s) reach it **beyond** "
|
|
48
|
-
f"{rep['max_depth']} steps
|
|
64
|
+
f"{rep['max_depth']} steps (nearest at step "
|
|
65
|
+
f"{rep['nearest_beyond']}) — counted, not listed.")
|
|
49
66
|
if rep["non_call_refs"]:
|
|
50
67
|
notes.append(f"{rep['non_call_refs']} direct reference(s) arrive by an edge that "
|
|
51
68
|
"is not a call (import / inheritance / decoration / attribute) and "
|
|
52
69
|
"cannot appear in a flow at all.")
|
|
53
|
-
notes.append("Flows follow resolved `calls` edges only
|
|
54
|
-
"unresolved caller
|
|
55
|
-
"point
|
|
56
|
-
"
|
|
57
|
-
"
|
|
70
|
+
notes.append("Flows follow resolved `calls` edges only, and the entry-point set is "
|
|
71
|
+
"best-effort in **both** directions: an unresolved caller leaves a real "
|
|
72
|
+
"internal looking like an entry point (the denominator is an upper "
|
|
73
|
+
"bound), while resolving one *removes* an entry point and can lengthen "
|
|
74
|
+
"a chain past `--flow-depth` — so a **more** complete graph can answer "
|
|
75
|
+
"with **fewer** flows. A call from another root (`tests`, `examples`) is "
|
|
76
|
+
"a use, not an internal caller, and does not disqualify a head. "
|
|
77
|
+
"*Reached*, not *broken*: the graph knows the symbol is on the path, not "
|
|
78
|
+
"whether the change breaks it.")
|
|
58
79
|
lines.append("_" + " ".join(notes) + "_")
|
|
59
80
|
lines.append("")
|
|
60
81
|
return lines
|
|
@@ -117,4 +117,16 @@ def render_mermaid(query: Query, kind: str, scope: str | None = None,
|
|
|
117
117
|
return render_call_graph(query, root, depth)
|
|
118
118
|
if kind not in _KINDS:
|
|
119
119
|
raise ValueError(f"unknown mermaid kind: {kind}")
|
|
120
|
-
|
|
120
|
+
out = _KINDS[kind](query, scope)
|
|
121
|
+
if scope:
|
|
122
|
+
# R1-C53: `--scope` cut this diagram from 144 lines to 47 on the dogfood tree with
|
|
123
|
+
# no marker of any kind, so the picture read as "the class diagram of this package".
|
|
124
|
+
# A diagram is an answer too. Mermaid ignores `%%` lines, so the note costs nothing
|
|
125
|
+
# rendered and is visible in the source a reader diffs or pastes.
|
|
126
|
+
hidden = len(_KINDS[kind](query, None).splitlines()) - len(out.splitlines())
|
|
127
|
+
note = (f"%% scope: {scope} — {hidden} further line(s) exist in the unscoped "
|
|
128
|
+
"diagram of this graph")
|
|
129
|
+
lines = out.splitlines()
|
|
130
|
+
at = 1 if lines and lines[0].startswith("```") else 0
|
|
131
|
+
out = "\n".join(lines[:at] + [note] + lines[at:]) + "\n"
|
|
132
|
+
return out
|
|
@@ -50,6 +50,15 @@ _REPORTS = {
|
|
|
50
50
|
# jedi sample (R1-C43).
|
|
51
51
|
# `accessors` used to be in neither — it reads `accesses`, modelled best-effort, and
|
|
52
52
|
# carried no `epistemic` at all (found by the R1-C44 measurement).
|
|
53
|
+
# R1-C53 added the three column ops: they read `reads`/`writes`, which are an **over-set**
|
|
54
|
+
# rather than a lower bound — a different partiality, so it gets its own reason below.
|
|
55
|
+
#
|
|
56
|
+
# `query` was considered here and deliberately left out. Its dossier *does* carry
|
|
57
|
+
# call-derived fields, but most of it — where the symbol is defined, its signature, its
|
|
58
|
+
# file — is exact, and one envelope-wide "this is a lower bound" would say the definition
|
|
59
|
+
# is uncertain too. The rule this file already states is "absence of the label means
|
|
60
|
+
# exact", so a **mixed** answer declares per field (see `_op_query`), not per envelope.
|
|
61
|
+
# Over-claiming partiality is the same defect as hiding it, pointed the other way.
|
|
53
62
|
_OP_EDGE_CLASSES = {
|
|
54
63
|
"callers": frozenset({"calls"}),
|
|
55
64
|
"callees": frozenset({"calls"}),
|
|
@@ -59,10 +68,19 @@ _OP_EDGE_CLASSES = {
|
|
|
59
68
|
"accessors": frozenset({"accesses"}),
|
|
60
69
|
"tests": frozenset({"calls", "references"}),
|
|
61
70
|
"covers": frozenset({"calls", "references"}),
|
|
71
|
+
"column": frozenset({"reads", "writes"}),
|
|
72
|
+
"columns": frozenset({"reads", "writes"}),
|
|
73
|
+
"columns_of": frozenset({"reads", "writes"}),
|
|
62
74
|
}
|
|
63
75
|
_BEST_EFFORT_CLASSES = frozenset({"calls", "accesses"})
|
|
76
|
+
#: Classes that are an over-set, not a lower bound: a literal subscript key cannot be told
|
|
77
|
+
#: from a dict key, so the column set contains more than the DataFrame columns (F15/M12).
|
|
78
|
+
#: The opposite error from `_BEST_EFFORT_CLASSES`, and it must not borrow its wording.
|
|
79
|
+
_OVER_SET_CLASSES = frozenset({"reads", "writes"})
|
|
64
80
|
_PARTIAL_OPS = frozenset(op for op, cls in _OP_EDGE_CLASSES.items()
|
|
65
|
-
if cls & _BEST_EFFORT_CLASSES)
|
|
81
|
+
if cls & (_BEST_EFFORT_CLASSES | _OVER_SET_CLASSES))
|
|
82
|
+
_OVER_SET_OPS = frozenset(op for op, cls in _OP_EDGE_CLASSES.items()
|
|
83
|
+
if cls & _OVER_SET_CLASSES)
|
|
66
84
|
_SPLICE_OPS = frozenset(op for op, cls in _OP_EDGE_CLASSES.items()
|
|
67
85
|
if cls & _SPLICED_EDGE_TYPES)
|
|
68
86
|
_EPISTEMIC_PARTIAL = {
|
|
@@ -70,6 +88,15 @@ _EPISTEMIC_PARTIAL = {
|
|
|
70
88
|
"reason": "leans on static resolution of calls and attribute accesses (partial "
|
|
71
89
|
"for Python) — a lower bound; pair with grep/tests before acting.",
|
|
72
90
|
}
|
|
91
|
+
# R1-C53: the over-set is partial in the *other* direction, and saying "a lower bound"
|
|
92
|
+
# about it would be worse than saying nothing. A caller that trims by this set would trim
|
|
93
|
+
# too little, not too much.
|
|
94
|
+
_EPISTEMIC_OVER_SET = {
|
|
95
|
+
"epistemic": "partial",
|
|
96
|
+
"reason": "string-keyed dataflow is an **over-set**, not a lower bound: a literal "
|
|
97
|
+
"subscript key cannot be told from a dict key, so this set contains more "
|
|
98
|
+
"than the DataFrame columns. Confirm a key before acting on it.",
|
|
99
|
+
}
|
|
73
100
|
# D4 — the second reason, named separately: "a lower bound by static resolution" and
|
|
74
101
|
# "part of this graph was carried over, not recomputed" are different facts.
|
|
75
102
|
_EPISTEMIC_SPLICE = (
|
|
@@ -118,6 +145,38 @@ _UNLIMITED_BY_DESIGN = {
|
|
|
118
145
|
}
|
|
119
146
|
|
|
120
147
|
|
|
148
|
+
def _root_scope(query: Query, root: str, judged: list) -> dict:
|
|
149
|
+
"""The `scope` block: which provenance root the answer covers, and what it leaves out.
|
|
150
|
+
|
|
151
|
+
R1-C53. `communities` judges the package and is silent about it; on a repo-scoped graph
|
|
152
|
+
a caller reads "9 subsystems" without learning that `tests` was never considered. Same
|
|
153
|
+
shape as `check`'s "not judged" line and `diff`'s root filter, now stated in one place.
|
|
154
|
+
"""
|
|
155
|
+
others: dict[str, int] = {}
|
|
156
|
+
for n in query.graph.nodes.values():
|
|
157
|
+
if n.kind != "module":
|
|
158
|
+
continue
|
|
159
|
+
r = query.root_of(n.id)
|
|
160
|
+
if r != root:
|
|
161
|
+
others[r] = others.get(r, 0) + 1
|
|
162
|
+
return {"root": root, "judged": len(judged),
|
|
163
|
+
"not_judged": dict(sorted(others.items()))}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _grade_filter(min_confidence, every: list, kept: list, grades: dict) -> dict:
|
|
167
|
+
"""The `filter` block: what the route-grade filter kept, dropped, and out of what.
|
|
168
|
+
|
|
169
|
+
R1-C51, the filter half of R1-C28. ``by_grade`` is the composition of the
|
|
170
|
+
*unfiltered* answer, so an empty ``kept`` is readable on its own — `{"heuristic": 2}`
|
|
171
|
+
with `returned: 0` says "called, but only through routes you excluded", which a bare
|
|
172
|
+
`[]` cannot. Emitted even with no filter in force (`min_confidence: null`), because
|
|
173
|
+
that is when a reader most needs to know what a filter *would* remove.
|
|
174
|
+
"""
|
|
175
|
+
return {"min_confidence": min_confidence, "returned": len(kept),
|
|
176
|
+
"total": len(every), "dropped": len(every) - len(kept),
|
|
177
|
+
"by_grade": grades}
|
|
178
|
+
|
|
179
|
+
|
|
121
180
|
def _match(q: Query, n) -> dict:
|
|
122
181
|
"""One entry of the ``matches`` list: where the symbol is, and how it is declared.
|
|
123
182
|
|
|
@@ -243,6 +302,17 @@ def build_query_result(q: Query, name: str) -> dict:
|
|
|
243
302
|
used_by[n.id] = by_root
|
|
244
303
|
if used_by:
|
|
245
304
|
result["used_by"] = used_by
|
|
305
|
+
# R1-C53: the dossier is *mixed* — `defined_at`, `matches` and the signatures
|
|
306
|
+
# are exact, while `used_by` comes from the same best-effort call/reference
|
|
307
|
+
# layer that makes `callers` a lower bound. An envelope-wide `epistemic` would
|
|
308
|
+
# have said the definition is uncertain too, and over-claiming partiality is
|
|
309
|
+
# the same defect as hiding it. So it is declared on the field, here — where
|
|
310
|
+
# the field is built — so the CLI dossier carries it as well as the op.
|
|
311
|
+
result["used_by_epistemic"] = (
|
|
312
|
+
"lower bound — `used_by` comes from statically resolved calls and "
|
|
313
|
+
"references (partial for Python); `defined_at` and the signatures are "
|
|
314
|
+
"exact. `callers`/`impact` carry the same caveat for a whole answer."
|
|
315
|
+
)
|
|
246
316
|
col = q.column(name)
|
|
247
317
|
if col and (col["writes"] or col["reads"]):
|
|
248
318
|
result["column"] = col
|
|
@@ -319,6 +389,8 @@ class Session:
|
|
|
319
389
|
"ops": sorted(_OPS)}
|
|
320
390
|
self._resolution = None
|
|
321
391
|
self._limit = None
|
|
392
|
+
self._filter = None
|
|
393
|
+
self._scope = None
|
|
322
394
|
try:
|
|
323
395
|
env = {"ok": True, "result": fn(self, args)}
|
|
324
396
|
except Exception as exc: # a bad arg must not kill the resident process
|
|
@@ -335,6 +407,21 @@ class Session:
|
|
|
335
407
|
env["epistemic"] = self._epistemic(op)
|
|
336
408
|
if self._limit is not None: # R1-C28: how much of the answer survived the cut
|
|
337
409
|
env["limit"] = self._limit
|
|
410
|
+
# R1-C51: a *filter* is the same partiality as a limit, and it was not declared.
|
|
411
|
+
# `callers(min_confidence="exact")` came back as a bare `[]` on symbols that are
|
|
412
|
+
# called on every run through a factory — reported by the dogfood target with the
|
|
413
|
+
# measurement that makes it matter (13 of 25 registry targets have no exact
|
|
414
|
+
# caller at all). Emitted whenever the op accepts the filter, including when it
|
|
415
|
+
# dropped nothing: a caller must not have to tell "nothing was filtered" from
|
|
416
|
+
# "this build does not report filtering".
|
|
417
|
+
if self._filter is not None:
|
|
418
|
+
env["filter"] = self._filter
|
|
419
|
+
# R1-C53: the third class. A `scope` narrowing is not a cut of a computed list and
|
|
420
|
+
# not a predicate over it — it is the answer covering **one provenance root** while
|
|
421
|
+
# the graph holds several, which is how `diff` came to count 40 test functions as
|
|
422
|
+
# API and how `communities` judges only the package while saying nothing.
|
|
423
|
+
if self._scope is not None:
|
|
424
|
+
env["scope"] = self._scope
|
|
338
425
|
return env
|
|
339
426
|
|
|
340
427
|
def _epistemic(self, op: str) -> dict:
|
|
@@ -344,7 +431,7 @@ class Session:
|
|
|
344
431
|
sample, and only an op reading a spliced class reads it. On a full deep graph
|
|
345
432
|
and on the fast tier the block is exactly the R1-C13 one.
|
|
346
433
|
"""
|
|
347
|
-
block = dict(_EPISTEMIC_PARTIAL)
|
|
434
|
+
block = dict(_EPISTEMIC_OVER_SET if op in _OVER_SET_OPS else _EPISTEMIC_PARTIAL)
|
|
348
435
|
prov = self.graph.provenance or {}
|
|
349
436
|
if op in _SPLICE_OPS and prov.get("tier") == "deep":
|
|
350
437
|
if prov.get("incremental") is True:
|
|
@@ -443,7 +530,7 @@ class Session:
|
|
|
443
530
|
result = build_query_result(self.query, args["name"])
|
|
444
531
|
if not result["matches"]: # R1-C44: an empty dossier says why it is empty
|
|
445
532
|
self._resolution = _not_found(args["name"])
|
|
446
|
-
return result
|
|
533
|
+
return result # the mixed-answer note rides on the field (see build_query_result)
|
|
447
534
|
|
|
448
535
|
def _op_impact(self, args) -> dict:
|
|
449
536
|
sym = args["symbol"]
|
|
@@ -488,7 +575,18 @@ class Session:
|
|
|
488
575
|
def _op_columns(self, args) -> list:
|
|
489
576
|
# F15: default to subscript-accessed keys (the real column-like set);
|
|
490
577
|
# pass all=true for the full over-set incl. dict-literal payload keys.
|
|
491
|
-
|
|
578
|
+
narrow = not args.get("all", False)
|
|
579
|
+
kept = self.query.columns(subscripted_only=narrow)
|
|
580
|
+
# R1-C53: the default hides the larger half and said nothing — measured on the
|
|
581
|
+
# dogfood tree, 331 of 1057. A deliberate narrowing still has to be declared, or
|
|
582
|
+
# the caller reads 331 as the answer to "which columns exist".
|
|
583
|
+
every = self.query.columns(subscripted_only=False) if narrow else kept
|
|
584
|
+
self._filter = {"basis": "subscripted_only" if narrow else None,
|
|
585
|
+
"returned": len(kept), "total": len(every),
|
|
586
|
+
"dropped": len(every) - len(kept),
|
|
587
|
+
"note": "a key reached only through a dict literal is not a "
|
|
588
|
+
"subscript access; pass all=true for the full over-set"}
|
|
589
|
+
return kept
|
|
492
590
|
|
|
493
591
|
def _op_columns_of(self, args) -> dict:
|
|
494
592
|
return self.query.columns_of(self._canon(args["symbol"]))
|
|
@@ -518,12 +616,20 @@ class Session:
|
|
|
518
616
|
return res
|
|
519
617
|
|
|
520
618
|
def _op_callers(self, args) -> list:
|
|
521
|
-
|
|
522
|
-
|
|
619
|
+
sid = self._canon(args["symbol"])
|
|
620
|
+
kept = self.query.callers(sid, min_confidence=args.get("min_confidence"))
|
|
621
|
+
self._filter = _grade_filter(args.get("min_confidence"),
|
|
622
|
+
self.query.callers(sid), kept,
|
|
623
|
+
self.query.caller_grades(sid))
|
|
624
|
+
return kept
|
|
523
625
|
|
|
524
626
|
def _op_callees(self, args) -> list:
|
|
525
|
-
|
|
526
|
-
|
|
627
|
+
sid = self._canon(args["symbol"])
|
|
628
|
+
kept = self.query.callees(sid, min_confidence=args.get("min_confidence"))
|
|
629
|
+
self._filter = _grade_filter(args.get("min_confidence"),
|
|
630
|
+
self.query.callees(sid), kept,
|
|
631
|
+
self.query.callee_grades(sid))
|
|
632
|
+
return kept
|
|
527
633
|
|
|
528
634
|
def _op_implementers(self, args) -> list:
|
|
529
635
|
return self.query.implementers(self._canon(args["protocol"]))
|
|
@@ -583,14 +689,38 @@ class Session:
|
|
|
583
689
|
return build_check(self.query, contract, check_contract(self.query, contract))
|
|
584
690
|
|
|
585
691
|
def _op_communities(self, args) -> list:
|
|
586
|
-
"""R1-C18: data-driven module subsystems (greedy modularity).
|
|
587
|
-
|
|
692
|
+
"""R1-C18: data-driven module subsystems (greedy modularity).
|
|
693
|
+
|
|
694
|
+
R1-C53: subsystems are computed over the **package** only — a consumer root is not
|
|
695
|
+
a subsystem of it — and that was true in the code and absent from the answer.
|
|
696
|
+
"""
|
|
697
|
+
out = self.query.communities()
|
|
698
|
+
self._scope = _root_scope(self.query, "core",
|
|
699
|
+
[m for c in out for m in c["modules"]])
|
|
700
|
+
return out
|
|
588
701
|
|
|
589
702
|
def _op_flows(self, args) -> dict:
|
|
590
|
-
"""R1-C18: forward call-flow from a symbol, or entry points if none given.
|
|
703
|
+
"""R1-C18: forward call-flow from a symbol, or entry points if none given.
|
|
704
|
+
|
|
705
|
+
R1-C53: the entry-point list is shaped by a *definition* — "calls out, and nothing
|
|
706
|
+
in its own root calls it" — which R1-C50 had to change after it disqualified a
|
|
707
|
+
library's public API for being used. A definition that can be wrong for a target
|
|
708
|
+
belongs in the answer, not only in a docstring.
|
|
709
|
+
"""
|
|
591
710
|
sym = args.get("symbol")
|
|
592
711
|
if not sym:
|
|
593
|
-
|
|
712
|
+
entries = self.query.entry_points()
|
|
713
|
+
self._scope = _root_scope(self.query, "core", entries)
|
|
714
|
+
return {
|
|
715
|
+
"entry_points": entries,
|
|
716
|
+
"definition": "a function that calls out and is called by nothing in its "
|
|
717
|
+
"own provenance root; a call from `tests`/`examples` is a "
|
|
718
|
+
"use, not an internal caller (R1-C50)",
|
|
719
|
+
"best_effort_both_ways": "an unresolved caller leaves a real internal "
|
|
720
|
+
"looking like an entry point, and resolving one "
|
|
721
|
+
"removes an entry point — so a more complete "
|
|
722
|
+
"graph can list fewer",
|
|
723
|
+
}
|
|
594
724
|
return self.query.flow(self._canon(sym), max_depth=int(args.get("depth", 5)))
|
|
595
725
|
|
|
596
726
|
def _op_semantic(self, args) -> dict:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codmap
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.18
|
|
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
|
|
@@ -44,7 +44,7 @@ and a **SCIP index** for interop with Sourcegraph / Glean and other precise-code
|
|
|
44
44
|
|
|
45
45
|
[](https://github.com/kogriv/codemap/actions/workflows/ci.yml)
|
|
46
46
|
|
|
47
|
-
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **
|
|
47
|
+
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **938 tests with no failures on
|
|
48
48
|
Python 3.11–3.14** ([in CI](docs/ci.md): the full suite including the dogfood pass, a determinism check, a
|
|
49
49
|
wheel smoke test, and ctags/SCIP interop against the real CLIs), warm serve surface with 31 ops (28 exposed
|
|
50
50
|
as MCP tools), and SCIP export. See **[DESIGN.md](DESIGN.md)** (product design &
|
|
@@ -121,6 +121,9 @@ tests/test_r1c47_incremental_resample.py
|
|
|
121
121
|
tests/test_r1c48_type_checking_imports.py
|
|
122
122
|
tests/test_r1c49_type_only_cycles.py
|
|
123
123
|
tests/test_r1c4_complexity.py
|
|
124
|
+
tests/test_r1c50_flow_entry_points.py
|
|
125
|
+
tests/test_r1c53_narrowing_declared.py
|
|
126
|
+
tests/test_r1c54_deterministic_rendering.py
|
|
124
127
|
tests/test_r1c5_apidiff.py
|
|
125
128
|
tests/test_r1c6_pack.py
|
|
126
129
|
tests/test_r1c7_edge_vocab.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.18"
|
|
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
|