codmap 0.0.20__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.20/codmap.egg-info → codmap-0.0.22}/PKG-INFO +17 -4
- {codmap-0.0.20 → codmap-0.0.22}/README.md +16 -3
- {codmap-0.0.20 → codmap-0.0.22}/codemap/arch.py +58 -1
- {codmap-0.0.20 → codmap-0.0.22}/codemap/cli.py +97 -17
- codmap-0.0.22/codemap/extract/probe.py +152 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/model.py +28 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/provenance.py +71 -1
- {codmap-0.0.20 → codmap-0.0.22}/codemap/query.py +101 -16
- {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/apidiff.py +4 -1
- {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/audit.py +8 -3
- {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/check.py +54 -6
- {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/mcp_server.py +17 -6
- codmap-0.0.22/codemap/serve/pack.py +224 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/session.py +9 -3
- {codmap-0.0.20 → codmap-0.0.22/codmap.egg-info}/PKG-INFO +17 -4
- {codmap-0.0.20 → codmap-0.0.22}/codmap.egg-info/SOURCES.txt +8 -0
- {codmap-0.0.20 → 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.22/tests/test_r1c62_phantom_rules.py +167 -0
- codmap-0.0.22/tests/test_r1c63_answer_format.py +150 -0
- 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.20/codemap/serve/pack.py +0 -93
- {codmap-0.0.20 → codmap-0.0.22}/LICENSE +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/__init__.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/apidiff.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/diagnostics.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/extract/__init__.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/extract/attrflow.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/extract/behavior.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/extract/dataflow.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/extract/dispatch.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/extract/griffe_extractor.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/extract/gsource.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/extract/roots.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/extract/union.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/freshness.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/incremental.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/integrations/__init__.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/integrations/base.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/integrations/cocoindex.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/integrations/gate.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/integrations/gitnexus.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/integrations/registry.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/integrations/transport.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/scope.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/__init__.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/_scip_pb2.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/api_surface.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/architecture.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/ctags.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/impact.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/limits.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/livingdocs.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/mermaid.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/rag.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/review.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/scip.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/semantic.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/server.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/subsystems.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/vault.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/store.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/tomlio.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codemap/watch.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codmap.egg-info/dependency_links.txt +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codmap.egg-info/entry_points.txt +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codmap.egg-info/requires.txt +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/codmap.egg-info/top_level.txt +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/setup.cfg +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_epistemic.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_gitnexus_router.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_impact_depth.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_integrations.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_issue3_serve_freshness.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_livingdocs.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_m0_api_surface.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_m11_argcontract.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_m12_dataflow.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_m15_review.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_m16_architecture.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_m17_mcp.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_m18_freshness.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_m1_5_semantics.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_m1_query.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_m2_views.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_m32_watch.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_m3_serve.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_m4_behavior.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_m5_deep.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_m6_repo_scope.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_m7_dispatch.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_m9_family.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c13_callgraph_accuracy.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c13_grep_vs_graph.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c16_semantic.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c20_attribute_edges.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c21_flat_layout.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c22_source_visible_refs.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c23_hard_python.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c24_test_mapping.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c25_provenance.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c26_deep_union.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c27_config_honesty.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c28_limit_envelope.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c29_lazy_imports.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c2_ctags.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c30_local_import_calls.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c30f1_reexport_calls.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c30f2_check_scope.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c31_path_origin.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c32_report_json.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c33_query_signature.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c34_signature_kinds.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c35_contract_location.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c36_target_resolution.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c37_rule_mutation.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c38_tool_drift.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c39_edge_resolution.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c3_arch_contract.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c3_dogfood.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c40_flow_reach.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c41_scope_membership.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c42_deep_tier_stability.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c43_incremental_splice.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c44_absent_answers.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c45_repeat_union.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c46_shadowed_definitions.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c47_incremental_resample.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c48_type_checking_imports.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c49_type_only_cycles.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c4_complexity.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c50_flow_entry_points.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c53_narrowing_declared.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c54_deterministic_rendering.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c55_override_grade.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c56_stub_files.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c57_facade_package.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c58_cycle_tangles.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c5_apidiff.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c6_pack.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c7_edge_vocab.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c8_deadcode.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c9_incremental.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_scip_export.py +0 -0
- {codmap-0.0.20 → codmap-0.0.22}/tests/test_scope.py +0 -0
- {codmap-0.0.20 → 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
|
|
@@ -57,6 +57,11 @@ class ArchitectureContract:
|
|
|
57
57
|
no_lazy_cycles: bool = False
|
|
58
58
|
no_type_only_cycles: bool = False
|
|
59
59
|
exhaustive: bool = False
|
|
60
|
+
# R1-C62: the mirror of `exhaustive`. That one asks "is every layer of the code
|
|
61
|
+
# declared?"; this asks "does every declared name exist?". Opt-in, because a contract
|
|
62
|
+
# written ahead of the code is a documented, legitimate use (see the module docstring)
|
|
63
|
+
# and failing on it by default would turn that practice red on somebody else's tree.
|
|
64
|
+
no_phantom_rules: bool = False
|
|
60
65
|
error: str | None = None
|
|
61
66
|
# R1-C35: the file this contract was looked for in. "No contract found" is only
|
|
62
67
|
# actionable next to *where* we looked — a reader in the wrong directory cannot tell
|
|
@@ -66,7 +71,8 @@ class ArchitectureContract:
|
|
|
66
71
|
def is_empty(self) -> bool:
|
|
67
72
|
return not (self.layers or self.independent or self.forbidden
|
|
68
73
|
or self.no_cycles or self.no_lazy_cycles
|
|
69
|
-
or self.no_type_only_cycles or self.exhaustive
|
|
74
|
+
or self.no_type_only_cycles or self.exhaustive
|
|
75
|
+
or self.no_phantom_rules)
|
|
70
76
|
|
|
71
77
|
|
|
72
78
|
@dataclass(frozen=True)
|
|
@@ -117,9 +123,46 @@ def parse_contract(section: dict) -> ArchitectureContract:
|
|
|
117
123
|
no_lazy_cycles=bool(section.get("no_lazy_cycles", False)),
|
|
118
124
|
no_type_only_cycles=bool(section.get("no_type_only_cycles", False)),
|
|
119
125
|
exhaustive=bool(section.get("exhaustive", False)),
|
|
126
|
+
no_phantom_rules=bool(section.get("no_phantom_rules", False)),
|
|
120
127
|
)
|
|
121
128
|
|
|
122
129
|
|
|
130
|
+
def _graph_layers(query) -> set[str]:
|
|
131
|
+
"""Layer names that exist in the core import graph."""
|
|
132
|
+
return {query._layer_of(m) for m in query.import_graph.nodes
|
|
133
|
+
if query.root_of(m) == "core"}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def applicability(query, contract: ArchitectureContract) -> dict:
|
|
137
|
+
"""Per-rule *declared* vs *applicable* counts, plus the names that do not exist (R1-C62).
|
|
138
|
+
|
|
139
|
+
A rule naming a layer the graph does not contain is **inert by design** — no module, no
|
|
140
|
+
edge, nothing to break — which is what lets a contract be written ahead of the code. The
|
|
141
|
+
defect that made this function necessary is not the inertness: it is that the gate
|
|
142
|
+
counted such a rule as *enforced*, so a renamed layer left a green tick over a rule that
|
|
143
|
+
could no longer fire.
|
|
144
|
+
|
|
145
|
+
Applicability is presence of the **name**, nothing cleverer: `layers` needs the layer,
|
|
146
|
+
`independent` needs at least two members of the group (one cannot import itself), and
|
|
147
|
+
`forbidden` needs both ends. Whether a violation is even possible between two existing
|
|
148
|
+
layers is not asked, and should not be.
|
|
149
|
+
"""
|
|
150
|
+
present = _graph_layers(query)
|
|
151
|
+
named = set(contract.layers) | {m for grp in contract.independent for m in grp}
|
|
152
|
+
named |= {n for pair in contract.forbidden for n in pair}
|
|
153
|
+
return {
|
|
154
|
+
"layers": {"declared": len(contract.layers),
|
|
155
|
+
"applicable": sum(1 for x in contract.layers if x in present)},
|
|
156
|
+
"independent": {"declared": len(contract.independent),
|
|
157
|
+
"applicable": sum(1 for g in contract.independent
|
|
158
|
+
if len([x for x in g if x in present]) >= 2)},
|
|
159
|
+
"forbidden": {"declared": len(contract.forbidden),
|
|
160
|
+
"applicable": sum(1 for (a, b) in contract.forbidden
|
|
161
|
+
if a in present and b in present)},
|
|
162
|
+
"absent_names": tuple(sorted(n for n in named if n not in present)),
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
123
166
|
def _core_layer_edges(query) -> list[tuple[str, str, str, str]]:
|
|
124
167
|
"""Cross-layer core→core import edges as (importer, imported, layer_i, layer_j)."""
|
|
125
168
|
ig = query.import_graph
|
|
@@ -266,4 +309,18 @@ def check_contract(query, contract: ArchitectureContract) -> list[Violation]:
|
|
|
266
309
|
modules=tuple(undeclared),
|
|
267
310
|
))
|
|
268
311
|
|
|
312
|
+
# -- no_phantom_rules: every name the contract mentions must exist -----------
|
|
313
|
+
# R1-C62, the mirror of `exhaustive`. Off by default on purpose: inert rules are how a
|
|
314
|
+
# contract gets written ahead of the code. On, it catches the drift the count alone
|
|
315
|
+
# only *discloses* — a layer renamed while the contract kept guarding the old name.
|
|
316
|
+
if contract.no_phantom_rules:
|
|
317
|
+
absent = applicability(query, contract)["absent_names"]
|
|
318
|
+
if absent:
|
|
319
|
+
violations.append(Violation(
|
|
320
|
+
"no_phantom_rules",
|
|
321
|
+
f"{len(absent)} name(s) in the contract are absent from the graph, "
|
|
322
|
+
"so the rules naming them cannot apply",
|
|
323
|
+
modules=absent,
|
|
324
|
+
))
|
|
325
|
+
|
|
269
326
|
return violations
|
|
@@ -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
|
|
|
@@ -73,6 +75,23 @@ _REPORTS = {
|
|
|
73
75
|
_REPORT_KINDS = sorted(_REPORTS) + ["impact", "communities", "flows"] # extra args
|
|
74
76
|
|
|
75
77
|
|
|
78
|
+
def _emit_answer(text: str) -> None:
|
|
79
|
+
"""Print a markdown answer with the answer-format trailer (R1-C63).
|
|
80
|
+
|
|
81
|
+
One seam rather than a line in every renderer: the renderers stay pure functions of the
|
|
82
|
+
graph, and there is one place to forget rather than nine. The trailer is what a consumer
|
|
83
|
+
diffing our output sees when the *shape* of an answer changes deliberately — 0.0.20
|
|
84
|
+
changed the gate's lines while the graph stayed byte-identical, and on a text diff that
|
|
85
|
+
looked exactly like the #20 defect, where the text moved between runs of one version. The
|
|
86
|
+
only thing that told them apart was a message written by hand.
|
|
87
|
+
"""
|
|
88
|
+
from codemap.model import ANSWER_FORMAT, SCHEMA_VERSION
|
|
89
|
+
print(text, end="")
|
|
90
|
+
if not text.endswith("\n"):
|
|
91
|
+
print()
|
|
92
|
+
print(f"_answer format {ANSWER_FORMAT} · schema {SCHEMA_VERSION}_")
|
|
93
|
+
|
|
94
|
+
|
|
76
95
|
def _graph_from(args):
|
|
77
96
|
if getattr(args, "build", None):
|
|
78
97
|
return extract(args.build, deep=getattr(args, "deep", False))
|
|
@@ -138,6 +157,7 @@ def _cmd_build(args) -> int:
|
|
|
138
157
|
text = " ".join(p for p in (d["message"], d.get("consequence")) if p)
|
|
139
158
|
print(f"[{d.get('severity', 'warning')}] {text}", file=sys.stderr)
|
|
140
159
|
if args.out:
|
|
160
|
+
_report_completeness_drop(args.out, graph)
|
|
141
161
|
store.save(graph, args.out)
|
|
142
162
|
# M18: record the build recipe beside the graph so `codemap refresh` can
|
|
143
163
|
# rebuild it, and so the graph's age is meaningful to `serve`/stats.
|
|
@@ -158,6 +178,36 @@ def _cmd_build(args) -> int:
|
|
|
158
178
|
return 0
|
|
159
179
|
|
|
160
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
|
+
|
|
161
211
|
def _inputs_with_membership(graph, scope, args):
|
|
162
212
|
"""The extractor's ``inputs`` block plus the manifest membership result (R1-C41).
|
|
163
213
|
|
|
@@ -316,6 +366,22 @@ def _cmd_watch(args) -> int:
|
|
|
316
366
|
return 0
|
|
317
367
|
|
|
318
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
|
+
|
|
319
385
|
def _cmd_scope(args) -> int:
|
|
320
386
|
"""Resolve/print the input scope manifest, or diff two (M19.A)."""
|
|
321
387
|
from codemap.scope import resolve_scope, diff_scopes
|
|
@@ -507,28 +573,28 @@ def _cmd_report(args) -> int:
|
|
|
507
573
|
if args.kind == "impact":
|
|
508
574
|
if not args.symbol:
|
|
509
575
|
raise SystemExit("error: report impact needs --symbol <name>")
|
|
510
|
-
|
|
511
|
-
|
|
576
|
+
_emit_answer(render_impact(Query(graph), args.symbol, depth=args.depth,
|
|
577
|
+
flow_depth=args.flow_depth))
|
|
512
578
|
return 0
|
|
513
579
|
if args.kind in ("communities", "flows"):
|
|
514
580
|
from codemap.serve.subsystems import render_communities, render_flows
|
|
515
581
|
q = Query(graph)
|
|
516
582
|
out = (render_communities(q) if args.kind == "communities"
|
|
517
583
|
else render_flows(q, args.symbol, depth=args.depth))
|
|
518
|
-
|
|
584
|
+
_emit_answer(out)
|
|
519
585
|
return 0
|
|
520
586
|
if args.kind == "dead-code":
|
|
521
587
|
from codemap.serve.audit import load_dead_code_whitelist
|
|
522
588
|
root = getattr(args, "source_root", None) or os.getcwd()
|
|
523
589
|
whitelist, wl_error = load_dead_code_whitelist(root)
|
|
524
|
-
|
|
590
|
+
_emit_answer(render_dead_code(Query(graph),
|
|
525
591
|
whitelist=whitelist,
|
|
526
592
|
min_confidence=args.min_confidence,
|
|
527
|
-
whitelist_error=wl_error)
|
|
593
|
+
whitelist_error=wl_error))
|
|
528
594
|
return 0
|
|
529
595
|
renderer = _REPORTS[args.kind]
|
|
530
596
|
payload = renderer(graph) if args.kind == "api-surface" else renderer(Query(graph))
|
|
531
|
-
|
|
597
|
+
_emit_answer(payload)
|
|
532
598
|
return 0
|
|
533
599
|
|
|
534
600
|
|
|
@@ -724,15 +790,21 @@ def _cmd_semantic(args) -> int:
|
|
|
724
790
|
|
|
725
791
|
def _cmd_pack(args) -> int:
|
|
726
792
|
"""Token-budgeted context pack — most relevant graph slice under N tokens (R1-C6)."""
|
|
727
|
-
from codemap.serve.pack import build_pack,
|
|
728
|
-
q = Query(_graph_from(args))
|
|
793
|
+
from codemap.serve.pack import build_pack, render_payload
|
|
729
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)
|
|
730
801
|
if args.format == "json":
|
|
731
|
-
print(json.dumps(
|
|
732
|
-
ensure_ascii=False, indent=2, sort_keys=True))
|
|
802
|
+
print(json.dumps(p, ensure_ascii=False, indent=2, sort_keys=True))
|
|
733
803
|
else:
|
|
734
|
-
print(
|
|
735
|
-
|
|
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
|
|
736
808
|
|
|
737
809
|
|
|
738
810
|
def _cmd_check(args) -> int:
|
|
@@ -755,13 +827,13 @@ def _cmd_check(args) -> int:
|
|
|
755
827
|
# would sort an unreadable contract into the success branch of every `if rc == 2` that
|
|
756
828
|
# already exists in someone's pipeline.
|
|
757
829
|
if contract.error:
|
|
758
|
-
|
|
830
|
+
_emit_answer(render_check(q, contract, []))
|
|
759
831
|
return 2
|
|
760
832
|
if contract.is_empty() and args.require_contract:
|
|
761
|
-
|
|
833
|
+
_emit_answer(render_check(q, contract, []))
|
|
762
834
|
raise SystemExit("error: no [architecture] contract found (--require-contract)")
|
|
763
835
|
violations = check_contract(q, contract)
|
|
764
|
-
|
|
836
|
+
_emit_answer(render_check(q, contract, violations))
|
|
765
837
|
return 2 if violations else 0
|
|
766
838
|
|
|
767
839
|
|
|
@@ -778,7 +850,7 @@ def _cmd_diff(args) -> int:
|
|
|
778
850
|
# R1-C25/D4: two graphs built by different tools are a before/after of the *tool*,
|
|
779
851
|
# not of the code. Never a refusal — comparing across an upgrade is legitimate; what
|
|
780
852
|
# was missing is being told, since a clean "no breaking changes" reads as proof.
|
|
781
|
-
cmp = comparability(old.provenance, new.provenance)
|
|
853
|
+
cmp = comparability(old.provenance, new.provenance, old_graph=old, new_graph=new)
|
|
782
854
|
if not cmp["comparable"]:
|
|
783
855
|
for line in cmp["differences"]:
|
|
784
856
|
print(f"[warning] {line}", file=sys.stderr)
|
|
@@ -930,6 +1002,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
930
1002
|
"of the chain: a different N restarts it from a full build.")
|
|
931
1003
|
b.set_defaults(func=_cmd_build)
|
|
932
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
|
+
|
|
933
1010
|
sc = sub.add_parser("scope", help="Resolve the input scope manifest (scope_id + profile), or --diff two.")
|
|
934
1011
|
sc.add_argument("path", nargs="?", help="Package/dir to scope (like build's path).")
|
|
935
1012
|
sc.add_argument("--consumer", action="append", metavar="PATH",
|
|
@@ -1077,6 +1154,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1077
1154
|
pk.add_argument("--seed", action="append", metavar="X",
|
|
1078
1155
|
help="Bias relevance to this symbol / file (repeatable). "
|
|
1079
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.")
|
|
1080
1160
|
pk.add_argument("--format", choices=["markdown", "json"], default="markdown")
|
|
1081
1161
|
pk.set_defaults(func=_cmd_pack)
|
|
1082
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"
|
|
@@ -77,6 +77,34 @@ from typing import Any
|
|
|
77
77
|
# (they had ``lineno`` alone, so `search` answered a line number with no file).
|
|
78
78
|
SCHEMA_VERSION = "0.13"
|
|
79
79
|
|
|
80
|
+
# R1-C63: the *answer* has a shape too, and consumers diff it. `SCHEMA_VERSION` versions the
|
|
81
|
+
# artifact; 0.0.20 left the graph byte-identical and changed the architecture report and the
|
|
82
|
+
# gate's lines completely (a cycle answer became a tangle), which on a text diff is
|
|
83
|
+
# indistinguishable from the #20 defect — where the text moved between runs of one version,
|
|
84
|
+
# without anyone deciding it. The only thing that told the consumer apart was a message I
|
|
85
|
+
# wrote by hand.
|
|
86
|
+
#
|
|
87
|
+
# Bump on a change to the **structure a consumer sees**: a block appears or vanishes, a
|
|
88
|
+
# counted unit changes meaning, a verdict or heading changes shape, a field in a structured
|
|
89
|
+
# answer changes type or semantics. Do **not** bump for wording, typos or added prose inside
|
|
90
|
+
# an existing note — a version that ticks on whitespace stops being read, which is worse than
|
|
91
|
+
# not having one. Independent of SCHEMA_VERSION by design: the graph can be identical while
|
|
92
|
+
# this moves, and that is the case it exists for.
|
|
93
|
+
#
|
|
94
|
+
# Starts at 1 with 0.0.21 and makes no claim about earlier releases: the answer format changed
|
|
95
|
+
# many times before it, and numbering that retroactively would be inventing a history.
|
|
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
|
|
107
|
+
|
|
80
108
|
# Closed vocabulary of edge types (R1-C7). Node ``kind`` is deliberately an OPEN set
|
|
81
109
|
# (DESIGN §2 — new entity kinds may appear), but edges are TYPED: every relationship
|
|
82
110
|
# codemap emits is one of these, each with a fixed meaning. This is the machine-
|