codmap 0.0.3__tar.gz → 0.0.4__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 (116) hide show
  1. {codmap-0.0.3/codmap.egg-info → codmap-0.0.4}/PKG-INFO +91 -10
  2. {codmap-0.0.3 → codmap-0.0.4}/README.md +90 -9
  3. codmap-0.0.4/codemap/__init__.py +17 -0
  4. {codmap-0.0.3 → codmap-0.0.4}/codemap/arch.py +35 -4
  5. {codmap-0.0.3 → codmap-0.0.4}/codemap/cli.py +313 -4
  6. {codmap-0.0.3 → codmap-0.0.4}/codemap/extract/behavior.py +161 -20
  7. {codmap-0.0.3 → codmap-0.0.4}/codemap/extract/griffe_extractor.py +103 -49
  8. {codmap-0.0.3 → codmap-0.0.4}/codemap/extract/gsource.py +24 -7
  9. {codmap-0.0.3 → codmap-0.0.4}/codemap/extract/roots.py +69 -15
  10. {codmap-0.0.3 → codmap-0.0.4}/codemap/freshness.py +8 -1
  11. {codmap-0.0.3 → codmap-0.0.4}/codemap/model.py +11 -1
  12. {codmap-0.0.3 → codmap-0.0.4}/codemap/provenance.py +8 -2
  13. {codmap-0.0.3 → codmap-0.0.4}/codemap/query.py +52 -2
  14. {codmap-0.0.3 → codmap-0.0.4}/codemap/serve/api_surface.py +37 -0
  15. {codmap-0.0.3 → codmap-0.0.4}/codemap/serve/architecture.py +34 -2
  16. {codmap-0.0.3 → codmap-0.0.4}/codemap/serve/audit.py +86 -2
  17. {codmap-0.0.3 → codmap-0.0.4}/codemap/serve/check.py +53 -2
  18. codmap-0.0.4/codemap/serve/limits.py +64 -0
  19. {codmap-0.0.3 → codmap-0.0.4}/codemap/serve/livingdocs.py +7 -1
  20. {codmap-0.0.3 → codmap-0.0.4}/codemap/serve/mcp_server.py +40 -16
  21. {codmap-0.0.3 → codmap-0.0.4}/codemap/serve/semantic.py +23 -2
  22. {codmap-0.0.3 → codmap-0.0.4}/codemap/serve/session.py +57 -10
  23. codmap-0.0.4/codemap/watch.py +208 -0
  24. {codmap-0.0.3 → codmap-0.0.4/codmap.egg-info}/PKG-INFO +91 -10
  25. {codmap-0.0.3 → codmap-0.0.4}/codmap.egg-info/SOURCES.txt +10 -0
  26. {codmap-0.0.3 → codmap-0.0.4}/pyproject.toml +1 -1
  27. {codmap-0.0.3 → codmap-0.0.4}/tests/test_m16_architecture.py +5 -1
  28. {codmap-0.0.3 → codmap-0.0.4}/tests/test_m17_mcp.py +4 -1
  29. codmap-0.0.4/tests/test_m32_watch.py +401 -0
  30. {codmap-0.0.3 → codmap-0.0.4}/tests/test_r1c16_semantic.py +11 -3
  31. {codmap-0.0.3 → codmap-0.0.4}/tests/test_r1c25_provenance.py +39 -2
  32. codmap-0.0.4/tests/test_r1c28_limit_envelope.py +285 -0
  33. codmap-0.0.4/tests/test_r1c29_lazy_imports.py +227 -0
  34. codmap-0.0.4/tests/test_r1c30_local_import_calls.py +229 -0
  35. codmap-0.0.4/tests/test_r1c30f1_reexport_calls.py +176 -0
  36. codmap-0.0.4/tests/test_r1c30f2_check_scope.py +145 -0
  37. codmap-0.0.4/tests/test_r1c31_path_origin.py +155 -0
  38. codmap-0.0.4/tests/test_r1c32_report_json.py +129 -0
  39. codmap-0.0.3/codemap/__init__.py +0 -10
  40. {codmap-0.0.3 → codmap-0.0.4}/LICENSE +0 -0
  41. {codmap-0.0.3 → codmap-0.0.4}/codemap/apidiff.py +0 -0
  42. {codmap-0.0.3 → codmap-0.0.4}/codemap/diagnostics.py +0 -0
  43. {codmap-0.0.3 → codmap-0.0.4}/codemap/extract/__init__.py +0 -0
  44. {codmap-0.0.3 → codmap-0.0.4}/codemap/extract/attrflow.py +0 -0
  45. {codmap-0.0.3 → codmap-0.0.4}/codemap/extract/dataflow.py +0 -0
  46. {codmap-0.0.3 → codmap-0.0.4}/codemap/extract/dispatch.py +0 -0
  47. {codmap-0.0.3 → codmap-0.0.4}/codemap/incremental.py +0 -0
  48. {codmap-0.0.3 → codmap-0.0.4}/codemap/integrations/__init__.py +0 -0
  49. {codmap-0.0.3 → codmap-0.0.4}/codemap/integrations/base.py +0 -0
  50. {codmap-0.0.3 → codmap-0.0.4}/codemap/integrations/cocoindex.py +0 -0
  51. {codmap-0.0.3 → codmap-0.0.4}/codemap/integrations/gate.py +0 -0
  52. {codmap-0.0.3 → codmap-0.0.4}/codemap/integrations/gitnexus.py +0 -0
  53. {codmap-0.0.3 → codmap-0.0.4}/codemap/integrations/registry.py +0 -0
  54. {codmap-0.0.3 → codmap-0.0.4}/codemap/integrations/transport.py +0 -0
  55. {codmap-0.0.3 → codmap-0.0.4}/codemap/scope.py +0 -0
  56. {codmap-0.0.3 → codmap-0.0.4}/codemap/serve/__init__.py +0 -0
  57. {codmap-0.0.3 → codmap-0.0.4}/codemap/serve/_scip_pb2.py +0 -0
  58. {codmap-0.0.3 → codmap-0.0.4}/codemap/serve/apidiff.py +0 -0
  59. {codmap-0.0.3 → codmap-0.0.4}/codemap/serve/ctags.py +0 -0
  60. {codmap-0.0.3 → codmap-0.0.4}/codemap/serve/impact.py +0 -0
  61. {codmap-0.0.3 → codmap-0.0.4}/codemap/serve/mermaid.py +0 -0
  62. {codmap-0.0.3 → codmap-0.0.4}/codemap/serve/pack.py +0 -0
  63. {codmap-0.0.3 → codmap-0.0.4}/codemap/serve/rag.py +0 -0
  64. {codmap-0.0.3 → codmap-0.0.4}/codemap/serve/review.py +0 -0
  65. {codmap-0.0.3 → codmap-0.0.4}/codemap/serve/scip.py +0 -0
  66. {codmap-0.0.3 → codmap-0.0.4}/codemap/serve/server.py +0 -0
  67. {codmap-0.0.3 → codmap-0.0.4}/codemap/serve/subsystems.py +0 -0
  68. {codmap-0.0.3 → codmap-0.0.4}/codemap/serve/vault.py +0 -0
  69. {codmap-0.0.3 → codmap-0.0.4}/codemap/store.py +0 -0
  70. {codmap-0.0.3 → codmap-0.0.4}/codemap/tomlio.py +0 -0
  71. {codmap-0.0.3 → codmap-0.0.4}/codmap.egg-info/dependency_links.txt +0 -0
  72. {codmap-0.0.3 → codmap-0.0.4}/codmap.egg-info/entry_points.txt +0 -0
  73. {codmap-0.0.3 → codmap-0.0.4}/codmap.egg-info/requires.txt +0 -0
  74. {codmap-0.0.3 → codmap-0.0.4}/codmap.egg-info/top_level.txt +0 -0
  75. {codmap-0.0.3 → codmap-0.0.4}/setup.cfg +0 -0
  76. {codmap-0.0.3 → codmap-0.0.4}/tests/test_epistemic.py +0 -0
  77. {codmap-0.0.3 → codmap-0.0.4}/tests/test_gitnexus_router.py +0 -0
  78. {codmap-0.0.3 → codmap-0.0.4}/tests/test_impact_depth.py +0 -0
  79. {codmap-0.0.3 → codmap-0.0.4}/tests/test_integrations.py +0 -0
  80. {codmap-0.0.3 → codmap-0.0.4}/tests/test_issue3_serve_freshness.py +0 -0
  81. {codmap-0.0.3 → codmap-0.0.4}/tests/test_livingdocs.py +0 -0
  82. {codmap-0.0.3 → codmap-0.0.4}/tests/test_m0_api_surface.py +0 -0
  83. {codmap-0.0.3 → codmap-0.0.4}/tests/test_m11_argcontract.py +0 -0
  84. {codmap-0.0.3 → codmap-0.0.4}/tests/test_m12_dataflow.py +0 -0
  85. {codmap-0.0.3 → codmap-0.0.4}/tests/test_m15_review.py +0 -0
  86. {codmap-0.0.3 → codmap-0.0.4}/tests/test_m18_freshness.py +0 -0
  87. {codmap-0.0.3 → codmap-0.0.4}/tests/test_m1_5_semantics.py +0 -0
  88. {codmap-0.0.3 → codmap-0.0.4}/tests/test_m1_query.py +0 -0
  89. {codmap-0.0.3 → codmap-0.0.4}/tests/test_m2_views.py +0 -0
  90. {codmap-0.0.3 → codmap-0.0.4}/tests/test_m3_serve.py +0 -0
  91. {codmap-0.0.3 → codmap-0.0.4}/tests/test_m4_behavior.py +0 -0
  92. {codmap-0.0.3 → codmap-0.0.4}/tests/test_m5_deep.py +0 -0
  93. {codmap-0.0.3 → codmap-0.0.4}/tests/test_m6_repo_scope.py +0 -0
  94. {codmap-0.0.3 → codmap-0.0.4}/tests/test_m7_dispatch.py +0 -0
  95. {codmap-0.0.3 → codmap-0.0.4}/tests/test_m9_family.py +0 -0
  96. {codmap-0.0.3 → codmap-0.0.4}/tests/test_r1c13_callgraph_accuracy.py +0 -0
  97. {codmap-0.0.3 → codmap-0.0.4}/tests/test_r1c13_grep_vs_graph.py +0 -0
  98. {codmap-0.0.3 → codmap-0.0.4}/tests/test_r1c20_attribute_edges.py +0 -0
  99. {codmap-0.0.3 → codmap-0.0.4}/tests/test_r1c21_flat_layout.py +0 -0
  100. {codmap-0.0.3 → codmap-0.0.4}/tests/test_r1c22_source_visible_refs.py +0 -0
  101. {codmap-0.0.3 → codmap-0.0.4}/tests/test_r1c23_hard_python.py +0 -0
  102. {codmap-0.0.3 → codmap-0.0.4}/tests/test_r1c24_test_mapping.py +0 -0
  103. {codmap-0.0.3 → codmap-0.0.4}/tests/test_r1c26_deep_union.py +0 -0
  104. {codmap-0.0.3 → codmap-0.0.4}/tests/test_r1c27_config_honesty.py +0 -0
  105. {codmap-0.0.3 → codmap-0.0.4}/tests/test_r1c2_ctags.py +0 -0
  106. {codmap-0.0.3 → codmap-0.0.4}/tests/test_r1c3_arch_contract.py +0 -0
  107. {codmap-0.0.3 → codmap-0.0.4}/tests/test_r1c3_dogfood.py +0 -0
  108. {codmap-0.0.3 → codmap-0.0.4}/tests/test_r1c4_complexity.py +0 -0
  109. {codmap-0.0.3 → codmap-0.0.4}/tests/test_r1c5_apidiff.py +0 -0
  110. {codmap-0.0.3 → codmap-0.0.4}/tests/test_r1c6_pack.py +0 -0
  111. {codmap-0.0.3 → codmap-0.0.4}/tests/test_r1c7_edge_vocab.py +0 -0
  112. {codmap-0.0.3 → codmap-0.0.4}/tests/test_r1c8_deadcode.py +0 -0
  113. {codmap-0.0.3 → codmap-0.0.4}/tests/test_r1c9_incremental.py +0 -0
  114. {codmap-0.0.3 → codmap-0.0.4}/tests/test_scip_export.py +0 -0
  115. {codmap-0.0.3 → codmap-0.0.4}/tests/test_scope.py +0 -0
  116. {codmap-0.0.3 → codmap-0.0.4}/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.4
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"
@@ -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
@@ -50,19 +53,21 @@ 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
55
59
 
