codmap 0.0.3__tar.gz → 0.0.5__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.3/codmap.egg-info → codmap-0.0.5}/PKG-INFO +91 -10
- {codmap-0.0.3 → codmap-0.0.5}/README.md +90 -9
- codmap-0.0.5/codemap/__init__.py +17 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/apidiff.py +15 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/arch.py +48 -8
- {codmap-0.0.3 → codmap-0.0.5}/codemap/cli.py +321 -5
- {codmap-0.0.3 → codmap-0.0.5}/codemap/extract/behavior.py +161 -20
- {codmap-0.0.3 → codmap-0.0.5}/codemap/extract/griffe_extractor.py +174 -58
- {codmap-0.0.3 → codmap-0.0.5}/codemap/extract/gsource.py +24 -7
- {codmap-0.0.3 → codmap-0.0.5}/codemap/extract/roots.py +69 -15
- {codmap-0.0.3 → codmap-0.0.5}/codemap/freshness.py +8 -1
- {codmap-0.0.3 → codmap-0.0.5}/codemap/model.py +11 -1
- {codmap-0.0.3 → codmap-0.0.5}/codemap/provenance.py +8 -2
- {codmap-0.0.3 → codmap-0.0.5}/codemap/query.py +52 -2
- {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/api_surface.py +37 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/architecture.py +34 -2
- {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/audit.py +86 -2
- codmap-0.0.5/codemap/serve/check.py +141 -0
- codmap-0.0.5/codemap/serve/limits.py +64 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/livingdocs.py +7 -1
- {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/mcp_server.py +48 -18
- {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/semantic.py +23 -2
- {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/session.py +88 -12
- codmap-0.0.5/codemap/watch.py +208 -0
- {codmap-0.0.3 → codmap-0.0.5/codmap.egg-info}/PKG-INFO +91 -10
- {codmap-0.0.3 → codmap-0.0.5}/codmap.egg-info/SOURCES.txt +14 -0
- {codmap-0.0.3 → codmap-0.0.5}/pyproject.toml +1 -1
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_m16_architecture.py +5 -1
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_m17_mcp.py +4 -1
- codmap-0.0.5/tests/test_m32_watch.py +401 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c16_semantic.py +11 -3
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c25_provenance.py +39 -2
- codmap-0.0.5/tests/test_r1c28_limit_envelope.py +285 -0
- codmap-0.0.5/tests/test_r1c29_lazy_imports.py +227 -0
- codmap-0.0.5/tests/test_r1c30_local_import_calls.py +229 -0
- codmap-0.0.5/tests/test_r1c30f1_reexport_calls.py +176 -0
- codmap-0.0.5/tests/test_r1c30f2_check_scope.py +145 -0
- codmap-0.0.5/tests/test_r1c31_path_origin.py +155 -0
- codmap-0.0.5/tests/test_r1c32_report_json.py +129 -0
- codmap-0.0.5/tests/test_r1c33_query_signature.py +189 -0
- codmap-0.0.5/tests/test_r1c34_signature_kinds.py +150 -0
- codmap-0.0.5/tests/test_r1c35_contract_location.py +101 -0
- codmap-0.0.5/tests/test_r1c36_target_resolution.py +107 -0
- codmap-0.0.3/codemap/__init__.py +0 -10
- codmap-0.0.3/codemap/serve/check.py +0 -80
- {codmap-0.0.3 → codmap-0.0.5}/LICENSE +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/diagnostics.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/extract/__init__.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/extract/attrflow.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/extract/dataflow.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/extract/dispatch.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/incremental.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/integrations/__init__.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/integrations/base.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/integrations/cocoindex.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/integrations/gate.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/integrations/gitnexus.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/integrations/registry.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/integrations/transport.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/scope.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/__init__.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/_scip_pb2.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/apidiff.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/ctags.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/impact.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/mermaid.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/pack.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/rag.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/review.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/scip.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/server.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/subsystems.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/vault.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/store.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codemap/tomlio.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codmap.egg-info/dependency_links.txt +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codmap.egg-info/entry_points.txt +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codmap.egg-info/requires.txt +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/codmap.egg-info/top_level.txt +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/setup.cfg +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_epistemic.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_gitnexus_router.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_impact_depth.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_integrations.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_issue3_serve_freshness.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_livingdocs.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_m0_api_surface.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_m11_argcontract.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_m12_dataflow.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_m15_review.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_m18_freshness.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_m1_5_semantics.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_m1_query.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_m2_views.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_m3_serve.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_m4_behavior.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_m5_deep.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_m6_repo_scope.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_m7_dispatch.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_m9_family.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c13_callgraph_accuracy.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c13_grep_vs_graph.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c20_attribute_edges.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c21_flat_layout.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c22_source_visible_refs.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c23_hard_python.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c24_test_mapping.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c26_deep_union.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c27_config_honesty.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c2_ctags.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c3_arch_contract.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c3_dogfood.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c4_complexity.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c5_apidiff.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c6_pack.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c7_edge_vocab.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c8_deadcode.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c9_incremental.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_scip_export.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/tests/test_scope.py +0 -0
- {codmap-0.0.3 → codmap-0.0.5}/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.5
|
|
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.
|
|
47
|
+
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **655 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 &
|
|
@@ -61,6 +61,73 @@ Design principles: **source-only** (static `ast`/`griffe`, never imports the tar
|
|
|
61
61
|
(canonical sorted JSON, no timestamps — diffable), **CLI-AI-first** (JSON by default, stable exit
|
|
62
62
|
codes), **honest** (approximations are labeled, not hidden).
|
|
63
63
|
|
|
64
|
+
## Point questions and whole-graph questions
|
|
65
|
+
|
|
66
|
+
Most code-intelligence tools answer **point questions** — you name a symbol and they walk outward a
|
|
67
|
+
few steps. *Where is this defined? Who calls it? What breaks if I change it?* The answer's cost
|
|
68
|
+
scales with the neighbourhood, not the repository, which is why an index or a vector store can serve
|
|
69
|
+
it. The field is good at this, and several tools are faster at it than codemap.
|
|
70
|
+
|
|
71
|
+
**Whole-graph questions** have no starting symbol, because the property being asked about belongs to
|
|
72
|
+
the graph and to no node in it:
|
|
73
|
+
|
|
74
|
+
> Is there a dependency cycle anywhere? · Which module is most expensive to change? · Does the code
|
|
75
|
+
> still respect the layering I intended? · Where has behaviour concentrated into one class?
|
|
76
|
+
|
|
77
|
+
A cycle is invisible from inside every file that participates in it — each one looks perfectly
|
|
78
|
+
reasonable alone. You cannot seed the question, and there is no partial answer.
|
|
79
|
+
`codemap report architecture` computes all of them in one pass; on the dogfood target (88 modules,
|
|
80
|
+
**715 import edges**) that is:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
layers core 9 · data 12 · indicators 16 · analysis 42 · visualization 7 · cli 1
|
|
84
|
+
analysis → core 38 edges · indicators → core 22 · data → core 13
|
|
85
|
+
violation ⚠ analysis ↔ core — one backward edge, written inside a function
|
|
86
|
+
cycle pipeline → cache → pipeline — the classic Python import-order landmine
|
|
87
|
+
lazy cycles 40 more, closed only by a function-local import — not import-time failures,
|
|
88
|
+
still mutual coupling: neither module can be extracted without the other
|
|
89
|
+
coupling core.logging_config Ca 96 — a breaking change here reaches 96 modules
|
|
90
|
+
concentration ZoneVisualizer 35 methods, worst function CC 66 / MI 12.5
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
That single layer violation is worth its line: it is reached only through an import written *inside a
|
|
94
|
+
function*, so until [R1-C29](gaps/import_map_module_level_2026-08-28.md) codemap could not see it and
|
|
95
|
+
reported the architecture as clean. A gate you can walk around by making the import lazy is not a gate.
|
|
96
|
+
|
|
97
|
+
Then [`codemap check`](docs/architecture-contracts.md) turns the shape you *want* into a CI gate, so
|
|
98
|
+
description and intent cannot drift apart.
|
|
99
|
+
|
|
100
|
+
Four rival tools have now been measured hands-on on the same benchmark scope — three of them on
|
|
101
|
+
byte-identical input verified by content hash, the fourth (graphlens) on a near-identical staging that
|
|
102
|
+
predates the harness — including the field's most-adopted tool at 68k stars. **All four answer the point
|
|
103
|
+
questions. None answers these on a surface a caller can reach** — nor the related "how does each caller
|
|
104
|
+
actually *pass* its arguments" (`call_contract`) — because a slice of source is the wrong shape for the
|
|
105
|
+
answer.
|
|
106
|
+
|
|
107
|
+
That sentence used to end at "None answers these", and a second pass over the 68k-star tool made it
|
|
108
|
+
narrower: its *importable library* does carry a cycle finder, reachable from neither its CLI nor its MCP
|
|
109
|
+
tools and called nowhere in its own source. Measured on the same tree it reports **136 cycles**; codemap
|
|
110
|
+
reports **1**. Scoring both against the truth set — every intra-package import, function-local ones
|
|
111
|
+
included — is less flattering than that sounds:
|
|
112
|
+
|
|
113
|
+
| | reported | of the 41 real ones | precision | recall |
|
|
114
|
+
|---|---:|---:|---:|---:|
|
|
115
|
+
| codemap, that morning | 1 | 1 | 100% | **2.4%** |
|
|
116
|
+
| the peer's library API | 136 | 13 | 10% | 32% |
|
|
117
|
+
| codemap, after R1-C29 | 41 | 41 | 100% | 100% |
|
|
118
|
+
|
|
119
|
+
Theirs over-reports because it walks name-resolved call edges — a `dict.get` becomes a call into an
|
|
120
|
+
unrelated class. **Ours under-reported because its import map was module-level only**, and it phrased
|
|
121
|
+
that as *"import graph is acyclic"* — a property claim over a partial map, which is the worse of the two
|
|
122
|
+
errors even though it was the smaller one. Both halves of that came from
|
|
123
|
+
[#11](https://github.com/kogriv/codemap/issues/11), filed off a different target and fixed the same day
|
|
124
|
+
([R1-C29](gaps/import_map_module_level_2026-08-28.md)). The lesson is sharper than the win: a
|
|
125
|
+
whole-graph answer inherits every flaw of the graph it is computed from, **including the edges that
|
|
126
|
+
graph never read** — and the tool cannot be the judge of its own recall.
|
|
127
|
+
|
|
128
|
+
**→ [docs/whole-graph-questions.md](docs/whole-graph-questions.md)** — the full argument, every number
|
|
129
|
+
above reproduced from one run, and the honest limits.
|
|
130
|
+
|
|
64
131
|
## Install
|
|
65
132
|
|
|
66
133
|
**Python 3.11+** — the range is measured on 3.11–3.14 in CI, not assumed ([docs/ci.md](docs/ci.md)).
|
|
@@ -79,11 +146,7 @@ pip install 'codmap[mcp]'
|
|
|
79
146
|
pip install 'codmap[scip]'
|
|
80
147
|
```
|
|
81
148
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
pip install git+https://github.com/kogriv/codemap
|
|
86
|
-
```
|
|
149
|
+
Or straight from source: `pip install git+https://github.com/kogriv/codemap`.
|
|
87
150
|
|
|
88
151
|
Working on codemap itself, from a clone:
|
|
89
152
|
|
|
@@ -111,6 +174,7 @@ codemap report architecture --graph graph.json # layers, coupling, god-objec
|
|
|
111
174
|
codemap report dependencies --graph graph.json
|
|
112
175
|
codemap report dead-code --graph graph.json
|
|
113
176
|
codemap report impact --symbol MyClass --graph graph.json
|
|
177
|
+
codemap report api-surface --graph graph.json --format json # every kind has a json form
|
|
114
178
|
|
|
115
179
|
# change-set review straight from a diff → risk-sorted dossier
|
|
116
180
|
git diff | codemap review - --graph graph.json
|
|
@@ -128,11 +192,15 @@ codemap semantic "detect swing pivots" --build ./pkg --root pkg
|
|
|
128
192
|
# token-budgeted context pack — most relevant graph slice under N tokens (ranked; --seed to focus)
|
|
129
193
|
codemap pack --graph graph.json --budget 2000 --seed analyze_zones
|
|
130
194
|
|
|
131
|
-
# warm resident process — JSON requests over stdin/stdout (
|
|
195
|
+
# warm resident process — JSON requests over stdin/stdout (31 ops)
|
|
132
196
|
codemap serve --graph graph.json --source-root .
|
|
133
197
|
|
|
134
198
|
# …or expose the same surface as MCP tools for an AI-agent host (needs [mcp] extra)
|
|
135
199
|
codemap serve --graph graph.json --source-root . --mcp
|
|
200
|
+
|
|
201
|
+
# keep it current while you work: source → incremental rebuild → the warm server reloads
|
|
202
|
+
codemap watch ./pkg -o graph.json &
|
|
203
|
+
codemap serve --graph graph.json --watch
|
|
136
204
|
```
|
|
137
205
|
|
|
138
206
|
## What it answers
|
|
@@ -167,6 +235,12 @@ The [research track](research/) measures this against the field hands-on, on a s
|
|
|
167
235
|
[positioning doc](research/positioning.md) is the publication layer — the narrative and the numbers behind the
|
|
168
236
|
claims above; [comparison.md](research/comparison.md) is the coverage matrix that backs them.
|
|
169
237
|
|
|
238
|
+
Where the comparison actually lands, after four hands-on cards: peers are **faster**, cover **more
|
|
239
|
+
languages**, and several are **easier to install**. What none of them answers is the whole-graph
|
|
240
|
+
class — architecture, layers, cycles, coupling — and per-call argument contracts. That is the
|
|
241
|
+
substance behind "complements rather than competes", written out in
|
|
242
|
+
**[docs/whole-graph-questions.md](docs/whole-graph-questions.md)**.
|
|
243
|
+
|
|
170
244
|
Honesty is part of the bet: the call graph is a **measured lower bound**, not a guess. [docs/accuracy.md](docs/accuracy.md)
|
|
171
245
|
reports it — 100% precision / 100% decidable-recall on a hand-labeled suite, an openly-stated ~60% recall
|
|
172
246
|
against *all* true edges (the price of Python's dynamism), and a grep-vs-graph proof that the graph is ~2×
|
|
@@ -184,6 +258,9 @@ the live graph, findings, and the milestone that closed them.
|
|
|
184
258
|
## Documentation
|
|
185
259
|
|
|
186
260
|
- **[DESIGN.md](DESIGN.md)** — product design, the query catalog, v1 boundaries.
|
|
261
|
+
- **[docs/whole-graph-questions.md](docs/whole-graph-questions.md)** — **start here for what codemap is
|
|
262
|
+
*for*.** Point questions vs whole-graph questions, why the field answers only the first class, the
|
|
263
|
+
five questions codemap answers with a real run behind each, and the honest limits.
|
|
187
264
|
- **[docs/export.md](docs/export.md)** — export recipes: RAG, mermaid, Obsidian vault, SCIP + ctags interop.
|
|
188
265
|
- **[docs/accuracy.md](docs/accuracy.md)** — measured call-graph accuracy, the honest static ceiling, and
|
|
189
266
|
the grep-vs-graph value proof (both harnesses guarded in CI).
|
|
@@ -200,7 +277,9 @@ the live graph, findings, and the milestone that closed them.
|
|
|
200
277
|
- **[docs/attribute-edges.md](docs/attribute-edges.md)** — `accesses` edges: who reads/writes a class field,
|
|
201
278
|
honest field-level `impact` (`accessors`; `unknown` vs `none`).
|
|
202
279
|
- **[docs/incremental.md](docs/incremental.md)** — `codemap build --incremental`: recompute only changed
|
|
203
|
-
modules (~12× faster on `--deep`), byte-identical on the fast tier
|
|
280
|
+
modules (~12× faster on `--deep`), byte-identical on the fast tier; plus the automatic
|
|
281
|
+
`codemap watch` + `serve --watch` loop (save → answerable in ~8 s on a real package at the defaults,
|
|
282
|
+
of which 4.3 s is the rebuild).
|
|
204
283
|
- **[docs/test-mapping.md](docs/test-mapping.md)** — `codemap tests <symbol>`: which tests exercise a
|
|
205
284
|
symbol, as runnable pytest node ids, with the measured distance cutoff and an `unknown` that never
|
|
206
285
|
pretends to be "untested".
|
|
@@ -235,8 +314,10 @@ tools. Published here in the repo; every post exists in English and Russian.
|
|
|
235
314
|
| 2 | **The one that does more — and why that's fine.** — a 1.7 GB hybrid rival that proved the thesis instead of threatening it. | [EN](research/blog/02-the-one-that-does-more.md) · [RU](research/blog/02-the-one-that-does-more.ru.md) |
|
|
236
315
|
| 3 | **The competitor that does *less* — and that's why I take it.** — the emptiest coverage row was the most useful find. For its license, not its features. | [EN](research/blog/03-the-one-that-does-less.md) · [RU](research/blog/03-the-one-that-does-less.ru.md) |
|
|
237
316
|
| 4 | **My determinism test went red. The tool was fine.** — the input was moving under it, and the artifact could not say so. How the graph learned to name what built it. | [EN](research/blog/04-the-determinism-test-that-was-right.md) · [RU](research/blog/04-the-determinism-test-that-was-right.ru.md) |
|
|
317
|
+
| 5 | **A month of dogfooding. Then one more repository found seven bugs in two days.** — eleven pre-registered axes, all asked of one tree. What was missing was not an angle but a shape. | [EN](research/blog/05-the-second-repository.md) · [RU](research/blog/05-the-second-repository.ru.md) |
|
|
318
|
+
| 6 | **I measured the 68,000-star competitor. It was faster than mine. That wasn't the finding.** — four rivals, and the same two columns empty in all of them. | [EN](research/blog/06-two-empty-columns.md) · [RU](research/blog/06-two-empty-columns.ru.md) |
|
|
238
319
|
|
|
239
|
-
New here? Read **1 → 0 → 2 → 3 → 4**. Every number in every post reproduces from a
|
|
320
|
+
New here? Read **1 → 0 → 2 → 3 → 4 → 5 → 6**. Every number in every post reproduces from a
|
|
240
321
|
[tool card](research/tools/) or the [comparison hub](research/comparison.md) — measurements,
|
|
241
322
|
not verdict.
|
|
242
323
|
|
|
@@ -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.
|
|
11
|
+
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **655 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 &
|
|
@@ -25,6 +25,73 @@ Design principles: **source-only** (static `ast`/`griffe`, never imports the tar
|
|
|
25
25
|
(canonical sorted JSON, no timestamps — diffable), **CLI-AI-first** (JSON by default, stable exit
|
|
26
26
|
codes), **honest** (approximations are labeled, not hidden).
|
|
27
27
|
|
|
28
|
+
## Point questions and whole-graph questions
|
|
29
|
+
|
|
30
|
+
Most code-intelligence tools answer **point questions** — you name a symbol and they walk outward a
|
|
31
|
+
few steps. *Where is this defined? Who calls it? What breaks if I change it?* The answer's cost
|
|
32
|
+
scales with the neighbourhood, not the repository, which is why an index or a vector store can serve
|
|
33
|
+
it. The field is good at this, and several tools are faster at it than codemap.
|
|
34
|
+
|
|
35
|
+
**Whole-graph questions** have no starting symbol, because the property being asked about belongs to
|
|
36
|
+
the graph and to no node in it:
|
|
37
|
+
|
|
38
|
+
> Is there a dependency cycle anywhere? · Which module is most expensive to change? · Does the code
|
|
39
|
+
> still respect the layering I intended? · Where has behaviour concentrated into one class?
|
|
40
|
+
|
|
41
|
+
A cycle is invisible from inside every file that participates in it — each one looks perfectly
|
|
42
|
+
reasonable alone. You cannot seed the question, and there is no partial answer.
|
|
43
|
+
`codemap report architecture` computes all of them in one pass; on the dogfood target (88 modules,
|
|
44
|
+
**715 import edges**) that is:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
layers core 9 · data 12 · indicators 16 · analysis 42 · visualization 7 · cli 1
|
|
48
|
+
analysis → core 38 edges · indicators → core 22 · data → core 13
|
|
49
|
+
violation ⚠ analysis ↔ core — one backward edge, written inside a function
|
|
50
|
+
cycle pipeline → cache → pipeline — the classic Python import-order landmine
|
|
51
|
+
lazy cycles 40 more, closed only by a function-local import — not import-time failures,
|
|
52
|
+
still mutual coupling: neither module can be extracted without the other
|
|
53
|
+
coupling core.logging_config Ca 96 — a breaking change here reaches 96 modules
|
|
54
|
+
concentration ZoneVisualizer 35 methods, worst function CC 66 / MI 12.5
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
That single layer violation is worth its line: it is reached only through an import written *inside a
|
|
58
|
+
function*, so until [R1-C29](gaps/import_map_module_level_2026-08-28.md) codemap could not see it and
|
|
59
|
+
reported the architecture as clean. A gate you can walk around by making the import lazy is not a gate.
|
|
60
|
+
|
|
61
|
+
Then [`codemap check`](docs/architecture-contracts.md) turns the shape you *want* into a CI gate, so
|
|
62
|
+
description and intent cannot drift apart.
|
|
63
|
+
|
|
64
|
+
Four rival tools have now been measured hands-on on the same benchmark scope — three of them on
|
|
65
|
+
byte-identical input verified by content hash, the fourth (graphlens) on a near-identical staging that
|
|
66
|
+
predates the harness — including the field's most-adopted tool at 68k stars. **All four answer the point
|
|
67
|
+
questions. None answers these on a surface a caller can reach** — nor the related "how does each caller
|
|
68
|
+
actually *pass* its arguments" (`call_contract`) — because a slice of source is the wrong shape for the
|
|
69
|
+
answer.
|
|
70
|
+
|
|
71
|
+
That sentence used to end at "None answers these", and a second pass over the 68k-star tool made it
|
|
72
|
+
narrower: its *importable library* does carry a cycle finder, reachable from neither its CLI nor its MCP
|
|
73
|
+
tools and called nowhere in its own source. Measured on the same tree it reports **136 cycles**; codemap
|
|
74
|
+
reports **1**. Scoring both against the truth set — every intra-package import, function-local ones
|
|
75
|
+
included — is less flattering than that sounds:
|
|
76
|
+
|
|
77
|
+
| | reported | of the 41 real ones | precision | recall |
|
|
78
|
+
|---|---:|---:|---:|---:|
|
|
79
|
+
| codemap, that morning | 1 | 1 | 100% | **2.4%** |
|
|
80
|
+
| the peer's library API | 136 | 13 | 10% | 32% |
|
|
81
|
+
| codemap, after R1-C29 | 41 | 41 | 100% | 100% |
|
|
82
|
+
|
|
83
|
+
Theirs over-reports because it walks name-resolved call edges — a `dict.get` becomes a call into an
|
|
84
|
+
unrelated class. **Ours under-reported because its import map was module-level only**, and it phrased
|
|
85
|
+
that as *"import graph is acyclic"* — a property claim over a partial map, which is the worse of the two
|
|
86
|
+
errors even though it was the smaller one. Both halves of that came from
|
|
87
|
+
[#11](https://github.com/kogriv/codemap/issues/11), filed off a different target and fixed the same day
|
|
88
|
+
([R1-C29](gaps/import_map_module_level_2026-08-28.md)). The lesson is sharper than the win: a
|
|
89
|
+
whole-graph answer inherits every flaw of the graph it is computed from, **including the edges that
|
|
90
|
+
graph never read** — and the tool cannot be the judge of its own recall.
|
|
91
|
+
|
|
92
|
+
**→ [docs/whole-graph-questions.md](docs/whole-graph-questions.md)** — the full argument, every number
|
|
93
|
+
above reproduced from one run, and the honest limits.
|
|
94
|
+
|
|
28
95
|
## Install
|
|
29
96
|
|
|
30
97
|
**Python 3.11+** — the range is measured on 3.11–3.14 in CI, not assumed ([docs/ci.md](docs/ci.md)).
|
|
@@ -43,11 +110,7 @@ pip install 'codmap[mcp]'
|
|
|
43
110
|
pip install 'codmap[scip]'
|
|
44
111
|
```
|
|
45
112
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
pip install git+https://github.com/kogriv/codemap
|
|
50
|
-
```
|
|
113
|
+
Or straight from source: `pip install git+https://github.com/kogriv/codemap`.
|
|
51
114
|
|
|
52
115
|
Working on codemap itself, from a clone:
|
|
53
116
|
|
|
@@ -75,6 +138,7 @@ codemap report architecture --graph graph.json # layers, coupling, god-objec
|
|
|
75
138
|
codemap report dependencies --graph graph.json
|
|
76
139
|
codemap report dead-code --graph graph.json
|
|
77
140
|
codemap report impact --symbol MyClass --graph graph.json
|
|
141
|
+
codemap report api-surface --graph graph.json --format json # every kind has a json form
|
|
78
142
|
|
|
79
143
|
# change-set review straight from a diff → risk-sorted dossier
|
|
80
144
|
git diff | codemap review - --graph graph.json
|
|
@@ -92,11 +156,15 @@ codemap semantic "detect swing pivots" --build ./pkg --root pkg
|
|
|
92
156
|
# token-budgeted context pack — most relevant graph slice under N tokens (ranked; --seed to focus)
|
|
93
157
|
codemap pack --graph graph.json --budget 2000 --seed analyze_zones
|
|
94
158
|
|
|
95
|
-
# warm resident process — JSON requests over stdin/stdout (
|
|
159
|
+
# warm resident process — JSON requests over stdin/stdout (31 ops)
|
|
96
160
|
codemap serve --graph graph.json --source-root .
|
|
97
161
|
|
|
98
162
|
# …or expose the same surface as MCP tools for an AI-agent host (needs [mcp] extra)
|
|
99
163
|
codemap serve --graph graph.json --source-root . --mcp
|
|
164
|
+
|
|
165
|
+
# keep it current while you work: source → incremental rebuild → the warm server reloads
|
|
166
|
+
codemap watch ./pkg -o graph.json &
|
|
167
|
+
codemap serve --graph graph.json --watch
|
|
100
168
|
```
|
|
101
169
|
|
|
102
170
|
## What it answers
|
|
@@ -131,6 +199,12 @@ The [research track](research/) measures this against the field hands-on, on a s
|
|
|
131
199
|
[positioning doc](research/positioning.md) is the publication layer — the narrative and the numbers behind the
|
|
132
200
|
claims above; [comparison.md](research/comparison.md) is the coverage matrix that backs them.
|
|
133
201
|
|
|
202
|
+
Where the comparison actually lands, after four hands-on cards: peers are **faster**, cover **more
|
|
203
|
+
languages**, and several are **easier to install**. What none of them answers is the whole-graph
|
|
204
|
+
class — architecture, layers, cycles, coupling — and per-call argument contracts. That is the
|
|
205
|
+
substance behind "complements rather than competes", written out in
|
|
206
|
+
**[docs/whole-graph-questions.md](docs/whole-graph-questions.md)**.
|
|
207
|
+
|
|
134
208
|
Honesty is part of the bet: the call graph is a **measured lower bound**, not a guess. [docs/accuracy.md](docs/accuracy.md)
|
|
135
209
|
reports it — 100% precision / 100% decidable-recall on a hand-labeled suite, an openly-stated ~60% recall
|
|
136
210
|
against *all* true edges (the price of Python's dynamism), and a grep-vs-graph proof that the graph is ~2×
|
|
@@ -148,6 +222,9 @@ the live graph, findings, and the milestone that closed them.
|
|
|
148
222
|
## Documentation
|
|
149
223
|
|
|
150
224
|
- **[DESIGN.md](DESIGN.md)** — product design, the query catalog, v1 boundaries.
|
|
225
|
+
- **[docs/whole-graph-questions.md](docs/whole-graph-questions.md)** — **start here for what codemap is
|
|
226
|
+
*for*.** Point questions vs whole-graph questions, why the field answers only the first class, the
|
|
227
|
+
five questions codemap answers with a real run behind each, and the honest limits.
|
|
151
228
|
- **[docs/export.md](docs/export.md)** — export recipes: RAG, mermaid, Obsidian vault, SCIP + ctags interop.
|
|
152
229
|
- **[docs/accuracy.md](docs/accuracy.md)** — measured call-graph accuracy, the honest static ceiling, and
|
|
153
230
|
the grep-vs-graph value proof (both harnesses guarded in CI).
|
|
@@ -164,7 +241,9 @@ the live graph, findings, and the milestone that closed them.
|
|
|
164
241
|
- **[docs/attribute-edges.md](docs/attribute-edges.md)** — `accesses` edges: who reads/writes a class field,
|
|
165
242
|
honest field-level `impact` (`accessors`; `unknown` vs `none`).
|
|
166
243
|
- **[docs/incremental.md](docs/incremental.md)** — `codemap build --incremental`: recompute only changed
|
|
167
|
-
modules (~12× faster on `--deep`), byte-identical on the fast tier
|
|
244
|
+
modules (~12× faster on `--deep`), byte-identical on the fast tier; plus the automatic
|
|
245
|
+
`codemap watch` + `serve --watch` loop (save → answerable in ~8 s on a real package at the defaults,
|
|
246
|
+
of which 4.3 s is the rebuild).
|
|
168
247
|
- **[docs/test-mapping.md](docs/test-mapping.md)** — `codemap tests <symbol>`: which tests exercise a
|
|
169
248
|
symbol, as runnable pytest node ids, with the measured distance cutoff and an `unknown` that never
|
|
170
249
|
pretends to be "untested".
|
|
@@ -199,8 +278,10 @@ tools. Published here in the repo; every post exists in English and Russian.
|
|
|
199
278
|
| 2 | **The one that does more — and why that's fine.** — a 1.7 GB hybrid rival that proved the thesis instead of threatening it. | [EN](research/blog/02-the-one-that-does-more.md) · [RU](research/blog/02-the-one-that-does-more.ru.md) |
|
|
200
279
|
| 3 | **The competitor that does *less* — and that's why I take it.** — the emptiest coverage row was the most useful find. For its license, not its features. | [EN](research/blog/03-the-one-that-does-less.md) · [RU](research/blog/03-the-one-that-does-less.ru.md) |
|
|
201
280
|
| 4 | **My determinism test went red. The tool was fine.** — the input was moving under it, and the artifact could not say so. How the graph learned to name what built it. | [EN](research/blog/04-the-determinism-test-that-was-right.md) · [RU](research/blog/04-the-determinism-test-that-was-right.ru.md) |
|
|
281
|
+
| 5 | **A month of dogfooding. Then one more repository found seven bugs in two days.** — eleven pre-registered axes, all asked of one tree. What was missing was not an angle but a shape. | [EN](research/blog/05-the-second-repository.md) · [RU](research/blog/05-the-second-repository.ru.md) |
|
|
282
|
+
| 6 | **I measured the 68,000-star competitor. It was faster than mine. That wasn't the finding.** — four rivals, and the same two columns empty in all of them. | [EN](research/blog/06-two-empty-columns.md) · [RU](research/blog/06-two-empty-columns.ru.md) |
|
|
202
283
|
|
|
203
|
-
New here? Read **1 → 0 → 2 → 3 → 4**. Every number in every post reproduces from a
|
|
284
|
+
New here? Read **1 → 0 → 2 → 3 → 4 → 5 → 6**. Every number in every post reproduces from a
|
|
204
285
|
[tool card](research/tools/) or the [comparison hub](research/comparison.md) — measurements,
|
|
205
286
|
not verdict.
|
|
206
287
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""codemap — static code-graph builder.
|
|
2
|
+
|
|
3
|
+
Pipeline: Extract (griffe) -> Build (neutral model) -> Store (JSON) -> Serve (reports).
|
|
4
|
+
See DESIGN.md.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from codemap.model import Edge, Graph, Node
|
|
8
|
+
from codemap.provenance import tool_version
|
|
9
|
+
|
|
10
|
+
__all__ = ["Graph", "Node", "Edge"]
|
|
11
|
+
|
|
12
|
+
#: Read from the installed distribution rather than written down, because it had been
|
|
13
|
+
#: written down and had drifted: this said ``0.0.2`` while `codmap` 0.0.3 was on PyPI, so
|
|
14
|
+
#: every SCIP index and ctags file stamped a version the package had not been for a
|
|
15
|
+
#: release. `provenance` was already asking `importlib.metadata`, which is why graphs were
|
|
16
|
+
#: right and these two exports were not — one source now, and nothing left to drift.
|
|
17
|
+
__version__ = tool_version() or "0+unknown"
|
|
@@ -142,6 +142,21 @@ def _classify_signature(sid: str, old: Node, new: Node) -> list[Change]:
|
|
|
142
142
|
if old_p is not None and p.required and not old_p.required:
|
|
143
143
|
out.append(Change(sid, "param-made-required", BREAKING,
|
|
144
144
|
f"parameter `{name}` is now required"))
|
|
145
|
+
# R1-C34: a parameter that became keyword-only breaks every positional caller,
|
|
146
|
+
# and the reverse only widens. `_Param.keyword_only` was parsed from the start and
|
|
147
|
+
# never read — and could not have been trusted anyway: until R1-C34 the renderer
|
|
148
|
+
# dropped the `*` marker, so every stored signature parsed as if nothing were
|
|
149
|
+
# keyword-only, and `def f(a, b)` -> `def f(a, *, b)` was a no-op here.
|
|
150
|
+
for name, p in sn.params.items():
|
|
151
|
+
old_p = so.params.get(name)
|
|
152
|
+
if old_p is None:
|
|
153
|
+
continue
|
|
154
|
+
if p.keyword_only and not old_p.keyword_only:
|
|
155
|
+
out.append(Change(sid, "param-made-keyword-only", BREAKING,
|
|
156
|
+
f"parameter `{name}` is now keyword-only"))
|
|
157
|
+
elif old_p.keyword_only and not p.keyword_only:
|
|
158
|
+
out.append(Change(sid, "param-made-positional", INFO,
|
|
159
|
+
f"parameter `{name}` is no longer keyword-only"))
|
|
145
160
|
# variadic removed (*args / **kwargs that existed).
|
|
146
161
|
if so.has_vararg and not sn.has_vararg:
|
|
147
162
|
out.append(Change(sid, "variadic-removed", BREAKING, "`*args` removed"))
|
|
@@ -16,8 +16,11 @@ integration gate reads). All rules operate on the **core** module import graph
|
|
|
16
16
|
independent = [["indicators", "data"]]
|
|
17
17
|
# hard bans regardless of layering: `from` must not import `to`.
|
|
18
18
|
forbidden = [{ from = "core", to = "analysis" }]
|
|
19
|
-
# the import graph must be acyclic.
|
|
19
|
+
# the import graph must be acyclic *at import time* (the eager graph).
|
|
20
20
|
no_cycles = true
|
|
21
|
+
# also gate the coupling a lazy import hides: cycles closed only by an import
|
|
22
|
+
# written inside a function. Off by default — see below.
|
|
23
|
+
no_lazy_cycles = false
|
|
21
24
|
# every core module's layer must appear in `layers` (catches a new,
|
|
22
25
|
# undeclared top-level package slipping in).
|
|
23
26
|
exhaustive = false
|
|
@@ -30,7 +33,7 @@ of the code.
|
|
|
30
33
|
|
|
31
34
|
from __future__ import annotations
|
|
32
35
|
|
|
33
|
-
from dataclasses import dataclass, field
|
|
36
|
+
from dataclasses import dataclass, field, replace
|
|
34
37
|
from pathlib import Path
|
|
35
38
|
|
|
36
39
|
from codemap.tomlio import read_toml
|
|
@@ -50,19 +53,25 @@ class ArchitectureContract:
|
|
|
50
53
|
independent: tuple[tuple[str, ...], ...] = ()
|
|
51
54
|
forbidden: tuple[tuple[str, str], ...] = ()
|
|
52
55
|
no_cycles: bool = False
|
|
56
|
+
no_lazy_cycles: bool = False
|
|
53
57
|
exhaustive: bool = False
|
|
54
58
|
error: str | None = None
|
|
59
|
+
# R1-C35: the file this contract was looked for in. "No contract found" is only
|
|
60
|
+
# actionable next to *where* we looked — a reader in the wrong directory cannot tell
|
|
61
|
+
# an absent contract from a mislocated one, and both exit 0.
|
|
62
|
+
path: str | None = None
|
|
55
63
|
|
|
56
64
|
def is_empty(self) -> bool:
|
|
57
65
|
return not (self.layers or self.independent or self.forbidden
|
|
58
|
-
or self.no_cycles or self.exhaustive)
|
|
66
|
+
or self.no_cycles or self.no_lazy_cycles or self.exhaustive)
|
|
59
67
|
|
|
60
68
|
|
|
61
69
|
@dataclass(frozen=True)
|
|
62
70
|
class Violation:
|
|
63
71
|
"""One broken rule, with the concrete import edges (or cycle) that break it."""
|
|
64
72
|
|
|
65
|
-
rule: str # layered | independent | forbidden | no_cycles |
|
|
73
|
+
rule: str # layered | independent | forbidden | no_cycles | no_lazy_cycles
|
|
74
|
+
# | exhaustive
|
|
66
75
|
summary: str # human one-liner
|
|
67
76
|
edges: tuple[tuple[str, str], ...] = field(default=()) # offending (importer, imported)
|
|
68
77
|
modules: tuple[str, ...] = field(default=()) # for exhaustive / cycles
|
|
@@ -75,10 +84,15 @@ def load_contract(root: str | Path = ".") -> ArchitectureContract:
|
|
|
75
84
|
file that will not parse is reported through ``error`` rather than being returned as an
|
|
76
85
|
absent contract: a typo used to turn a failing gate green (R1-C27).
|
|
77
86
|
"""
|
|
78
|
-
|
|
87
|
+
# Absolute on purpose: "not found in codemap.toml" is what the reader already
|
|
88
|
+
# assumed. The whole value of the line is *which* codemap.toml, so it answers with a
|
|
89
|
+
# path that is unambiguous from any working directory. (The graph artifact stays free
|
|
90
|
+
# of absolute paths — D5 — but this is terminal output, not the artifact.)
|
|
91
|
+
path = Path(root).resolve() / "codemap.toml"
|
|
92
|
+
data, error = read_toml(path)
|
|
79
93
|
if error:
|
|
80
|
-
return ArchitectureContract(error=error)
|
|
81
|
-
return parse_contract(data.get("architecture", {}))
|
|
94
|
+
return ArchitectureContract(error=error, path=str(path))
|
|
95
|
+
return replace(parse_contract(data.get("architecture", {})), path=str(path))
|
|
82
96
|
|
|
83
97
|
|
|
84
98
|
def parse_contract(section: dict) -> ArchitectureContract:
|
|
@@ -97,6 +111,7 @@ def parse_contract(section: dict) -> ArchitectureContract:
|
|
|
97
111
|
independent=independent,
|
|
98
112
|
forbidden=tuple(forbidden),
|
|
99
113
|
no_cycles=bool(section.get("no_cycles", False)),
|
|
114
|
+
no_lazy_cycles=bool(section.get("no_lazy_cycles", False)),
|
|
100
115
|
exhaustive=bool(section.get("exhaustive", False)),
|
|
101
116
|
)
|
|
102
117
|
|
|
@@ -160,7 +175,18 @@ def check_contract(query, contract: ArchitectureContract) -> list[Violation]:
|
|
|
160
175
|
edges=tuple(sorted(bad)),
|
|
161
176
|
))
|
|
162
177
|
|
|
163
|
-
# -- no_cycles:
|
|
178
|
+
# -- no_cycles: no cycle among imports that run at import time ---------------
|
|
179
|
+
# R1-C29: deliberately the eager graph. `no_cycles` is a gate against the import-order
|
|
180
|
+
# failure, and a function-local import is the accepted fix for it — failing a build
|
|
181
|
+
# because someone applied that fix would punish the remedy.
|
|
182
|
+
#
|
|
183
|
+
# R1-C30-f2 settles what §7 of gaps/import_map_module_level_2026-08-28.md left open,
|
|
184
|
+
# after the second real target ran the gate: what it must NOT do is *stay silent*. A
|
|
185
|
+
# clean `no_cycles` run on a tree with 48 lazy cycles printed "Contract satisfied.
|
|
186
|
+
# Rules enforced: no_cycles", from which a reader concludes the graph is acyclic — the
|
|
187
|
+
# same property claim over a partial judgement that R1-C29 removed from the report,
|
|
188
|
+
# migrated into the gate. So: the gate stays eager, the *disclosure* is mandatory (see
|
|
189
|
+
# `build_check`), and a contract that wants the coupling gated says so.
|
|
164
190
|
if contract.no_cycles:
|
|
165
191
|
cycles = query.import_cycles()
|
|
166
192
|
if cycles:
|
|
@@ -171,6 +197,20 @@ def check_contract(query, contract: ArchitectureContract) -> list[Violation]:
|
|
|
171
197
|
modules=tuple(" → ".join(c) + " → " + c[0] for c in worst),
|
|
172
198
|
))
|
|
173
199
|
|
|
200
|
+
# -- no_lazy_cycles: opt in to gating the coupling a lazy import hides -------
|
|
201
|
+
# Both facts are true at once — a gate you walk around by making the import lazy is
|
|
202
|
+
# not a gate, and a lazy import is the accepted way to break an import cycle — so this
|
|
203
|
+
# is the contract owner's call to state, not a default to pick on their behalf.
|
|
204
|
+
if contract.no_lazy_cycles:
|
|
205
|
+
lazy = query.lazy_import_cycles()
|
|
206
|
+
if lazy:
|
|
207
|
+
worst = sorted(lazy, key=lambda c: (len(c), c))
|
|
208
|
+
violations.append(Violation(
|
|
209
|
+
"no_lazy_cycles",
|
|
210
|
+
f"{len(lazy)} dependency cycle(s) closed only by a function-local import",
|
|
211
|
+
modules=tuple(" → ".join(c) + " → " + c[0] for c in worst),
|
|
212
|
+
))
|
|
213
|
+
|
|
174
214
|
# -- exhaustive: every core module's layer must be declared -----------------
|
|
175
215
|
if contract.exhaustive and contract.layers:
|
|
176
216
|
declared = set(contract.layers)
|