codmap 0.0.11__tar.gz → 0.0.12__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 (129) hide show
  1. {codmap-0.0.11/codmap.egg-info → codmap-0.0.12}/PKG-INFO +6 -4
  2. {codmap-0.0.11 → codmap-0.0.12}/README.md +5 -3
  3. {codmap-0.0.11 → codmap-0.0.12}/codemap/cli.py +32 -16
  4. {codmap-0.0.11 → codmap-0.0.12}/codemap/extract/griffe_extractor.py +13 -5
  5. {codmap-0.0.11 → codmap-0.0.12}/codemap/incremental.py +63 -18
  6. {codmap-0.0.11 → codmap-0.0.12}/codemap/model.py +7 -0
  7. {codmap-0.0.11 → codmap-0.0.12}/codemap/query.py +8 -0
  8. {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/impact.py +3 -1
  9. {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/session.py +94 -10
  10. {codmap-0.0.11 → codmap-0.0.12/codmap.egg-info}/PKG-INFO +6 -4
  11. {codmap-0.0.11 → codmap-0.0.12}/codmap.egg-info/SOURCES.txt +2 -0
  12. {codmap-0.0.11 → codmap-0.0.12}/pyproject.toml +1 -1
  13. codmap-0.0.12/tests/test_r1c44_absent_answers.py +197 -0
  14. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c45_repeat_union.py +6 -6
  15. codmap-0.0.12/tests/test_r1c47_incremental_resample.py +155 -0
  16. {codmap-0.0.11 → codmap-0.0.12}/LICENSE +0 -0
  17. {codmap-0.0.11 → codmap-0.0.12}/codemap/__init__.py +0 -0
  18. {codmap-0.0.11 → codmap-0.0.12}/codemap/apidiff.py +0 -0
  19. {codmap-0.0.11 → codmap-0.0.12}/codemap/arch.py +0 -0
  20. {codmap-0.0.11 → codmap-0.0.12}/codemap/diagnostics.py +0 -0
  21. {codmap-0.0.11 → codmap-0.0.12}/codemap/extract/__init__.py +0 -0
  22. {codmap-0.0.11 → codmap-0.0.12}/codemap/extract/attrflow.py +0 -0
  23. {codmap-0.0.11 → codmap-0.0.12}/codemap/extract/behavior.py +0 -0
  24. {codmap-0.0.11 → codmap-0.0.12}/codemap/extract/dataflow.py +0 -0
  25. {codmap-0.0.11 → codmap-0.0.12}/codemap/extract/dispatch.py +0 -0
  26. {codmap-0.0.11 → codmap-0.0.12}/codemap/extract/gsource.py +0 -0
  27. {codmap-0.0.11 → codmap-0.0.12}/codemap/extract/roots.py +0 -0
  28. {codmap-0.0.11 → codmap-0.0.12}/codemap/extract/union.py +0 -0
  29. {codmap-0.0.11 → codmap-0.0.12}/codemap/freshness.py +0 -0
  30. {codmap-0.0.11 → codmap-0.0.12}/codemap/integrations/__init__.py +0 -0
  31. {codmap-0.0.11 → codmap-0.0.12}/codemap/integrations/base.py +0 -0
  32. {codmap-0.0.11 → codmap-0.0.12}/codemap/integrations/cocoindex.py +0 -0
  33. {codmap-0.0.11 → codmap-0.0.12}/codemap/integrations/gate.py +0 -0
  34. {codmap-0.0.11 → codmap-0.0.12}/codemap/integrations/gitnexus.py +0 -0
  35. {codmap-0.0.11 → codmap-0.0.12}/codemap/integrations/registry.py +0 -0
  36. {codmap-0.0.11 → codmap-0.0.12}/codemap/integrations/transport.py +0 -0
  37. {codmap-0.0.11 → codmap-0.0.12}/codemap/provenance.py +0 -0
  38. {codmap-0.0.11 → codmap-0.0.12}/codemap/scope.py +0 -0
  39. {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/__init__.py +0 -0
  40. {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/_scip_pb2.py +0 -0
  41. {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/api_surface.py +0 -0
  42. {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/apidiff.py +0 -0
  43. {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/architecture.py +0 -0
  44. {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/audit.py +0 -0
  45. {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/check.py +0 -0
  46. {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/ctags.py +0 -0
  47. {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/limits.py +0 -0
  48. {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/livingdocs.py +0 -0
  49. {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/mcp_server.py +0 -0
  50. {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/mermaid.py +0 -0
  51. {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/pack.py +0 -0
  52. {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/rag.py +0 -0
  53. {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/review.py +0 -0
  54. {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/scip.py +0 -0
  55. {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/semantic.py +0 -0
  56. {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/server.py +0 -0
  57. {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/subsystems.py +0 -0
  58. {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/vault.py +0 -0
  59. {codmap-0.0.11 → codmap-0.0.12}/codemap/store.py +0 -0
  60. {codmap-0.0.11 → codmap-0.0.12}/codemap/tomlio.py +0 -0
  61. {codmap-0.0.11 → codmap-0.0.12}/codemap/watch.py +0 -0
  62. {codmap-0.0.11 → codmap-0.0.12}/codmap.egg-info/dependency_links.txt +0 -0
  63. {codmap-0.0.11 → codmap-0.0.12}/codmap.egg-info/entry_points.txt +0 -0
  64. {codmap-0.0.11 → codmap-0.0.12}/codmap.egg-info/requires.txt +0 -0
  65. {codmap-0.0.11 → codmap-0.0.12}/codmap.egg-info/top_level.txt +0 -0
  66. {codmap-0.0.11 → codmap-0.0.12}/setup.cfg +0 -0
  67. {codmap-0.0.11 → codmap-0.0.12}/tests/test_epistemic.py +0 -0
  68. {codmap-0.0.11 → codmap-0.0.12}/tests/test_gitnexus_router.py +0 -0
  69. {codmap-0.0.11 → codmap-0.0.12}/tests/test_impact_depth.py +0 -0
  70. {codmap-0.0.11 → codmap-0.0.12}/tests/test_integrations.py +0 -0
  71. {codmap-0.0.11 → codmap-0.0.12}/tests/test_issue3_serve_freshness.py +0 -0
  72. {codmap-0.0.11 → codmap-0.0.12}/tests/test_livingdocs.py +0 -0
  73. {codmap-0.0.11 → codmap-0.0.12}/tests/test_m0_api_surface.py +0 -0
  74. {codmap-0.0.11 → codmap-0.0.12}/tests/test_m11_argcontract.py +0 -0
  75. {codmap-0.0.11 → codmap-0.0.12}/tests/test_m12_dataflow.py +0 -0
  76. {codmap-0.0.11 → codmap-0.0.12}/tests/test_m15_review.py +0 -0
  77. {codmap-0.0.11 → codmap-0.0.12}/tests/test_m16_architecture.py +0 -0
  78. {codmap-0.0.11 → codmap-0.0.12}/tests/test_m17_mcp.py +0 -0
  79. {codmap-0.0.11 → codmap-0.0.12}/tests/test_m18_freshness.py +0 -0
  80. {codmap-0.0.11 → codmap-0.0.12}/tests/test_m1_5_semantics.py +0 -0
  81. {codmap-0.0.11 → codmap-0.0.12}/tests/test_m1_query.py +0 -0
  82. {codmap-0.0.11 → codmap-0.0.12}/tests/test_m2_views.py +0 -0
  83. {codmap-0.0.11 → codmap-0.0.12}/tests/test_m32_watch.py +0 -0
  84. {codmap-0.0.11 → codmap-0.0.12}/tests/test_m3_serve.py +0 -0
  85. {codmap-0.0.11 → codmap-0.0.12}/tests/test_m4_behavior.py +0 -0
  86. {codmap-0.0.11 → codmap-0.0.12}/tests/test_m5_deep.py +0 -0
  87. {codmap-0.0.11 → codmap-0.0.12}/tests/test_m6_repo_scope.py +0 -0
  88. {codmap-0.0.11 → codmap-0.0.12}/tests/test_m7_dispatch.py +0 -0
  89. {codmap-0.0.11 → codmap-0.0.12}/tests/test_m9_family.py +0 -0
  90. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c13_callgraph_accuracy.py +0 -0
  91. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c13_grep_vs_graph.py +0 -0
  92. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c16_semantic.py +0 -0
  93. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c20_attribute_edges.py +0 -0
  94. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c21_flat_layout.py +0 -0
  95. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c22_source_visible_refs.py +0 -0
  96. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c23_hard_python.py +0 -0
  97. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c24_test_mapping.py +0 -0
  98. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c25_provenance.py +0 -0
  99. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c26_deep_union.py +0 -0
  100. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c27_config_honesty.py +0 -0
  101. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c28_limit_envelope.py +0 -0
  102. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c29_lazy_imports.py +0 -0
  103. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c2_ctags.py +0 -0
  104. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c30_local_import_calls.py +0 -0
  105. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c30f1_reexport_calls.py +0 -0
  106. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c30f2_check_scope.py +0 -0
  107. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c31_path_origin.py +0 -0
  108. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c32_report_json.py +0 -0
  109. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c33_query_signature.py +0 -0
  110. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c34_signature_kinds.py +0 -0
  111. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c35_contract_location.py +0 -0
  112. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c36_target_resolution.py +0 -0
  113. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c37_rule_mutation.py +0 -0
  114. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c38_tool_drift.py +0 -0
  115. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c3_arch_contract.py +0 -0
  116. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c3_dogfood.py +0 -0
  117. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c41_scope_membership.py +0 -0
  118. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c42_deep_tier_stability.py +0 -0
  119. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c43_incremental_splice.py +0 -0
  120. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c46_shadowed_definitions.py +0 -0
  121. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c4_complexity.py +0 -0
  122. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c5_apidiff.py +0 -0
  123. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c6_pack.py +0 -0
  124. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c7_edge_vocab.py +0 -0
  125. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c8_deadcode.py +0 -0
  126. {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c9_incremental.py +0 -0
  127. {codmap-0.0.11 → codmap-0.0.12}/tests/test_scip_export.py +0 -0
  128. {codmap-0.0.11 → codmap-0.0.12}/tests/test_scope.py +0 -0
  129. {codmap-0.0.11 → codmap-0.0.12}/tests/test_subsystems.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codmap
3
- Version: 0.0.11
3
+ Version: 0.0.12
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
@@ -161,7 +161,7 @@ Optional extras: `mcp` (Model Context Protocol server), `scip` (protobuf, for SC
161
161
  ## Quickstart
162
162
 
163
163
  ```bash
164
- # build the canonical graph of a package
164
+ # build the canonical graph of a package (its directory name is the package name every id starts with)
165
165
  codemap build ./yourpkg -o graph.json
166
166
 
167
167
  # repo-scoped: add consumers (tests/examples) + docs for blast-radius/impact
@@ -271,8 +271,10 @@ unstable among 13 675). So one build misses such an edge about 1 time in 4, two
271
271
  1 in 64. `codemap build --deep --repeat N` builds N samples — each in a fresh interpreter, the
272
272
  regime the share was measured in; in-process repeats come in correlated streaks — and unions them: an edge seen in fewer
273
273
  than N runs carries `extras.seen`, `provenance.samples` records N and how many edges varied, and the
274
- note on the graph says both. Refused out loud on the fast tier (byte-stable already) and together with
275
- `--incremental` (a spliced graph cannot be resampled).
274
+ note on the graph says both. Refused out loud on the fast tier (byte-stable already). With `--incremental`
275
+ and `watch`, N becomes a property of the chain and every place the chain samples — base, fallback, each
276
+ tick's recompute — samples N times; measured on twenty real commits, that is what removes the chain's misses,
277
+ and a periodic full rebuild does not.
276
278
 
277
279
  **Measurement:** [gaps/deep_tier_nondeterminism_2026-09-02.md](gaps/deep_tier_nondeterminism_2026-09-02.md),
278
280
  [gaps/deep_tier_union_by_repeat_2026-09-04.md](gaps/deep_tier_union_by_repeat_2026-09-04.md).
@@ -125,7 +125,7 @@ Optional extras: `mcp` (Model Context Protocol server), `scip` (protobuf, for SC
125
125
  ## Quickstart
126
126
 
127
127
  ```bash
128
- # build the canonical graph of a package
128
+ # build the canonical graph of a package (its directory name is the package name every id starts with)
129
129
  codemap build ./yourpkg -o graph.json
130
130
 
131
131
  # repo-scoped: add consumers (tests/examples) + docs for blast-radius/impact
@@ -235,8 +235,10 @@ unstable among 13 675). So one build misses such an edge about 1 time in 4, two
235
235
  1 in 64. `codemap build --deep --repeat N` builds N samples — each in a fresh interpreter, the
236
236
  regime the share was measured in; in-process repeats come in correlated streaks — and unions them: an edge seen in fewer
237
237
  than N runs carries `extras.seen`, `provenance.samples` records N and how many edges varied, and the
238
- note on the graph says both. Refused out loud on the fast tier (byte-stable already) and together with
239
- `--incremental` (a spliced graph cannot be resampled).
238
+ note on the graph says both. Refused out loud on the fast tier (byte-stable already). With `--incremental`
239
+ and `watch`, N becomes a property of the chain and every place the chain samples — base, fallback, each
240
+ tick's recompute — samples N times; measured on twenty real commits, that is what removes the chain's misses,
241
+ and a periodic full rebuild does not.
240
242
 
241
243
  **Measurement:** [gaps/deep_tier_nondeterminism_2026-09-02.md](gaps/deep_tier_nondeterminism_2026-09-02.md),
242
244
  [gaps/deep_tier_union_by_repeat_2026-09-04.md](gaps/deep_tier_union_by_repeat_2026-09-04.md).
@@ -85,10 +85,9 @@ def _cmd_build(args) -> int:
85
85
  incr_info = None
86
86
  repeat = getattr(args, "repeat", None)
87
87
  repeat = 1 if repeat is None else repeat
88
- # R1-C45 / D7–D8: refused out loud, never swallowed. The fast tier is byte-stable and
89
- # CI pins it, so N runs there buy nothing and cost N minutes; and an incremental graph's
90
- # spliced part is an earlier build's sample by definition (R1-C43), so resampling only
91
- # the affected modules would leave `seen` relative to different N on neighbouring edges.
88
+ # R1-C45 / D7: refused out loud, never swallowed. The fast tier is byte-stable and CI
89
+ # pins it, so N runs there buy nothing and cost N minutes. With --incremental the flag
90
+ # is allowed since R1-C47: N is a property of the chain, and a different N restarts it.
92
91
  if repeat < 1:
93
92
  print("error: --repeat must be >= 1", file=sys.stderr)
94
93
  return 2
@@ -96,12 +95,13 @@ def _cmd_build(args) -> int:
96
95
  print(f"error: --repeat {repeat} needs --deep — the fast tier is byte-stable, so "
97
96
  "repeating it unions identical samples", file=sys.stderr)
98
97
  return 2
99
- if repeat > 1 and getattr(args, "incremental", False):
100
- print(f"error: --repeat {repeat} cannot be combined with --incremental — the spliced "
101
- "part of an incremental graph is an earlier build's sample and cannot be "
102
- "resampled; run a full build", file=sys.stderr)
103
- return 2
104
98
  if args.consumer or args.docs:
99
+ if getattr(args, "incremental", False):
100
+ # R1-C44 / D6: the flag used to be swallowed here without a word. Not an
101
+ # error — a stated boundary: the incremental path is single-package.
102
+ print("[note] --incremental was not applied: --consumer/--docs make this a "
103
+ "repo-scoped build, which is always full (the incremental path is "
104
+ "single-package only)", file=sys.stderr)
105
105
  graph = extract_repo(
106
106
  args.path,
107
107
  consumers=tuple(args.consumer or ()),
@@ -201,12 +201,15 @@ def _incremental_build(args):
201
201
  old_meta = read_meta(args.out)
202
202
  old_scope = (old_meta or {}).get("scope")
203
203
  if not (Path(args.out).exists() and old_scope):
204
- return extract(args.path, deep=args.deep), {"mode": "full", "affected": []}
204
+ return extract(args.path, deep=args.deep, repeat=getattr(args, "repeat", None) or 1), \
205
+ {"mode": "full", "affected": []}
205
206
  old_graph = store.load(args.out)
206
207
  new_scope = resolve_scope(args.path)
208
+ repeat = getattr(args, "repeat", None) or 1
207
209
  if old_graph.target != Path(args.path).resolve().name:
208
- return extract(args.path, deep=args.deep), {"mode": "full", "affected": []}
209
- return update_graph(old_graph, args.path, old_scope, new_scope, deep=args.deep)
210
+ return extract(args.path, deep=args.deep, repeat=repeat), {"mode": "full", "affected": []}
211
+ return update_graph(old_graph, args.path, old_scope, new_scope, deep=args.deep,
212
+ repeat=repeat)
210
213
 
211
214
 
212
215
  def _watch_build_argv(args) -> list[str]:
@@ -219,6 +222,8 @@ def _watch_build_argv(args) -> list[str]:
219
222
  argv = ["build", args.path, "-o", args.out, "--incremental"]
220
223
  if args.deep:
221
224
  argv.append("--deep")
225
+ if (getattr(args, "repeat", None) or 1) > 1:
226
+ argv += ["--repeat", str(args.repeat)] # R1-C47: N is a property of the chain
222
227
  for c in args.consumer or ():
223
228
  argv += ["--consumer", c]
224
229
  for d in args.docs or ():
@@ -261,6 +266,7 @@ def _cmd_watch(args) -> int:
261
266
  build_args = _argparse.Namespace(
262
267
  path=args.path, out=args.out, deep=args.deep, consumer=args.consumer,
263
268
  docs=args.docs, mode=args.mode, incremental=True,
269
+ repeat=getattr(args, "repeat", None) or 1,
264
270
  _argv=_watch_build_argv(args))
265
271
 
266
272
  reported: set[str] = set()
@@ -891,7 +897,9 @@ def build_parser() -> argparse.ArgumentParser:
891
897
  sub = p.add_subparsers(dest="command", required=True)
892
898
 
893
899
  b = sub.add_parser("build", help="Build the canonical graph from a package path.")
894
- b.add_argument("path", help="Path to the package directory (holds __init__.py).")
900
+ b.add_argument("path", help="Path to the package directory (holds __init__.py). Its "
901
+ "directory name is the package name every node id starts "
902
+ "with — a copy under another name answers to that name.")
895
903
  b.add_argument("-o", "--out", help="Write graph.json here (default: stdout JSON).")
896
904
  b.add_argument("--deep", action="store_true",
897
905
  help="Deep call resolution via jedi (richer, ~1 min; default fast).")
@@ -904,14 +912,17 @@ def build_parser() -> argparse.ArgumentParser:
904
912
  help="Consumer granularity: thin=per-file (default), full=per-function.")
905
913
  b.add_argument("--incremental", action="store_true",
906
914
  help="Reuse an existing --out graph + its scope sidecar: recompute "
907
- "only changed modules (R1-C9). Identical to a full build; much "
908
- "faster on --deep. Single-package only (no --consumer/--docs).")
915
+ "only changed modules (R1-C9). Byte-identical to a full build on "
916
+ "the fast tier; on --deep it carries the earlier build's jedi "
917
+ "sample forward (docs/incremental.md). Much faster on --deep. "
918
+ "Single-package only (no --consumer/--docs).")
909
919
  b.add_argument("--repeat", type=int, default=1, metavar="N",
910
920
  help="--deep only: run the jedi layer N times and union the samples. "
911
921
  "One deep build is one sample (a real edge was present in 75%% "
912
922
  "of single builds, measured); an edge seen in fewer than N runs "
913
923
  "carries extras.seen, provenance.samples records N. Costs N× the "
914
- "jedi time. Not combinable with --incremental.")
924
+ "jedi time, in parallel. With --incremental N becomes a property "
925
+ "of the chain: a different N restarts it from a full build.")
915
926
  b.set_defaults(func=_cmd_build)
916
927
 
917
928
  sc = sub.add_parser("scope", help="Resolve the input scope manifest (scope_id + profile), or --diff two.")
@@ -1015,6 +1026,11 @@ def build_parser() -> argparse.ArgumentParser:
1015
1026
  w.add_argument("--docs", action="append", metavar="PATH", help="Docs root. Repeatable.")
1016
1027
  w.add_argument("--mode", choices=["thin", "full"], default="thin",
1017
1028
  help="Consumer granularity (as for `build`).")
1029
+ w.add_argument("--repeat", type=int, default=1, metavar="N",
1030
+ help="--deep only: sample the jedi layer N times on every build this "
1031
+ "loop makes — base, fallback and each tick's recompute (R1-C47). "
1032
+ "Measured: an incremental chain's misses come from single samples, "
1033
+ "not from age.")
1018
1034
  w.add_argument("--cycles", type=int, default=None,
1019
1035
  help="Stop after N polls instead of running forever (CI / testing).")
1020
1036
  _add_poll_options(
@@ -140,15 +140,22 @@ def add_behavioral_layer(graph, root, module_name, search_path, *, deep: bool,
140
140
  only=attr_only)
141
141
 
142
142
 
143
- def _sample_worker(package_path: str, deep: bool) -> dict:
144
- """One full extraction in a child interpreter; returns the graph as a dict (R1-C45)."""
143
+ def _sample_worker(package_path: str, deep: bool, only=None) -> dict:
144
+ """One extraction in a child interpreter; returns the graph as a dict (R1-C45).
145
+
146
+ ``only`` (a set of module paths) restricts the two jedi passes to those modules —
147
+ the incremental hook (R1-C9), so a chain can resample just what a tick touched
148
+ (R1-C47). The structural base is rebuilt in every worker regardless: cheap,
149
+ deterministic, and cheaper than shipping it across processes.
150
+ """
145
151
  graph, root, module_name, search_path = build_structural(package_path)
146
- add_behavioral_layer(graph, root, module_name, search_path, deep=deep)
152
+ add_behavioral_layer(graph, root, module_name, search_path, deep=deep,
153
+ behavior_only=only, attr_only=only)
147
154
  return graph.to_dict()
148
155
 
149
156
 
150
157
  def collect_samples(package_path, *, deep: bool, runs: int,
151
- workers: int | None = None) -> list[Graph]:
158
+ workers: int | None = None, only=None) -> list[Graph]:
152
159
  """``runs`` independent samples of ``package_path``, each in a fresh interpreter.
153
160
 
154
161
  Lives here, beside the two functions the worker calls, so ``extract/union.py`` stays
@@ -161,7 +168,8 @@ def collect_samples(package_path, *, deep: bool, runs: int,
161
168
  ctx = multiprocessing.get_context("spawn")
162
169
  procs = max(1, min(runs, workers or os.cpu_count() or 1))
163
170
  with ctx.Pool(processes=procs) as pool:
164
- dicts = pool.starmap(_sample_worker, [(str(package_path), deep)] * runs)
171
+ dicts = pool.starmap(_sample_worker,
172
+ [(str(package_path), deep, set(only) if only else None)] * runs)
165
173
  samples = []
166
174
  for d in dicts:
167
175
  g = Graph.from_dict(d)
@@ -54,10 +54,10 @@ import copy
54
54
  from pathlib import Path
55
55
 
56
56
  from codemap.extract.griffe_extractor import (
57
- add_behavioral_layer, build_structural, extract,
57
+ add_behavioral_layer, build_structural, collect_samples, extract,
58
58
  )
59
- from codemap.extract.union import BEHAVIOR_CALL_RES
60
- from codemap.model import Graph
59
+ from codemap.extract.union import BEHAVIOR_CALL_RES, merge_samples
60
+ from codemap.model import SPLICED_EDGE_TYPES, Graph
61
61
  from codemap.provenance import build_provenance, tool_identity
62
62
  from codemap.scope import diff_scopes
63
63
 
@@ -75,8 +75,9 @@ _BEHAVIOR_REF_RES = frozenset({"name", "annotation"})
75
75
  # it — or the fast tier's byte-identity to a full build (R1-C9) breaks on it.
76
76
  _BEHAVIORAL_EXTRAS = ("calls", "control", "complexity", "attr_access", "shadows")
77
77
  # Old edge types whose target landing in a changed/removed module makes the source
78
- # module stale (it must re-resolve). Only the spliced passes matter here.
79
- _DEP_EDGE_TYPES = frozenset({"calls", "accesses", "references"})
78
+ # module stale (it must re-resolve). Only the spliced passes matter here — and the serve
79
+ # layer reads the same set to know which ops answer from a spliced class (R1-C44).
80
+ _DEP_EDGE_TYPES = SPLICED_EDGE_TYPES
80
81
 
81
82
  # Above this fraction of modules affected, a full rebuild is cheaper (and trivially
82
83
  # correct) — no point splicing most of the graph.
@@ -159,21 +160,38 @@ def _splice_unaffected(new_graph, old_graph, unaffected, module_of) -> None:
159
160
  node.extras[k] = copy.deepcopy(old_extras[k])
160
161
 
161
162
 
162
- def _same_builder(provenance: dict, tier: str) -> bool:
163
- """Was the old graph produced by *this* codemap, on this tier? (R1-C25)
163
+ def _same_builder(provenance: dict, tier: str, repeat: int = 1) -> str | None:
164
+ """Why the old graph is *not* from this builder — ``None`` when it is (R1-C25).
164
165
 
165
166
  A graph with no provenance (pre-0.12) cannot answer, so it is treated as a
166
167
  different builder — the conservative direction: a needless full rebuild costs a
167
168
  minute, a silently stale graph costs a wrong answer.
169
+
170
+ R1-C47: the sample count is part of the builder. ``extras.seen`` on a spliced edge
171
+ means "k of N", and it can only mean that if N is the same N the chain has used all
172
+ along — a request with a different N starts a new chain from a full build.
168
173
  """
169
174
  if not provenance:
170
- return False
171
- return (provenance.get("tool") == tool_identity()
172
- and provenance.get("tier") == tier)
175
+ return "builder-changed"
176
+ if (provenance.get("tool") != tool_identity()
177
+ or provenance.get("tier") != tier):
178
+ return "builder-changed"
179
+ if ((provenance.get("samples") or {}).get("runs", 1)) != repeat:
180
+ return "samples-changed"
181
+ return None
182
+
183
+
184
+ def _full(package_path, *, deep: bool, repeat: int) -> Graph:
185
+ """A full build with the chain's sample count (R1-C47 / D2)."""
186
+ return extract(package_path, deep=deep, repeat=repeat)
187
+
188
+
189
+ def _count_seen(graph: Graph) -> int:
190
+ return sum(1 for e in graph.edges if "seen" in e.extras)
173
191
 
174
192
 
175
193
  def update_graph(old_graph: Graph, package_path, old_scope: dict, new_scope: dict,
176
- *, deep: bool = False) -> tuple[Graph, dict]:
194
+ *, deep: bool = False, repeat: int = 1) -> tuple[Graph, dict]:
177
195
  """Incrementally rebuild ``old_graph`` for the current source tree.
178
196
 
179
197
  Returns ``(graph, info)`` where ``info`` records the decision (``mode``:
@@ -186,16 +204,25 @@ def update_graph(old_graph: Graph, package_path, old_scope: dict, new_scope: dic
186
204
  (R1-C42): the spliced regions carry the *previous* build's sample, and the splice
187
205
  is self-perpetuating — see the module docstring (R1-C43). Such a graph is stamped
188
206
  ``provenance.incremental: true``.
207
+
208
+ ``repeat=N`` (R1-C47) makes every place this path samples — the fallback full build
209
+ and the recompute of the affected modules — take N samples in fresh interpreters,
210
+ merged as a full ``--repeat`` build would be. Measured on twenty real commits: the
211
+ chain's misses come from single samples, not from age, so this is the remedy and a
212
+ periodic full rebuild is not. N must equal the old graph's ``samples.runs`` or the
213
+ chain restarts from a full build (``reason: samples-changed``).
189
214
  """
190
215
  target_pkg = old_graph.target
191
216
  tier = "deep" if deep else "fast"
192
217
  # R1-C25: the input is not the only thing that can change. `unchanged` below decides
193
218
  # from the source tree alone, so an upgraded codemap over an untouched tree used to
194
219
  # return yesterday's graph built by yesterday's extractor — the exact confusion the
195
- # provenance block exists to name. A different tool or tier is a full rebuild.
196
- if not _same_builder(old_graph.provenance, tier):
197
- graph = extract(package_path, deep=deep)
198
- return graph, {"mode": "full", "affected": [], "reason": "builder-changed"}
220
+ # provenance block exists to name. A different tool, tier or sample count is a full
221
+ # rebuild.
222
+ reason = _same_builder(old_graph.provenance, tier, repeat)
223
+ if reason is not None:
224
+ return _full(package_path, deep=deep, repeat=repeat), \
225
+ {"mode": "full", "affected": [], "reason": reason}
199
226
 
200
227
  d = diff_scopes(old_scope, new_scope)
201
228
  changed = {m for p in d["changed"] if (m := _path_to_module(p, target_pkg))}
@@ -218,14 +245,32 @@ def update_graph(old_graph: Graph, package_path, old_scope: dict, new_scope: dic
218
245
  module_of) & module_ids
219
246
 
220
247
  if not module_ids or len(affected) >= _FULL_FALLBACK_FRACTION * len(module_ids):
248
+ if repeat > 1:
249
+ # R1-C47: the fallback used to be the chain's weakest point — a 63-module
250
+ # commit reset a --repeat 3 chain to one sample on the replay's third tick.
251
+ return _full(package_path, deep=deep, repeat=repeat), \
252
+ {"mode": "full", "affected": sorted(affected)}
221
253
  add_behavioral_layer(graph, root, module_name, search_path, deep=deep)
222
254
  graph.provenance = build_provenance(tier=tier, inputs=graph.provenance.get("inputs"))
223
255
  return graph, {"mode": "full", "affected": sorted(affected)}
224
256
 
225
- add_behavioral_layer(graph, root, module_name, search_path, deep=deep,
226
- behavior_only=affected, attr_only=affected)
257
+ if repeat > 1:
258
+ # R1-C47 / D2: N fresh interpreters, each restricted to the affected modules;
259
+ # the structural base is rebuilt in each (deterministic, ~4 s) so the merge
260
+ # sees identical nodes. The spliced part keeps its own `seen` — same N (D1).
261
+ graph, stats = merge_samples(collect_samples(package_path, deep=deep,
262
+ runs=repeat, only=affected))
263
+ graph.loaded_schema = None
264
+ else:
265
+ add_behavioral_layer(graph, root, module_name, search_path, deep=deep,
266
+ behavior_only=affected, attr_only=affected)
267
+ stats = {"runs": 1}
227
268
  unaffected = module_ids - affected
228
269
  _splice_unaffected(graph, old_graph, unaffected, module_of)
270
+ if repeat > 1:
271
+ # `unstable` describes the artifact a reader holds, not the part this tick
272
+ # touched: recount over recomputed and spliced edges alike.
273
+ stats = {"runs": repeat, "unstable": _count_seen(graph)}
229
274
  graph.provenance = build_provenance(tier=tier, inputs=graph.provenance.get("inputs"),
230
- incremental=True)
275
+ incremental=True, samples=stats)
231
276
  return graph, {"mode": "incremental", "affected": sorted(affected)}
@@ -97,6 +97,13 @@ EDGE_TYPES = frozenset({
97
97
  "accesses", # function → attribute node it reads/writes (extras.access, R1-C20)
98
98
  })
99
99
 
100
+ # The subset of EDGE_TYPES an incremental build splices from the old graph for modules
101
+ # it did not recompute (``incremental.py``) — and therefore the classes a deep+incremental
102
+ # graph answers from an *earlier* build's jedi sample (R1-C43). Lives with the vocabulary
103
+ # because two layers that may not import each other need it: the extractor's union
104
+ # (R1-C45) and the serve layer, which derives from it which ops must say so (R1-C44 / D4).
105
+ SPLICED_EDGE_TYPES = frozenset({"calls", "accesses", "references"})
106
+
100
107
 
101
108
  @dataclass
102
109
  class Node:
@@ -696,6 +696,14 @@ class Query:
696
696
  "by_distance": by_distance, "max_distance": max_distance,
697
697
  "risk": self._impact_risk(len(refs), max_distance, len(by_root),
698
698
  kind=kind)}
699
+ # R1-C44 / D1: a symbol the graph does not hold has no blast radius to report —
700
+ # `none` here used to be an affirmative "nothing depends on it" about a name the
701
+ # tool had never seen (a typo, another tree, a copy under another directory name).
702
+ if node is None:
703
+ out["risk"] = "unknown"
704
+ out["risk_reason"] = ("symbol not in graph — nothing is known about it; "
705
+ "this is not an empty blast radius")
706
+ return out
699
707
  # Honesty (R1-C20 P0, issue #1): a field with no modelled accessor is a
700
708
  # *lower bound*, not proof of safety — attribute access resolution is
701
709
  # best-effort. Say so instead of the affirmative "none".
@@ -22,7 +22,9 @@ def render_impact(query: Query, symbol: str, *, depth: int = 2) -> str:
22
22
  )
23
23
  lines.append("")
24
24
  if not ids:
25
- lines.append(f"_No definition found for `{symbol}`._")
25
+ lines.append(f"_No definition found for `{symbol}` — nothing is known about it; "
26
+ "this is not an empty blast radius. Check the name (ids start with "
27
+ "the package directory's name)._")
26
28
  return "\n".join(lines) + "\n"
27
29
 
28
30
  for sid in ids:
@@ -14,7 +14,7 @@ from __future__ import annotations
14
14
 
15
15
  from collections import Counter
16
16
 
17
- from codemap.model import SCHEMA_VERSION, Graph
17
+ from codemap.model import SCHEMA_VERSION, SPLICED_EDGE_TYPES as _SPLICED_EDGE_TYPES, Graph
18
18
  from codemap.query import Query
19
19
  from codemap.serve.api_surface import render_api_surface
20
20
  from codemap.serve.architecture import build_architecture, render_architecture
@@ -40,13 +40,61 @@ _REPORTS = {
40
40
  # disclaimers). Absence of the label = structural/complete (imports, contains,
41
41
  # inherits, exports — exact). One label per answer (no per-edge confidence: edges
42
42
  # already carry `resolution`). From the GitNexus разбор, built natively.
43
- _PARTIAL_OPS = frozenset({"callers", "callees", "impact", "flows", "call_contract",
44
- "tests", "covers"})
43
+ # R1-C44 / D4: which edge classes each relational op reads. Two sets are *derived* from
44
+ # this table instead of being listed by hand, so that adding an op — or adding an edge
45
+ # class to the incremental splice — forces a decision here rather than a silent gap:
46
+ # `_PARTIAL_OPS` — ops that lean on best-effort static resolution (calls, accesses),
47
+ # and so answer a lower bound (R1-C13);
48
+ # `_SPLICE_OPS` — ops that read a class the incremental build splices from the old
49
+ # graph, and so on a deep+incremental graph read the *earlier* build's
50
+ # jedi sample (R1-C43).
51
+ # `accessors` used to be in neither — it reads `accesses`, modelled best-effort, and
52
+ # carried no `epistemic` at all (found by the R1-C44 measurement).
53
+ _OP_EDGE_CLASSES = {
54
+ "callers": frozenset({"calls"}),
55
+ "callees": frozenset({"calls"}),
56
+ "call_contract": frozenset({"calls"}),
57
+ "flows": frozenset({"calls"}),
58
+ "impact": frozenset({"calls", "references", "accesses"}),
59
+ "accessors": frozenset({"accesses"}),
60
+ "tests": frozenset({"calls", "references"}),
61
+ "covers": frozenset({"calls", "references"}),
62
+ }
63
+ _BEST_EFFORT_CLASSES = frozenset({"calls", "accesses"})
64
+ _PARTIAL_OPS = frozenset(op for op, cls in _OP_EDGE_CLASSES.items()
65
+ if cls & _BEST_EFFORT_CLASSES)
66
+ _SPLICE_OPS = frozenset(op for op, cls in _OP_EDGE_CLASSES.items()
67
+ if cls & _SPLICED_EDGE_TYPES)
45
68
  _EPISTEMIC_PARTIAL = {
46
69
  "epistemic": "partial",
47
- "reason": "leans on static call resolution (partial for Python) — a lower "
48
- "bound; pair with grep/tests before acting.",
70
+ "reason": "leans on static resolution of calls and attribute accesses (partial "
71
+ "for Python) — a lower bound; pair with grep/tests before acting.",
49
72
  }
73
+ # D4 — the second reason, named separately: "a lower bound by static resolution" and
74
+ # "part of this graph was carried over, not recomputed" are different facts.
75
+ _EPISTEMIC_SPLICE = (
76
+ "parts of this deep graph were carried over from an earlier build rather than "
77
+ "recomputed (provenance.incremental: true), and this op reads edge classes the "
78
+ "splice copies — a missing edge here may be the earlier build's jedi miss, and "
79
+ "rebuilding incrementally will not resample it; only a full build (or --repeat) does."
80
+ )
81
+ # D5 — a graph older than the field: say what is not known, not more.
82
+ _EPISTEMIC_SPLICE_UNKNOWN = (
83
+ "this deep graph predates provenance.incremental, so whether parts of it were "
84
+ "carried over from an earlier build is unknown; this op reads edge classes an "
85
+ "incremental build would splice."
86
+ )
87
+
88
+
89
+ def _not_found(name_or_id: str) -> dict:
90
+ """The resolution block for a symbol the graph does not hold (R1-C44 / D1).
91
+
92
+ ``ok`` stays true — the question was well-formed and this *is* the answer. What
93
+ must not happen is the answer being indistinguishable from "found, and nothing
94
+ references it": that envelope used to be byte-identical for both.
95
+ """
96
+ return {"input": name_or_id, "id": None, "found": False,
97
+ "ambiguous": False, "alternatives": []}
50
98
 
51
99
  # R1-C28: ops that cut a computed answer down to a limit. Orthogonal to _PARTIAL_OPS
52
100
  # — that one is about how well calls *resolve*; this one is about how much of the
@@ -132,6 +180,13 @@ def tool_drift(running: str | None, installed: str | None) -> dict | None:
132
180
  def build_query_result(q: Query, name: str) -> dict:
133
181
  """The full symbol dossier — shared by ``codemap query`` and warm serve."""
134
182
  matches = q.find(name)
183
+ if not matches:
184
+ # R1-C44 (the F23 gap, on this surface): `find` matches short names only, so the
185
+ # full id an agent gets back from `search` used to open an *empty* dossier here.
186
+ # Resolve it the way every relational op does before concluding "no such symbol".
187
+ info = q.canonical_info(name)
188
+ if info:
189
+ matches = [q.graph.nodes[info["id"]]]
135
190
  result: dict = {
136
191
  "name": name,
137
192
  "defined_at": q.where_defined(name),
@@ -233,7 +288,10 @@ class Session:
233
288
  warning when a bare short name resolved to one of many defs arbitrarily.
234
289
  """
235
290
  info = self.query.canonical_info(name_or_id)
236
- self._resolution = info
291
+ # R1-C44 / D2: `canonical_info` computes "no such symbol" as None, and this
292
+ # method used to drop it on the floor — the raw input went on to be looked up
293
+ # in the graph and answered with an honest-looking empty list.
294
+ self._resolution = {**info, "found": True} if info else _not_found(name_or_id)
237
295
  return info["id"] if info else name_or_id
238
296
 
239
297
  # -- dispatch ------------------------------------------------------------
@@ -263,14 +321,35 @@ class Session:
263
321
  except Exception as exc: # a bad arg must not kill the resident process
264
322
  return {"ok": False, "error": f"{type(exc).__name__}: {exc}"}
265
323
  r = self._resolution
266
- if r and (r["ambiguous"] or r["input"] != r["id"]):
267
- env["resolved"] = r
324
+ # R1-C44 / D2: three-way, not a truthiness test. "not found" is the one case the
325
+ # block exists for and was the one case `if r and …` threw away.
326
+ if r is not None:
327
+ if not r["found"]:
328
+ env["resolved"] = r
329
+ elif r["ambiguous"] or r["input"] != r["id"]:
330
+ env["resolved"] = r
268
331
  if op in _PARTIAL_OPS: # R1-C13: machine-readable "this is a lower bound"
269
- env["epistemic"] = _EPISTEMIC_PARTIAL
332
+ env["epistemic"] = self._epistemic(op)
270
333
  if self._limit is not None: # R1-C28: how much of the answer survived the cut
271
334
  env["limit"] = self._limit
272
335
  return env
273
336
 
337
+ def _epistemic(self, op: str) -> dict:
338
+ """The lower-bound block for ``op``, with the splice reason when it applies (D4/D5).
339
+
340
+ Conditioned on the graph *and* the op: only a deep graph can carry a frozen
341
+ sample, and only an op reading a spliced class reads it. On a full deep graph
342
+ and on the fast tier the block is exactly the R1-C13 one.
343
+ """
344
+ block = dict(_EPISTEMIC_PARTIAL)
345
+ prov = self.graph.provenance or {}
346
+ if op in _SPLICE_OPS and prov.get("tier") == "deep":
347
+ if prov.get("incremental") is True:
348
+ block["splice"] = _EPISTEMIC_SPLICE
349
+ elif "incremental" not in prov:
350
+ block["splice"] = _EPISTEMIC_SPLICE_UNKNOWN
351
+ return block
352
+
274
353
  # -- ops (each takes an args dict) ---------------------------------------
275
354
 
276
355
  def _op_ping(self, args) -> str:
@@ -355,12 +434,17 @@ class Session:
355
434
  return out
356
435
 
357
436
  def _op_query(self, args) -> dict:
358
- return build_query_result(self.query, args["name"])
437
+ result = build_query_result(self.query, args["name"])
438
+ if not result["matches"]: # R1-C44: an empty dossier says why it is empty
439
+ self._resolution = _not_found(args["name"])
440
+ return result
359
441
 
360
442
  def _op_impact(self, args) -> dict:
361
443
  sym = args["symbol"]
362
444
  depth = int(args.get("depth", 2))
363
445
  ids = self.query.impact_targets(sym) # F23: accept full id / re-export too
446
+ if not ids: # R1-C44: this op resolves on its own, so it records on its own
447
+ self._resolution = _not_found(sym)
364
448
  return {
365
449
  "symbol": sym,
366
450
  "impact": [self.query.impact(sid, depth=depth) for sid in ids],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codmap
3
- Version: 0.0.11
3
+ Version: 0.0.12
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
@@ -161,7 +161,7 @@ Optional extras: `mcp` (Model Context Protocol server), `scip` (protobuf, for SC
161
161
  ## Quickstart
162
162
 
163
163
  ```bash
164
- # build the canonical graph of a package
164
+ # build the canonical graph of a package (its directory name is the package name every id starts with)
165
165
  codemap build ./yourpkg -o graph.json
166
166
 
167
167
  # repo-scoped: add consumers (tests/examples) + docs for blast-radius/impact
@@ -271,8 +271,10 @@ unstable among 13 675). So one build misses such an edge about 1 time in 4, two
271
271
  1 in 64. `codemap build --deep --repeat N` builds N samples — each in a fresh interpreter, the
272
272
  regime the share was measured in; in-process repeats come in correlated streaks — and unions them: an edge seen in fewer
273
273
  than N runs carries `extras.seen`, `provenance.samples` records N and how many edges varied, and the
274
- note on the graph says both. Refused out loud on the fast tier (byte-stable already) and together with
275
- `--incremental` (a spliced graph cannot be resampled).
274
+ note on the graph says both. Refused out loud on the fast tier (byte-stable already). With `--incremental`
275
+ and `watch`, N becomes a property of the chain and every place the chain samples — base, fallback, each
276
+ tick's recompute — samples N times; measured on twenty real commits, that is what removes the chain's misses,
277
+ and a periodic full rebuild does not.
276
278
 
277
279
  **Measurement:** [gaps/deep_tier_nondeterminism_2026-09-02.md](gaps/deep_tier_nondeterminism_2026-09-02.md),
278
280
  [gaps/deep_tier_union_by_repeat_2026-09-04.md](gaps/deep_tier_union_by_repeat_2026-09-04.md).
@@ -112,8 +112,10 @@ tests/test_r1c3_dogfood.py
112
112
  tests/test_r1c41_scope_membership.py
113
113
  tests/test_r1c42_deep_tier_stability.py
114
114
  tests/test_r1c43_incremental_splice.py
115
+ tests/test_r1c44_absent_answers.py
115
116
  tests/test_r1c45_repeat_union.py
116
117
  tests/test_r1c46_shadowed_definitions.py
118
+ tests/test_r1c47_incremental_resample.py
117
119
  tests/test_r1c4_complexity.py
118
120
  tests/test_r1c5_apidiff.py
119
121
  tests/test_r1c6_pack.py
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
9
9
  # version. README says this plainly, since `pip install codmap` / `import codemap` is a
10
10
  # seam a reader will otherwise hit by surprise.
11
11
  name = "codmap"
12
- version = "0.0.11"
12
+ version = "0.0.12"
13
13
  description = "Static analyzer that turns a package's source into a queryable code graph."
14
14
  readme = "README.md"
15
15
  # M20/D1: measured, not assumed. Each release of the declared range runs the full suite in