56
60
  def is_empty(self) -> bool:
57
61
  return not (self.layers or self.independent or self.forbidden
58
- or self.no_cycles or self.exhaustive)
62
+ or self.no_cycles or self.no_lazy_cycles or self.exhaustive)
59
63
 
60
64
 
61
65
  @dataclass(frozen=True)
62
66
  class Violation:
63
67
  """One broken rule, with the concrete import edges (or cycle) that break it."""
64
68
 
65
- rule: str # layered | independent | forbidden | no_cycles | exhaustive
69
+ rule: str # layered | independent | forbidden | no_cycles | no_lazy_cycles
70
+ # | exhaustive
66
71
  summary: str # human one-liner
67
72
  edges: tuple[tuple[str, str], ...] = field(default=()) # offending (importer, imported)
68
73
  modules: tuple[str, ...] = field(default=()) # for exhaustive / cycles
@@ -97,6 +102,7 @@ def parse_contract(section: dict) -> ArchitectureContract:
97
102
  independent=independent,
98
103
  forbidden=tuple(forbidden),
99
104
  no_cycles=bool(section.get("no_cycles", False)),
105
+ no_lazy_cycles=bool(section.get("no_lazy_cycles", False)),
100
106
  exhaustive=bool(section.get("exhaustive", False)),
