codmap 0.0.21__tar.gz → 0.0.23__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.21/codmap.egg-info → codmap-0.0.23}/PKG-INFO +33 -14
- {codmap-0.0.21 → codmap-0.0.23}/README.md +32 -13
- {codmap-0.0.21 → codmap-0.0.23}/codemap/arch.py +76 -1
- {codmap-0.0.21 → codmap-0.0.23}/codemap/cli.py +91 -12
- {codmap-0.0.21 → codmap-0.0.23}/codemap/extract/attrflow.py +3 -2
- {codmap-0.0.21 → codmap-0.0.23}/codemap/extract/behavior.py +45 -13
- {codmap-0.0.21 → codmap-0.0.23}/codemap/extract/griffe_extractor.py +58 -12
- {codmap-0.0.21 → codmap-0.0.23}/codemap/extract/gsource.py +23 -0
- codmap-0.0.23/codemap/extract/probe.py +152 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/extract/roots.py +2 -4
- {codmap-0.0.21 → codmap-0.0.23}/codemap/model.py +17 -1
- {codmap-0.0.21 → codmap-0.0.23}/codemap/provenance.py +71 -1
- {codmap-0.0.21 → codmap-0.0.23}/codemap/query.py +101 -16
- {codmap-0.0.21 → codmap-0.0.23}/codemap/serve/apidiff.py +4 -1
- {codmap-0.0.21 → codmap-0.0.23}/codemap/serve/audit.py +8 -3
- {codmap-0.0.21 → codmap-0.0.23}/codemap/serve/check.py +42 -3
- codmap-0.0.23/codemap/serve/currency.py +67 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/serve/mcp_server.py +17 -6
- codmap-0.0.23/codemap/serve/pack.py +224 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/serve/session.py +10 -2
- {codmap-0.0.21 → codmap-0.0.23/codmap.egg-info}/PKG-INFO +33 -14
- {codmap-0.0.21 → codmap-0.0.23}/codmap.egg-info/SOURCES.txt +12 -0
- {codmap-0.0.21 → codmap-0.0.23}/pyproject.toml +1 -1
- codmap-0.0.23/tests/test_pack_honesty.py +228 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c3_arch_contract.py +17 -2
- codmap-0.0.23/tests/test_r1c59_completeness_drop.py +198 -0
- codmap-0.0.23/tests/test_r1c60_nested_definitions.py +117 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c63_answer_format.py +14 -1
- codmap-0.0.23/tests/test_r1c64_dependency_probe.py +140 -0
- codmap-0.0.23/tests/test_r1c66_pack_direction.py +150 -0
- codmap-0.0.23/tests/test_r1c68_dotted_imports.py +123 -0
- codmap-0.0.23/tests/test_r1c69_contract_validation.py +106 -0
- codmap-0.0.23/tests/test_r1c70_check_graph_currency.py +130 -0
- codmap-0.0.23/tests/test_r1c71_attribute_chain_calls.py +82 -0
- codmap-0.0.23/tests/test_r1c72_flat_inference_in_packages.py +99 -0
- codmap-0.0.21/codemap/serve/pack.py +0 -93
- {codmap-0.0.21 → codmap-0.0.23}/LICENSE +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/__init__.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/apidiff.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/diagnostics.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/extract/__init__.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/extract/dataflow.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/extract/dispatch.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/extract/union.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/freshness.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/incremental.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/integrations/__init__.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/integrations/base.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/integrations/cocoindex.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/integrations/gate.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/integrations/gitnexus.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/integrations/registry.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/integrations/transport.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/scope.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/serve/__init__.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/serve/_scip_pb2.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/serve/api_surface.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/serve/architecture.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/serve/ctags.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/serve/impact.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/serve/limits.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/serve/livingdocs.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/serve/mermaid.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/serve/rag.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/serve/review.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/serve/scip.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/serve/semantic.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/serve/server.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/serve/subsystems.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/serve/vault.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/store.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/tomlio.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codemap/watch.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codmap.egg-info/dependency_links.txt +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codmap.egg-info/entry_points.txt +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codmap.egg-info/requires.txt +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/codmap.egg-info/top_level.txt +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/setup.cfg +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_epistemic.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_gitnexus_router.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_impact_depth.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_integrations.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_issue3_serve_freshness.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_livingdocs.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_m0_api_surface.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_m11_argcontract.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_m12_dataflow.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_m15_review.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_m16_architecture.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_m17_mcp.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_m18_freshness.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_m1_5_semantics.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_m1_query.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_m2_views.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_m32_watch.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_m3_serve.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_m4_behavior.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_m5_deep.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_m6_repo_scope.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_m7_dispatch.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_m9_family.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c13_callgraph_accuracy.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c13_grep_vs_graph.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c16_semantic.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c20_attribute_edges.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c21_flat_layout.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c22_source_visible_refs.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c23_hard_python.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c24_test_mapping.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c25_provenance.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c26_deep_union.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c27_config_honesty.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c28_limit_envelope.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c29_lazy_imports.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c2_ctags.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c30_local_import_calls.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c30f1_reexport_calls.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c30f2_check_scope.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c31_path_origin.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c32_report_json.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c33_query_signature.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c34_signature_kinds.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c35_contract_location.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c36_target_resolution.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c37_rule_mutation.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c38_tool_drift.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c39_edge_resolution.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c3_dogfood.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c40_flow_reach.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c41_scope_membership.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c42_deep_tier_stability.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c43_incremental_splice.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c44_absent_answers.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c45_repeat_union.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c46_shadowed_definitions.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c47_incremental_resample.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c48_type_checking_imports.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c49_type_only_cycles.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c4_complexity.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c50_flow_entry_points.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c53_narrowing_declared.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c54_deterministic_rendering.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c55_override_grade.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c56_stub_files.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c57_facade_package.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c58_cycle_tangles.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c5_apidiff.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c62_phantom_rules.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c6_pack.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c7_edge_vocab.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c8_deadcode.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_r1c9_incremental.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_scip_export.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/tests/test_scope.py +0 -0
- {codmap-0.0.21 → codmap-0.0.23}/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.23
|
|
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, **1115 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 &
|
|
@@ -107,18 +107,24 @@ answer.
|
|
|
107
107
|
|
|
108
108
|
That sentence used to end at "None answers these", and a second pass over the 68k-star tool made it
|
|
109
109
|
narrower: its *importable library* does carry a cycle finder, reachable from neither its CLI nor its MCP
|
|
110
|
-
tools and called nowhere in its own source. Measured on the same tree it
|
|
111
|
-
|
|
112
|
-
included — is less flattering than that sounds:
|
|
110
|
+
tools and called nowhere in its own source. Measured on the same tree (its 1.6.0) it reported **136
|
|
111
|
+
cycles**; codemap reported **1**. Scoring both against the truth set — every intra-package import,
|
|
112
|
+
function-local ones included — is less flattering than that sounds:
|
|
113
113
|
|
|
114
|
-
| | reported | of the
|
|
114
|
+
| | reported | of the 45 real ones | precision | recall |
|
|
115
115
|
|---|---:|---:|---:|---:|
|
|
116
|
-
| codemap,
|
|
117
|
-
| the peer's library API | 136 | 13 | 10% |
|
|
118
|
-
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
116
|
+
| codemap, before R1-C29 | 1 | 1 | 100% | **2.2%** |
|
|
117
|
+
| the peer's library API, 1.6.0 | 136 | 13 | 10% | 29% |
|
|
118
|
+
| the peer's library API, **1.6.2** (re-measured 2026-10-15) | 25 | 16 | **64%** | 36% |
|
|
119
|
+
| codemap, after R1-C29 (0.0.4) | 45 | 45 | 100% | 100% |
|
|
120
|
+
|
|
121
|
+
*Corrected 2026-10-15.* This table first said **41** real cycles, recall 32% and 2.4%. The 41 were counted
|
|
122
|
+
on a later commit of the same repository than the one both tools were measured on; on the measured tree
|
|
123
|
+
there are 45 ([re-measure](research/codegraph_remeasure.md) §7, C3). The peer fixed the call edges we
|
|
124
|
+
reported and its 1.6.2 row is the current one.
|
|
125
|
+
|
|
126
|
+
Theirs over-reported because it walks name-resolved call edges — a `dict.get` became a call into an
|
|
127
|
+
unrelated class (fixed upstream in 1.6.1). **Ours under-reported because its import map was module-level only**, and it phrased
|
|
122
128
|
that as *"import graph is acyclic"* — a property claim over a partial map, which is the worse of the two
|
|
123
129
|
errors even though it was the smaller one. Both halves of that came from
|
|
124
130
|
[#11](https://github.com/kogriv/codemap/issues/11), filed off a different target and fixed the same day
|
|
@@ -221,8 +227,14 @@ codemap serve --graph graph.json --watch
|
|
|
221
227
|
- **Dataflow** — producers/consumers of a string-keyed DataFrame column.
|
|
222
228
|
- **Semantic search** (opt-in) — a concept query routed to an external adapter, with each fuzzy hit
|
|
223
229
|
resolved to the exact codemap symbol at its location (`codemap semantic`). See [docs/integrations.md](docs/integrations.md).
|
|
224
|
-
- **Context pack** — the
|
|
225
|
-
by relevance to seed symbols (`codemap pack --budget N [--seed X]`).
|
|
230
|
+
- **Context pack** — the highest-ranked modules, classes and functions under a token budget, by
|
|
231
|
+
importance or by relevance to seed symbols (`codemap pack --budget N [--seed X]`). With a seed it
|
|
232
|
+
walks both ways — what the seed uses and what uses it (`--direction uses` for one way) — documents are
|
|
233
|
+
never in it, the answer says which walk it took, and a seed that resolves to nothing is refused rather
|
|
234
|
+
than silently answered globally. Deterministic and budget-tested. Against git history, seeded with a
|
|
235
|
+
changed file, it puts 42 % and 60 % of the co-changed files in its first ten on two repositories —
|
|
236
|
+
ahead of `impact` and of grep by names, though on one of the two not beyond the noise; grep still
|
|
237
|
+
finds tests best. **Whether it helps an agent is not measured.** See [docs/pack.md](docs/pack.md).
|
|
226
238
|
- **Interop** — export the graph as a [SCIP](https://scip-code.org/) index (definitions + symbol
|
|
227
239
|
info + inherits/implements relationships) so Sourcegraph, Glean and other SCIP consumers can drive
|
|
228
240
|
go-to-definition, symbol search and type hierarchy over it. See [docs/export.md](docs/export.md).
|
|
@@ -266,6 +278,13 @@ build says so in its own diagnostics, and `codemap diff` says so when both sides
|
|
|
266
278
|
delta of a few call edges as possible tool noise before reading it as a change in the code. Anything
|
|
267
279
|
that must be reproducible byte-for-byte — a gate, a two-release comparison — belongs on the fast tier.
|
|
268
280
|
|
|
281
|
+
**A deep build does not check jedi.** With jedi answering nothing, a deep build still exits 0 and
|
|
282
|
+
declares `tier: deep` — and its call edges equal the fast tier's. `codemap doctor` asks griffe and jedi
|
|
283
|
+
one question each with a known answer and exits 1 when one is absent or answers wrongly
|
|
284
|
+
([accuracy.md](docs/accuracy.md#what-this-means-for-trust)); rebuilding over the previous graph of the
|
|
285
|
+
same tree (`build -o` on the existing file) names the classes of edges that shrank
|
|
286
|
+
([provenance.md](docs/provenance.md#fewer-edges-on-the-same-input-is-named)).
|
|
287
|
+
|
|
269
288
|
**How many times to build.** A consumer measured 175 full deep builds of one tree and found a real
|
|
270
289
|
edge present in **75 %** of them; eight builds here agreed (5 of 8, the same edge, and nothing else
|
|
271
290
|
unstable among 13 675). So one build misses such an edge about 1 time in 4, two builds 1 in 16, three
|
|
@@ -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, **1115 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 &
|
|
@@ -71,18 +71,24 @@ answer.
|
|
|
71
71
|
|
|
72
72
|
That sentence used to end at "None answers these", and a second pass over the 68k-star tool made it
|
|
73
73
|
narrower: its *importable library* does carry a cycle finder, reachable from neither its CLI nor its MCP
|
|
74
|
-
tools and called nowhere in its own source. Measured on the same tree it
|
|
75
|
-
|
|
76
|
-
included — is less flattering than that sounds:
|
|
74
|
+
tools and called nowhere in its own source. Measured on the same tree (its 1.6.0) it reported **136
|
|
75
|
+
cycles**; codemap reported **1**. Scoring both against the truth set — every intra-package import,
|
|
76
|
+
function-local ones included — is less flattering than that sounds:
|
|
77
77
|
|
|
78
|
-
| | reported | of the
|
|
78
|
+
| | reported | of the 45 real ones | precision | recall |
|
|
79
79
|
|---|---:|---:|---:|---:|
|
|
80
|
-
| codemap,
|
|
81
|
-
| the peer's library API | 136 | 13 | 10% |
|
|
82
|
-
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
| codemap, before R1-C29 | 1 | 1 | 100% | **2.2%** |
|
|
81
|
+
| the peer's library API, 1.6.0 | 136 | 13 | 10% | 29% |
|
|
82
|
+
| the peer's library API, **1.6.2** (re-measured 2026-10-15) | 25 | 16 | **64%** | 36% |
|
|
83
|
+
| codemap, after R1-C29 (0.0.4) | 45 | 45 | 100% | 100% |
|
|
84
|
+
|
|
85
|
+
*Corrected 2026-10-15.* This table first said **41** real cycles, recall 32% and 2.4%. The 41 were counted
|
|
86
|
+
on a later commit of the same repository than the one both tools were measured on; on the measured tree
|
|
87
|
+
there are 45 ([re-measure](research/codegraph_remeasure.md) §7, C3). The peer fixed the call edges we
|
|
88
|
+
reported and its 1.6.2 row is the current one.
|
|
89
|
+
|
|
90
|
+
Theirs over-reported because it walks name-resolved call edges — a `dict.get` became a call into an
|
|
91
|
+
unrelated class (fixed upstream in 1.6.1). **Ours under-reported because its import map was module-level only**, and it phrased
|
|
86
92
|
that as *"import graph is acyclic"* — a property claim over a partial map, which is the worse of the two
|
|
87
93
|
errors even though it was the smaller one. Both halves of that came from
|
|
88
94
|
[#11](https://github.com/kogriv/codemap/issues/11), filed off a different target and fixed the same day
|
|
@@ -185,8 +191,14 @@ codemap serve --graph graph.json --watch
|
|
|
185
191
|
- **Dataflow** — producers/consumers of a string-keyed DataFrame column.
|
|
186
192
|
- **Semantic search** (opt-in) — a concept query routed to an external adapter, with each fuzzy hit
|
|
187
193
|
resolved to the exact codemap symbol at its location (`codemap semantic`). See [docs/integrations.md](docs/integrations.md).
|
|
188
|
-
- **Context pack** — the
|
|
189
|
-
by relevance to seed symbols (`codemap pack --budget N [--seed X]`).
|
|
194
|
+
- **Context pack** — the highest-ranked modules, classes and functions under a token budget, by
|
|
195
|
+
importance or by relevance to seed symbols (`codemap pack --budget N [--seed X]`). With a seed it
|
|
196
|
+
walks both ways — what the seed uses and what uses it (`--direction uses` for one way) — documents are
|
|
197
|
+
never in it, the answer says which walk it took, and a seed that resolves to nothing is refused rather
|
|
198
|
+
than silently answered globally. Deterministic and budget-tested. Against git history, seeded with a
|
|
199
|
+
changed file, it puts 42 % and 60 % of the co-changed files in its first ten on two repositories —
|
|
200
|
+
ahead of `impact` and of grep by names, though on one of the two not beyond the noise; grep still
|
|
201
|
+
finds tests best. **Whether it helps an agent is not measured.** See [docs/pack.md](docs/pack.md).
|
|
190
202
|
- **Interop** — export the graph as a [SCIP](https://scip-code.org/) index (definitions + symbol
|
|
191
203
|
info + inherits/implements relationships) so Sourcegraph, Glean and other SCIP consumers can drive
|
|
192
204
|
go-to-definition, symbol search and type hierarchy over it. See [docs/export.md](docs/export.md).
|
|
@@ -230,6 +242,13 @@ build says so in its own diagnostics, and `codemap diff` says so when both sides
|
|
|
230
242
|
delta of a few call edges as possible tool noise before reading it as a change in the code. Anything
|
|
231
243
|
that must be reproducible byte-for-byte — a gate, a two-release comparison — belongs on the fast tier.
|
|
232
244
|
|
|
245
|
+
**A deep build does not check jedi.** With jedi answering nothing, a deep build still exits 0 and
|
|
246
|
+
declares `tier: deep` — and its call edges equal the fast tier's. `codemap doctor` asks griffe and jedi
|
|
247
|
+
one question each with a known answer and exits 1 when one is absent or answers wrongly
|
|
248
|
+
([accuracy.md](docs/accuracy.md#what-this-means-for-trust)); rebuilding over the previous graph of the
|
|
249
|
+
same tree (`build -o` on the existing file) names the classes of edges that shrank
|
|
250
|
+
([provenance.md](docs/provenance.md#fewer-edges-on-the-same-input-is-named)).
|
|
251
|
+
|
|
233
252
|
**How many times to build.** A consumer measured 175 full deep builds of one tree and found a real
|
|
234
253
|
edge present in **75 %** of them; eight builds here agreed (5 of 8, the same edge, and nothing else
|
|
235
254
|
unstable among 13 675). So one build misses such an edge about 1 time in 4, two builds 1 in 16, three
|
|
@@ -104,8 +104,83 @@ def load_contract(root: str | Path = ".") -> ArchitectureContract:
|
|
|
104
104
|
return replace(parse_contract(data.get("architecture", {})), path=str(path))
|
|
105
105
|
|
|
106
106
|
|
|
107
|
+
#: R1-C69: the closed schema of ``[architecture]``. A key outside it is an error, not a skip.
|
|
108
|
+
_FLAGS = ("no_cycles", "no_lazy_cycles", "no_type_only_cycles", "exhaustive", "no_phantom_rules")
|
|
109
|
+
_KNOWN_KEYS = ("layers", "independent", "forbidden") + _FLAGS
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _is_str_list(v) -> bool:
|
|
113
|
+
return isinstance(v, list) and all(isinstance(x, str) for x in v)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def contract_problems(section) -> list[str]:
|
|
117
|
+
"""Every way ``section`` departs from the ``[architecture]`` schema, in reading order (R1-C69).
|
|
118
|
+
|
|
119
|
+
Empty when the section is valid. Design ``docs/design/contract_validation.md`` D1: unknown
|
|
120
|
+
keys (with the nearest known one suggested), values of the wrong type — `"false"` is a
|
|
121
|
+
string and used to switch a rule *on* — and malformed items, each named by key and index.
|
|
122
|
+
Names that do not exist in the graph are not a schema question (D3, R1-C62).
|
|
123
|
+
"""
|
|
124
|
+
import difflib
|
|
125
|
+
|
|
126
|
+
if not isinstance(section, dict):
|
|
127
|
+
return [f"`[architecture]` must be a table, got {type(section).__name__}"]
|
|
128
|
+
problems: list[str] = []
|
|
129
|
+
for key in section:
|
|
130
|
+
if key not in _KNOWN_KEYS:
|
|
131
|
+
near = difflib.get_close_matches(key, _KNOWN_KEYS, n=1, cutoff=0.6)
|
|
132
|
+
hint = f" (did you mean `{near[0]}`?)" if near else ""
|
|
133
|
+
problems.append(f"unknown key `{key}`{hint}; known keys: {', '.join(_KNOWN_KEYS)}")
|
|
134
|
+
if "layers" in section and not _is_str_list(section["layers"]):
|
|
135
|
+
problems.append("`layers` must be a list of strings")
|
|
136
|
+
if "independent" in section:
|
|
137
|
+
groups = section["independent"]
|
|
138
|
+
if not isinstance(groups, list):
|
|
139
|
+
problems.append("`independent` must be a list of lists of strings")
|
|
140
|
+
else:
|
|
141
|
+
for i, grp in enumerate(groups):
|
|
142
|
+
if not _is_str_list(grp) or len(grp) < 2:
|
|
143
|
+
problems.append(f"`independent[{i}]` must be a list of at least 2 strings")
|
|
144
|
+
if "forbidden" in section:
|
|
145
|
+
items = section["forbidden"]
|
|
146
|
+
if not isinstance(items, list):
|
|
147
|
+
problems.append("`forbidden` must be a list of `{ from = \"…\", to = \"…\" }` tables")
|
|
148
|
+
else:
|
|
149
|
+
for i, item in enumerate(items):
|
|
150
|
+
if not isinstance(item, dict):
|
|
151
|
+
problems.append(f"`forbidden[{i}]` must be a table `{{ from = \"…\", to = \"…\" }}`")
|
|
152
|
+
continue
|
|
153
|
+
extra = sorted(set(item) - {"from", "to"})
|
|
154
|
+
missing = [k for k in ("from", "to") if k not in item]
|
|
155
|
+
wrong = [k for k in ("from", "to") if k in item and not isinstance(item[k], str)]
|
|
156
|
+
if extra:
|
|
157
|
+
problems.append(f"`forbidden[{i}]` has unknown key(s) "
|
|
158
|
+
+ ", ".join(f"`{k}`" for k in extra))
|
|
159
|
+
if missing:
|
|
160
|
+
problems.append(f"`forbidden[{i}]` is missing " + ", ".join(f"`{k}`" for k in missing))
|
|
161
|
+
if wrong:
|
|
162
|
+
problems.append(f"`forbidden[{i}]`: " + ", ".join(f"`{k}`" for k in wrong)
|
|
163
|
+
+ " must be a string")
|
|
164
|
+
for flag in _FLAGS:
|
|
165
|
+
if flag in section and not isinstance(section[flag], bool):
|
|
166
|
+
problems.append(f"`{flag}` must be true or false, got {type(section[flag]).__name__} "
|
|
167
|
+
f"`{section[flag]!r}`")
|
|
168
|
+
return problems
|
|
169
|
+
|
|
170
|
+
|
|
107
171
|
def parse_contract(section: dict) -> ArchitectureContract:
|
|
108
|
-
"""Build a contract from the parsed ``[architecture]`` table
|
|
172
|
+
"""Build a contract from the parsed ``[architecture]`` table.
|
|
173
|
+
|
|
174
|
+
R1-C69: validated against a closed schema first. A section with any problem is **not
|
|
175
|
+
enforced at all** — it comes back as an ``error`` contract, the R1-C27 path, so the CLI
|
|
176
|
+
exits 2 and MCP answers ``ok: false`` naming every problem. Enforcing the valid half used
|
|
177
|
+
to be the silent part: ``forbiden = [...]`` beside ``no_cycles = true`` printed a green
|
|
178
|
+
tick over a rule the gate never ran.
|
|
179
|
+
"""
|
|
180
|
+
problems = contract_problems(section)
|
|
181
|
+
if problems:
|
|
182
|
+
return ArchitectureContract(
|
|
183
|
+
error="`[architecture]` is not a valid contract: " + "; ".join(problems))
|
|
109
184
|
forbidden = []
|
|
110
185
|
for item in section.get("forbidden", []) or []:
|
|
111
186
|
if isinstance(item, dict) and "from" in item and "to" in item:
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
codemap build <path> [-o graph.json] [--deep]
|
|
4
4
|
[--consumer PATH ...] [--docs PATH ...] [--mode thin|full]
|
|
5
|
+
codemap doctor [--json]
|
|
6
|
+
probe griffe and jedi with a question whose answer is known (R1-C64)
|
|
5
7
|
codemap scope <path> [--consumer PATH ...] [--docs PATH ...] [--no-git] [--json]
|
|
6
8
|
| --diff A.meta.json B.meta.json → input scope manifest (M19.A)
|
|
7
9
|
codemap query <name> (--graph g.json | --build <path>) [--format json|text]
|
|
@@ -30,7 +32,7 @@
|
|
|
30
32
|
codemap.toml [integrations].enabled + the tool installed)
|
|
31
33
|
codemap semantic <query> (--graph g.json | --build <path>) [--root DIR] [--limit N]
|
|
32
34
|
semantic search via an opt-in adapter, enriched to codemap symbols (R1-C16)
|
|
33
|
-
codemap pack (--graph g.json | --build <path>) [--budget N] [--seed X …]
|
|
35
|
+
codemap pack (--graph g.json | --build <path>) [--budget N] [--seed X …] [--direction both|uses]
|
|
34
36
|
token-budgeted context pack: most relevant graph slice under N tokens (R1-C6)
|
|
35
37
|
"""
|
|
36
38
|
|
|
@@ -155,6 +157,7 @@ def _cmd_build(args) -> int:
|
|
|
155
157
|
text = " ".join(p for p in (d["message"], d.get("consequence")) if p)
|
|
156
158
|
print(f"[{d.get('severity', 'warning')}] {text}", file=sys.stderr)
|
|
157
159
|
if args.out:
|
|
160
|
+
_report_completeness_drop(args.out, graph)
|
|
158
161
|
store.save(graph, args.out)
|
|
159
162
|
# M18: record the build recipe beside the graph so `codemap refresh` can
|
|
160
163
|
# rebuild it, and so the graph's age is meaningful to `serve`/stats.
|
|
@@ -175,6 +178,36 @@ def _cmd_build(args) -> int:
|
|
|
175
178
|
return 0
|
|
176
179
|
|
|
177
180
|
|
|
181
|
+
def _report_completeness_drop(out: str, graph) -> None:
|
|
182
|
+
"""Compare a fresh build with the graph it is about to overwrite (R1-C59).
|
|
183
|
+
|
|
184
|
+
The one moment both builds of an input are in hand without anyone asking: the previous
|
|
185
|
+
graph is still on disk at ``-o``. Read it before the write and say when an edge class
|
|
186
|
+
shrank on the **same** input — on one input that is the builder (tool, dependency,
|
|
187
|
+
sampling), never the code. A warning, not a refusal: rebuilding with a new tool is
|
|
188
|
+
legitimate, and the build still succeeds.
|
|
189
|
+
|
|
190
|
+
A file that exists and cannot be read is not a file that is absent (R1-C27), so it is
|
|
191
|
+
named; a missing file is the first build, and there is nothing to say.
|
|
192
|
+
"""
|
|
193
|
+
from codemap.provenance import completeness_drops, describe_drops
|
|
194
|
+
if not Path(out).exists():
|
|
195
|
+
return
|
|
196
|
+
try:
|
|
197
|
+
previous = store.load(out)
|
|
198
|
+
except Exception as exc: # any unreadable previous graph — never fatal to a build
|
|
199
|
+
print(f"[note] the graph already at {out} could not be read "
|
|
200
|
+
f"({type(exc).__name__}); completeness against it not compared",
|
|
201
|
+
file=sys.stderr)
|
|
202
|
+
return
|
|
203
|
+
drops = completeness_drops(previous, graph)
|
|
204
|
+
if drops:
|
|
205
|
+
print(f"[warning] {describe_drops(drops)} — against the graph this build replaces. "
|
|
206
|
+
"Same input, fewer edges: the difference is the builder (the tool, a "
|
|
207
|
+
"dependency, or the deep tier's sampling), not the code; whatever reads "
|
|
208
|
+
"those edges now answers from less than before.", file=sys.stderr)
|
|
209
|
+
|
|
210
|
+
|
|
178
211
|
def _inputs_with_membership(graph, scope, args):
|
|
179
212
|
"""The extractor's ``inputs`` block plus the manifest membership result (R1-C41).
|
|
180
213
|
|
|
@@ -333,6 +366,22 @@ def _cmd_watch(args) -> int:
|
|
|
333
366
|
return 0
|
|
334
367
|
|
|
335
368
|
|
|
369
|
+
def _cmd_doctor(args) -> int:
|
|
370
|
+
"""Probe each dependency with a question whose answer is known (R1-C64).
|
|
371
|
+
|
|
372
|
+
Exit 0 when every probe answers as expected, 1 otherwise. A dependency that imports
|
|
373
|
+
and answers wrongly is the case this exists for: it never breaks a build, it makes the
|
|
374
|
+
graph poorer while the graph still declares its tier.
|
|
375
|
+
"""
|
|
376
|
+
from codemap.extract.probe import render, run_probes
|
|
377
|
+
report = run_probes()
|
|
378
|
+
if args.json:
|
|
379
|
+
print(json.dumps(report, indent=2, sort_keys=True, ensure_ascii=False))
|
|
380
|
+
else:
|
|
381
|
+
print(render(report), end="")
|
|
382
|
+
return 0 if report["ok"] else 1
|
|
383
|
+
|
|
384
|
+
|
|
336
385
|
def _cmd_scope(args) -> int:
|
|
337
386
|
"""Resolve/print the input scope manifest, or diff two (M19.A)."""
|
|
338
387
|
from codemap.scope import resolve_scope, diff_scopes
|
|
@@ -741,15 +790,27 @@ def _cmd_semantic(args) -> int:
|
|
|
741
790
|
|
|
742
791
|
def _cmd_pack(args) -> int:
|
|
743
792
|
"""Token-budgeted context pack — most relevant graph slice under N tokens (R1-C6)."""
|
|
744
|
-
from codemap.serve.pack import build_pack,
|
|
745
|
-
q = Query(_graph_from(args))
|
|
793
|
+
from codemap.serve.pack import build_pack, render_payload
|
|
746
794
|
seeds = tuple(args.seed or ())
|
|
795
|
+
if args.direction and not seeds: # before any build: a usage error costs nothing
|
|
796
|
+
print("codemap pack: --direction needs --seed — the global rank walks from no "
|
|
797
|
+
"seed", file=sys.stderr)
|
|
798
|
+
return 2
|
|
799
|
+
q = Query(_graph_from(args))
|
|
800
|
+
p = build_pack(q, budget=args.budget, seeds=seeds, direction=args.direction)
|
|
747
801
|
if args.format == "json":
|
|
748
|
-
print(json.dumps(
|
|
749
|
-
ensure_ascii=False, indent=2, sort_keys=True))
|
|
802
|
+
print(json.dumps(p, ensure_ascii=False, indent=2, sort_keys=True))
|
|
750
803
|
else:
|
|
751
|
-
print(
|
|
752
|
-
|
|
804
|
+
print(render_payload(p, target=q.graph.target, seeds=seeds), end="")
|
|
805
|
+
# No seed resolved → a refusal, not an empty pack: exit 1, as `query` does on an
|
|
806
|
+
# unknown name (pack honesty).
|
|
807
|
+
return 1 if "error" in p else 0
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def _check_exit(violations, currency: dict | None, allow_stale: bool) -> int:
|
|
811
|
+
"""2 on a violation, or on a graph behind its source unless ``--allow-stale`` (R1-C70)."""
|
|
812
|
+
behind = currency is not None and currency.get("status") == "behind"
|
|
813
|
+
return 2 if (violations or (behind and not allow_stale)) else 0
|
|
753
814
|
|
|
754
815
|
|
|
755
816
|
def _cmd_check(args) -> int:
|
|
@@ -765,21 +826,28 @@ def _cmd_check(args) -> int:
|
|
|
765
826
|
from codemap.serve.check import render_check
|
|
766
827
|
q = Query(_graph_from(args))
|
|
767
828
|
contract = load_contract(args.root)
|
|
829
|
+
# R1-C70: a graph read from disk may be behind the tree it is about to be judged as.
|
|
830
|
+
currency = None
|
|
831
|
+
if getattr(args, "graph", None) and not getattr(args, "build", None):
|
|
832
|
+
from codemap.serve.currency import graph_currency
|
|
833
|
+
currency = graph_currency(args.graph, (q.graph.provenance or {}).get("scope_id"))
|
|
834
|
+
allow_stale = getattr(args, "allow_stale", False)
|
|
768
835
|
# R1-C27: check `error` before anything else. A malformed codemap.toml used to be
|
|
769
836
|
# indistinguishable from an absent one, so removing a single `]` turned a gate that
|
|
770
837
|
# reported 14 violations into exit 0. Exit 2 — the same code as a violation, not a new
|
|
771
838
|
# one: the status answers "may the pipeline proceed?" (no, either way), and a new code
|
|
772
839
|
# would sort an unreadable contract into the success branch of every `if rc == 2` that
|
|
773
840
|
# already exists in someone's pipeline.
|
|
841
|
+
kw = {"currency": currency, "allow_stale": allow_stale}
|
|
774
842
|
if contract.error:
|
|
775
|
-
_emit_answer(render_check(q, contract, []))
|
|
843
|
+
_emit_answer(render_check(q, contract, [], **kw))
|
|
776
844
|
return 2
|
|
777
845
|
if contract.is_empty() and args.require_contract:
|
|
778
|
-
_emit_answer(render_check(q, contract, []))
|
|
846
|
+
_emit_answer(render_check(q, contract, [], **kw))
|
|
779
847
|
raise SystemExit("error: no [architecture] contract found (--require-contract)")
|
|
780
848
|
violations = check_contract(q, contract)
|
|
781
|
-
_emit_answer(render_check(q, contract, violations))
|
|
782
|
-
return
|
|
849
|
+
_emit_answer(render_check(q, contract, violations, **kw))
|
|
850
|
+
return _check_exit(violations, currency, allow_stale)
|
|
783
851
|
|
|
784
852
|
|
|
785
853
|
def _cmd_diff(args) -> int:
|
|
@@ -795,7 +863,7 @@ def _cmd_diff(args) -> int:
|
|
|
795
863
|
# R1-C25/D4: two graphs built by different tools are a before/after of the *tool*,
|
|
796
864
|
# not of the code. Never a refusal — comparing across an upgrade is legitimate; what
|
|
797
865
|
# was missing is being told, since a clean "no breaking changes" reads as proof.
|
|
798
|
-
cmp = comparability(old.provenance, new.provenance)
|
|
866
|
+
cmp = comparability(old.provenance, new.provenance, old_graph=old, new_graph=new)
|
|
799
867
|
if not cmp["comparable"]:
|
|
800
868
|
for line in cmp["differences"]:
|
|
801
869
|
print(f"[warning] {line}", file=sys.stderr)
|
|
@@ -947,6 +1015,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
947
1015
|
"of the chain: a different N restarts it from a full build.")
|
|
948
1016
|
b.set_defaults(func=_cmd_build)
|
|
949
1017
|
|
|
1018
|
+
dr = sub.add_parser("doctor", help="Probe griffe and jedi with a question whose answer is "
|
|
1019
|
+
"known; exit 1 if one is absent or answers wrongly.")
|
|
1020
|
+
dr.add_argument("--json", action="store_true", help="Structured report instead of text.")
|
|
1021
|
+
dr.set_defaults(func=_cmd_doctor)
|
|
1022
|
+
|
|
950
1023
|
sc = sub.add_parser("scope", help="Resolve the input scope manifest (scope_id + profile), or --diff two.")
|
|
951
1024
|
sc.add_argument("path", nargs="?", help="Package/dir to scope (like build's path).")
|
|
952
1025
|
sc.add_argument("--consumer", action="append", metavar="PATH",
|
|
@@ -1094,6 +1167,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1094
1167
|
pk.add_argument("--seed", action="append", metavar="X",
|
|
1095
1168
|
help="Bias relevance to this symbol / file (repeatable). "
|
|
1096
1169
|
"Omit for global importance.")
|
|
1170
|
+
pk.add_argument("--direction", choices=["both", "uses"],
|
|
1171
|
+
help="Which way a seeded walk runs: both (default) — what the seeds "
|
|
1172
|
+
"use and what uses them; uses — only what they use.")
|
|
1097
1173
|
pk.add_argument("--format", choices=["markdown", "json"], default="markdown")
|
|
1098
1174
|
pk.set_defaults(func=_cmd_pack)
|
|
1099
1175
|
|
|
@@ -1110,6 +1186,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1110
1186
|
help="Dir with codemap.toml holding [architecture] (default: cwd).")
|
|
1111
1187
|
ck.add_argument("--require-contract", action="store_true",
|
|
1112
1188
|
help="Fail if no [architecture] contract is present (default: no-op success).")
|
|
1189
|
+
ck.add_argument("--allow-stale", action="store_true",
|
|
1190
|
+
help="With --graph: do not fail when the graph is behind its source tree "
|
|
1191
|
+
"(R1-C70); the 'behind' line is still printed.")
|
|
1113
1192
|
ck.set_defaults(func=_cmd_check)
|
|
1114
1193
|
|
|
1115
1194
|
return p
|
|
@@ -50,7 +50,7 @@ from codemap.extract.behavior import (
|
|
|
50
50
|
_node_id,
|
|
51
51
|
_own_nodes,
|
|
52
52
|
)
|
|
53
|
-
from codemap.extract.gsource import module_file, module_imports
|
|
53
|
+
from codemap.extract.gsource import flat_inference_applies, module_file, module_imports
|
|
54
54
|
from codemap.model import Edge
|
|
55
55
|
|
|
56
56
|
|
|
@@ -66,7 +66,8 @@ def add_attrflow(graph, griffe_root, target_pkg: str, *, deep: bool = False,
|
|
|
66
66
|
(R1-C9), mirroring :func:`add_behavior`.
|
|
67
67
|
"""
|
|
68
68
|
modules = _index_modules(griffe_root)
|
|
69
|
-
|
|
69
|
+
# R1-C21 sibling lookup, gated like behavior's (R1-C72).
|
|
70
|
+
known_modules = set(modules) if flat_inference_applies(graph, target_pkg) else frozenset()
|
|
70
71
|
project = _jedi_project(search_path) if deep else None
|
|
71
72
|
pkg = target_pkg + "."
|
|
72
73
|
# (func_id, attr_id, access, resolution) — dedup collapses repeated sites.
|
|
@@ -27,8 +27,8 @@ import ast
|
|
|
27
27
|
import math
|
|
28
28
|
from pathlib import Path
|
|
29
29
|
|
|
30
|
-
from codemap.extract.gsource import (NESTED_IMPORT_HINT,
|
|
31
|
-
qualify_target)
|
|
30
|
+
from codemap.extract.gsource import (NESTED_IMPORT_HINT, flat_inference_applies, module_file,
|
|
31
|
+
module_imports, qualify_target)
|
|
32
32
|
from codemap.model import Edge
|
|
33
33
|
|
|
34
34
|
_BUILTINS = set(vars(__import__("builtins")))
|
|
@@ -51,7 +51,9 @@ def add_behavior(graph, griffe_root, target_pkg: str, *, deep: bool = False,
|
|
|
51
51
|
modules left out. When ``None`` (default) every module is processed.
|
|
52
52
|
"""
|
|
53
53
|
modules = _index_modules(griffe_root)
|
|
54
|
-
|
|
54
|
+
# R1-C21 flat-layout sibling lookup — empty, so it never fires, in a core that imports
|
|
55
|
+
# itself by name (R1-C72): there `import warnings` is the standard library.
|
|
56
|
+
known_modules = set(modules) if flat_inference_applies(graph, target_pkg) else frozenset()
|
|
55
57
|
project = _jedi_project(search_path) if deep else None
|
|
56
58
|
# R1-C30-f1: a call to a re-exported name points at a name that is not a definition.
|
|
57
59
|
# The structural pass already recorded where each re-export really leads.
|
|
@@ -588,16 +590,33 @@ def _arg_contract(shapes: list[tuple]) -> dict:
|
|
|
588
590
|
return contract
|
|
589
591
|
|
|
590
592
|
|
|
593
|
+
def _is_internal(target: str, modpath: str) -> bool:
|
|
594
|
+
"""R1-C71/D3: the package root is inside the package. ``startswith(pkg + ".")`` alone
|
|
595
|
+
filed ``import pkg; pkg.f()`` — and the bound name of ``import pkg.a.b`` — as external."""
|
|
596
|
+
root = modpath.split(".")[0]
|
|
597
|
+
return target == root or target.startswith(root + ".")
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _attr_chain(node) -> tuple[str, list[str]] | None:
|
|
601
|
+
"""``a.b.c`` → ``("a", ["b", "c"])`` when the chain bottoms out in a plain name."""
|
|
602
|
+
parts = []
|
|
603
|
+
while isinstance(node, ast.Attribute):
|
|
604
|
+
parts.append(node.attr)
|
|
605
|
+
node = node.value
|
|
606
|
+
if not isinstance(node, ast.Name):
|
|
607
|
+
return None
|
|
608
|
+
return node.id, parts[::-1]
|
|
609
|
+
|
|
610
|
+
|
|
591
611
|
def _resolve(call, modpath, class_prefix, imports, modmembers, members, reexports=None,
|
|
592
612
|
nodes=frozenset()):
|
|
593
613
|
"""Return (target_id, resolution). Only module/self/imported become edges."""
|
|
594
|
-
pkg = modpath.split(".")[0] + "."
|
|
595
614
|
f = call.func
|
|
596
615
|
if isinstance(f, ast.Name):
|
|
597
616
|
name = f.id
|
|
598
617
|
if name in imports:
|
|
599
618
|
tgt = imports[name]
|
|
600
|
-
if tgt
|
|
619
|
+
if _is_internal(tgt, modpath):
|
|
601
620
|
return _follow_reexport(tgt, reexports or {}, nodes), "imported"
|
|
602
621
|
return tgt, "external"
|
|
603
622
|
if name in modmembers:
|
|
@@ -613,13 +632,27 @@ def _resolve(call, modpath, class_prefix, imports, modmembers, members, reexport
|
|
|
613
632
|
# method call on self — resolve to the class that actually defines
|
|
614
633
|
# the member (own or a base), never a phantom same-class id.
|
|
615
634
|
return f"{members[attr]}.{attr}", "self"
|
|
616
|
-
if recv.id in imports:
|
|
617
|
-
tgt = imports[recv.id]
|
|
618
|
-
if tgt.startswith(pkg):
|
|
619
|
-
return _follow_reexport(f"{tgt}.{attr}", reexports or {}, nodes), "imported"
|
|
620
|
-
return f"{tgt}.{attr}", "external"
|
|
621
|
-
if recv.id in modmembers:
|
|
635
|
+
if recv.id in modmembers and recv.id not in imports:
|
|
622
636
|
return f"{modpath}.{recv.id}.{attr}", "module"
|
|
637
|
+
# R1-C71 (design ``attribute_chain_calls.md``): a callee rooted at an imported name,
|
|
638
|
+
# at any depth. Internal: follow it hop by hop, each hop through a re-export when the
|
|
639
|
+
# name is no node (D1) — the depth-1 case is the one-hop instance and answers as it
|
|
640
|
+
# always did; the soundness guard downstream still refuses a target that is no node,
|
|
641
|
+
# so `b.registry.get()` stays unresolved. External: external at any depth (D2) —
|
|
642
|
+
# `np.random.rand()` is no more unknown than `np.zeros()`.
|
|
643
|
+
chain = _attr_chain(f)
|
|
644
|
+
if chain is not None and chain[0] in imports:
|
|
645
|
+
head, parts = chain
|
|
646
|
+
tgt = imports[head]
|
|
647
|
+
if not _is_internal(tgt, modpath):
|
|
648
|
+
return f"{tgt}.{'.'.join(parts)}", "external"
|
|
649
|
+
cur = tgt
|
|
650
|
+
for part in parts:
|
|
651
|
+
cur = _follow_reexport(f"{cur}.{part}", reexports or {}, nodes)
|
|
652
|
+
if len(parts) == 1 or cur in nodes:
|
|
653
|
+
return cur, "imported"
|
|
654
|
+
# a deeper chain that lands on no definition (`b.registry.get("k")`) keeps the
|
|
655
|
+
# answer it had before R1-C71: string-keyed dispatch, else unresolved
|
|
623
656
|
# X.method('literal', ...) — dynamic string-keyed dispatch (registry/dict)
|
|
624
657
|
if call.args and isinstance(call.args[0], ast.Constant) \
|
|
625
658
|
and isinstance(call.args[0].value, str) and attr in ("create", "get", "register"):
|
|
@@ -868,10 +901,9 @@ def _module_level_nodes(tree):
|
|
|
868
901
|
def _resolve_name(name, modpath, imports, modmembers, reexports=None, nodes=frozenset()):
|
|
869
902
|
"""(target_id, resolution) for a bare name used as a value — the `_resolve` rules for
|
|
870
903
|
``ast.Name`` minus the call. Only module/imported become edges."""
|
|
871
|
-
pkg = modpath.split(".")[0] + "."
|
|
872
904
|
if name in imports:
|
|
873
905
|
tgt = imports[name]
|
|
874
|
-
if tgt
|
|
906
|
+
if _is_internal(tgt, modpath):
|
|
875
907
|
return _follow_reexport(tgt, reexports or {}, nodes), "imported"
|
|
876
908
|
return tgt, "external"
|
|
877
909
|
if name in modmembers:
|