codmap 0.0.8__tar.gz → 0.0.9__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.9}/PKG-INFO +21 -4
- {codmap-0.0.8 → codmap-0.0.9}/README.md +20 -3
- {codmap-0.0.8 → codmap-0.0.9}/codemap/cli.py +2 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/diagnostics.py +34 -1
- {codmap-0.0.8 → codmap-0.0.9}/codemap/provenance.py +12 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/serve/apidiff.py +4 -0
- {codmap-0.0.8 → codmap-0.0.9/codmap.egg-info}/PKG-INFO +21 -4
- {codmap-0.0.8 → codmap-0.0.9}/codmap.egg-info/SOURCES.txt +1 -0
- {codmap-0.0.8 → codmap-0.0.9}/pyproject.toml +1 -1
- codmap-0.0.9/tests/test_r1c42_deep_tier_stability.py +92 -0
- {codmap-0.0.8 → codmap-0.0.9}/LICENSE +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/__init__.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/apidiff.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/arch.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/extract/__init__.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/extract/attrflow.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/extract/behavior.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/extract/dataflow.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/extract/dispatch.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/extract/griffe_extractor.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/extract/gsource.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/extract/roots.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/freshness.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/incremental.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/integrations/__init__.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/integrations/base.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/integrations/cocoindex.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/integrations/gate.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/integrations/gitnexus.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/integrations/registry.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/integrations/transport.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/model.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/query.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/scope.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/serve/__init__.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/serve/_scip_pb2.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/serve/api_surface.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/serve/architecture.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/serve/audit.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/serve/check.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/serve/ctags.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/serve/impact.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/serve/limits.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/serve/livingdocs.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/serve/mcp_server.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/serve/mermaid.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/serve/pack.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/serve/rag.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/serve/review.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/serve/scip.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/serve/semantic.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/serve/server.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/serve/session.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/serve/subsystems.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/serve/vault.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/store.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/tomlio.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codemap/watch.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codmap.egg-info/dependency_links.txt +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codmap.egg-info/entry_points.txt +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codmap.egg-info/requires.txt +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/codmap.egg-info/top_level.txt +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/setup.cfg +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_epistemic.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_gitnexus_router.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_impact_depth.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_integrations.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_issue3_serve_freshness.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_livingdocs.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_m0_api_surface.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_m11_argcontract.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_m12_dataflow.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_m15_review.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_m16_architecture.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_m17_mcp.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_m18_freshness.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_m1_5_semantics.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_m1_query.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_m2_views.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_m32_watch.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_m3_serve.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_m4_behavior.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_m5_deep.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_m6_repo_scope.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_m7_dispatch.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_m9_family.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c13_callgraph_accuracy.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c13_grep_vs_graph.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c16_semantic.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c20_attribute_edges.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c21_flat_layout.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c22_source_visible_refs.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c23_hard_python.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c24_test_mapping.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c25_provenance.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c26_deep_union.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c27_config_honesty.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c28_limit_envelope.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c29_lazy_imports.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c2_ctags.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c30_local_import_calls.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c30f1_reexport_calls.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c30f2_check_scope.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c31_path_origin.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c32_report_json.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c33_query_signature.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c34_signature_kinds.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c35_contract_location.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c36_target_resolution.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c37_rule_mutation.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c38_tool_drift.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c3_arch_contract.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c3_dogfood.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c41_scope_membership.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c4_complexity.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c5_apidiff.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c6_pack.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c7_edge_vocab.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c8_deadcode.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_r1c9_incremental.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_scip_export.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/tests/test_scope.py +0 -0
- {codmap-0.0.8 → codmap-0.0.9}/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.9
|
|
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, **752 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, **752 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
|
|
@@ -747,6 +747,8 @@ def _cmd_diff(args) -> int:
|
|
|
747
747
|
print(f"[warning] {line}", file=sys.stderr)
|
|
748
748
|
print(f"[warning] differences below may be tool changes, not code changes "
|
|
749
749
|
f"(old: {cmp['old']} | new: {cmp['new']})", file=sys.stderr)
|
|
750
|
+
for caveat in cmp.get("caveats") or ():
|
|
751
|
+
print(f"[note] {caveat}", file=sys.stderr)
|
|
750
752
|
print(render_apidiff(old, new), end="")
|
|
751
753
|
if args.exit_code and not build_apidiff(old, new)["ok"]:
|
|
752
754
|
return 1
|
|
@@ -22,6 +22,7 @@ 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"
|
|
25
26
|
|
|
26
27
|
#: A ``warning`` invalidates the conclusions a surface draws from the graph — read them as
|
|
27
28
|
#: unknown. A ``note`` states a fact about how the graph was built and invalidates nothing.
|
|
@@ -190,6 +191,38 @@ def unread_inputs_diagnostic(graph) -> dict | None:
|
|
|
190
191
|
}
|
|
191
192
|
|
|
192
193
|
|
|
194
|
+
def deep_tier_diagnostic(graph) -> dict | None:
|
|
195
|
+
"""State the deep tier's noise floor on the artifact itself (R1-C42).
|
|
196
|
+
|
|
197
|
+
A **note**, not a warning: nothing here is wrong, and no finding below is invalid.
|
|
198
|
+
What was missing is that the tier's instability was known — measured at R1-C9, and
|
|
199
|
+
the reason the CI determinism job runs the fast tier only — while living exclusively
|
|
200
|
+
in a comment in a workflow file. A consumer read "deterministic" and built a
|
|
201
|
+
two-release comparison on it; the difference they saw was the tool, not the code.
|
|
202
|
+
|
|
203
|
+
Measured on two trees: ten deep builds of an unchanged tree produced **two** distinct
|
|
204
|
+
artifacts (7/3), differing in two per-symbol call counters and no edges; on a larger
|
|
205
|
+
tree one build in seven lost one real call edge of 9524. The cause is jedi's
|
|
206
|
+
per-script execution budget — an inference that runs out of it returns nothing, and
|
|
207
|
+
that reads as `unresolved` rather than as an error.
|
|
208
|
+
"""
|
|
209
|
+
if (graph.provenance or {}).get("tier") != "deep":
|
|
210
|
+
return None
|
|
211
|
+
return {
|
|
212
|
+
"code": DEEP_TIER_UNSTABLE,
|
|
213
|
+
"severity": NOTE,
|
|
214
|
+
"tier": "deep",
|
|
215
|
+
"consequence": ("Everything here is a lower bound as usual; treat a difference "
|
|
216
|
+
"of a few call edges between two deep graphs as possible tool "
|
|
217
|
+
"noise rather than a change in the code."),
|
|
218
|
+
"message": (
|
|
219
|
+
"built on the deep (jedi) tier, which is not byte-stable: two builds of an "
|
|
220
|
+
"unchanged tree can differ by a few call edges — measured at roughly one run "
|
|
221
|
+
"in three on a 2133-node tree, by two per-symbol counters and no edges."
|
|
222
|
+
),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
193
226
|
def scope_membership_diagnostic(graph) -> dict | None:
|
|
194
227
|
"""Flag files the graph was built from that the input manifest never listed (R1-C41).
|
|
195
228
|
|
|
@@ -274,7 +307,7 @@ def diagnostics(graph) -> list[dict]:
|
|
|
274
307
|
checks = (import_graph_diagnostic(graph), namespace_target_diagnostic(graph),
|
|
275
308
|
cross_root_diagnostic(graph), schema_diagnostic(graph),
|
|
276
309
|
unread_inputs_diagnostic(graph), module_count_diagnostic(graph),
|
|
277
|
-
scope_membership_diagnostic(graph))
|
|
310
|
+
scope_membership_diagnostic(graph), deep_tier_diagnostic(graph))
|
|
278
311
|
return [d for d in checks if d is not None]
|
|
279
312
|
|
|
280
313
|
|
|
@@ -253,6 +253,7 @@ def comparability(old: dict | None, new: dict | None) -> dict:
|
|
|
253
253
|
were dead.
|
|
254
254
|
"""
|
|
255
255
|
differences: list[str] = []
|
|
256
|
+
caveats: list[str] = []
|
|
256
257
|
if not old or not new:
|
|
257
258
|
differences.append("one of the graphs records no provenance "
|
|
258
259
|
"(built before schema 0.12) — the pair cannot be verified")
|
|
@@ -264,9 +265,20 @@ def comparability(old: dict | None, new: dict | None) -> dict:
|
|
|
264
265
|
old_roots, new_roots = old.get("roots"), new.get("roots")
|
|
265
266
|
if old_roots != new_roots:
|
|
266
267
|
differences.append(f"different scope roots: {old_roots} → {new_roots}")
|
|
268
|
+
if old.get("tier") == new.get("tier") == "deep":
|
|
269
|
+
# R1-C42: matching tiers make the pair comparable, and on the deep tier that
|
|
270
|
+
# is not the same as "every difference here is a code change". Measured on
|
|
271
|
+
# two trees: builds of an *unchanged* tree differ in roughly one run of
|
|
272
|
+
# three, by a couple of call classifications — and on the larger tree by one
|
|
273
|
+
# real call edge in ~9500. A caveat, not a difference: it is the right pair,
|
|
274
|
+
# the reader just must not read a two-edge delta as a fact about the code.
|
|
275
|
+
caveats.append("both sides are deep-tier, which is not byte-stable: two "
|
|
276
|
+
"builds of an unchanged tree can differ by a few call edges, "
|
|
277
|
+
"so read a small call-graph delta as possible tool noise")
|
|
267
278
|
return {
|
|
268
279
|
"comparable": not differences,
|
|
269
280
|
"differences": differences,
|
|
281
|
+
"caveats": caveats,
|
|
270
282
|
"old": describe(old),
|
|
271
283
|
"new": describe(new),
|
|
272
284
|
}
|
|
@@ -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.9
|
|
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, **752 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,7 @@ 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
|
|
112
113
|
tests/test_r1c4_complexity.py
|
|
113
114
|
tests/test_r1c5_apidiff.py
|
|
114
115
|
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.9"
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|