codmap 0.0.21__tar.gz → 0.0.22__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.22}/PKG-INFO +17 -4
- {codmap-0.0.21 → codmap-0.0.22}/README.md +16 -3
- {codmap-0.0.21 → codmap-0.0.22}/codemap/cli.py +71 -8
- codmap-0.0.22/codemap/extract/probe.py +152 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/model.py +11 -1
- {codmap-0.0.21 → codmap-0.0.22}/codemap/provenance.py +71 -1
- {codmap-0.0.21 → codmap-0.0.22}/codemap/query.py +101 -16
- {codmap-0.0.21 → codmap-0.0.22}/codemap/serve/apidiff.py +4 -1
- {codmap-0.0.21 → codmap-0.0.22}/codemap/serve/audit.py +8 -3
- {codmap-0.0.21 → codmap-0.0.22}/codemap/serve/mcp_server.py +17 -6
- codmap-0.0.22/codemap/serve/pack.py +224 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/serve/session.py +2 -1
- {codmap-0.0.21 → codmap-0.0.22/codmap.egg-info}/PKG-INFO +17 -4
- {codmap-0.0.21 → codmap-0.0.22}/codmap.egg-info/SOURCES.txt +6 -0
- {codmap-0.0.21 → codmap-0.0.22}/pyproject.toml +1 -1
- codmap-0.0.22/tests/test_pack_honesty.py +227 -0
- codmap-0.0.22/tests/test_r1c59_completeness_drop.py +198 -0
- codmap-0.0.22/tests/test_r1c60_nested_definitions.py +117 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c63_answer_format.py +3 -1
- codmap-0.0.22/tests/test_r1c64_dependency_probe.py +140 -0
- codmap-0.0.22/tests/test_r1c66_pack_direction.py +150 -0
- codmap-0.0.21/codemap/serve/pack.py +0 -93
- {codmap-0.0.21 → codmap-0.0.22}/LICENSE +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/__init__.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/apidiff.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/arch.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/diagnostics.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/extract/__init__.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/extract/attrflow.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/extract/behavior.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/extract/dataflow.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/extract/dispatch.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/extract/griffe_extractor.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/extract/gsource.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/extract/roots.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/extract/union.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/freshness.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/incremental.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/integrations/__init__.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/integrations/base.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/integrations/cocoindex.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/integrations/gate.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/integrations/gitnexus.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/integrations/registry.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/integrations/transport.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/scope.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/serve/__init__.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/serve/_scip_pb2.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/serve/api_surface.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/serve/architecture.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/serve/check.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/serve/ctags.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/serve/impact.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/serve/limits.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/serve/livingdocs.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/serve/mermaid.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/serve/rag.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/serve/review.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/serve/scip.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/serve/semantic.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/serve/server.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/serve/subsystems.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/serve/vault.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/store.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/tomlio.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codemap/watch.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codmap.egg-info/dependency_links.txt +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codmap.egg-info/entry_points.txt +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codmap.egg-info/requires.txt +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/codmap.egg-info/top_level.txt +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/setup.cfg +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_epistemic.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_gitnexus_router.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_impact_depth.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_integrations.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_issue3_serve_freshness.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_livingdocs.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_m0_api_surface.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_m11_argcontract.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_m12_dataflow.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_m15_review.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_m16_architecture.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_m17_mcp.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_m18_freshness.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_m1_5_semantics.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_m1_query.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_m2_views.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_m32_watch.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_m3_serve.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_m4_behavior.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_m5_deep.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_m6_repo_scope.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_m7_dispatch.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_m9_family.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c13_callgraph_accuracy.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c13_grep_vs_graph.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c16_semantic.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c20_attribute_edges.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c21_flat_layout.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c22_source_visible_refs.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c23_hard_python.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c24_test_mapping.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c25_provenance.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c26_deep_union.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c27_config_honesty.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c28_limit_envelope.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c29_lazy_imports.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c2_ctags.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c30_local_import_calls.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c30f1_reexport_calls.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c30f2_check_scope.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c31_path_origin.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c32_report_json.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c33_query_signature.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c34_signature_kinds.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c35_contract_location.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c36_target_resolution.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c37_rule_mutation.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c38_tool_drift.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c39_edge_resolution.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c3_arch_contract.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c3_dogfood.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c40_flow_reach.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c41_scope_membership.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c42_deep_tier_stability.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c43_incremental_splice.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c44_absent_answers.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c45_repeat_union.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c46_shadowed_definitions.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c47_incremental_resample.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c48_type_checking_imports.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c49_type_only_cycles.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c4_complexity.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c50_flow_entry_points.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c53_narrowing_declared.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c54_deterministic_rendering.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c55_override_grade.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c56_stub_files.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c57_facade_package.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c58_cycle_tangles.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c5_apidiff.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c62_phantom_rules.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c6_pack.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c7_edge_vocab.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c8_deadcode.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_r1c9_incremental.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_scip_export.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/tests/test_scope.py +0 -0
- {codmap-0.0.21 → codmap-0.0.22}/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.22
|
|
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, **1054 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 &
|
|
@@ -221,8 +221,14 @@ codemap serve --graph graph.json --watch
|
|
|
221
221
|
- **Dataflow** — producers/consumers of a string-keyed DataFrame column.
|
|
222
222
|
- **Semantic search** (opt-in) — a concept query routed to an external adapter, with each fuzzy hit
|
|
223
223
|
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]`).
|
|
224
|
+
- **Context pack** — the highest-ranked modules, classes and functions under a token budget, by
|
|
225
|
+
importance or by relevance to seed symbols (`codemap pack --budget N [--seed X]`). With a seed it
|
|
226
|
+
walks both ways — what the seed uses and what uses it (`--direction uses` for one way) — documents are
|
|
227
|
+
never in it, the answer says which walk it took, and a seed that resolves to nothing is refused rather
|
|
228
|
+
than silently answered globally. Deterministic and budget-tested. Against git history, seeded with a
|
|
229
|
+
changed file, it puts 42 % and 60 % of the co-changed files in its first ten on two repositories —
|
|
230
|
+
ahead of `impact` and of grep by names, though on one of the two not beyond the noise; grep still
|
|
231
|
+
finds tests best. **Whether it helps an agent is not measured.** See [docs/pack.md](docs/pack.md).
|
|
226
232
|
- **Interop** — export the graph as a [SCIP](https://scip-code.org/) index (definitions + symbol
|
|
227
233
|
info + inherits/implements relationships) so Sourcegraph, Glean and other SCIP consumers can drive
|
|
228
234
|
go-to-definition, symbol search and type hierarchy over it. See [docs/export.md](docs/export.md).
|
|
@@ -266,6 +272,13 @@ build says so in its own diagnostics, and `codemap diff` says so when both sides
|
|
|
266
272
|
delta of a few call edges as possible tool noise before reading it as a change in the code. Anything
|
|
267
273
|
that must be reproducible byte-for-byte — a gate, a two-release comparison — belongs on the fast tier.
|
|
268
274
|
|
|
275
|
+
**A deep build does not check jedi.** With jedi answering nothing, a deep build still exits 0 and
|
|
276
|
+
declares `tier: deep` — and its call edges equal the fast tier's. `codemap doctor` asks griffe and jedi
|
|
277
|
+
one question each with a known answer and exits 1 when one is absent or answers wrongly
|
|
278
|
+
([accuracy.md](docs/accuracy.md#what-this-means-for-trust)); rebuilding over the previous graph of the
|
|
279
|
+
same tree (`build -o` on the existing file) names the classes of edges that shrank
|
|
280
|
+
([provenance.md](docs/provenance.md#fewer-edges-on-the-same-input-is-named)).
|
|
281
|
+
|
|
269
282
|
**How many times to build.** A consumer measured 175 full deep builds of one tree and found a real
|
|
270
283
|
edge present in **75 %** of them; eight builds here agreed (5 of 8, the same edge, and nothing else
|
|
271
284
|
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, **1054 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 &
|
|
@@ -185,8 +185,14 @@ codemap serve --graph graph.json --watch
|
|
|
185
185
|
- **Dataflow** — producers/consumers of a string-keyed DataFrame column.
|
|
186
186
|
- **Semantic search** (opt-in) — a concept query routed to an external adapter, with each fuzzy hit
|
|
187
187
|
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]`).
|
|
188
|
+
- **Context pack** — the highest-ranked modules, classes and functions under a token budget, by
|
|
189
|
+
importance or by relevance to seed symbols (`codemap pack --budget N [--seed X]`). With a seed it
|
|
190
|
+
walks both ways — what the seed uses and what uses it (`--direction uses` for one way) — documents are
|
|
191
|
+
never in it, the answer says which walk it took, and a seed that resolves to nothing is refused rather
|
|
192
|
+
than silently answered globally. Deterministic and budget-tested. Against git history, seeded with a
|
|
193
|
+
changed file, it puts 42 % and 60 % of the co-changed files in its first ten on two repositories —
|
|
194
|
+
ahead of `impact` and of grep by names, though on one of the two not beyond the noise; grep still
|
|
195
|
+
finds tests best. **Whether it helps an agent is not measured.** See [docs/pack.md](docs/pack.md).
|
|
190
196
|
- **Interop** — export the graph as a [SCIP](https://scip-code.org/) index (definitions + symbol
|
|
191
197
|
info + inherits/implements relationships) so Sourcegraph, Glean and other SCIP consumers can drive
|
|
192
198
|
go-to-definition, symbol search and type hierarchy over it. See [docs/export.md](docs/export.md).
|
|
@@ -230,6 +236,13 @@ build says so in its own diagnostics, and `codemap diff` says so when both sides
|
|
|
230
236
|
delta of a few call edges as possible tool noise before reading it as a change in the code. Anything
|
|
231
237
|
that must be reproducible byte-for-byte — a gate, a two-release comparison — belongs on the fast tier.
|
|
232
238
|
|
|
239
|
+
**A deep build does not check jedi.** With jedi answering nothing, a deep build still exits 0 and
|
|
240
|
+
declares `tier: deep` — and its call edges equal the fast tier's. `codemap doctor` asks griffe and jedi
|
|
241
|
+
one question each with a known answer and exits 1 when one is absent or answers wrongly
|
|
242
|
+
([accuracy.md](docs/accuracy.md#what-this-means-for-trust)); rebuilding over the previous graph of the
|
|
243
|
+
same tree (`build -o` on the existing file) names the classes of edges that shrank
|
|
244
|
+
([provenance.md](docs/provenance.md#fewer-edges-on-the-same-input-is-named)).
|
|
245
|
+
|
|
233
246
|
**How many times to build.** A consumer measured 175 full deep builds of one tree and found a real
|
|
234
247
|
edge present in **75 %** of them; eight builds here agreed (5 of 8, the same edge, and nothing else
|
|
235
248
|
unstable among 13 675). So one build misses such an edge about 1 time in 4, two builds 1 in 16, three
|
|
@@ -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,21 @@ 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
|
|
753
808
|
|
|
754
809
|
|
|
755
810
|
def _cmd_check(args) -> int:
|
|
@@ -795,7 +850,7 @@ def _cmd_diff(args) -> int:
|
|
|
795
850
|
# R1-C25/D4: two graphs built by different tools are a before/after of the *tool*,
|
|
796
851
|
# not of the code. Never a refusal — comparing across an upgrade is legitimate; what
|
|
797
852
|
# was missing is being told, since a clean "no breaking changes" reads as proof.
|
|
798
|
-
cmp = comparability(old.provenance, new.provenance)
|
|
853
|
+
cmp = comparability(old.provenance, new.provenance, old_graph=old, new_graph=new)
|
|
799
854
|
if not cmp["comparable"]:
|
|
800
855
|
for line in cmp["differences"]:
|
|
801
856
|
print(f"[warning] {line}", file=sys.stderr)
|
|
@@ -947,6 +1002,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
947
1002
|
"of the chain: a different N restarts it from a full build.")
|
|
948
1003
|
b.set_defaults(func=_cmd_build)
|
|
949
1004
|
|
|
1005
|
+
dr = sub.add_parser("doctor", help="Probe griffe and jedi with a question whose answer is "
|
|
1006
|
+
"known; exit 1 if one is absent or answers wrongly.")
|
|
1007
|
+
dr.add_argument("--json", action="store_true", help="Structured report instead of text.")
|
|
1008
|
+
dr.set_defaults(func=_cmd_doctor)
|
|
1009
|
+
|
|
950
1010
|
sc = sub.add_parser("scope", help="Resolve the input scope manifest (scope_id + profile), or --diff two.")
|
|
951
1011
|
sc.add_argument("path", nargs="?", help="Package/dir to scope (like build's path).")
|
|
952
1012
|
sc.add_argument("--consumer", action="append", metavar="PATH",
|
|
@@ -1094,6 +1154,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1094
1154
|
pk.add_argument("--seed", action="append", metavar="X",
|
|
1095
1155
|
help="Bias relevance to this symbol / file (repeatable). "
|
|
1096
1156
|
"Omit for global importance.")
|
|
1157
|
+
pk.add_argument("--direction", choices=["both", "uses"],
|
|
1158
|
+
help="Which way a seeded walk runs: both (default) — what the seeds "
|
|
1159
|
+
"use and what uses them; uses — only what they use.")
|
|
1097
1160
|
pk.add_argument("--format", choices=["markdown", "json"], default="markdown")
|
|
1098
1161
|
pk.set_defaults(func=_cmd_pack)
|
|
1099
1162
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Does each dependency still answer a known question correctly? (R1-C64)
|
|
2
|
+
|
|
3
|
+
griffe and jedi do work codemap never re-checks: griffe resolves re-export aliases into the
|
|
4
|
+
structure, jedi types a local variable into a call target. Both are hard dependencies, so the
|
|
5
|
+
only "check" they ever got was that ``import`` did not fail. That is the wrong check for the
|
|
6
|
+
failure that matters: a jedi that stopped resolving after an upgrade does not break a build —
|
|
7
|
+
``_resolve_jedi`` reads any exception as ``unresolved`` — it turns every deep call into a
|
|
8
|
+
fast-tier one, and the graph is simply poorer. Measured: with ``jedi.Script`` answering
|
|
9
|
+
nothing, a deep build of a real 89-module tree exited 0 with ``calls`` equal to the fast tier's
|
|
10
|
+
to the edge, while still declaring ``tier: deep``.
|
|
11
|
+
|
|
12
|
+
The probe is the shape `jg doctor` uses (research/tools/jevgrep.md, S2): a synthetic input
|
|
13
|
+
whose correct answer is known, and a backend that answers it wrongly is refused rather than
|
|
14
|
+
counted as present. One fact per dependency, through the real pipeline — not a benchmark,
|
|
15
|
+
which would be noisy and get switched off.
|
|
16
|
+
|
|
17
|
+
Design: ``docs/design/dependency_probe.md``.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import importlib
|
|
23
|
+
import tempfile
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
PROBE_PACKAGE = "codemap_probe"
|
|
27
|
+
|
|
28
|
+
#: The probe tree. ``engine.run()`` needs a local variable's type (jedi only — the fast tier
|
|
29
|
+
#: does not build that edge); ``ph()`` goes through a re-export alias whose target griffe
|
|
30
|
+
#: resolves (``Alias.target_path``), which is where the ``export`` edge comes from.
|
|
31
|
+
PROBE_FILES = {
|
|
32
|
+
"__init__.py": f"from {PROBE_PACKAGE}.core import helper as public_helper\n",
|
|
33
|
+
"core.py": ("class Engine:\n"
|
|
34
|
+
" def run(self):\n"
|
|
35
|
+
" return 1\n\n\n"
|
|
36
|
+
"def helper():\n"
|
|
37
|
+
" return 2\n"),
|
|
38
|
+
"app.py": (f"from {PROBE_PACKAGE} import public_helper as ph\n"
|
|
39
|
+
f"from {PROBE_PACKAGE}.core import Engine\n\n\n"
|
|
40
|
+
"def main():\n"
|
|
41
|
+
" engine = Engine()\n"
|
|
42
|
+
" engine.run()\n"
|
|
43
|
+
" return ph()\n"),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#: Outcomes. ``wrong`` is the case this module exists for: present, answering, mistaken.
|
|
47
|
+
OK, WRONG, ABSENT, ERROR, NOT_RUN = "ok", "wrong", "absent", "error", "not-run"
|
|
48
|
+
|
|
49
|
+
#: dependency → (edge type, source, target, resolution or None, what the fact means)
|
|
50
|
+
EXPECTED = {
|
|
51
|
+
"griffe": ("export", PROBE_PACKAGE, f"{PROBE_PACKAGE}.core.helper", None,
|
|
52
|
+
f"the re-export `{PROBE_PACKAGE}.public_helper` resolves to "
|
|
53
|
+
f"`{PROBE_PACKAGE}.core.helper`"),
|
|
54
|
+
"jedi": ("calls", f"{PROBE_PACKAGE}.app.main", f"{PROBE_PACKAGE}.core.Engine.run", "deep",
|
|
55
|
+
f"`engine.run()` on a local `Engine()` resolves to "
|
|
56
|
+
f"`{PROBE_PACKAGE}.core.Engine.run`"),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def write_probe(directory: str | Path) -> Path:
|
|
61
|
+
"""Write the probe package under ``directory`` and return its path."""
|
|
62
|
+
pkg = Path(directory) / PROBE_PACKAGE
|
|
63
|
+
pkg.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
for name, body in PROBE_FILES.items():
|
|
65
|
+
(pkg / name).write_text(body, encoding="utf-8")
|
|
66
|
+
return pkg
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _version(name: str) -> str | None:
|
|
70
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
71
|
+
try:
|
|
72
|
+
return version(name)
|
|
73
|
+
except PackageNotFoundError:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def holds(graph, dependency: str) -> bool:
|
|
78
|
+
"""Is the dependency's fact an edge of ``graph``?"""
|
|
79
|
+
etype, source, target, resolution, _ = EXPECTED[dependency]
|
|
80
|
+
return any(e.type == etype and e.source == source and e.target == target
|
|
81
|
+
and (resolution is None or e.extras.get("resolution") == resolution)
|
|
82
|
+
for e in graph.edges)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _expected_str(dependency: str) -> str:
|
|
86
|
+
etype, source, target, resolution, _ = EXPECTED[dependency]
|
|
87
|
+
return f"{etype} {source} → {target}" + (f" ({resolution})" if resolution else "")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _row(dependency: str, status: str, detail: str) -> dict:
|
|
91
|
+
return {"dependency": dependency, "version": _version(dependency), "status": status,
|
|
92
|
+
"fact": EXPECTED[dependency][4], "expected": _expected_str(dependency),
|
|
93
|
+
"detail": detail}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _importable(name: str) -> str | None:
|
|
97
|
+
"""``None`` when ``name`` imports; otherwise why it does not."""
|
|
98
|
+
try:
|
|
99
|
+
importlib.import_module(name)
|
|
100
|
+
except Exception as exc: # ImportError, or a broken install raising anything else
|
|
101
|
+
return f"not importable ({type(exc).__name__}: {exc})"
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _check(dependency: str, build) -> tuple[dict, bool]:
|
|
106
|
+
"""Run one probe. Returns the row and whether its build produced a graph at all."""
|
|
107
|
+
why = _importable(dependency)
|
|
108
|
+
if why:
|
|
109
|
+
return _row(dependency, ABSENT, why), False
|
|
110
|
+
try:
|
|
111
|
+
graph = build()
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
return _row(dependency, ERROR, f"the probe build raised {type(exc).__name__}: {exc}"), False
|
|
114
|
+
if holds(graph, dependency):
|
|
115
|
+
return _row(dependency, OK, "answered as expected"), True
|
|
116
|
+
return _row(dependency, WRONG, f"expected edge `{_expected_str(dependency)}` is not in "
|
|
117
|
+
"the graph — the dependency imports and answers, and the "
|
|
118
|
+
"answer is wrong"), True
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def run_probes() -> dict:
|
|
122
|
+
"""Build the probe twice (fast for griffe, deep for jedi) and grade each dependency.
|
|
123
|
+
|
|
124
|
+
jedi's probe stands on griffe's structure: when the fast build already failed, a deep
|
|
125
|
+
result would mean nothing, so jedi is ``not-run`` — unknown, not wrong.
|
|
126
|
+
"""
|
|
127
|
+
from codemap.extract import extract
|
|
128
|
+
with tempfile.TemporaryDirectory(prefix="codemap-probe-") as tmp:
|
|
129
|
+
pkg = write_probe(tmp)
|
|
130
|
+
griffe_row, built = _check("griffe", lambda: extract(str(pkg)))
|
|
131
|
+
if not built:
|
|
132
|
+
jedi_row = _row("jedi", NOT_RUN, "not run: the structural pass it stands on "
|
|
133
|
+
f"did not produce a graph ({griffe_row['status']})")
|
|
134
|
+
else:
|
|
135
|
+
jedi_row, _ = _check("jedi", lambda: extract(str(pkg), deep=True))
|
|
136
|
+
rows = [griffe_row, jedi_row]
|
|
137
|
+
return {"ok": all(r["status"] == OK for r in rows), "probes": rows}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def render(report: dict) -> str:
|
|
141
|
+
"""Plain text, one line per dependency, then a verdict line."""
|
|
142
|
+
lines = ["codemap doctor — does each dependency still answer a known question correctly?", ""]
|
|
143
|
+
for r in report["probes"]:
|
|
144
|
+
name = f"{r['dependency']} {r['version'] or '(version unknown)'}"
|
|
145
|
+
lines.append(f"{name:<22} {r['status'].upper():<8} {r['fact']}")
|
|
146
|
+
if r["status"] != OK:
|
|
147
|
+
lines.append(f"{'':<22} {'':<8} {r['detail']}")
|
|
148
|
+
lines.append("")
|
|
149
|
+
lines.append("✅ every probe answered as expected." if report["ok"] else
|
|
150
|
+
"❌ at least one dependency is absent or answers wrongly — a graph built "
|
|
151
|
+
"here may be poorer than its tier declares.")
|
|
152
|
+
return "\n".join(lines) + "\n"
|
|
@@ -93,7 +93,17 @@ SCHEMA_VERSION = "0.13"
|
|
|
93
93
|
#
|
|
94
94
|
# Starts at 1 with 0.0.21 and makes no claim about earlier releases: the answer format changed
|
|
95
95
|
# many times before it, and numbering that retroactively would be inventing a history.
|
|
96
|
-
|
|
96
|
+
#
|
|
97
|
+
# History:
|
|
98
|
+
# 1 — 0.0.21: first numbered format.
|
|
99
|
+
# 2 — pack honesty: `pack` carries `seeds`, `unreachable_from_seeds` and `coverage`
|
|
100
|
+
# blocks, and a seed set that resolves to nothing is a refusal instead of the
|
|
101
|
+
# global pack (docs/design/pack_honesty.md). Before 2 was released, a seeded
|
|
102
|
+
# pack also began walking both ways by default — what the seeds use and what
|
|
103
|
+
# uses them — with `coverage.walk` naming the walk (R1-C66,
|
|
104
|
+
# docs/design/pack_direction.md). Not a separate number: no release ever
|
|
105
|
+
# answered 2 without it.
|
|
106
|
+
ANSWER_FORMAT = 2
|
|
97
107
|
|
|
98
108
|
# Closed vocabulary of edge types (R1-C7). Node ``kind`` is deliberately an OPEN set
|
|
99
109
|
# (DESIGN §2 — new entity kinds may appear), but edges are TYPED: every relationship
|
|
@@ -269,13 +269,79 @@ def _samples_str(prov: dict) -> str:
|
|
|
269
269
|
return f"{runs} sample{'s' if runs != 1 else ''}"
|
|
270
270
|
|
|
271
271
|
|
|
272
|
-
|
|
272
|
+
# -- completeness of two builds of one input (R1-C59) -------------------------
|
|
273
|
+
|
|
274
|
+
#: A class of edges has *dropped* when it lost at least this many edges **and** at least
|
|
275
|
+
#: this share of its previous count. Above the measured noise, not tuned to a case: the
|
|
276
|
+
#: fast tier is byte-stable (any drop there is another builder), and the deep tier's
|
|
277
|
+
#: sampling noise was measured at single edges — ten builds of one tree differed by no
|
|
278
|
+
#: edge at all, a larger tree lost one real edge in 9524 once in seven builds (R1-C42).
|
|
279
|
+
#: The share keeps a large graph from flagging a handful of edges out of tens of thousands.
|
|
280
|
+
COMPLETENESS_MIN_EDGES = 3
|
|
281
|
+
COMPLETENESS_MIN_SHARE = 0.01
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def same_input(old: dict | None, new: dict | None) -> bool:
|
|
285
|
+
"""Do two provenance blocks describe one input, read on one tier? (R1-C59 / D2)
|
|
286
|
+
|
|
287
|
+
Needs a recorded ``scope_id`` on both sides: a graph without one (pre-0.12, or built
|
|
288
|
+
by ``extract()`` without a manifest) has an *unknown* input, which is not an equal one.
|
|
289
|
+
"""
|
|
290
|
+
if not old or not new or not old.get("scope_id"):
|
|
291
|
+
return False
|
|
292
|
+
return (old.get("scope_id") == new.get("scope_id")
|
|
293
|
+
and old.get("tier") == new.get("tier")
|
|
294
|
+
and old.get("roots") == new.get("roots"))
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def edge_counts(graph) -> dict[str, int]:
|
|
298
|
+
"""Edges per type — derived from the graph, never stored beside it (D1)."""
|
|
299
|
+
counts: dict[str, int] = {}
|
|
300
|
+
for e in graph.edges:
|
|
301
|
+
counts[e.type] = counts.get(e.type, 0) + 1
|
|
302
|
+
return counts
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def completeness_drops(old_graph, new_graph) -> list[dict] | None:
|
|
306
|
+
"""Edge classes the newer build of the **same input** holds fewer of (R1-C59).
|
|
307
|
+
|
|
308
|
+
``None`` when the pair is not comparable this way — another input, another tier, or an
|
|
309
|
+
input nobody recorded; that is "not compared", and it must stay distinguishable from
|
|
310
|
+
``[]``, "compared, nothing fell" (R1-C44). Measured on one frozen tree: codmap 0.0.3
|
|
311
|
+
and the current code read the same ``scope_id`` and differed by −25 ``imports``,
|
|
312
|
+
−28 ``references`` and −26 ``calls``, and nothing said so — ``diff`` named the other
|
|
313
|
+
tool, never what it had lost.
|
|
314
|
+
"""
|
|
315
|
+
if not same_input(old_graph.provenance, new_graph.provenance):
|
|
316
|
+
return None
|
|
317
|
+
before, after = edge_counts(old_graph), edge_counts(new_graph)
|
|
318
|
+
drops = []
|
|
319
|
+
for etype in sorted(before):
|
|
320
|
+
lost = before[etype] - after.get(etype, 0)
|
|
321
|
+
if lost >= COMPLETENESS_MIN_EDGES and lost >= COMPLETENESS_MIN_SHARE * before[etype]:
|
|
322
|
+
drops.append({"type": etype, "old": before[etype], "new": after.get(etype, 0)})
|
|
323
|
+
return drops
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def describe_drops(drops: list[dict]) -> str:
|
|
327
|
+
"""``completeness fell on the same input: calls 899 → 873 (−2.9 %), …``"""
|
|
328
|
+
parts = [f"{d['type']} {d['old']} → {d['new']} "
|
|
329
|
+
f"(−{(d['old'] - d['new']) * 100 / d['old']:.1f} %)" for d in drops]
|
|
330
|
+
return "completeness fell on the same input: " + ", ".join(parts)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def comparability(old: dict | None, new: dict | None, *,
|
|
334
|
+
old_graph=None, new_graph=None) -> dict:
|
|
273
335
|
"""Are two graphs a before/after of the *code*, or of the *tool*? (D4)
|
|
274
336
|
|
|
275
337
|
Never a refusal — comparing across an upgrade is a legitimate thing to want. What
|
|
276
338
|
was missing is being told: on the R1-C25 evidence pair, ``diff`` said "no breaking
|
|
277
339
|
changes" (true, and useless) about two graphs that disagreed on which functions
|
|
278
340
|
were dead.
|
|
341
|
+
|
|
342
|
+
With both graphs passed, a completeness drop on the same input is one more
|
|
343
|
+
difference (R1-C59): on one input, everything the two graphs disagree on came from
|
|
344
|
+
the builder, and a class that shrank is the part a reader most needs named.
|
|
279
345
|
"""
|
|
280
346
|
differences: list[str] = []
|
|
281
347
|
caveats: list[str] = []
|
|
@@ -304,6 +370,10 @@ def comparability(old: dict | None, new: dict | None) -> dict:
|
|
|
304
370
|
"builds of an unchanged tree can differ by a few jedi-resolved "
|
|
305
371
|
"edges, so read a small delta as possible tool noise "
|
|
306
372
|
f"(old: {_samples_str(old)}, new: {_samples_str(new)})")
|
|
373
|
+
if old_graph is not None and new_graph is not None:
|
|
374
|
+
drops = completeness_drops(old_graph, new_graph)
|
|
375
|
+
if drops:
|
|
376
|
+
differences.append(describe_drops(drops))
|
|
307
377
|
return {
|
|
308
378
|
"comparable": not differences,
|
|
309
379
|
"differences": differences,
|
|
@@ -74,6 +74,23 @@ _IMPACT_EDGES = ("calls", "references", "inherits", "imports", "decorated_by",
|
|
|
74
74
|
# is excluded on purpose (structural, would just inflate big modules).
|
|
75
75
|
_RANK_EDGE_TYPES = ("calls", "imports", "references", "inherits", "implements")
|
|
76
76
|
|
|
77
|
+
# Node kinds `rank` walks (R1-C6). Everything else — documents, attributes, columns — can
|
|
78
|
+
# never be ranked, so never enters a pack; `pack` declares that rather than implying a
|
|
79
|
+
# "most relevant slice" of the whole graph (pack honesty).
|
|
80
|
+
RANKED_KINDS = ("module", "class", "function")
|
|
81
|
+
|
|
82
|
+
#: Which way a *seeded* rank walks (R1-C66). ``uses``: from the seeds to what they use —
|
|
83
|
+
#: their callers are never reached. ``both``: that, and everything that uses the seeds.
|
|
84
|
+
#: The default for a seeded `pack` is ``both``, decided by a pre-registered rule
|
|
85
|
+
#: (research/pack_direction.md §5); ``uses`` stays available for "what does this need".
|
|
86
|
+
SEED_DIRECTIONS = ("uses", "both")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _check_direction(direction: str) -> None:
|
|
90
|
+
if direction not in SEED_DIRECTIONS:
|
|
91
|
+
raise ValueError(f"unknown direction {direction!r}; expected one of "
|
|
92
|
+
f"{', '.join(SEED_DIRECTIONS)}")
|
|
93
|
+
|
|
77
94
|
# Test-mapping walk (R1-C24). Execution and naming relations only: `imports` is
|
|
78
95
|
# module→module and would spread the answer across the package without saying anything
|
|
79
96
|
# about what a *test* runs; `inherits` is kept because exercising a subclass does exercise
|
|
@@ -1063,21 +1080,87 @@ class Query:
|
|
|
1063
1080
|
|
|
1064
1081
|
# -- relevance ranking (R1-C6) -------------------------------------------
|
|
1065
1082
|
|
|
1083
|
+
def _expand_seed(self, seed: str) -> tuple[str, set[str]]:
|
|
1084
|
+
"""One seed → ``(via, node ids)``: how it was read and what it matched.
|
|
1085
|
+
|
|
1086
|
+
``via`` is ``id`` | ``file`` | ``name`` | ``none``. The ids are *all* matches,
|
|
1087
|
+
rankable or not — whether they can bias a rank is the caller's question.
|
|
1088
|
+
"""
|
|
1089
|
+
if seed in self.graph.nodes:
|
|
1090
|
+
return "id", {seed}
|
|
1091
|
+
in_file = {n.id for n in self.graph.nodes.values() if n.file == seed}
|
|
1092
|
+
if in_file:
|
|
1093
|
+
return "file", in_file
|
|
1094
|
+
by_name = {n.id for n in self.find(seed)} # short name → matches
|
|
1095
|
+
return ("name", by_name) if by_name else ("none", set())
|
|
1096
|
+
|
|
1066
1097
|
def _expand_seeds(self, seeds) -> set[str]:
|
|
1067
1098
|
"""Map each seed (node id / short name / file path) to concrete node ids."""
|
|
1068
1099
|
out: set[str] = set()
|
|
1069
|
-
files = {n.file for n in self.graph.nodes.values() if n.file}
|
|
1070
1100
|
for s in seeds:
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1101
|
+
out |= self._expand_seed(s)[1]
|
|
1102
|
+
return out
|
|
1103
|
+
|
|
1104
|
+
def _rank_graph(self, edge_types=_RANK_EDGE_TYPES,
|
|
1105
|
+
root: str | None = None) -> "nx.DiGraph":
|
|
1106
|
+
"""The graph `rank` walks: rankable kinds only, usage edges user→used."""
|
|
1107
|
+
g = nx.DiGraph()
|
|
1108
|
+
for nid, n in self.graph.nodes.items():
|
|
1109
|
+
if n.kind in RANKED_KINDS and (root is None or self.root_of(nid) == root):
|
|
1110
|
+
g.add_node(nid)
|
|
1111
|
+
ets = set(edge_types)
|
|
1112
|
+
for e in self.graph.edges:
|
|
1113
|
+
if e.type in ets and e.source in g and e.target in g:
|
|
1114
|
+
g.add_edge(e.source, e.target)
|
|
1115
|
+
return g
|
|
1116
|
+
|
|
1117
|
+
def seed_resolution(self, seeds, *, root: str | None = None) -> dict:
|
|
1118
|
+
"""What each seed became — the answer to "did my seed count?" (pack honesty).
|
|
1119
|
+
|
|
1120
|
+
A seed that matched only nodes `rank` does not walk (a document, an attribute,
|
|
1121
|
+
a column) biases nothing, so it is **unresolved**, and ``unrankable`` says what
|
|
1122
|
+
it did match. Before this, all three shapes fell through to the global rank
|
|
1123
|
+
under a header naming the seed.
|
|
1124
|
+
"""
|
|
1125
|
+
from collections import Counter
|
|
1126
|
+
g = self._rank_graph(root=root)
|
|
1127
|
+
per, unresolved, ids = [], [], set()
|
|
1128
|
+
for s in seeds:
|
|
1129
|
+
via, matched = self._expand_seed(s)
|
|
1130
|
+
rankable = sorted(m for m in matched if m in g)
|
|
1131
|
+
other = Counter(self.graph.nodes[m].kind for m in matched if m not in g)
|
|
1132
|
+
per.append({"input": s, "via": via, "matched": rankable,
|
|
1133
|
+
"ambiguous": via == "name" and len(rankable) > 1,
|
|
1134
|
+
"unrankable": dict(sorted(other.items()))})
|
|
1135
|
+
if rankable:
|
|
1136
|
+
ids.update(rankable)
|
|
1075
1137
|
else:
|
|
1076
|
-
|
|
1138
|
+
unresolved.append(s)
|
|
1139
|
+
return {"seeds": per, "unresolved": unresolved, "ids": sorted(ids)}
|
|
1140
|
+
|
|
1141
|
+
def reachable_from(self, ids, *, root: str | None = None,
|
|
1142
|
+
direction: str = "uses") -> set[str]:
|
|
1143
|
+
"""Rankable nodes a seeded walk reaches from ``ids``.
|
|
1144
|
+
|
|
1145
|
+
``direction`` (R1-C66): ``uses`` — along usage edges, what the seeds use;
|
|
1146
|
+
``both`` — that, plus everything that uses the seeds, directly or transitively
|
|
1147
|
+
(ancestors ∪ descendants; a *sibling* that only shares a dependency is not
|
|
1148
|
+
reached). Exactly the set a seeded rank of that direction may surface. Decided by
|
|
1149
|
+
walking, never by ``rank == 0`` — ranks are rounded, and a far reachable node can
|
|
1150
|
+
round to zero.
|
|
1151
|
+
"""
|
|
1152
|
+
_check_direction(direction)
|
|
1153
|
+
g = self._rank_graph(root=root)
|
|
1154
|
+
seeds = [i for i in ids if i in g]
|
|
1155
|
+
out = set(seeds)
|
|
1156
|
+
for i in seeds:
|
|
1157
|
+
out |= nx.descendants(g, i)
|
|
1158
|
+
if direction == "both":
|
|
1159
|
+
out |= nx.ancestors(g, i)
|
|
1077
1160
|
return out
|
|
1078
1161
|
|
|
1079
1162
|
def rank(self, *, seeds=(), edge_types=_RANK_EDGE_TYPES,
|
|
1080
|
-
root: str | None = None) -> dict[str, float]:
|
|
1163
|
+
root: str | None = None, direction: str = "uses") -> dict[str, float]:
|
|
1081
1164
|
"""Importance rank over usage edges via PageRank (R1-C6).
|
|
1082
1165
|
|
|
1083
1166
|
Without ``seeds``: global importance — heavily-depended-upon symbols (imports
|
|
@@ -1086,16 +1169,15 @@ class Query:
|
|
|
1086
1169
|
context (aider's repo-map trick). ``root`` restricts to one provenance root.
|
|
1087
1170
|
Deterministic: PageRank is a fixed power-iteration; scores rounded, ties broken
|
|
1088
1171
|
by id at the call sites that order them.
|
|
1172
|
+
|
|
1173
|
+
``direction`` applies only with seeds. ``both`` (R1-C66) walks the usage edges in
|
|
1174
|
+
both directions — the restart is still the seeds, so the rank is closeness to them
|
|
1175
|
+
whichever way the edge points. Measured against git history on two repositories
|
|
1176
|
+
(``research/pack_direction.md``): recall@10 of the co-changed files 0.19 → 0.42
|
|
1177
|
+
and 0.20 → 0.60.
|
|
1089
1178
|
"""
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
if n.kind in ("module", "class", "function") and (
|
|
1093
|
-
root is None or self.root_of(nid) == root):
|
|
1094
|
-
g.add_node(nid)
|
|
1095
|
-
ets = set(edge_types)
|
|
1096
|
-
for e in self.graph.edges:
|
|
1097
|
-
if e.type in ets and e.source in g and e.target in g:
|
|
1098
|
-
g.add_edge(e.source, e.target)
|
|
1179
|
+
_check_direction(direction)
|
|
1180
|
+
g = self._rank_graph(edge_types, root)
|
|
1099
1181
|
if g.number_of_nodes() == 0:
|
|
1100
1182
|
return {}
|
|
1101
1183
|
personalization = None
|
|
@@ -1103,6 +1185,9 @@ class Query:
|
|
|
1103
1185
|
seed_ids = {s for s in self._expand_seeds(seeds) if s in g}
|
|
1104
1186
|
if seed_ids: # restart biased to seeds (normalized inside _pagerank)
|
|
1105
1187
|
personalization = {n: (1.0 if n in seed_ids else 0.0) for n in g}
|
|
1188
|
+
if direction == "both":
|
|
1189
|
+
g = nx.DiGraph(g)
|
|
1190
|
+
g.add_edges_from([(b, a) for a, b in g.edges])
|
|
1106
1191
|
pr = _pagerank(g, personalization)
|
|
1107
1192
|
return {n: round(v, 8) for n, v in pr.items()}
|
|
1108
1193
|
|