codmap 0.0.8__tar.gz → 0.0.10__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.8/codmap.egg-info → codmap-0.0.10}/PKG-INFO +21 -4
- {codmap-0.0.8 → codmap-0.0.10}/README.md +20 -3
- {codmap-0.0.8 → codmap-0.0.10}/codemap/cli.py +7 -1
- {codmap-0.0.8 → codmap-0.0.10}/codemap/diagnostics.py +73 -1
- {codmap-0.0.8 → codmap-0.0.10}/codemap/incremental.py +36 -5
- {codmap-0.0.8 → codmap-0.0.10}/codemap/provenance.py +22 -3
- {codmap-0.0.8 → codmap-0.0.10}/codemap/serve/apidiff.py +4 -0
- {codmap-0.0.8 → codmap-0.0.10/codmap.egg-info}/PKG-INFO +21 -4
- {codmap-0.0.8 → codmap-0.0.10}/codmap.egg-info/SOURCES.txt +2 -0
- {codmap-0.0.8 → codmap-0.0.10}/pyproject.toml +1 -1
- codmap-0.0.10/tests/test_r1c42_deep_tier_stability.py +92 -0
- codmap-0.0.10/tests/test_r1c43_incremental_splice.py +220 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c9_incremental.py +32 -5
- {codmap-0.0.8 → codmap-0.0.10}/LICENSE +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/__init__.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/apidiff.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/arch.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/extract/__init__.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/extract/attrflow.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/extract/behavior.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/extract/dataflow.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/extract/dispatch.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/extract/griffe_extractor.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/extract/gsource.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/extract/roots.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/freshness.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/integrations/__init__.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/integrations/base.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/integrations/cocoindex.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/integrations/gate.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/integrations/gitnexus.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/integrations/registry.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/integrations/transport.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/model.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/query.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/scope.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/serve/__init__.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/serve/_scip_pb2.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/serve/api_surface.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/serve/architecture.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/serve/audit.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/serve/check.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/serve/ctags.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/serve/impact.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/serve/limits.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/serve/livingdocs.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/serve/mcp_server.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/serve/mermaid.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/serve/pack.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/serve/rag.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/serve/review.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/serve/scip.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/serve/semantic.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/serve/server.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/serve/session.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/serve/subsystems.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/serve/vault.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/store.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/tomlio.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codemap/watch.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codmap.egg-info/dependency_links.txt +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codmap.egg-info/entry_points.txt +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codmap.egg-info/requires.txt +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/codmap.egg-info/top_level.txt +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/setup.cfg +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_epistemic.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_gitnexus_router.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_impact_depth.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_integrations.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_issue3_serve_freshness.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_livingdocs.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_m0_api_surface.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_m11_argcontract.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_m12_dataflow.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_m15_review.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_m16_architecture.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_m17_mcp.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_m18_freshness.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_m1_5_semantics.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_m1_query.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_m2_views.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_m32_watch.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_m3_serve.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_m4_behavior.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_m5_deep.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_m6_repo_scope.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_m7_dispatch.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_m9_family.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c13_callgraph_accuracy.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c13_grep_vs_graph.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c16_semantic.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c20_attribute_edges.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c21_flat_layout.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c22_source_visible_refs.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c23_hard_python.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c24_test_mapping.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c25_provenance.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c26_deep_union.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c27_config_honesty.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c28_limit_envelope.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c29_lazy_imports.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c2_ctags.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c30_local_import_calls.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c30f1_reexport_calls.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c30f2_check_scope.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c31_path_origin.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c32_report_json.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c33_query_signature.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c34_signature_kinds.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c35_contract_location.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c36_target_resolution.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c37_rule_mutation.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c38_tool_drift.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c3_arch_contract.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c3_dogfood.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c41_scope_membership.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c4_complexity.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c5_apidiff.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c6_pack.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c7_edge_vocab.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_r1c8_deadcode.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_scip_export.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/tests/test_scope.py +0 -0
- {codmap-0.0.8 → codmap-0.0.10}/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.10
|
|
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
|
[](https://github.com/kogriv/codemap/actions/workflows/ci.yml)
|
|
46
46
|
|
|
47
|
-
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **
|
|
47
|
+
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **765 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 &
|
|
@@ -58,8 +58,9 @@ export edges, best-effort call edges, registry-family `implements` links, string
|
|
|
58
58
|
and per-call argument contracts — then answers questions over it.
|
|
59
59
|
|
|
60
60
|
Design principles: **source-only** (static `ast`/`griffe`, never imports the target), **deterministic**
|
|
61
|
-
(canonical sorted JSON, no timestamps — diffable
|
|
62
|
-
|
|
61
|
+
(canonical sorted JSON, no timestamps — diffable; byte-stable on the fast tier, see the caveat
|
|
62
|
+
[below](#one-caveat-on-deterministic)), **CLI-AI-first** (JSON by default, stable exit codes),
|
|
63
|
+
**honest** (approximations are labeled, not hidden).
|
|
63
64
|
|
|
64
65
|
## Point questions and whole-graph questions
|
|
65
66
|
|
|
@@ -247,6 +248,22 @@ against *all* true edges (the price of Python's dynamism), and a grep-vs-graph p
|
|
|
247
248
|
cheaper than grep for impact on unique names, tens of × on polymorphic ones, and no cheaper for locating a
|
|
248
249
|
symbol.
|
|
249
250
|
|
|
251
|
+
## One caveat on "deterministic"
|
|
252
|
+
|
|
253
|
+
The **fast tier is byte-stable**: two builds of an unchanged tree produce identical files, and CI
|
|
254
|
+
compares the bytes on every push. The **deep tier (`--deep`, jedi) is not**. Ten deep builds of an
|
|
255
|
+
unchanged tree here produced two distinct artifacts — differing in two per-symbol call counters out
|
|
256
|
+
of 2133 nodes, no edges — and on a larger external tree one build in seven lost one real call edge of
|
|
257
|
+
9524. The cause is jedi's per-script execution budget: an inference that runs out of it returns
|
|
258
|
+
nothing, and nothing is recorded as `unresolved`.
|
|
259
|
+
|
|
260
|
+
So a deep graph is one sample of a slightly fuzzy function, not a function of its input. Every deep
|
|
261
|
+
build says so in its own diagnostics, and `codemap diff` says so when both sides are deep — read a
|
|
262
|
+
delta of a few call edges as possible tool noise before reading it as a change in the code. Anything
|
|
263
|
+
that must be reproducible byte-for-byte — a gate, a two-release comparison — belongs on the fast tier.
|
|
264
|
+
|
|
265
|
+
**Measurement:** [gaps/deep_tier_nondeterminism_2026-09-02.md](gaps/deep_tier_nondeterminism_2026-09-02.md).
|
|
266
|
+
|
|
250
267
|
## Dogfooding
|
|
251
268
|
|
|
252
269
|
codemap is validated end-to-end against a real external package. Place a target repo as a sibling and
|
|
@@ -8,7 +8,7 @@ and a **SCIP index** for interop with Sourcegraph / Glean and other precise-code
|
|
|
8
8
|
|
|
9
9
|
[](https://github.com/kogriv/codemap/actions/workflows/ci.yml)
|
|
10
10
|
|
|
11
|
-
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **
|
|
11
|
+
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **765 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 &
|
|
@@ -22,8 +22,9 @@ export edges, best-effort call edges, registry-family `implements` links, string
|
|
|
22
22
|
and per-call argument contracts — then answers questions over it.
|
|
23
23
|
|
|
24
24
|
Design principles: **source-only** (static `ast`/`griffe`, never imports the target), **deterministic**
|
|
25
|
-
(canonical sorted JSON, no timestamps — diffable
|
|
26
|
-
|
|
25
|
+
(canonical sorted JSON, no timestamps — diffable; byte-stable on the fast tier, see the caveat
|
|
26
|
+
[below](#one-caveat-on-deterministic)), **CLI-AI-first** (JSON by default, stable exit codes),
|
|
27
|
+
**honest** (approximations are labeled, not hidden).
|
|
27
28
|
|
|
28
29
|
## Point questions and whole-graph questions
|
|
29
30
|
|
|
@@ -211,6 +212,22 @@ against *all* true edges (the price of Python's dynamism), and a grep-vs-graph p
|
|
|
211
212
|
cheaper than grep for impact on unique names, tens of × on polymorphic ones, and no cheaper for locating a
|
|
212
213
|
symbol.
|
|
213
214
|
|
|
215
|
+
## One caveat on "deterministic"
|
|
216
|
+
|
|
217
|
+
The **fast tier is byte-stable**: two builds of an unchanged tree produce identical files, and CI
|
|
218
|
+
compares the bytes on every push. The **deep tier (`--deep`, jedi) is not**. Ten deep builds of an
|
|
219
|
+
unchanged tree here produced two distinct artifacts — differing in two per-symbol call counters out
|
|
220
|
+
of 2133 nodes, no edges — and on a larger external tree one build in seven lost one real call edge of
|
|
221
|
+
9524. The cause is jedi's per-script execution budget: an inference that runs out of it returns
|
|
222
|
+
nothing, and nothing is recorded as `unresolved`.
|
|
223
|
+
|
|
224
|
+
So a deep graph is one sample of a slightly fuzzy function, not a function of its input. Every deep
|
|
225
|
+
build says so in its own diagnostics, and `codemap diff` says so when both sides are deep — read a
|
|
226
|
+
delta of a few call edges as possible tool noise before reading it as a change in the code. Anything
|
|
227
|
+
that must be reproducible byte-for-byte — a gate, a two-release comparison — belongs on the fast tier.
|
|
228
|
+
|
|
229
|
+
**Measurement:** [gaps/deep_tier_nondeterminism_2026-09-02.md](gaps/deep_tier_nondeterminism_2026-09-02.md).
|
|
230
|
+
|
|
214
231
|
## Dogfooding
|
|
215
232
|
|
|
216
233
|
codemap is validated end-to-end against a real external package. Place a target repo as a sibling and
|
|
@@ -104,7 +104,11 @@ def _cmd_build(args) -> int:
|
|
|
104
104
|
# roots come from extract_repo in this same call; never inherited from a graph
|
|
105
105
|
# loaded off disk, which is how a stale scope would sneak into a fresh build.
|
|
106
106
|
roots=graph.provenance.get("roots") if (args.consumer or args.docs) else None,
|
|
107
|
-
inputs=_inputs_with_membership(graph, scope, args)
|
|
107
|
+
inputs=_inputs_with_membership(graph, scope, args),
|
|
108
|
+
# R1-C43: this call *overwrites* whatever update_graph stamped, so the flag has
|
|
109
|
+
# to be re-derived here or it is lost. `unchanged` counts as carried over too —
|
|
110
|
+
# nothing in that graph was recomputed by this build at all.
|
|
111
|
+
incremental=(incr_info or {}).get("mode") in ("incremental", "unchanged"))
|
|
108
112
|
# R1-C21: a well-formed but vacuous graph must announce itself — silence is what
|
|
109
113
|
# lets an unparsed layout read as a clean bill of health downstream.
|
|
110
114
|
for d in diagnostics(graph):
|
|
@@ -747,6 +751,8 @@ def _cmd_diff(args) -> int:
|
|
|
747
751
|
print(f"[warning] {line}", file=sys.stderr)
|
|
748
752
|
print(f"[warning] differences below may be tool changes, not code changes "
|
|
749
753
|
f"(old: {cmp['old']} | new: {cmp['new']})", file=sys.stderr)
|
|
754
|
+
for caveat in cmp.get("caveats") or ():
|
|
755
|
+
print(f"[note] {caveat}", file=sys.stderr)
|
|
750
756
|
print(render_apidiff(old, new), end="")
|
|
751
757
|
if args.exit_code and not build_apidiff(old, new)["ok"]:
|
|
752
758
|
return 1
|
|
@@ -22,6 +22,8 @@ SCHEMA_MISMATCH = "schema_mismatch"
|
|
|
22
22
|
UNREAD_INPUTS = "unread_inputs"
|
|
23
23
|
MODULE_COUNT_MISMATCH = "module_count_mismatch"
|
|
24
24
|
SCOPE_MEMBERSHIP = "scope_membership"
|
|
25
|
+
DEEP_TIER_UNSTABLE = "deep_tier_unstable"
|
|
26
|
+
INCREMENTAL_DEEP_SPLICE = "incremental_deep_splice"
|
|
25
27
|
|
|
26
28
|
#: A ``warning`` invalidates the conclusions a surface draws from the graph — read them as
|
|
27
29
|
#: unknown. A ``note`` states a fact about how the graph was built and invalidates nothing.
|
|
@@ -190,6 +192,75 @@ def unread_inputs_diagnostic(graph) -> dict | None:
|
|
|
190
192
|
}
|
|
191
193
|
|
|
192
194
|
|
|
195
|
+
def deep_tier_diagnostic(graph) -> dict | None:
|
|
196
|
+
"""State the deep tier's noise floor on the artifact itself (R1-C42).
|
|
197
|
+
|
|
198
|
+
A **note**, not a warning: nothing here is wrong, and no finding below is invalid.
|
|
199
|
+
What was missing is that the tier's instability was known — measured at R1-C9, and
|
|
200
|
+
the reason the CI determinism job runs the fast tier only — while living exclusively
|
|
201
|
+
in a comment in a workflow file. A consumer read "deterministic" and built a
|
|
202
|
+
two-release comparison on it; the difference they saw was the tool, not the code.
|
|
203
|
+
|
|
204
|
+
Measured on two trees: ten deep builds of an unchanged tree produced **two** distinct
|
|
205
|
+
artifacts (7/3), differing in two per-symbol call counters and no edges; on a larger
|
|
206
|
+
tree one build in seven lost one real call edge of 9524. The cause is jedi's
|
|
207
|
+
per-script execution budget — an inference that runs out of it returns nothing, and
|
|
208
|
+
that reads as `unresolved` rather than as an error.
|
|
209
|
+
"""
|
|
210
|
+
if (graph.provenance or {}).get("tier") != "deep":
|
|
211
|
+
return None
|
|
212
|
+
return {
|
|
213
|
+
"code": DEEP_TIER_UNSTABLE,
|
|
214
|
+
"severity": NOTE,
|
|
215
|
+
"tier": "deep",
|
|
216
|
+
"consequence": ("Everything here is a lower bound as usual; treat a difference "
|
|
217
|
+
"of a few jedi-resolved edges between two deep graphs as tool "
|
|
218
|
+
"noise rather than a change in the code."),
|
|
219
|
+
"message": (
|
|
220
|
+
"built on the deep (jedi) tier, which is not byte-stable: two builds of an "
|
|
221
|
+
"unchanged tree can differ by a few jedi-resolved edges — measured at roughly "
|
|
222
|
+
"one run in three on two trees: two per-symbol call counters on one, one "
|
|
223
|
+
"`accesses` edge of 12190 on the other."
|
|
224
|
+
),
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def incremental_splice_diagnostic(graph) -> dict | None:
|
|
229
|
+
"""Say when a deep graph carries regions this build did not recompute (R1-C43).
|
|
230
|
+
|
|
231
|
+
Sibling of :func:`deep_tier_diagnostic`, and the reason it is a separate code: that
|
|
232
|
+
one says *this graph is one sample*, which a reader can act on by building again.
|
|
233
|
+
This one says the sample is **frozen** — building again incrementally returns it
|
|
234
|
+
unchanged, so the standard remedy silently does nothing.
|
|
235
|
+
|
|
236
|
+
Measured on an 88-module package: from a graph missing one real `accesses` edge,
|
|
237
|
+
five consecutive incremental builds recovered it **0 times** while full builds of
|
|
238
|
+
the very same tree recovered it **5 of 5**. And the miss defends itself — the
|
|
239
|
+
invalidation rule that would have recomputed the writer reads the old graph, where
|
|
240
|
+
the edge is exactly what is absent (gaps/incremental_noise_persistence_2026-09-02.md).
|
|
241
|
+
|
|
242
|
+
Fast tier is excluded on purpose: there the splice is exact and byte-identity to a
|
|
243
|
+
full build is pinned by the suite, so there is no sample to freeze.
|
|
244
|
+
"""
|
|
245
|
+
prov = graph.provenance or {}
|
|
246
|
+
if prov.get("tier") != "deep" or not prov.get("incremental"):
|
|
247
|
+
return None
|
|
248
|
+
return {
|
|
249
|
+
"code": INCREMENTAL_DEEP_SPLICE,
|
|
250
|
+
"severity": NOTE,
|
|
251
|
+
"tier": "deep",
|
|
252
|
+
"consequence": ("Do not read a missing call or attribute edge here as evidence "
|
|
253
|
+
"that nothing depends on a symbol, and do not test that by "
|
|
254
|
+
"rebuilding incrementally — only a full rebuild resamples."),
|
|
255
|
+
"message": (
|
|
256
|
+
"parts of this deep graph were spliced from an earlier build rather than "
|
|
257
|
+
"recomputed: on this tier that carries the earlier build's jedi sample "
|
|
258
|
+
"forward, including anything it missed — measured at 0 recoveries in 5 "
|
|
259
|
+
"incremental builds against 5 of 5 full builds of the same tree."
|
|
260
|
+
),
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
193
264
|
def scope_membership_diagnostic(graph) -> dict | None:
|
|
194
265
|
"""Flag files the graph was built from that the input manifest never listed (R1-C41).
|
|
195
266
|
|
|
@@ -274,7 +345,8 @@ def diagnostics(graph) -> list[dict]:
|
|
|
274
345
|
checks = (import_graph_diagnostic(graph), namespace_target_diagnostic(graph),
|
|
275
346
|
cross_root_diagnostic(graph), schema_diagnostic(graph),
|
|
276
347
|
unread_inputs_diagnostic(graph), module_count_diagnostic(graph),
|
|
277
|
-
scope_membership_diagnostic(graph))
|
|
348
|
+
scope_membership_diagnostic(graph), deep_tier_diagnostic(graph),
|
|
349
|
+
incremental_splice_diagnostic(graph))
|
|
278
350
|
return [d for d in checks if d is not None]
|
|
279
351
|
|
|
280
352
|
|
|
@@ -25,6 +25,27 @@ so we fall back to it.
|
|
|
25
25
|
|
|
26
26
|
The acceptance bar (BACKLOG R1-C9) is **byte-identical to a full rebuild**; the test
|
|
27
27
|
suite pins exactly that across edit / add / remove scenarios on both tiers.
|
|
28
|
+
|
|
29
|
+
**That bar is reachable on the fast tier only** — and saying it without a tier was
|
|
30
|
+
wrong twice over (R1-C43). Two full *deep* builds of an unchanged tree are not
|
|
31
|
+
byte-identical to each other (R1-C42), so there is no fixed artifact to be identical
|
|
32
|
+
*to*. Worse, measurement found a divergence this path introduces on its own:
|
|
33
|
+
|
|
34
|
+
- The splice **freezes a sample.** An edge jedi missed in the old build is copied
|
|
35
|
+
forward verbatim; measured 0 recoveries in 5 incremental builds against 5 in 5 full
|
|
36
|
+
builds of the same tree. "Build it again and see" — the standard answer to tier
|
|
37
|
+
noise — does not work here.
|
|
38
|
+
- The splice **blinds the invalidation that would undo it.** ``_affected_modules``
|
|
39
|
+
rule (b) below reads the *old* graph, so a missing edge is a missing reason to
|
|
40
|
+
recompute: editing the module that owns the target left the writer unaffected when
|
|
41
|
+
the edge was absent, and affected when it was present. Same edit, same tree.
|
|
42
|
+
|
|
43
|
+
The `unresolved` set cannot be indexed by the changed module — not knowing where an
|
|
44
|
+
edge went is what `unresolved` *means* — so this is structural for a cache keyed on
|
|
45
|
+
its own incomplete answer, not an oversight. What we do about it today is declare it:
|
|
46
|
+
``provenance.incremental`` marks such a graph and the diagnostic says what follows.
|
|
47
|
+
|
|
48
|
+
Measurement: ``gaps/incremental_noise_persistence_2026-09-02.md``.
|
|
28
49
|
"""
|
|
29
50
|
|
|
30
51
|
from __future__ import annotations
|
|
@@ -99,6 +120,10 @@ def _affected_modules(old_graph, new_graph, base_mods, changed_removed, module_o
|
|
|
99
120
|
if e.type == "imports" and e.target in base_mods:
|
|
100
121
|
affected.add(e.source)
|
|
101
122
|
# rule (b): a module whose old behavioral edge targeted a changed/removed module.
|
|
123
|
+
# R1-C43, the limit stated where it lives: this reads the OLD graph, so on the deep
|
|
124
|
+
# tier it is only as complete as that build's jedi sample. An edge the old build
|
|
125
|
+
# missed is a dependency this rule cannot see — measured: with the edge present the
|
|
126
|
+
# writer was invalidated, with the same edge missing it was not, on the same edit.
|
|
102
127
|
for e in old_graph.edges:
|
|
103
128
|
if e.type in _DEP_EDGE_TYPES:
|
|
104
129
|
tgt_mod = module_of(e.target)
|
|
@@ -147,10 +172,15 @@ def update_graph(old_graph: Graph, package_path, old_scope: dict, new_scope: dic
|
|
|
147
172
|
"""Incrementally rebuild ``old_graph`` for the current source tree.
|
|
148
173
|
|
|
149
174
|
Returns ``(graph, info)`` where ``info`` records the decision (``mode``:
|
|
150
|
-
``unchanged`` | ``incremental`` | ``full`` and the affected module list). The
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
175
|
+
``unchanged`` | ``incremental`` | ``full`` and the affected module list). The cheap
|
|
176
|
+
layers are rebuilt whole; the expensive jedi passes are recomputed for the affected
|
|
177
|
+
modules and spliced from the old graph for the rest.
|
|
178
|
+
|
|
179
|
+
On the **fast** tier the result is byte-identical to ``extract(package_path,
|
|
180
|
+
deep=deep)``. On the **deep** tier it is not, and not only because the target moves
|
|
181
|
+
(R1-C42): the spliced regions carry the *previous* build's sample, and the splice
|
|
182
|
+
is self-perpetuating — see the module docstring (R1-C43). Such a graph is stamped
|
|
183
|
+
``provenance.incremental: true``.
|
|
154
184
|
"""
|
|
155
185
|
target_pkg = old_graph.target
|
|
156
186
|
tier = "deep" if deep else "fast"
|
|
@@ -191,5 +221,6 @@ def update_graph(old_graph: Graph, package_path, old_scope: dict, new_scope: dic
|
|
|
191
221
|
behavior_only=affected, attr_only=affected)
|
|
192
222
|
unaffected = module_ids - affected
|
|
193
223
|
_splice_unaffected(graph, old_graph, unaffected, module_of)
|
|
194
|
-
graph.provenance = build_provenance(tier=tier, inputs=graph.provenance.get("inputs")
|
|
224
|
+
graph.provenance = build_provenance(tier=tier, inputs=graph.provenance.get("inputs"),
|
|
225
|
+
incremental=True)
|
|
195
226
|
return graph, {"mode": "incremental", "affected": sorted(affected)}
|
|
@@ -155,9 +155,16 @@ def relative_root(root: str | Path | None, path: str | Path) -> str:
|
|
|
155
155
|
|
|
156
156
|
|
|
157
157
|
def build_provenance(*, tier: str, scope: dict | None = None,
|
|
158
|
-
roots: dict | None = None, inputs: dict | None = None
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
roots: dict | None = None, inputs: dict | None = None,
|
|
159
|
+
incremental: bool = False) -> dict:
|
|
160
|
+
"""Assemble the ``provenance`` block. Deterministic; no clock, no absolute path.
|
|
161
|
+
|
|
162
|
+
``incremental`` records whether parts of this graph were **carried over rather than
|
|
163
|
+
recomputed in this build** (R1-C43). Always written, including ``false`` — absence
|
|
164
|
+
means the graph predates the field, which is *unknown*, not *full* (R1-C28).
|
|
165
|
+
"""
|
|
166
|
+
prov: dict = {"tool": tool_identity(), "tier": tier,
|
|
167
|
+
"incremental": bool(incremental)}
|
|
161
168
|
if inputs:
|
|
162
169
|
# R1-C23/D2: what the extractor read, and what it could not. Belongs with the
|
|
163
170
|
# identity rather than in the sidecar — a consumer holding only the graph is
|
|
@@ -253,6 +260,7 @@ def comparability(old: dict | None, new: dict | None) -> dict:
|
|
|
253
260
|
were dead.
|
|
254
261
|
"""
|
|
255
262
|
differences: list[str] = []
|
|
263
|
+
caveats: list[str] = []
|
|
256
264
|
if not old or not new:
|
|
257
265
|
differences.append("one of the graphs records no provenance "
|
|
258
266
|
"(built before schema 0.12) — the pair cannot be verified")
|
|
@@ -264,9 +272,20 @@ def comparability(old: dict | None, new: dict | None) -> dict:
|
|
|
264
272
|
old_roots, new_roots = old.get("roots"), new.get("roots")
|
|
265
273
|
if old_roots != new_roots:
|
|
266
274
|
differences.append(f"different scope roots: {old_roots} → {new_roots}")
|
|
275
|
+
if old.get("tier") == new.get("tier") == "deep":
|
|
276
|
+
# R1-C42: matching tiers make the pair comparable, and on the deep tier that
|
|
277
|
+
# is not the same as "every difference here is a code change". Measured on
|
|
278
|
+
# two trees: builds of an *unchanged* tree differ in roughly one run of
|
|
279
|
+
# three, by a couple of call classifications — and on the larger tree by one
|
|
280
|
+
# real call edge in ~9500. A caveat, not a difference: it is the right pair,
|
|
281
|
+
# the reader just must not read a two-edge delta as a fact about the code.
|
|
282
|
+
caveats.append("both sides are deep-tier, which is not byte-stable: two "
|
|
283
|
+
"builds of an unchanged tree can differ by a few jedi-resolved "
|
|
284
|
+
"edges, so read a small delta as possible tool noise")
|
|
267
285
|
return {
|
|
268
286
|
"comparable": not differences,
|
|
269
287
|
"differences": differences,
|
|
288
|
+
"caveats": caveats,
|
|
270
289
|
"old": describe(old),
|
|
271
290
|
"new": describe(new),
|
|
272
291
|
}
|
|
@@ -53,6 +53,10 @@ def render_apidiff(old: Graph, new: Graph) -> str:
|
|
|
53
53
|
+ f". Old: {prov['old']} | new: {prov['new']}. "
|
|
54
54
|
"Differences below may be tool changes, not code changes.")
|
|
55
55
|
out.append("")
|
|
56
|
+
# R1-C42: comparable, and still carrying a noise floor the reader has to know about.
|
|
57
|
+
for caveat in prov.get("caveats") or ():
|
|
58
|
+
out.append(f"> ℹ️ {caveat}.")
|
|
59
|
+
out.append("")
|
|
56
60
|
|
|
57
61
|
removed = d["removed"]
|
|
58
62
|
if removed:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codmap
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.10
|
|
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
|
[](https://github.com/kogriv/codemap/actions/workflows/ci.yml)
|
|
46
46
|
|
|
47
|
-
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **
|
|
47
|
+
**Status:** 🟢 M0–M20 implemented + research track (R1/R2) — schema 0.13, **765 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 &
|
|
@@ -58,8 +58,9 @@ export edges, best-effort call edges, registry-family `implements` links, string
|
|
|
58
58
|
and per-call argument contracts — then answers questions over it.
|
|
59
59
|
|
|
60
60
|
Design principles: **source-only** (static `ast`/`griffe`, never imports the target), **deterministic**
|
|
61
|
-
(canonical sorted JSON, no timestamps — diffable
|
|
62
|
-
|
|
61
|
+
(canonical sorted JSON, no timestamps — diffable; byte-stable on the fast tier, see the caveat
|
|
62
|
+
[below](#one-caveat-on-deterministic)), **CLI-AI-first** (JSON by default, stable exit codes),
|
|
63
|
+
**honest** (approximations are labeled, not hidden).
|
|
63
64
|
|
|
64
65
|
## Point questions and whole-graph questions
|
|
65
66
|
|
|
@@ -247,6 +248,22 @@ against *all* true edges (the price of Python's dynamism), and a grep-vs-graph p
|
|
|
247
248
|
cheaper than grep for impact on unique names, tens of × on polymorphic ones, and no cheaper for locating a
|
|
248
249
|
symbol.
|
|
249
250
|
|
|
251
|
+
## One caveat on "deterministic"
|
|
252
|
+
|
|
253
|
+
The **fast tier is byte-stable**: two builds of an unchanged tree produce identical files, and CI
|
|
254
|
+
compares the bytes on every push. The **deep tier (`--deep`, jedi) is not**. Ten deep builds of an
|
|
255
|
+
unchanged tree here produced two distinct artifacts — differing in two per-symbol call counters out
|
|
256
|
+
of 2133 nodes, no edges — and on a larger external tree one build in seven lost one real call edge of
|
|
257
|
+
9524. The cause is jedi's per-script execution budget: an inference that runs out of it returns
|
|
258
|
+
nothing, and nothing is recorded as `unresolved`.
|
|
259
|
+
|
|
260
|
+
So a deep graph is one sample of a slightly fuzzy function, not a function of its input. Every deep
|
|
261
|
+
build says so in its own diagnostics, and `codemap diff` says so when both sides are deep — read a
|
|
262
|
+
delta of a few call edges as possible tool noise before reading it as a change in the code. Anything
|
|
263
|
+
that must be reproducible byte-for-byte — a gate, a two-release comparison — belongs on the fast tier.
|
|
264
|
+
|
|
265
|
+
**Measurement:** [gaps/deep_tier_nondeterminism_2026-09-02.md](gaps/deep_tier_nondeterminism_2026-09-02.md).
|
|
266
|
+
|
|
250
267
|
## Dogfooding
|
|
251
268
|
|
|
252
269
|
codemap is validated end-to-end against a real external package. Place a target repo as a sibling and
|
|
@@ -109,6 +109,8 @@ tests/test_r1c38_tool_drift.py
|
|
|
109
109
|
tests/test_r1c3_arch_contract.py
|
|
110
110
|
tests/test_r1c3_dogfood.py
|
|
111
111
|
tests/test_r1c41_scope_membership.py
|
|
112
|
+
tests/test_r1c42_deep_tier_stability.py
|
|
113
|
+
tests/test_r1c43_incremental_splice.py
|
|
112
114
|
tests/test_r1c4_complexity.py
|
|
113
115
|
tests/test_r1c5_apidiff.py
|
|
114
116
|
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.10"
|
|
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
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""R1-C42 — the deep tier is not byte-stable, and the artifact must say so.
|
|
2
|
+
|
|
3
|
+
Raised by the second real target: seven builds of an unchanged clean tree, and in one
|
|
4
|
+
of them a real `calls` edge (resolved through `getattr`) was gone — 9524 edges against
|
|
5
|
+
9523. They had briefly concluded a regression between two releases from it, which is
|
|
6
|
+
the failure this suite exists to prevent: a consumer reading "deterministic" and
|
|
7
|
+
comparing two deep graphs as if every difference were a change in the code.
|
|
8
|
+
|
|
9
|
+
The instability itself was **known** — measured at R1-C9, and the reason the CI
|
|
10
|
+
determinism job runs the fast tier only. It lived in a comment in a workflow file,
|
|
11
|
+
while README said "deterministic" unqualified and `provenance.md` said two builds of an
|
|
12
|
+
unchanged tree are byte-identical. So the fix is disclosure in the places a consumer
|
|
13
|
+
actually reads: the graph's own diagnostics, and any comparison of two deep graphs.
|
|
14
|
+
|
|
15
|
+
Reproduced here on our own tree before the fix: ten deep builds → two distinct
|
|
16
|
+
artifacts (7/3), differing in two per-symbol call counters and no edges. The cause is
|
|
17
|
+
jedi's per-script execution budget: an inference that runs out of it returns nothing,
|
|
18
|
+
and nothing reads as `unresolved`. That is not testable in a unit suite — a build takes
|
|
19
|
+
~40 s and flips about one run in three — so what is pinned here is the disclosure.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from codemap.diagnostics import DEEP_TIER_UNSTABLE, NOTE, WARNING, deep_tier_diagnostic, diagnostics
|
|
25
|
+
from codemap.model import Graph
|
|
26
|
+
from codemap.provenance import comparability
|
|
27
|
+
from codemap.serve.apidiff import render_apidiff
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _graph(tier: str) -> Graph:
|
|
31
|
+
g = Graph(target="pkg")
|
|
32
|
+
g.provenance = {"tier": tier, "tool": {"name": "codemap", "version": "0.0.9"}}
|
|
33
|
+
return g
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# -- the note on the artifact --------------------------------------------------
|
|
37
|
+
|
|
38
|
+
def test_a_fast_graph_says_nothing():
|
|
39
|
+
assert deep_tier_diagnostic(_graph("fast")) is None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_a_deep_graph_declares_its_noise_floor():
|
|
43
|
+
d = deep_tier_diagnostic(_graph("deep"))
|
|
44
|
+
assert d is not None and d["code"] == DEEP_TIER_UNSTABLE
|
|
45
|
+
assert "not byte-stable" in d["message"]
|
|
46
|
+
assert d["consequence"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_it_is_a_note_and_not_a_warning():
|
|
50
|
+
"""A warning says the findings below are invalid. Nothing here is invalid — the
|
|
51
|
+
graph is sound, it is one sample of a slightly fuzzy function (issue #8's rule)."""
|
|
52
|
+
d = deep_tier_diagnostic(_graph("deep"))
|
|
53
|
+
assert d["severity"] == NOTE != WARNING
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_it_reaches_the_surface_a_consumer_reads():
|
|
57
|
+
codes = [d["code"] for d in diagnostics(_graph("deep"))]
|
|
58
|
+
assert DEEP_TIER_UNSTABLE in codes
|
|
59
|
+
assert DEEP_TIER_UNSTABLE not in [d["code"] for d in diagnostics(_graph("fast"))]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_a_pre_provenance_graph_is_not_guessed_about():
|
|
63
|
+
g = Graph(target="pkg")
|
|
64
|
+
g.provenance = {}
|
|
65
|
+
assert deep_tier_diagnostic(g) is None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# -- the caveat on a comparison ------------------------------------------------
|
|
69
|
+
|
|
70
|
+
def test_two_deep_graphs_stay_comparable_and_carry_the_caveat():
|
|
71
|
+
c = comparability(_graph("deep").provenance, _graph("deep").provenance)
|
|
72
|
+
assert c["comparable"] is True, "matching tiers are the right pair — never a refusal"
|
|
73
|
+
assert c["caveats"] and "not byte-stable" in c["caveats"][0]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_two_fast_graphs_carry_no_caveat():
|
|
77
|
+
c = comparability(_graph("fast").provenance, _graph("fast").provenance)
|
|
78
|
+
assert c["caveats"] == []
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_mixed_tiers_are_still_an_incomparability_not_a_caveat():
|
|
82
|
+
c = comparability(_graph("fast").provenance, _graph("deep").provenance)
|
|
83
|
+
assert c["comparable"] is False
|
|
84
|
+
assert any("different tier" in d for d in c["differences"])
|
|
85
|
+
assert c["caveats"] == [], "a caveat must not soften a genuine incomparability"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_the_rendered_diff_shows_it_above_the_verdict():
|
|
89
|
+
out = render_apidiff(_graph("deep"), _graph("deep"))
|
|
90
|
+
assert "not byte-stable" in out
|
|
91
|
+
head = out.split("## ")[0]
|
|
92
|
+
assert "not byte-stable" in head, "a footnote is not a disclosure"
|