codmap 0.0.20__tar.gz → 0.0.22__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. {codmap-0.0.20/codmap.egg-info → codmap-0.0.22}/PKG-INFO +17 -4
  2. {codmap-0.0.20 → codmap-0.0.22}/README.md +16 -3
  3. {codmap-0.0.20 → codmap-0.0.22}/codemap/arch.py +58 -1
  4. {codmap-0.0.20 → codmap-0.0.22}/codemap/cli.py +97 -17
  5. codmap-0.0.22/codemap/extract/probe.py +152 -0
  6. {codmap-0.0.20 → codmap-0.0.22}/codemap/model.py +28 -0
  7. {codmap-0.0.20 → codmap-0.0.22}/codemap/provenance.py +71 -1
  8. {codmap-0.0.20 → codmap-0.0.22}/codemap/query.py +101 -16
  9. {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/apidiff.py +4 -1
  10. {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/audit.py +8 -3
  11. {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/check.py +54 -6
  12. {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/mcp_server.py +17 -6
  13. codmap-0.0.22/codemap/serve/pack.py +224 -0
  14. {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/session.py +9 -3
  15. {codmap-0.0.20 → codmap-0.0.22/codmap.egg-info}/PKG-INFO +17 -4
  16. {codmap-0.0.20 → codmap-0.0.22}/codmap.egg-info/SOURCES.txt +8 -0
  17. {codmap-0.0.20 → codmap-0.0.22}/pyproject.toml +1 -1
  18. codmap-0.0.22/tests/test_pack_honesty.py +227 -0
  19. codmap-0.0.22/tests/test_r1c59_completeness_drop.py +198 -0
  20. codmap-0.0.22/tests/test_r1c60_nested_definitions.py +117 -0
  21. codmap-0.0.22/tests/test_r1c62_phantom_rules.py +167 -0
  22. codmap-0.0.22/tests/test_r1c63_answer_format.py +150 -0
  23. codmap-0.0.22/tests/test_r1c64_dependency_probe.py +140 -0
  24. codmap-0.0.22/tests/test_r1c66_pack_direction.py +150 -0
  25. codmap-0.0.20/codemap/serve/pack.py +0 -93
  26. {codmap-0.0.20 → codmap-0.0.22}/LICENSE +0 -0
  27. {codmap-0.0.20 → codmap-0.0.22}/codemap/__init__.py +0 -0
  28. {codmap-0.0.20 → codmap-0.0.22}/codemap/apidiff.py +0 -0
  29. {codmap-0.0.20 → codmap-0.0.22}/codemap/diagnostics.py +0 -0
  30. {codmap-0.0.20 → codmap-0.0.22}/codemap/extract/__init__.py +0 -0
  31. {codmap-0.0.20 → codmap-0.0.22}/codemap/extract/attrflow.py +0 -0
  32. {codmap-0.0.20 → codmap-0.0.22}/codemap/extract/behavior.py +0 -0
  33. {codmap-0.0.20 → codmap-0.0.22}/codemap/extract/dataflow.py +0 -0
  34. {codmap-0.0.20 → codmap-0.0.22}/codemap/extract/dispatch.py +0 -0
  35. {codmap-0.0.20 → codmap-0.0.22}/codemap/extract/griffe_extractor.py +0 -0
  36. {codmap-0.0.20 → codmap-0.0.22}/codemap/extract/gsource.py +0 -0
  37. {codmap-0.0.20 → codmap-0.0.22}/codemap/extract/roots.py +0 -0
  38. {codmap-0.0.20 → codmap-0.0.22}/codemap/extract/union.py +0 -0
  39. {codmap-0.0.20 → codmap-0.0.22}/codemap/freshness.py +0 -0
  40. {codmap-0.0.20 → codmap-0.0.22}/codemap/incremental.py +0 -0
  41. {codmap-0.0.20 → codmap-0.0.22}/codemap/integrations/__init__.py +0 -0
  42. {codmap-0.0.20 → codmap-0.0.22}/codemap/integrations/base.py +0 -0
  43. {codmap-0.0.20 → codmap-0.0.22}/codemap/integrations/cocoindex.py +0 -0
  44. {codmap-0.0.20 → codmap-0.0.22}/codemap/integrations/gate.py +0 -0
  45. {codmap-0.0.20 → codmap-0.0.22}/codemap/integrations/gitnexus.py +0 -0
  46. {codmap-0.0.20 → codmap-0.0.22}/codemap/integrations/registry.py +0 -0
  47. {codmap-0.0.20 → codmap-0.0.22}/codemap/integrations/transport.py +0 -0
  48. {codmap-0.0.20 → codmap-0.0.22}/codemap/scope.py +0 -0
  49. {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/__init__.py +0 -0
  50. {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/_scip_pb2.py +0 -0
  51. {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/api_surface.py +0 -0
  52. {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/architecture.py +0 -0
  53. {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/ctags.py +0 -0
  54. {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/impact.py +0 -0
  55. {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/limits.py +0 -0
  56. {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/livingdocs.py +0 -0
  57. {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/mermaid.py +0 -0
  58. {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/rag.py +0 -0
  59. {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/review.py +0 -0
  60. {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/scip.py +0 -0
  61. {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/semantic.py +0 -0
  62. {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/server.py +0 -0
  63. {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/subsystems.py +0 -0
  64. {codmap-0.0.20 → codmap-0.0.22}/codemap/serve/vault.py +0 -0
  65. {codmap-0.0.20 → codmap-0.0.22}/codemap/store.py +0 -0
  66. {codmap-0.0.20 → codmap-0.0.22}/codemap/tomlio.py +0 -0
  67. {codmap-0.0.20 → codmap-0.0.22}/codemap/watch.py +0 -0
  68. {codmap-0.0.20 → codmap-0.0.22}/codmap.egg-info/dependency_links.txt +0 -0
  69. {codmap-0.0.20 → codmap-0.0.22}/codmap.egg-info/entry_points.txt +0 -0
  70. {codmap-0.0.20 → codmap-0.0.22}/codmap.egg-info/requires.txt +0 -0
  71. {codmap-0.0.20 → codmap-0.0.22}/codmap.egg-info/top_level.txt +0 -0
  72. {codmap-0.0.20 → codmap-0.0.22}/setup.cfg +0 -0
  73. {codmap-0.0.20 → codmap-0.0.22}/tests/test_epistemic.py +0 -0
  74. {codmap-0.0.20 → codmap-0.0.22}/tests/test_gitnexus_router.py +0 -0
  75. {codmap-0.0.20 → codmap-0.0.22}/tests/test_impact_depth.py +0 -0
  76. {codmap-0.0.20 → codmap-0.0.22}/tests/test_integrations.py +0 -0
  77. {codmap-0.0.20 → codmap-0.0.22}/tests/test_issue3_serve_freshness.py +0 -0
  78. {codmap-0.0.20 → codmap-0.0.22}/tests/test_livingdocs.py +0 -0
  79. {codmap-0.0.20 → codmap-0.0.22}/tests/test_m0_api_surface.py +0 -0
  80. {codmap-0.0.20 → codmap-0.0.22}/tests/test_m11_argcontract.py +0 -0
  81. {codmap-0.0.20 → codmap-0.0.22}/tests/test_m12_dataflow.py +0 -0
  82. {codmap-0.0.20 → codmap-0.0.22}/tests/test_m15_review.py +0 -0
  83. {codmap-0.0.20 → codmap-0.0.22}/tests/test_m16_architecture.py +0 -0
  84. {codmap-0.0.20 → codmap-0.0.22}/tests/test_m17_mcp.py +0 -0
  85. {codmap-0.0.20 → codmap-0.0.22}/tests/test_m18_freshness.py +0 -0
  86. {codmap-0.0.20 → codmap-0.0.22}/tests/test_m1_5_semantics.py +0 -0
  87. {codmap-0.0.20 → codmap-0.0.22}/tests/test_m1_query.py +0 -0
  88. {codmap-0.0.20 → codmap-0.0.22}/tests/test_m2_views.py +0 -0
  89. {codmap-0.0.20 → codmap-0.0.22}/tests/test_m32_watch.py +0 -0
  90. {codmap-0.0.20 → codmap-0.0.22}/tests/test_m3_serve.py +0 -0
  91. {codmap-0.0.20 → codmap-0.0.22}/tests/test_m4_behavior.py +0 -0
  92. {codmap-0.0.20 → codmap-0.0.22}/tests/test_m5_deep.py +0 -0
  93. {codmap-0.0.20 → codmap-0.0.22}/tests/test_m6_repo_scope.py +0 -0
  94. {codmap-0.0.20 → codmap-0.0.22}/tests/test_m7_dispatch.py +0 -0
  95. {codmap-0.0.20 → codmap-0.0.22}/tests/test_m9_family.py +0 -0
  96. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c13_callgraph_accuracy.py +0 -0
  97. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c13_grep_vs_graph.py +0 -0
  98. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c16_semantic.py +0 -0
  99. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c20_attribute_edges.py +0 -0
  100. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c21_flat_layout.py +0 -0
  101. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c22_source_visible_refs.py +0 -0
  102. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c23_hard_python.py +0 -0
  103. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c24_test_mapping.py +0 -0
  104. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c25_provenance.py +0 -0
  105. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c26_deep_union.py +0 -0
  106. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c27_config_honesty.py +0 -0
  107. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c28_limit_envelope.py +0 -0
  108. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c29_lazy_imports.py +0 -0
  109. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c2_ctags.py +0 -0
  110. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c30_local_import_calls.py +0 -0
  111. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c30f1_reexport_calls.py +0 -0
  112. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c30f2_check_scope.py +0 -0
  113. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c31_path_origin.py +0 -0
  114. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c32_report_json.py +0 -0
  115. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c33_query_signature.py +0 -0
  116. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c34_signature_kinds.py +0 -0
  117. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c35_contract_location.py +0 -0
  118. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c36_target_resolution.py +0 -0
  119. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c37_rule_mutation.py +0 -0
  120. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c38_tool_drift.py +0 -0
  121. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c39_edge_resolution.py +0 -0
  122. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c3_arch_contract.py +0 -0
  123. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c3_dogfood.py +0 -0
  124. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c40_flow_reach.py +0 -0
  125. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c41_scope_membership.py +0 -0
  126. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c42_deep_tier_stability.py +0 -0
  127. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c43_incremental_splice.py +0 -0
  128. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c44_absent_answers.py +0 -0
  129. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c45_repeat_union.py +0 -0
  130. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c46_shadowed_definitions.py +0 -0
  131. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c47_incremental_resample.py +0 -0
  132. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c48_type_checking_imports.py +0 -0
  133. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c49_type_only_cycles.py +0 -0
  134. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c4_complexity.py +0 -0
  135. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c50_flow_entry_points.py +0 -0
  136. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c53_narrowing_declared.py +0 -0
  137. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c54_deterministic_rendering.py +0 -0
  138. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c55_override_grade.py +0 -0
  139. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c56_stub_files.py +0 -0
  140. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c57_facade_package.py +0 -0
  141. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c58_cycle_tangles.py +0 -0
  142. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c5_apidiff.py +0 -0
  143. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c6_pack.py +0 -0
  144. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c7_edge_vocab.py +0 -0
  145. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c8_deadcode.py +0 -0
  146. {codmap-0.0.20 → codmap-0.0.22}/tests/test_r1c9_incremental.py +0 -0
  147. {codmap-0.0.20 → codmap-0.0.22}/tests/test_scip_export.py +0 -0
  148. {codmap-0.0.20 → codmap-0.0.22}/tests/test_scope.py +0 -0
  149. {codmap-0.0.20 → codmap-0.0.22}/tests/test_subsystems.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codmap
3
- Version: 0.0.20
3
+ Version: 0.0.22
4
4
  Summary: Static analyzer that turns a package's source into a queryable code graph.
5
5
  Author-email: kogriv <kogriv@gmail.com>
6
6
  License-Expression: MIT
@@ -44,7 +44,7 @@ and a **SCIP index** for interop with Sourcegraph / Glean and other precise-code
44
44
 
45
45
  [![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.13, **972 tests with no failures on
47
+ **Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **1054 tests with no failures on
48
48
  Python 3.11–3.14** ([in CI](docs/ci.md): the full suite including the dogfood pass, a determinism check, a
49
49
  wheel smoke test, and ctags/SCIP interop against the real CLIs), warm serve surface with 31 ops (28 exposed
50
50
  as MCP tools), and SCIP export. See **[DESIGN.md](DESIGN.md)** (product design &
@@ -221,8 +221,14 @@ codemap serve --graph graph.json --watch
221
221
  - **Dataflow** — producers/consumers of a string-keyed DataFrame column.
222
222
  - **Semantic search** (opt-in) — a concept query routed to an external adapter, with each fuzzy hit
223
223
  resolved to the exact codemap symbol at its location (`codemap semantic`). See [docs/integrations.md](docs/integrations.md).
224
- - **Context pack** — the most relevant slice of the graph under a token budget, ranked by importance or
225
- by relevance to seed symbols (`codemap pack --budget N [--seed X]`). See [docs/pack.md](docs/pack.md).
224
+ - **Context pack** — the highest-ranked modules, classes and functions under a token budget, by
225
+ importance or by relevance to seed symbols (`codemap pack --budget N [--seed X]`). With a seed it
226
+ walks both ways — what the seed uses and what uses it (`--direction uses` for one way) — documents are
227
+ never in it, the answer says which walk it took, and a seed that resolves to nothing is refused rather
228
+ than silently answered globally. Deterministic and budget-tested. Against git history, seeded with a
229
+ changed file, it puts 42 % and 60 % of the co-changed files in its first ten on two repositories —
230
+ ahead of `impact` and of grep by names, though on one of the two not beyond the noise; grep still
231
+ finds tests best. **Whether it helps an agent is not measured.** See [docs/pack.md](docs/pack.md).
226
232
  - **Interop** — export the graph as a [SCIP](https://scip-code.org/) index (definitions + symbol
227
233
  info + inherits/implements relationships) so Sourcegraph, Glean and other SCIP consumers can drive
228
234
  go-to-definition, symbol search and type hierarchy over it. See [docs/export.md](docs/export.md).
@@ -266,6 +272,13 @@ build says so in its own diagnostics, and `codemap diff` says so when both sides
266
272
  delta of a few call edges as possible tool noise before reading it as a change in the code. Anything
267
273
  that must be reproducible byte-for-byte — a gate, a two-release comparison — belongs on the fast tier.
268
274
 
275
+ **A deep build does not check jedi.** With jedi answering nothing, a deep build still exits 0 and
276
+ declares `tier: deep` — and its call edges equal the fast tier's. `codemap doctor` asks griffe and jedi
277
+ one question each with a known answer and exits 1 when one is absent or answers wrongly
278
+ ([accuracy.md](docs/accuracy.md#what-this-means-for-trust)); rebuilding over the previous graph of the
279
+ same tree (`build -o` on the existing file) names the classes of edges that shrank
280
+ ([provenance.md](docs/provenance.md#fewer-edges-on-the-same-input-is-named)).
281
+
269
282
  **How many times to build.** A consumer measured 175 full deep builds of one tree and found a real
270
283
  edge present in **75 %** of them; eight builds here agreed (5 of 8, the same edge, and nothing else
271
284
  unstable among 13 675). So one build misses such an edge about 1 time in 4, two builds 1 in 16, three
@@ -8,7 +8,7 @@ and a **SCIP index** for interop with Sourcegraph / Glean and other precise-code
8
8
 
9
9
  [![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.13, **972 tests with no failures on
11
+ **Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **1054 tests with no failures on
12
12
  Python 3.11–3.14** ([in CI](docs/ci.md): the full suite including the dogfood pass, a determinism check, a
13
13
  wheel smoke test, and ctags/SCIP interop against the real CLIs), warm serve surface with 31 ops (28 exposed
14
14
  as MCP tools), and SCIP export. See **[DESIGN.md](DESIGN.md)** (product design &
@@ -185,8 +185,14 @@ codemap serve --graph graph.json --watch
185
185
  - **Dataflow** — producers/consumers of a string-keyed DataFrame column.
186
186
  - **Semantic search** (opt-in) — a concept query routed to an external adapter, with each fuzzy hit
187
187
  resolved to the exact codemap symbol at its location (`codemap semantic`). See [docs/integrations.md](docs/integrations.md).
188
- - **Context pack** — the most relevant slice of the graph under a token budget, ranked by importance or
189
- by relevance to seed symbols (`codemap pack --budget N [--seed X]`). See [docs/pack.md](docs/pack.md).
188
+ - **Context pack** — the highest-ranked modules, classes and functions under a token budget, by
189
+ importance or by relevance to seed symbols (`codemap pack --budget N [--seed X]`). With a seed it
190
+ walks both ways — what the seed uses and what uses it (`--direction uses` for one way) — documents are
191
+ never in it, the answer says which walk it took, and a seed that resolves to nothing is refused rather
192
+ than silently answered globally. Deterministic and budget-tested. Against git history, seeded with a
193
+ changed file, it puts 42 % and 60 % of the co-changed files in its first ten on two repositories —
194
+ ahead of `impact` and of grep by names, though on one of the two not beyond the noise; grep still
195
+ finds tests best. **Whether it helps an agent is not measured.** See [docs/pack.md](docs/pack.md).
190
196
  - **Interop** — export the graph as a [SCIP](https://scip-code.org/) index (definitions + symbol
191
197
  info + inherits/implements relationships) so Sourcegraph, Glean and other SCIP consumers can drive
192
198
  go-to-definition, symbol search and type hierarchy over it. See [docs/export.md](docs/export.md).
@@ -230,6 +236,13 @@ build says so in its own diagnostics, and `codemap diff` says so when both sides
230
236
  delta of a few call edges as possible tool noise before reading it as a change in the code. Anything
231
237
  that must be reproducible byte-for-byte — a gate, a two-release comparison — belongs on the fast tier.
232
238
 
239
+ **A deep build does not check jedi.** With jedi answering nothing, a deep build still exits 0 and
240
+ declares `tier: deep` — and its call edges equal the fast tier's. `codemap doctor` asks griffe and jedi
241
+ one question each with a known answer and exits 1 when one is absent or answers wrongly
242
+ ([accuracy.md](docs/accuracy.md#what-this-means-for-trust)); rebuilding over the previous graph of the
243
+ same tree (`build -o` on the existing file) names the classes of edges that shrank
244
+ ([provenance.md](docs/provenance.md#fewer-edges-on-the-same-input-is-named)).
245
+
233
246
  **How many times to build.** A consumer measured 175 full deep builds of one tree and found a real
234
247
  edge present in **75 %** of them; eight builds here agreed (5 of 8, the same edge, and nothing else
235
248
  unstable among 13 675). So one build misses such an edge about 1 time in 4, two builds 1 in 16, three
@@ -57,6 +57,11 @@ class ArchitectureContract:
57
57
  no_lazy_cycles: bool = False
58
58
  no_type_only_cycles: bool = False
59
59
  exhaustive: bool = False
60
+ # R1-C62: the mirror of `exhaustive`. That one asks "is every layer of the code
61
+ # declared?"; this asks "does every declared name exist?". Opt-in, because a contract
62
+ # written ahead of the code is a documented, legitimate use (see the module docstring)
63
+ # and failing on it by default would turn that practice red on somebody else's tree.
64
+ no_phantom_rules: bool = False
60
65
  error: str | None = None
61
66
  # R1-C35: the file this contract was looked for in. "No contract found" is only
62
67
  # actionable next to *where* we looked — a reader in the wrong directory cannot tell
@@ -66,7 +71,8 @@ class ArchitectureContract:
66
71
  def is_empty(self) -> bool:
67
72
  return not (self.layers or self.independent or self.forbidden
68
73
  or self.no_cycles or self.no_lazy_cycles
69
- or self.no_type_only_cycles or self.exhaustive)
74
+ or self.no_type_only_cycles or self.exhaustive
75
+ or self.no_phantom_rules)
70
76
 
71
77
 
72
78
  @dataclass(frozen=True)
@@ -117,9 +123,46 @@ def parse_contract(section: dict) -> ArchitectureContract:
117
123
  no_lazy_cycles=bool(section.get("no_lazy_cycles", False)),
118
124
  no_type_only_cycles=bool(section.get("no_type_only_cycles", False)),
119
125
  exhaustive=bool(section.get("exhaustive", False)),
126
+ no_phantom_rules=bool(section.get("no_phantom_rules", False)),
120
127
  )
121
128
 
122
129
 
130
+ def _graph_layers(query) -> set[str]:
131
+ """Layer names that exist in the core import graph."""
132
+ return {query._layer_of(m) for m in query.import_graph.nodes
133
+ if query.root_of(m) == "core"}
134
+
135
+
136
+ def applicability(query, contract: ArchitectureContract) -> dict:
137
+ """Per-rule *declared* vs *applicable* counts, plus the names that do not exist (R1-C62).
138
+
139
+ A rule naming a layer the graph does not contain is **inert by design** — no module, no
140
+ edge, nothing to break — which is what lets a contract be written ahead of the code. The
141
+ defect that made this function necessary is not the inertness: it is that the gate
142
+ counted such a rule as *enforced*, so a renamed layer left a green tick over a rule that
143
+ could no longer fire.
144
+
145
+ Applicability is presence of the **name**, nothing cleverer: `layers` needs the layer,
146
+ `independent` needs at least two members of the group (one cannot import itself), and
147
+ `forbidden` needs both ends. Whether a violation is even possible between two existing
148
+ layers is not asked, and should not be.
149
+ """
150
+ present = _graph_layers(query)
151
+ named = set(contract.layers) | {m for grp in contract.independent for m in grp}
152
+ named |= {n for pair in contract.forbidden for n in pair}
153
+ return {
154
+ "layers": {"declared": len(contract.layers),
155
+ "applicable": sum(1 for x in contract.layers if x in present)},
156
+ "independent": {"declared": len(contract.independent),
157
+ "applicable": sum(1 for g in contract.independent
158
+ if len([x for x in g if x in present]) >= 2)},
159
+ "forbidden": {"declared": len(contract.forbidden),
160
+ "applicable": sum(1 for (a, b) in contract.forbidden
161
+ if a in present and b in present)},
162
+ "absent_names": tuple(sorted(n for n in named if n not in present)),
163
+ }
164
+
165
+
123
166
  def _core_layer_edges(query) -> list[tuple[str, str, str, str]]:
124
167
  """Cross-layer core→core import edges as (importer, imported, layer_i, layer_j)."""
125
168
  ig = query.import_graph
@@ -266,4 +309,18 @@ def check_contract(query, contract: ArchitectureContract) -> list[Violation]:
266
309
  modules=tuple(undeclared),
267
310
  ))
268
311
 
312
+ # -- no_phantom_rules: every name the contract mentions must exist -----------
313
+ # R1-C62, the mirror of `exhaustive`. Off by default on purpose: inert rules are how a
314
+ # contract gets written ahead of the code. On, it catches the drift the count alone
315
+ # only *discloses* — a layer renamed while the contract kept guarding the old name.
316
+ if contract.no_phantom_rules:
317
+ absent = applicability(query, contract)["absent_names"]
318
+ if absent:
319
+ violations.append(Violation(
320
+ "no_phantom_rules",
321
+ f"{len(absent)} name(s) in the contract are absent from the graph, "
322
+ "so the rules naming them cannot apply",
323
+ modules=absent,
324
+ ))
325
+
269
326
  return violations
@@ -2,6 +2,8 @@
2
2
 
3
3
  codemap build <path> [-o graph.json] [--deep]
4
4
  [--consumer PATH ...] [--docs PATH ...] [--mode thin|full]
5
+ codemap doctor [--json]
6
+ probe griffe and jedi with a question whose answer is known (R1-C64)
5
7
  codemap scope <path> [--consumer PATH ...] [--docs PATH ...] [--no-git] [--json]
6
8
  | --diff A.meta.json B.meta.json → input scope manifest (M19.A)
7
9
  codemap query <name> (--graph g.json | --build <path>) [--format json|text]
@@ -30,7 +32,7 @@
30
32
  codemap.toml [integrations].enabled + the tool installed)
31
33
  codemap semantic <query> (--graph g.json | --build <path>) [--root DIR] [--limit N]
32
34
  semantic search via an opt-in adapter, enriched to codemap symbols (R1-C16)
33
- codemap pack (--graph g.json | --build <path>) [--budget N] [--seed X …]
35
+ codemap pack (--graph g.json | --build <path>) [--budget N] [--seed X …] [--direction both|uses]
34
36
  token-budgeted context pack: most relevant graph slice under N tokens (R1-C6)
35
37
  """
36
38
 
@@ -73,6 +75,23 @@ _REPORTS = {
73
75
  _REPORT_KINDS = sorted(_REPORTS) + ["impact", "communities", "flows"] # extra args
74
76
 
75
77
 
78
+ def _emit_answer(text: str) -> None:
79
+ """Print a markdown answer with the answer-format trailer (R1-C63).
80
+
81
+ One seam rather than a line in every renderer: the renderers stay pure functions of the
82
+ graph, and there is one place to forget rather than nine. The trailer is what a consumer
83
+ diffing our output sees when the *shape* of an answer changes deliberately — 0.0.20
84
+ changed the gate's lines while the graph stayed byte-identical, and on a text diff that
85
+ looked exactly like the #20 defect, where the text moved between runs of one version. The
86
+ only thing that told them apart was a message written by hand.
87
+ """
88
+ from codemap.model import ANSWER_FORMAT, SCHEMA_VERSION
89
+ print(text, end="")
90
+ if not text.endswith("\n"):
91
+ print()
92
+ print(f"_answer format {ANSWER_FORMAT} · schema {SCHEMA_VERSION}_")
93
+
94
+
76
95
  def _graph_from(args):
77
96
  if getattr(args, "build", None):
78
97
  return extract(args.build, deep=getattr(args, "deep", False))
@@ -138,6 +157,7 @@ def _cmd_build(args) -> int:
138
157
  text = " ".join(p for p in (d["message"], d.get("consequence")) if p)
139
158
  print(f"[{d.get('severity', 'warning')}] {text}", file=sys.stderr)
140
159
  if args.out:
160
+ _report_completeness_drop(args.out, graph)
141
161
  store.save(graph, args.out)
142
162
  # M18: record the build recipe beside the graph so `codemap refresh` can
143
163
  # rebuild it, and so the graph's age is meaningful to `serve`/stats.
@@ -158,6 +178,36 @@ def _cmd_build(args) -> int:
158
178
  return 0
159
179
 
160
180
 
181
+ def _report_completeness_drop(out: str, graph) -> None:
182
+ """Compare a fresh build with the graph it is about to overwrite (R1-C59).
183
+
184
+ The one moment both builds of an input are in hand without anyone asking: the previous
185
+ graph is still on disk at ``-o``. Read it before the write and say when an edge class
186
+ shrank on the **same** input — on one input that is the builder (tool, dependency,
187
+ sampling), never the code. A warning, not a refusal: rebuilding with a new tool is
188
+ legitimate, and the build still succeeds.
189
+
190
+ A file that exists and cannot be read is not a file that is absent (R1-C27), so it is
191
+ named; a missing file is the first build, and there is nothing to say.
192
+ """
193
+ from codemap.provenance import completeness_drops, describe_drops
194
+ if not Path(out).exists():
195
+ return
196
+ try:
197
+ previous = store.load(out)
198
+ except Exception as exc: # any unreadable previous graph — never fatal to a build
199
+ print(f"[note] the graph already at {out} could not be read "
200
+ f"({type(exc).__name__}); completeness against it not compared",
201
+ file=sys.stderr)
202
+ return
203
+ drops = completeness_drops(previous, graph)
204
+ if drops:
205
+ print(f"[warning] {describe_drops(drops)} — against the graph this build replaces. "
206
+ "Same input, fewer edges: the difference is the builder (the tool, a "
207
+ "dependency, or the deep tier's sampling), not the code; whatever reads "
208
+ "those edges now answers from less than before.", file=sys.stderr)
209
+
210
+
161
211
  def _inputs_with_membership(graph, scope, args):
162
212
  """The extractor's ``inputs`` block plus the manifest membership result (R1-C41).
163
213
 
@@ -316,6 +366,22 @@ def _cmd_watch(args) -> int:
316
366
  return 0
317
367
 
318
368
 
369
+ def _cmd_doctor(args) -> int:
370
+ """Probe each dependency with a question whose answer is known (R1-C64).
371
+
372
+ Exit 0 when every probe answers as expected, 1 otherwise. A dependency that imports
373
+ and answers wrongly is the case this exists for: it never breaks a build, it makes the
374
+ graph poorer while the graph still declares its tier.
375
+ """
376
+ from codemap.extract.probe import render, run_probes
377
+ report = run_probes()
378
+ if args.json:
379
+ print(json.dumps(report, indent=2, sort_keys=True, ensure_ascii=False))
380
+ else:
381
+ print(render(report), end="")
382
+ return 0 if report["ok"] else 1
383
+
384
+
319
385
  def _cmd_scope(args) -> int:
320
386
  """Resolve/print the input scope manifest, or diff two (M19.A)."""
321
387
  from codemap.scope import resolve_scope, diff_scopes
@@ -507,28 +573,28 @@ def _cmd_report(args) -> int:
507
573
  if args.kind == "impact":
508
574
  if not args.symbol:
509
575
  raise SystemExit("error: report impact needs --symbol <name>")
510
- print(render_impact(Query(graph), args.symbol, depth=args.depth,
511
- flow_depth=args.flow_depth), end="")
576
+ _emit_answer(render_impact(Query(graph), args.symbol, depth=args.depth,
577
+ flow_depth=args.flow_depth))
512
578
  return 0
513
579
  if args.kind in ("communities", "flows"):
514
580
  from codemap.serve.subsystems import render_communities, render_flows
515
581
  q = Query(graph)
516
582
  out = (render_communities(q) if args.kind == "communities"
517
583
  else render_flows(q, args.symbol, depth=args.depth))
518
- print(out, end="")
584
+ _emit_answer(out)
519
585
  return 0
520
586
  if args.kind == "dead-code":
521
587
  from codemap.serve.audit import load_dead_code_whitelist
522
588
  root = getattr(args, "source_root", None) or os.getcwd()
523
589
  whitelist, wl_error = load_dead_code_whitelist(root)
524
- print(render_dead_code(Query(graph),
590
+ _emit_answer(render_dead_code(Query(graph),
525
591
  whitelist=whitelist,
526
592
  min_confidence=args.min_confidence,
527
- whitelist_error=wl_error), end="")
593
+ whitelist_error=wl_error))
528
594
  return 0
529
595
  renderer = _REPORTS[args.kind]
530
596
  payload = renderer(graph) if args.kind == "api-surface" else renderer(Query(graph))
531
- print(payload, end="")
597
+ _emit_answer(payload)
532
598
  return 0
533
599
 
534
600
 
@@ -724,15 +790,21 @@ def _cmd_semantic(args) -> int:
724
790
 
725
791
  def _cmd_pack(args) -> int:
726
792
  """Token-budgeted context pack — most relevant graph slice under N tokens (R1-C6)."""
727
- from codemap.serve.pack import build_pack, render_pack
728
- q = Query(_graph_from(args))
793
+ from codemap.serve.pack import build_pack, render_payload
729
794
  seeds = tuple(args.seed or ())
795
+ if args.direction and not seeds: # before any build: a usage error costs nothing
796
+ print("codemap pack: --direction needs --seed — the global rank walks from no "
797
+ "seed", file=sys.stderr)
798
+ return 2
799
+ q = Query(_graph_from(args))
800
+ p = build_pack(q, budget=args.budget, seeds=seeds, direction=args.direction)
730
801
  if args.format == "json":
731
- print(json.dumps(build_pack(q, budget=args.budget, seeds=seeds),
732
- ensure_ascii=False, indent=2, sort_keys=True))
802
+ print(json.dumps(p, ensure_ascii=False, indent=2, sort_keys=True))
733
803
  else:
734
- print(render_pack(q, budget=args.budget, seeds=seeds), end="")
735
- return 0
804
+ print(render_payload(p, target=q.graph.target, seeds=seeds), end="")
805
+ # No seed resolved → a refusal, not an empty pack: exit 1, as `query` does on an
806
+ # unknown name (pack honesty).
807
+ return 1 if "error" in p else 0
736
808
 
737
809
 
738
810
  def _cmd_check(args) -> int:
@@ -755,13 +827,13 @@ def _cmd_check(args) -> int:
755
827
  # would sort an unreadable contract into the success branch of every `if rc == 2` that
756
828
  # already exists in someone's pipeline.
757
829
  if contract.error:
758
- print(render_check(q, contract, []), end="")
830
+ _emit_answer(render_check(q, contract, []))
759
831
  return 2
760
832
  if contract.is_empty() and args.require_contract:
761
- print(render_check(q, contract, []), end="")
833
+ _emit_answer(render_check(q, contract, []))
762
834
  raise SystemExit("error: no [architecture] contract found (--require-contract)")
763
835
  violations = check_contract(q, contract)
764
- print(render_check(q, contract, violations), end="")
836
+ _emit_answer(render_check(q, contract, violations))
765
837
  return 2 if violations else 0
766
838
 
767
839
 
@@ -778,7 +850,7 @@ def _cmd_diff(args) -> int:
778
850
  # R1-C25/D4: two graphs built by different tools are a before/after of the *tool*,
779
851
  # not of the code. Never a refusal — comparing across an upgrade is legitimate; what
780
852
  # was missing is being told, since a clean "no breaking changes" reads as proof.
781
- cmp = comparability(old.provenance, new.provenance)
853
+ cmp = comparability(old.provenance, new.provenance, old_graph=old, new_graph=new)
782
854
  if not cmp["comparable"]:
783
855
  for line in cmp["differences"]:
784
856
  print(f"[warning] {line}", file=sys.stderr)
@@ -930,6 +1002,11 @@ def build_parser() -> argparse.ArgumentParser:
930
1002
  "of the chain: a different N restarts it from a full build.")
931
1003
  b.set_defaults(func=_cmd_build)
932
1004
 
1005
+ dr = sub.add_parser("doctor", help="Probe griffe and jedi with a question whose answer is "
1006
+ "known; exit 1 if one is absent or answers wrongly.")
1007
+ dr.add_argument("--json", action="store_true", help="Structured report instead of text.")
1008
+ dr.set_defaults(func=_cmd_doctor)
1009
+
933
1010
  sc = sub.add_parser("scope", help="Resolve the input scope manifest (scope_id + profile), or --diff two.")
934
1011
  sc.add_argument("path", nargs="?", help="Package/dir to scope (like build's path).")
935
1012
  sc.add_argument("--consumer", action="append", metavar="PATH",
@@ -1077,6 +1154,9 @@ def build_parser() -> argparse.ArgumentParser:
1077
1154
  pk.add_argument("--seed", action="append", metavar="X",
1078
1155
  help="Bias relevance to this symbol / file (repeatable). "
1079
1156
  "Omit for global importance.")
1157
+ pk.add_argument("--direction", choices=["both", "uses"],
1158
+ help="Which way a seeded walk runs: both (default) — what the seeds "
1159
+ "use and what uses them; uses — only what they use.")
1080
1160
  pk.add_argument("--format", choices=["markdown", "json"], default="markdown")
1081
1161
  pk.set_defaults(func=_cmd_pack)
1082
1162
 
@@ -0,0 +1,152 @@
1
+ """Does each dependency still answer a known question correctly? (R1-C64)
2
+
3
+ griffe and jedi do work codemap never re-checks: griffe resolves re-export aliases into the
4
+ structure, jedi types a local variable into a call target. Both are hard dependencies, so the
5
+ only "check" they ever got was that ``import`` did not fail. That is the wrong check for the
6
+ failure that matters: a jedi that stopped resolving after an upgrade does not break a build —
7
+ ``_resolve_jedi`` reads any exception as ``unresolved`` — it turns every deep call into a
8
+ fast-tier one, and the graph is simply poorer. Measured: with ``jedi.Script`` answering
9
+ nothing, a deep build of a real 89-module tree exited 0 with ``calls`` equal to the fast tier's
10
+ to the edge, while still declaring ``tier: deep``.
11
+
12
+ The probe is the shape `jg doctor` uses (research/tools/jevgrep.md, S2): a synthetic input
13
+ whose correct answer is known, and a backend that answers it wrongly is refused rather than
14
+ counted as present. One fact per dependency, through the real pipeline — not a benchmark,
15
+ which would be noisy and get switched off.
16
+
17
+ Design: ``docs/design/dependency_probe.md``.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import importlib
23
+ import tempfile
24
+ from pathlib import Path
25
+
26
+ PROBE_PACKAGE = "codemap_probe"
27
+
28
+ #: The probe tree. ``engine.run()`` needs a local variable's type (jedi only — the fast tier
29
+ #: does not build that edge); ``ph()`` goes through a re-export alias whose target griffe
30
+ #: resolves (``Alias.target_path``), which is where the ``export`` edge comes from.
31
+ PROBE_FILES = {
32
+ "__init__.py": f"from {PROBE_PACKAGE}.core import helper as public_helper\n",
33
+ "core.py": ("class Engine:\n"
34
+ " def run(self):\n"
35
+ " return 1\n\n\n"
36
+ "def helper():\n"
37
+ " return 2\n"),
38
+ "app.py": (f"from {PROBE_PACKAGE} import public_helper as ph\n"
39
+ f"from {PROBE_PACKAGE}.core import Engine\n\n\n"
40
+ "def main():\n"
41
+ " engine = Engine()\n"
42
+ " engine.run()\n"
43
+ " return ph()\n"),
44
+ }
45
+
46
+ #: Outcomes. ``wrong`` is the case this module exists for: present, answering, mistaken.
47
+ OK, WRONG, ABSENT, ERROR, NOT_RUN = "ok", "wrong", "absent", "error", "not-run"
48
+
49
+ #: dependency → (edge type, source, target, resolution or None, what the fact means)
50
+ EXPECTED = {
51
+ "griffe": ("export", PROBE_PACKAGE, f"{PROBE_PACKAGE}.core.helper", None,
52
+ f"the re-export `{PROBE_PACKAGE}.public_helper` resolves to "
53
+ f"`{PROBE_PACKAGE}.core.helper`"),
54
+ "jedi": ("calls", f"{PROBE_PACKAGE}.app.main", f"{PROBE_PACKAGE}.core.Engine.run", "deep",
55
+ f"`engine.run()` on a local `Engine()` resolves to "
56
+ f"`{PROBE_PACKAGE}.core.Engine.run`"),
57
+ }
58
+
59
+
60
+ def write_probe(directory: str | Path) -> Path:
61
+ """Write the probe package under ``directory`` and return its path."""
62
+ pkg = Path(directory) / PROBE_PACKAGE
63
+ pkg.mkdir(parents=True, exist_ok=True)
64
+ for name, body in PROBE_FILES.items():
65
+ (pkg / name).write_text(body, encoding="utf-8")
66
+ return pkg
67
+
68
+
69
+ def _version(name: str) -> str | None:
70
+ from importlib.metadata import PackageNotFoundError, version
71
+ try:
72
+ return version(name)
73
+ except PackageNotFoundError:
74
+ return None
75
+
76
+
77
+ def holds(graph, dependency: str) -> bool:
78
+ """Is the dependency's fact an edge of ``graph``?"""
79
+ etype, source, target, resolution, _ = EXPECTED[dependency]
80
+ return any(e.type == etype and e.source == source and e.target == target
81
+ and (resolution is None or e.extras.get("resolution") == resolution)
82
+ for e in graph.edges)
83
+
84
+
85
+ def _expected_str(dependency: str) -> str:
86
+ etype, source, target, resolution, _ = EXPECTED[dependency]
87
+ return f"{etype} {source} → {target}" + (f" ({resolution})" if resolution else "")
88
+
89
+
90
+ def _row(dependency: str, status: str, detail: str) -> dict:
91
+ return {"dependency": dependency, "version": _version(dependency), "status": status,
92
+ "fact": EXPECTED[dependency][4], "expected": _expected_str(dependency),
93
+ "detail": detail}
94
+
95
+
96
+ def _importable(name: str) -> str | None:
97
+ """``None`` when ``name`` imports; otherwise why it does not."""
98
+ try:
99
+ importlib.import_module(name)
100
+ except Exception as exc: # ImportError, or a broken install raising anything else
101
+ return f"not importable ({type(exc).__name__}: {exc})"
102
+ return None
103
+
104
+
105
+ def _check(dependency: str, build) -> tuple[dict, bool]:
106
+ """Run one probe. Returns the row and whether its build produced a graph at all."""
107
+ why = _importable(dependency)
108
+ if why:
109
+ return _row(dependency, ABSENT, why), False
110
+ try:
111
+ graph = build()
112
+ except Exception as exc:
113
+ return _row(dependency, ERROR, f"the probe build raised {type(exc).__name__}: {exc}"), False
114
+ if holds(graph, dependency):
115
+ return _row(dependency, OK, "answered as expected"), True
116
+ return _row(dependency, WRONG, f"expected edge `{_expected_str(dependency)}` is not in "
117
+ "the graph — the dependency imports and answers, and the "
118
+ "answer is wrong"), True
119
+
120
+
121
+ def run_probes() -> dict:
122
+ """Build the probe twice (fast for griffe, deep for jedi) and grade each dependency.
123
+
124
+ jedi's probe stands on griffe's structure: when the fast build already failed, a deep
125
+ result would mean nothing, so jedi is ``not-run`` — unknown, not wrong.
126
+ """
127
+ from codemap.extract import extract
128
+ with tempfile.TemporaryDirectory(prefix="codemap-probe-") as tmp:
129
+ pkg = write_probe(tmp)
130
+ griffe_row, built = _check("griffe", lambda: extract(str(pkg)))
131
+ if not built:
132
+ jedi_row = _row("jedi", NOT_RUN, "not run: the structural pass it stands on "
133
+ f"did not produce a graph ({griffe_row['status']})")
134
+ else:
135
+ jedi_row, _ = _check("jedi", lambda: extract(str(pkg), deep=True))
136
+ rows = [griffe_row, jedi_row]
137
+ return {"ok": all(r["status"] == OK for r in rows), "probes": rows}
138
+
139
+
140
+ def render(report: dict) -> str:
141
+ """Plain text, one line per dependency, then a verdict line."""
142
+ lines = ["codemap doctor — does each dependency still answer a known question correctly?", ""]
143
+ for r in report["probes"]:
144
+ name = f"{r['dependency']} {r['version'] or '(version unknown)'}"
145
+ lines.append(f"{name:<22} {r['status'].upper():<8} {r['fact']}")
146
+ if r["status"] != OK:
147
+ lines.append(f"{'':<22} {'':<8} {r['detail']}")
148
+ lines.append("")
149
+ lines.append("✅ every probe answered as expected." if report["ok"] else
150
+ "❌ at least one dependency is absent or answers wrongly — a graph built "
151
+ "here may be poorer than its tier declares.")
152
+ return "\n".join(lines) + "\n"
@@ -77,6 +77,34 @@ from typing import Any
77
77
  # (they had ``lineno`` alone, so `search` answered a line number with no file).
78
78
  SCHEMA_VERSION = "0.13"
79
79
 
80
+ # R1-C63: the *answer* has a shape too, and consumers diff it. `SCHEMA_VERSION` versions the
81
+ # artifact; 0.0.20 left the graph byte-identical and changed the architecture report and the
82
+ # gate's lines completely (a cycle answer became a tangle), which on a text diff is
83
+ # indistinguishable from the #20 defect — where the text moved between runs of one version,
84
+ # without anyone deciding it. The only thing that told the consumer apart was a message I
85
+ # wrote by hand.
86
+ #
87
+ # Bump on a change to the **structure a consumer sees**: a block appears or vanishes, a
88
+ # counted unit changes meaning, a verdict or heading changes shape, a field in a structured
89
+ # answer changes type or semantics. Do **not** bump for wording, typos or added prose inside
90
+ # an existing note — a version that ticks on whitespace stops being read, which is worse than
91
+ # not having one. Independent of SCHEMA_VERSION by design: the graph can be identical while
92
+ # this moves, and that is the case it exists for.
93
+ #
94
+ # Starts at 1 with 0.0.21 and makes no claim about earlier releases: the answer format changed
95
+ # many times before it, and numbering that retroactively would be inventing a history.
96
+ #
97
+ # History:
98
+ # 1 — 0.0.21: first numbered format.
99
+ # 2 — pack honesty: `pack` carries `seeds`, `unreachable_from_seeds` and `coverage`
100
+ # blocks, and a seed set that resolves to nothing is a refusal instead of the
101
+ # global pack (docs/design/pack_honesty.md). Before 2 was released, a seeded
102
+ # pack also began walking both ways by default — what the seeds use and what
103
+ # uses them — with `coverage.walk` naming the walk (R1-C66,
104
+ # docs/design/pack_direction.md). Not a separate number: no release ever
105
+ # answered 2 without it.
106
+ ANSWER_FORMAT = 2
107
+
80
108
  # Closed vocabulary of edge types (R1-C7). Node ``kind`` is deliberately an OPEN set
81
109
  # (DESIGN §2 — new entity kinds may appear), but edges are TYPED: every relationship
82
110
  # codemap emits is one of these, each with a fixed meaning. This is the machine-