codmap 0.0.9__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.9/codmap.egg-info → codmap-0.0.10}/PKG-INFO +2 -2
- {codmap-0.0.9 → codmap-0.0.10}/README.md +1 -1
- {codmap-0.0.9 → codmap-0.0.10}/codemap/cli.py +5 -1
- {codmap-0.0.9 → codmap-0.0.10}/codemap/diagnostics.py +43 -4
- {codmap-0.0.9 → codmap-0.0.10}/codemap/incremental.py +36 -5
- {codmap-0.0.9 → codmap-0.0.10}/codemap/provenance.py +12 -5
- {codmap-0.0.9 → codmap-0.0.10/codmap.egg-info}/PKG-INFO +2 -2
- {codmap-0.0.9 → codmap-0.0.10}/codmap.egg-info/SOURCES.txt +1 -0
- {codmap-0.0.9 → codmap-0.0.10}/pyproject.toml +1 -1
- codmap-0.0.10/tests/test_r1c43_incremental_splice.py +220 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c9_incremental.py +32 -5
- {codmap-0.0.9 → codmap-0.0.10}/LICENSE +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/__init__.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/apidiff.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/arch.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/extract/__init__.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/extract/attrflow.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/extract/behavior.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/extract/dataflow.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/extract/dispatch.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/extract/griffe_extractor.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/extract/gsource.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/extract/roots.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/freshness.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/integrations/__init__.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/integrations/base.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/integrations/cocoindex.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/integrations/gate.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/integrations/gitnexus.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/integrations/registry.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/integrations/transport.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/model.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/query.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/scope.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/serve/__init__.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/serve/_scip_pb2.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/serve/api_surface.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/serve/apidiff.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/serve/architecture.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/serve/audit.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/serve/check.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/serve/ctags.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/serve/impact.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/serve/limits.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/serve/livingdocs.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/serve/mcp_server.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/serve/mermaid.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/serve/pack.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/serve/rag.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/serve/review.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/serve/scip.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/serve/semantic.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/serve/server.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/serve/session.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/serve/subsystems.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/serve/vault.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/store.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/tomlio.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codemap/watch.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codmap.egg-info/dependency_links.txt +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codmap.egg-info/entry_points.txt +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codmap.egg-info/requires.txt +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/codmap.egg-info/top_level.txt +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/setup.cfg +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_epistemic.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_gitnexus_router.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_impact_depth.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_integrations.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_issue3_serve_freshness.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_livingdocs.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_m0_api_surface.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_m11_argcontract.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_m12_dataflow.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_m15_review.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_m16_architecture.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_m17_mcp.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_m18_freshness.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_m1_5_semantics.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_m1_query.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_m2_views.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_m32_watch.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_m3_serve.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_m4_behavior.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_m5_deep.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_m6_repo_scope.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_m7_dispatch.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_m9_family.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c13_callgraph_accuracy.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c13_grep_vs_graph.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c16_semantic.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c20_attribute_edges.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c21_flat_layout.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c22_source_visible_refs.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c23_hard_python.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c24_test_mapping.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c25_provenance.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c26_deep_union.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c27_config_honesty.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c28_limit_envelope.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c29_lazy_imports.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c2_ctags.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c30_local_import_calls.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c30f1_reexport_calls.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c30f2_check_scope.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c31_path_origin.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c32_report_json.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c33_query_signature.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c34_signature_kinds.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c35_contract_location.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c36_target_resolution.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c37_rule_mutation.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c38_tool_drift.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c3_arch_contract.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c3_dogfood.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c41_scope_membership.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c42_deep_tier_stability.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c4_complexity.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c5_apidiff.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c6_pack.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c7_edge_vocab.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_r1c8_deadcode.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_scip_export.py +0 -0
- {codmap-0.0.9 → codmap-0.0.10}/tests/test_scope.py +0 -0
- {codmap-0.0.9 → 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 &
|
|
@@ -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 &
|
|
@@ -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):
|
|
@@ -23,6 +23,7 @@ UNREAD_INPUTS = "unread_inputs"
|
|
|
23
23
|
MODULE_COUNT_MISMATCH = "module_count_mismatch"
|
|
24
24
|
SCOPE_MEMBERSHIP = "scope_membership"
|
|
25
25
|
DEEP_TIER_UNSTABLE = "deep_tier_unstable"
|
|
26
|
+
INCREMENTAL_DEEP_SPLICE = "incremental_deep_splice"
|
|
26
27
|
|
|
27
28
|
#: A ``warning`` invalidates the conclusions a surface draws from the graph — read them as
|
|
28
29
|
#: unknown. A ``note`` states a fact about how the graph was built and invalidates nothing.
|
|
@@ -213,12 +214,49 @@ def deep_tier_diagnostic(graph) -> dict | None:
|
|
|
213
214
|
"severity": NOTE,
|
|
214
215
|
"tier": "deep",
|
|
215
216
|
"consequence": ("Everything here is a lower bound as usual; treat a difference "
|
|
216
|
-
"of a few
|
|
217
|
+
"of a few jedi-resolved edges between two deep graphs as tool "
|
|
217
218
|
"noise rather than a change in the code."),
|
|
218
219
|
"message": (
|
|
219
220
|
"built on the deep (jedi) tier, which is not byte-stable: two builds of an "
|
|
220
|
-
"unchanged tree can differ by a few
|
|
221
|
-
"in three on
|
|
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."
|
|
222
260
|
),
|
|
223
261
|
}
|
|
224
262
|
|
|
@@ -307,7 +345,8 @@ def diagnostics(graph) -> list[dict]:
|
|
|
307
345
|
checks = (import_graph_diagnostic(graph), namespace_target_diagnostic(graph),
|
|
308
346
|
cross_root_diagnostic(graph), schema_diagnostic(graph),
|
|
309
347
|
unread_inputs_diagnostic(graph), module_count_diagnostic(graph),
|
|
310
|
-
scope_membership_diagnostic(graph), deep_tier_diagnostic(graph)
|
|
348
|
+
scope_membership_diagnostic(graph), deep_tier_diagnostic(graph),
|
|
349
|
+
incremental_splice_diagnostic(graph))
|
|
311
350
|
return [d for d in checks if d is not None]
|
|
312
351
|
|
|
313
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
|
|
@@ -273,8 +280,8 @@ def comparability(old: dict | None, new: dict | None) -> dict:
|
|
|
273
280
|
# real call edge in ~9500. A caveat, not a difference: it is the right pair,
|
|
274
281
|
# the reader just must not read a two-edge delta as a fact about the code.
|
|
275
282
|
caveats.append("both sides are deep-tier, which is not byte-stable: two "
|
|
276
|
-
"builds of an unchanged tree can differ by a few
|
|
277
|
-
"so read a small
|
|
283
|
+
"builds of an unchanged tree can differ by a few jedi-resolved "
|
|
284
|
+
"edges, so read a small delta as possible tool noise")
|
|
278
285
|
return {
|
|
279
286
|
"comparable": not differences,
|
|
280
287
|
"differences": differences,
|
|
@@ -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 &
|
|
@@ -110,6 +110,7 @@ tests/test_r1c3_arch_contract.py
|
|
|
110
110
|
tests/test_r1c3_dogfood.py
|
|
111
111
|
tests/test_r1c41_scope_membership.py
|
|
112
112
|
tests/test_r1c42_deep_tier_stability.py
|
|
113
|
+
tests/test_r1c43_incremental_splice.py
|
|
113
114
|
tests/test_r1c4_complexity.py
|
|
114
115
|
tests/test_r1c5_apidiff.py
|
|
115
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,220 @@
|
|
|
1
|
+
"""R1-C43 — an incremental deep graph says that parts of it were not recomputed.
|
|
2
|
+
|
|
3
|
+
Two separate things are pinned here, and only the second one is a *fix*:
|
|
4
|
+
|
|
5
|
+
1. ``provenance.incremental`` is written by every build path, ``false`` included — a
|
|
6
|
+
consumer must be able to tell "recomputed in full" from "predates the field", and an
|
|
7
|
+
absent key is the second of those (R1-C28).
|
|
8
|
+
2. The diagnostic that reads it, which fires on the deep tier only.
|
|
9
|
+
|
|
10
|
+
The third block pins a **known limit, not a guarantee**: ``_affected_modules`` rule (b)
|
|
11
|
+
reads the *old* graph, so an edge that build missed is a dependency it cannot see. On
|
|
12
|
+
the deep tier the old graph is one jedi sample, so a noise-dropped edge switches off the
|
|
13
|
+
recompute that would have restored it. Measured on an 88-module package — same edit,
|
|
14
|
+
same tree, the writer module was invalidated when the edge was present and not when it
|
|
15
|
+
was absent (gaps/incremental_noise_persistence_2026-09-02.md §6). These tests state that
|
|
16
|
+
behaviour deliberately: when it is fixed, they must be rewritten, not deleted quietly.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from codemap import cli, store
|
|
25
|
+
from codemap.diagnostics import (
|
|
26
|
+
DEEP_TIER_UNSTABLE, INCREMENTAL_DEEP_SPLICE, NOTE, WARNING,
|
|
27
|
+
diagnostics, incremental_splice_diagnostic, render_lines,
|
|
28
|
+
)
|
|
29
|
+
from codemap.extract import extract
|
|
30
|
+
from codemap.incremental import _affected_modules, _module_indexer, update_graph
|
|
31
|
+
from codemap.model import Edge, Graph, Node
|
|
32
|
+
from codemap.provenance import build_provenance
|
|
33
|
+
from codemap.scope import resolve_scope
|
|
34
|
+
|
|
35
|
+
PKG_INIT = '"""Tiny package."""\n'
|
|
36
|
+
BASE = '''\
|
|
37
|
+
"""Base."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Config:
|
|
41
|
+
"""A config."""
|
|
42
|
+
|
|
43
|
+
width = 10
|
|
44
|
+
'''
|
|
45
|
+
LEAF = '''\
|
|
46
|
+
"""Leaf."""
|
|
47
|
+
|
|
48
|
+
from .base import Config
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def widen(c: Config) -> int:
|
|
52
|
+
"""Read a field off a typed parameter."""
|
|
53
|
+
return c.width + 1
|
|
54
|
+
'''
|
|
55
|
+
OTHER = '''\
|
|
56
|
+
"""Unrelated to the pair above."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def helper() -> int:
|
|
60
|
+
return 1
|
|
61
|
+
'''
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _pkg(tmp_path: Path) -> Path:
|
|
65
|
+
pkg = tmp_path / "tiny"
|
|
66
|
+
pkg.mkdir()
|
|
67
|
+
(pkg / "__init__.py").write_text(PKG_INIT, encoding="utf-8")
|
|
68
|
+
(pkg / "base.py").write_text(BASE, encoding="utf-8")
|
|
69
|
+
(pkg / "leaf.py").write_text(LEAF, encoding="utf-8")
|
|
70
|
+
(pkg / "other.py").write_text(OTHER, encoding="utf-8")
|
|
71
|
+
return pkg
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _scope(pkg: Path) -> dict:
|
|
75
|
+
return resolve_scope(pkg, use_git=False)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# -- 1. the field ------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
def test_a_full_build_declares_that_it_recomputed_everything(tmp_path):
|
|
81
|
+
"""`false` is a statement; the reader must not have to infer it from absence."""
|
|
82
|
+
g = extract(_pkg(tmp_path), deep=False)
|
|
83
|
+
assert g.provenance["incremental"] is False
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_an_incremental_rebuild_declares_that_it_did_not(tmp_path):
|
|
87
|
+
pkg = _pkg(tmp_path)
|
|
88
|
+
g0 = extract(pkg, deep=False)
|
|
89
|
+
s0 = _scope(pkg)
|
|
90
|
+
(pkg / "other.py").write_text(OTHER + "\n# edited\n", encoding="utf-8")
|
|
91
|
+
g1, info = update_graph(g0, pkg, s0, _scope(pkg), deep=False)
|
|
92
|
+
if info["mode"] == "incremental":
|
|
93
|
+
assert g1.provenance["incremental"] is True
|
|
94
|
+
else:
|
|
95
|
+
# a four-module fixture trips the >=50% full-rebuild fallback easily; a full
|
|
96
|
+
# rebuild recomputes everything and must say so.
|
|
97
|
+
assert g1.provenance["incremental"] is False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_the_cli_does_not_drop_the_flag_when_it_restamps_provenance(tmp_path):
|
|
101
|
+
"""The subtle one: `_cmd_build` **overwrites** the block `update_graph` produced.
|
|
102
|
+
|
|
103
|
+
Nothing in the library-level tests can catch that — they read the graph object
|
|
104
|
+
before the CLI touches it — so the field would have been written, overwritten with
|
|
105
|
+
the default, and shipped as `false` on every incremental build.
|
|
106
|
+
"""
|
|
107
|
+
pkg = _pkg(tmp_path)
|
|
108
|
+
out = tmp_path / "g.json"
|
|
109
|
+
assert cli.main(["build", str(pkg), "-o", str(out)]) == 0
|
|
110
|
+
assert json.loads(out.read_text(encoding="utf-8"))["provenance"]["incremental"] is False
|
|
111
|
+
|
|
112
|
+
(pkg / "other.py").write_text(OTHER + "\n# edited\n", encoding="utf-8")
|
|
113
|
+
assert cli.main(["build", str(pkg), "-o", str(out), "--incremental"]) == 0
|
|
114
|
+
written = json.loads(out.read_text(encoding="utf-8"))["provenance"]
|
|
115
|
+
assert written["incremental"] is True
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_an_untouched_tree_is_carried_over_too(tmp_path):
|
|
119
|
+
"""`mode: unchanged` returns the previous graph whole — nothing was recomputed."""
|
|
120
|
+
pkg = _pkg(tmp_path)
|
|
121
|
+
out = tmp_path / "g.json"
|
|
122
|
+
assert cli.main(["build", str(pkg), "-o", str(out)]) == 0
|
|
123
|
+
assert cli.main(["build", str(pkg), "-o", str(out), "--incremental"]) == 0
|
|
124
|
+
assert json.loads(out.read_text(encoding="utf-8"))["provenance"]["incremental"] is True
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_the_flag_survives_a_round_trip_through_the_artifact(tmp_path):
|
|
128
|
+
g = extract(_pkg(tmp_path), deep=False)
|
|
129
|
+
g.provenance = build_provenance(tier="deep", incremental=True)
|
|
130
|
+
out = tmp_path / "g.json"
|
|
131
|
+
store.save(g, out)
|
|
132
|
+
assert store.load(out).provenance["incremental"] is True
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# -- 2. the diagnostic -------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
def _graph(tier: str, **kw) -> Graph:
|
|
138
|
+
g = Graph(target="tiny")
|
|
139
|
+
g.provenance = build_provenance(tier=tier, **kw)
|
|
140
|
+
return g
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_a_spliced_deep_graph_says_so():
|
|
144
|
+
d = incremental_splice_diagnostic(_graph("deep", incremental=True))
|
|
145
|
+
assert d is not None
|
|
146
|
+
assert d["code"] == INCREMENTAL_DEEP_SPLICE
|
|
147
|
+
assert d["severity"] == NOTE and d["severity"] != WARNING
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_a_full_deep_graph_is_not_accused_of_splicing():
|
|
151
|
+
assert incremental_splice_diagnostic(_graph("deep", incremental=False)) is None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_the_fast_tier_is_excluded_because_its_splice_is_exact():
|
|
155
|
+
"""Byte-identity to a full build is pinned there; there is no sample to freeze."""
|
|
156
|
+
assert incremental_splice_diagnostic(_graph("fast", incremental=True)) is None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_a_graph_predating_the_field_is_not_guessed_about():
|
|
160
|
+
g = Graph(target="tiny")
|
|
161
|
+
g.provenance = {"tier": "deep", "tool": {"name": "codemap"}} # no `incremental` key
|
|
162
|
+
assert incremental_splice_diagnostic(g) is None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_it_reaches_the_aggregator_alongside_the_tier_note():
|
|
166
|
+
"""Two different facts about one graph: it is a sample, and the sample is frozen."""
|
|
167
|
+
codes = {d["code"] for d in diagnostics(_graph("deep", incremental=True))}
|
|
168
|
+
assert {INCREMENTAL_DEEP_SPLICE, DEEP_TIER_UNSTABLE} <= codes
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_the_rendered_note_tells_the_reader_not_to_retry_incrementally():
|
|
172
|
+
text = " ".join(render_lines(_graph("deep", incremental=True)))
|
|
173
|
+
assert "full rebuild" in text or "full rebuild resamples" in text
|
|
174
|
+
assert "ℹ️" in text
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# -- 3. the limit this does NOT fix ------------------------------------------
|
|
178
|
+
|
|
179
|
+
def _old_graph(with_edge: bool) -> Graph:
|
|
180
|
+
"""Two old graphs differing by exactly one behavioral edge.
|
|
181
|
+
|
|
182
|
+
`leaf.widen` writes/reads `base.Config.width`. The pair is *not* import-linked in
|
|
183
|
+
the way rule (a) needs (that rule keys on module-level `imports` edges, added
|
|
184
|
+
below only for the unrelated module), so rule (b) — which reads this graph — is the
|
|
185
|
+
only route by which editing `base` can invalidate `leaf`.
|
|
186
|
+
"""
|
|
187
|
+
g = Graph(target="tiny")
|
|
188
|
+
for mid in ("tiny", "tiny.base", "tiny.leaf", "tiny.other"):
|
|
189
|
+
g.add_node(Node(id=mid, kind="module"))
|
|
190
|
+
g.add_node(Node(id="tiny.base.Config.width", kind="attribute"))
|
|
191
|
+
g.add_node(Node(id="tiny.leaf.widen", kind="function"))
|
|
192
|
+
if with_edge:
|
|
193
|
+
g.add_edge(Edge(type="accesses", source="tiny.leaf.widen",
|
|
194
|
+
target="tiny.base.Config.width",
|
|
195
|
+
extras={"access": "read", "resolution": "deep"}))
|
|
196
|
+
return g
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _affected_for(with_edge: bool) -> set[str]:
|
|
200
|
+
old = _old_graph(with_edge)
|
|
201
|
+
new = Graph(target="tiny") # no fresh `imports` edge into base → rule (a) idle
|
|
202
|
+
for mid in ("tiny", "tiny.base", "tiny.leaf", "tiny.other"):
|
|
203
|
+
new.add_node(Node(id=mid, kind="module"))
|
|
204
|
+
module_of = _module_indexer({n.id for n in old.nodes.values() if n.kind == "module"})
|
|
205
|
+
changed = {"tiny.base"}
|
|
206
|
+
return _affected_modules(old, new, changed, changed, module_of)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_the_edge_is_what_makes_the_writer_get_recomputed():
|
|
210
|
+
assert "tiny.leaf" in _affected_for(with_edge=True)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_and_without_it_the_writer_is_silently_left_alone():
|
|
214
|
+
"""The measured failure, stated: a dropped edge is a dropped reason to recompute.
|
|
215
|
+
|
|
216
|
+
Not an assertion that this is right — it is the limit R1-C43 names and does not
|
|
217
|
+
close. Door (2) (a periodic full rebuild) or door (3) (restricting what an
|
|
218
|
+
incremental deep graph may be used to conclude) would change this expectation.
|
|
219
|
+
"""
|
|
220
|
+
assert "tiny.leaf" not in _affected_for(with_edge=False)
|
|
@@ -7,14 +7,24 @@ fast and deep tiers, over a small multi-module package with cross-module calls,
|
|
|
7
7
|
inheritance, and attribute access (the relationships the splice must get right).
|
|
8
8
|
|
|
9
9
|
Deep byte-identity holds here because this fixture's jedi inference is single-hop and
|
|
10
|
-
stable. On large packages
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
stable. On large packages it does not, and the two reasons are worth keeping apart:
|
|
11
|
+
|
|
12
|
+
- Two full ``--deep`` builds of an unchanged tree already differ from each other
|
|
13
|
+
(R1-C42), so on that tier there is no fixed artifact to be identical *to*. This was
|
|
14
|
+
attributed to jedi's **cache warmth** until 2026-09-02, when a cold ``XDG_CACHE_HOME``
|
|
15
|
+
per build flipped just as often (5 of 10) and refuted it; the cause is jedi's
|
|
16
|
+
per-script execution budget. That correction reached ``docs/incremental.md`` the same
|
|
17
|
+
day and not this docstring — the copy nobody grepped for.
|
|
18
|
+
- The splice adds a divergence of its own (R1-C43): it carries the previous build's
|
|
19
|
+
sample forward, and the rule that would invalidate it reads that same incomplete
|
|
20
|
+
graph. Pinned in ``tests/test_r1c43_incremental_splice.py``.
|
|
21
|
+
|
|
13
22
|
The splice logic itself is exact, which is what this fixture proves.
|
|
14
23
|
"""
|
|
15
24
|
|
|
16
25
|
from __future__ import annotations
|
|
17
26
|
|
|
27
|
+
import json
|
|
18
28
|
from pathlib import Path
|
|
19
29
|
|
|
20
30
|
import pytest
|
|
@@ -95,13 +105,30 @@ def _scope(pkg: Path) -> dict:
|
|
|
95
105
|
|
|
96
106
|
|
|
97
107
|
def _assert_incremental_matches_full(old_graph, pkg, scope0, *, deep, expect_mode):
|
|
108
|
+
"""The splice reproduces a full build's *content* — and says it was not one.
|
|
109
|
+
|
|
110
|
+
R1-C43 split what used to be a single `dumps == dumps`. The graphs must agree on
|
|
111
|
+
every node and edge, which is the acceptance bar; they must **disagree** on
|
|
112
|
+
`provenance.incremental`, because an artifact that was partly carried over may not
|
|
113
|
+
present itself as one that was recomputed. Comparing the whole file conflated the
|
|
114
|
+
two, and the field that exists to distinguish them would have read as a regression.
|
|
115
|
+
"""
|
|
98
116
|
scope1 = _scope(pkg)
|
|
99
117
|
inc, info = update_graph(old_graph, pkg, scope0, scope1, deep=deep)
|
|
100
118
|
full = extract(pkg, deep=deep)
|
|
101
|
-
|
|
119
|
+
|
|
120
|
+
def content(g):
|
|
121
|
+
d = json.loads(store.dumps(g))
|
|
122
|
+
d.pop("provenance", None)
|
|
123
|
+
return json.dumps(d, sort_keys=True)
|
|
124
|
+
|
|
125
|
+
assert content(inc) == content(full), f"incremental != full ({info})"
|
|
126
|
+
assert full.provenance["incremental"] is False
|
|
102
127
|
# a small toy package trips the full-rebuild fallback easily; the correctness
|
|
103
|
-
# invariant
|
|
128
|
+
# invariant holds either way, so accept a set of allowed modes — but the flag must
|
|
129
|
+
# follow the mode that actually ran, not the one we hoped for.
|
|
104
130
|
assert info["mode"] in expect_mode
|
|
131
|
+
assert inc.provenance["incremental"] is (info["mode"] != "full")
|
|
105
132
|
return inc, info
|
|
106
133
|
|
|
107
134
|
|
|
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
|