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.
Files changed (121) hide show
  1. {codmap-0.0.3/codmap.egg-info → codmap-0.0.5}/PKG-INFO +91 -10
  2. {codmap-0.0.3 → codmap-0.0.5}/README.md +90 -9
  3. codmap-0.0.5/codemap/__init__.py +17 -0
  4. {codmap-0.0.3 → codmap-0.0.5}/codemap/apidiff.py +15 -0
  5. {codmap-0.0.3 → codmap-0.0.5}/codemap/arch.py +48 -8
  6. {codmap-0.0.3 → codmap-0.0.5}/codemap/cli.py +321 -5
  7. {codmap-0.0.3 → codmap-0.0.5}/codemap/extract/behavior.py +161 -20
  8. {codmap-0.0.3 → codmap-0.0.5}/codemap/extract/griffe_extractor.py +174 -58
  9. {codmap-0.0.3 → codmap-0.0.5}/codemap/extract/gsource.py +24 -7
  10. {codmap-0.0.3 → codmap-0.0.5}/codemap/extract/roots.py +69 -15
  11. {codmap-0.0.3 → codmap-0.0.5}/codemap/freshness.py +8 -1
  12. {codmap-0.0.3 → codmap-0.0.5}/codemap/model.py +11 -1
  13. {codmap-0.0.3 → codmap-0.0.5}/codemap/provenance.py +8 -2
  14. {codmap-0.0.3 → codmap-0.0.5}/codemap/query.py +52 -2
  15. {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/api_surface.py +37 -0
  16. {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/architecture.py +34 -2
  17. {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/audit.py +86 -2
  18. codmap-0.0.5/codemap/serve/check.py +141 -0
  19. codmap-0.0.5/codemap/serve/limits.py +64 -0
  20. {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/livingdocs.py +7 -1
  21. {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/mcp_server.py +48 -18
  22. {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/semantic.py +23 -2
  23. {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/session.py +88 -12
  24. codmap-0.0.5/codemap/watch.py +208 -0
  25. {codmap-0.0.3 → codmap-0.0.5/codmap.egg-info}/PKG-INFO +91 -10
  26. {codmap-0.0.3 → codmap-0.0.5}/codmap.egg-info/SOURCES.txt +14 -0
  27. {codmap-0.0.3 → codmap-0.0.5}/pyproject.toml +1 -1
  28. {codmap-0.0.3 → codmap-0.0.5}/tests/test_m16_architecture.py +5 -1
  29. {codmap-0.0.3 → codmap-0.0.5}/tests/test_m17_mcp.py +4 -1
  30. codmap-0.0.5/tests/test_m32_watch.py +401 -0
  31. {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c16_semantic.py +11 -3
  32. {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c25_provenance.py +39 -2
  33. codmap-0.0.5/tests/test_r1c28_limit_envelope.py +285 -0
  34. codmap-0.0.5/tests/test_r1c29_lazy_imports.py +227 -0
  35. codmap-0.0.5/tests/test_r1c30_local_import_calls.py +229 -0
  36. codmap-0.0.5/tests/test_r1c30f1_reexport_calls.py +176 -0
  37. codmap-0.0.5/tests/test_r1c30f2_check_scope.py +145 -0
  38. codmap-0.0.5/tests/test_r1c31_path_origin.py +155 -0
  39. codmap-0.0.5/tests/test_r1c32_report_json.py +129 -0
  40. codmap-0.0.5/tests/test_r1c33_query_signature.py +189 -0
  41. codmap-0.0.5/tests/test_r1c34_signature_kinds.py +150 -0
  42. codmap-0.0.5/tests/test_r1c35_contract_location.py +101 -0
  43. codmap-0.0.5/tests/test_r1c36_target_resolution.py +107 -0
  44. codmap-0.0.3/codemap/__init__.py +0 -10
  45. codmap-0.0.3/codemap/serve/check.py +0 -80
  46. {codmap-0.0.3 → codmap-0.0.5}/LICENSE +0 -0
  47. {codmap-0.0.3 → codmap-0.0.5}/codemap/diagnostics.py +0 -0
  48. {codmap-0.0.3 → codmap-0.0.5}/codemap/extract/__init__.py +0 -0
  49. {codmap-0.0.3 → codmap-0.0.5}/codemap/extract/attrflow.py +0 -0
  50. {codmap-0.0.3 → codmap-0.0.5}/codemap/extract/dataflow.py +0 -0
  51. {codmap-0.0.3 → codmap-0.0.5}/codemap/extract/dispatch.py +0 -0
  52. {codmap-0.0.3 → codmap-0.0.5}/codemap/incremental.py +0 -0
  53. {codmap-0.0.3 → codmap-0.0.5}/codemap/integrations/__init__.py +0 -0
  54. {codmap-0.0.3 → codmap-0.0.5}/codemap/integrations/base.py +0 -0
  55. {codmap-0.0.3 → codmap-0.0.5}/codemap/integrations/cocoindex.py +0 -0
  56. {codmap-0.0.3 → codmap-0.0.5}/codemap/integrations/gate.py +0 -0
  57. {codmap-0.0.3 → codmap-0.0.5}/codemap/integrations/gitnexus.py +0 -0
  58. {codmap-0.0.3 → codmap-0.0.5}/codemap/integrations/registry.py +0 -0
  59. {codmap-0.0.3 → codmap-0.0.5}/codemap/integrations/transport.py +0 -0
  60. {codmap-0.0.3 → codmap-0.0.5}/codemap/scope.py +0 -0
  61. {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/__init__.py +0 -0
  62. {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/_scip_pb2.py +0 -0
  63. {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/apidiff.py +0 -0
  64. {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/ctags.py +0 -0
  65. {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/impact.py +0 -0
  66. {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/mermaid.py +0 -0
  67. {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/pack.py +0 -0
  68. {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/rag.py +0 -0
  69. {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/review.py +0 -0
  70. {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/scip.py +0 -0
  71. {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/server.py +0 -0
  72. {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/subsystems.py +0 -0
  73. {codmap-0.0.3 → codmap-0.0.5}/codemap/serve/vault.py +0 -0
  74. {codmap-0.0.3 → codmap-0.0.5}/codemap/store.py +0 -0
  75. {codmap-0.0.3 → codmap-0.0.5}/codemap/tomlio.py +0 -0
  76. {codmap-0.0.3 → codmap-0.0.5}/codmap.egg-info/dependency_links.txt +0 -0
  77. {codmap-0.0.3 → codmap-0.0.5}/codmap.egg-info/entry_points.txt +0 -0
  78. {codmap-0.0.3 → codmap-0.0.5}/codmap.egg-info/requires.txt +0 -0
  79. {codmap-0.0.3 → codmap-0.0.5}/codmap.egg-info/top_level.txt +0 -0
  80. {codmap-0.0.3 → codmap-0.0.5}/setup.cfg +0 -0
  81. {codmap-0.0.3 → codmap-0.0.5}/tests/test_epistemic.py +0 -0
  82. {codmap-0.0.3 → codmap-0.0.5}/tests/test_gitnexus_router.py +0 -0
  83. {codmap-0.0.3 → codmap-0.0.5}/tests/test_impact_depth.py +0 -0
  84. {codmap-0.0.3 → codmap-0.0.5}/tests/test_integrations.py +0 -0
  85. {codmap-0.0.3 → codmap-0.0.5}/tests/test_issue3_serve_freshness.py +0 -0
  86. {codmap-0.0.3 → codmap-0.0.5}/tests/test_livingdocs.py +0 -0
  87. {codmap-0.0.3 → codmap-0.0.5}/tests/test_m0_api_surface.py +0 -0
  88. {codmap-0.0.3 → codmap-0.0.5}/tests/test_m11_argcontract.py +0 -0
  89. {codmap-0.0.3 → codmap-0.0.5}/tests/test_m12_dataflow.py +0 -0
  90. {codmap-0.0.3 → codmap-0.0.5}/tests/test_m15_review.py +0 -0
  91. {codmap-0.0.3 → codmap-0.0.5}/tests/test_m18_freshness.py +0 -0
  92. {codmap-0.0.3 → codmap-0.0.5}/tests/test_m1_5_semantics.py +0 -0
  93. {codmap-0.0.3 → codmap-0.0.5}/tests/test_m1_query.py +0 -0
  94. {codmap-0.0.3 → codmap-0.0.5}/tests/test_m2_views.py +0 -0
  95. {codmap-0.0.3 → codmap-0.0.5}/tests/test_m3_serve.py +0 -0
  96. {codmap-0.0.3 → codmap-0.0.5}/tests/test_m4_behavior.py +0 -0
  97. {codmap-0.0.3 → codmap-0.0.5}/tests/test_m5_deep.py +0 -0
  98. {codmap-0.0.3 → codmap-0.0.5}/tests/test_m6_repo_scope.py +0 -0
  99. {codmap-0.0.3 → codmap-0.0.5}/tests/test_m7_dispatch.py +0 -0
  100. {codmap-0.0.3 → codmap-0.0.5}/tests/test_m9_family.py +0 -0
  101. {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c13_callgraph_accuracy.py +0 -0
  102. {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c13_grep_vs_graph.py +0 -0
  103. {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c20_attribute_edges.py +0 -0
  104. {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c21_flat_layout.py +0 -0
  105. {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c22_source_visible_refs.py +0 -0
  106. {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c23_hard_python.py +0 -0
  107. {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c24_test_mapping.py +0 -0
  108. {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c26_deep_union.py +0 -0
  109. {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c27_config_honesty.py +0 -0
  110. {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c2_ctags.py +0 -0
  111. {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c3_arch_contract.py +0 -0
  112. {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c3_dogfood.py +0 -0
  113. {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c4_complexity.py +0 -0
  114. {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c5_apidiff.py +0 -0
  115. {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c6_pack.py +0 -0
  116. {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c7_edge_vocab.py +0 -0
  117. {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c8_deadcode.py +0 -0
  118. {codmap-0.0.3 → codmap-0.0.5}/tests/test_r1c9_incremental.py +0 -0
  119. {codmap-0.0.3 → codmap-0.0.5}/tests/test_scip_export.py +0 -0
  120. {codmap-0.0.3 → codmap-0.0.5}/tests/test_scope.py +0 -0
  121. {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
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
  [![CI](https://github.com/kogriv/codemap/actions/workflows/ci.yml/badge.svg)](https://github.com/kogriv/codemap/actions/workflows/ci.yml)
46
46
 
47
- **Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.12, **530 tests with no failures on
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
- Until the first release lands on PyPI, install straight from the repository instead:
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 (29 ops)
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
  [![CI](https://github.com/kogriv/codemap/actions/workflows/ci.yml/badge.svg)](https://github.com/kogriv/codemap/actions/workflows/ci.yml)
10
10
 
11
- **Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.12, **530 tests with no failures on
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
- Until the first release lands on PyPI, install straight from the repository instead:
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 (29 ops)
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 | exhaustive
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
- data, error = read_toml(Path(root) / "codemap.toml")
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: import graph must be acyclic --------------------------------
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)