101
107
  )
102
108
 
@@ -160,7 +166,18 @@ def check_contract(query, contract: ArchitectureContract) -> list[Violation]:
160
166
  edges=tuple(sorted(bad)),
161
167
  ))
162
168
 
163
- # -- no_cycles: import graph must be acyclic --------------------------------
169
+ # -- no_cycles: no cycle among imports that run at import time ---------------
170
+ # R1-C29: deliberately the eager graph. `no_cycles` is a gate against the import-order
171
+ # failure, and a function-local import is the accepted fix for it — failing a build
172
+ # because someone applied that fix would punish the remedy.
173
+ #
174
+ # R1-C30-f2 settles what §7 of gaps/import_map_module_level_2026-08-28.md left open,
175
+ # after the second real target ran the gate: what it must NOT do is *stay silent*. A
176
+ # clean `no_cycles` run on a tree with 48 lazy cycles printed "Contract satisfied.
177
+ # Rules enforced: no_cycles", from which a reader concludes the graph is acyclic — the
178
+ # same property claim over a partial judgement that R1-C29 removed from the report,
179
+ # migrated into the gate. So: the gate stays eager, the *disclosure* is mandatory (see
180
+ # `build_check`), and a contract that wants the coupling gated says so.
164
181
  if contract.no_cycles:
165
182
  cycles = query.import_cycles()
166
183
  if cycles:
@@ -171,6 +188,20 @@ def check_contract(query, contract: ArchitectureContract) -> list[Violation]:
171
188
  modules=tuple(" → ".join(c) + " → " + c[0] for c in worst),
172
189
  ))
173
190
 
191
+ # -- no_lazy_cycles: opt in to gating the coupling a lazy import hides -------
192
+ # Both facts are true at once — a gate you walk around by making the import lazy is
193
+ # not a gate, and a lazy import is the accepted way to break an import cycle — so this
194
+ # is the contract owner's call to state, not a default to pick on their behalf.
195
+ if contract.no_lazy_cycles:
196
+ lazy = query.lazy_import_cycles()
197
+ if lazy:
198
+ worst = sorted(lazy, key=lambda c: (len(c), c))
199
+ violations.append(Violation(
200
+ "no_lazy_cycles",
201
+ f"{len(lazy)} dependency cycle(s) closed only by a function-local import",
202
+ modules=tuple(" → ".join(c) + " → " + c[0] for c in worst),
203
+ ))
204
+
174
205
  # -- exhaustive: every core module's layer must be declared -----------------
175
206
  if contract.exhaustive and contract.layers:
176
207
  declared = set(contract.layers)