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.
- {codmap-0.0.11/codmap.egg-info → codmap-0.0.12}/PKG-INFO +6 -4
- {codmap-0.0.11 → codmap-0.0.12}/README.md +5 -3
- {codmap-0.0.11 → codmap-0.0.12}/codemap/cli.py +32 -16
- {codmap-0.0.11 → codmap-0.0.12}/codemap/extract/griffe_extractor.py +13 -5
- {codmap-0.0.11 → codmap-0.0.12}/codemap/incremental.py +63 -18
- {codmap-0.0.11 → codmap-0.0.12}/codemap/model.py +7 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/query.py +8 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/impact.py +3 -1
- {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/session.py +94 -10
- {codmap-0.0.11 → codmap-0.0.12/codmap.egg-info}/PKG-INFO +6 -4
- {codmap-0.0.11 → codmap-0.0.12}/codmap.egg-info/SOURCES.txt +2 -0
- {codmap-0.0.11 → codmap-0.0.12}/pyproject.toml +1 -1
- codmap-0.0.12/tests/test_r1c44_absent_answers.py +197 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c45_repeat_union.py +6 -6
- codmap-0.0.12/tests/test_r1c47_incremental_resample.py +155 -0
- {codmap-0.0.11 → codmap-0.0.12}/LICENSE +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/__init__.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/apidiff.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/arch.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/diagnostics.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/extract/__init__.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/extract/attrflow.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/extract/behavior.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/extract/dataflow.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/extract/dispatch.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/extract/gsource.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/extract/roots.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/extract/union.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/freshness.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/integrations/__init__.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/integrations/base.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/integrations/cocoindex.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/integrations/gate.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/integrations/gitnexus.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/integrations/registry.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/integrations/transport.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/provenance.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/scope.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/__init__.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/_scip_pb2.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/api_surface.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/apidiff.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/architecture.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/audit.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/check.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/ctags.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/limits.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/livingdocs.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/mcp_server.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/mermaid.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/pack.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/rag.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/review.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/scip.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/semantic.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/server.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/subsystems.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/serve/vault.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/store.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/tomlio.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codemap/watch.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codmap.egg-info/dependency_links.txt +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codmap.egg-info/entry_points.txt +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codmap.egg-info/requires.txt +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/codmap.egg-info/top_level.txt +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/setup.cfg +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_epistemic.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_gitnexus_router.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_impact_depth.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_integrations.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_issue3_serve_freshness.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_livingdocs.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_m0_api_surface.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_m11_argcontract.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_m12_dataflow.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_m15_review.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_m16_architecture.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_m17_mcp.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_m18_freshness.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_m1_5_semantics.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_m1_query.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_m2_views.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_m32_watch.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_m3_serve.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_m4_behavior.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_m5_deep.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_m6_repo_scope.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_m7_dispatch.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_m9_family.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c13_callgraph_accuracy.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c13_grep_vs_graph.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c16_semantic.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c20_attribute_edges.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c21_flat_layout.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c22_source_visible_refs.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c23_hard_python.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c24_test_mapping.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c25_provenance.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c26_deep_union.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c27_config_honesty.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c28_limit_envelope.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c29_lazy_imports.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c2_ctags.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c30_local_import_calls.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c30f1_reexport_calls.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c30f2_check_scope.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c31_path_origin.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c32_report_json.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c33_query_signature.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c34_signature_kinds.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c35_contract_location.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c36_target_resolution.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c37_rule_mutation.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c38_tool_drift.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c3_arch_contract.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c3_dogfood.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c41_scope_membership.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c42_deep_tier_stability.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c43_incremental_splice.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c46_shadowed_definitions.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c4_complexity.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c5_apidiff.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c6_pack.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c7_edge_vocab.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c8_deadcode.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_r1c9_incremental.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_scip_export.py +0 -0
- {codmap-0.0.11 → codmap-0.0.12}/tests/test_scope.py +0 -0
- {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.
|
|
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)
|
|
275
|
-
|
|
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)
|
|
239
|
-
|
|
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
|
|
89
|
-
#
|
|
90
|
-
#
|
|
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
|
|
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).
|
|
908
|
-
"
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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) ->
|
|
163
|
-
"""
|
|
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
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
267
|
-
|
|
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"] =
|
|
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
|
-
|
|
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.
|
|
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)
|
|
275
|
-
|
|
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.
|
|
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
|