codingest 0.1.2__tar.gz → 0.1.4__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.
- {codingest-0.1.2 → codingest-0.1.4}/Cargo.lock +11 -10
- codingest-0.1.4/Cargo.toml +40 -0
- {codingest-0.1.2 → codingest-0.1.4}/PKG-INFO +34 -15
- {codingest-0.1.2 → codingest-0.1.4}/README.md +32 -13
- codingest-0.1.4/codingest/mcp_server.py +38 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/Cargo.toml +2 -2
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/bin/codingest_bench.rs +214 -3
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/builder/call_edges.rs +22 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/builder/load/edge_frames.rs +176 -2
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/builder/load/entity_frames.rs +58 -4
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/builder/load.rs +71 -2
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/builder/mod.rs +46 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/builder/other_edges.rs +16 -0
- codingest-0.1.4/crates/codingest/src/builder/semantic_edges.rs +530 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/models.rs +91 -0
- codingest-0.1.4/crates/codingest/src/parsers/agc.rs +984 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/rev.rs +8 -2
- codingest-0.1.4/crates/codingest/tests/agc_apollo.rs +116 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/tests/goldens/README.md +11 -2
- codingest-0.1.4/crates/codingest/tests/goldens/agc_basic.sha256 +1 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/tests/parity.rs +75 -20
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-cli/Cargo.toml +1 -1
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-cli/skills/codingest-code-review/SKILL.md +2 -2
- codingest-0.1.4/crates/codingest-mcp/Cargo.toml +19 -0
- codingest-0.1.4/crates/codingest-mcp/src/lib.rs +99 -0
- codingest-0.1.4/crates/codingest-mcp/src/main.rs +5 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-py/Cargo.toml +7 -2
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-py/src/lib.rs +16 -1
- {codingest-0.1.2 → codingest-0.1.4}/pyproject.toml +16 -6
- codingest-0.1.2/Cargo.toml +0 -33
- codingest-0.1.2/crates/codingest/src/parsers/agc.rs +0 -522
- codingest-0.1.2/crates/codingest/tests/goldens/agc_basic.sha256 +0 -1
- {codingest-0.1.2 → codingest-0.1.4}/LICENSE +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/codingest/__init__.py +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/codingest/__init__.pyi +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/codingest/cli.py +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/bin/codingest_stats.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/builder/routes/django.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/builder/routes/fastapi.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/builder/routes/flask.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/builder/routes/mod.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/builder/type_edges.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/cross_lang.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/docs/mod.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/docs/rst.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/lib.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/manifest/mod.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/cpp.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/csharp.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/css.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/dart.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/go.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/html.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/java.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/mod.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/php.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/python.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/registry.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/rust_lang.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/shared.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/swift.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/typescript.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/repo.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/tests/goldens/cross_ts_py.sha256 +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/tests/goldens/dup_minified_assets.sha256 +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/tests/goldens/py_basic.sha256 +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/tests/goldens/py_inheritance.sha256 +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/tests/goldens/rust_xfile.sha256 +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/tests/goldens/ts_callback.sha256 +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-cli/skills/codingest-code-review/references/mcp-upgrade.md +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-cli/skills/codingest-code-review/references/public-repositories.md +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-cli/skills/codingest-code-review/references/queries.md +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-cli/src/code_tree_cli.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-cli/src/lib.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-cli/src/main.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-cli/src/skill.rs +0 -0
- {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-cli/src/skill_assets.rs +0 -0
|
@@ -313,7 +313,7 @@ dependencies = [
|
|
|
313
313
|
|
|
314
314
|
[[package]]
|
|
315
315
|
name = "codingest"
|
|
316
|
-
version = "0.1.
|
|
316
|
+
version = "0.1.4"
|
|
317
317
|
dependencies = [
|
|
318
318
|
"aho-corasick",
|
|
319
319
|
"base64",
|
|
@@ -345,7 +345,7 @@ dependencies = [
|
|
|
345
345
|
|
|
346
346
|
[[package]]
|
|
347
347
|
name = "codingest-cli"
|
|
348
|
-
version = "0.1.
|
|
348
|
+
version = "0.1.4"
|
|
349
349
|
dependencies = [
|
|
350
350
|
"anyhow",
|
|
351
351
|
"clap",
|
|
@@ -357,7 +357,7 @@ dependencies = [
|
|
|
357
357
|
|
|
358
358
|
[[package]]
|
|
359
359
|
name = "codingest-mcp"
|
|
360
|
-
version = "0.1.
|
|
360
|
+
version = "0.1.4"
|
|
361
361
|
dependencies = [
|
|
362
362
|
"anyhow",
|
|
363
363
|
"codingest",
|
|
@@ -366,10 +366,11 @@ dependencies = [
|
|
|
366
366
|
|
|
367
367
|
[[package]]
|
|
368
368
|
name = "codingest-py"
|
|
369
|
-
version = "0.1.
|
|
369
|
+
version = "0.1.4"
|
|
370
370
|
dependencies = [
|
|
371
371
|
"codingest",
|
|
372
372
|
"codingest-cli",
|
|
373
|
+
"codingest-mcp",
|
|
373
374
|
"kglite",
|
|
374
375
|
"pyo3",
|
|
375
376
|
"tempfile",
|
|
@@ -1392,9 +1393,9 @@ dependencies = [
|
|
|
1392
1393
|
|
|
1393
1394
|
[[package]]
|
|
1394
1395
|
name = "kglite"
|
|
1395
|
-
version = "0.
|
|
1396
|
+
version = "0.15.3"
|
|
1396
1397
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1397
|
-
checksum = "
|
|
1398
|
+
checksum = "4ec07586e3dac4fa5dd1e216ac057f41ffe056f95eefc8ddd866f09fd7e48394"
|
|
1398
1399
|
dependencies = [
|
|
1399
1400
|
"bzip2",
|
|
1400
1401
|
"chrono",
|
|
@@ -1427,9 +1428,9 @@ dependencies = [
|
|
|
1427
1428
|
|
|
1428
1429
|
[[package]]
|
|
1429
1430
|
name = "kglite-mcp-server"
|
|
1430
|
-
version = "0.
|
|
1431
|
+
version = "0.15.3"
|
|
1431
1432
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1432
|
-
checksum = "
|
|
1433
|
+
checksum = "f9dae485f96add5453ef56518cd14e57c3f20727c8ab151500cc6f1afb70deef"
|
|
1433
1434
|
dependencies = [
|
|
1434
1435
|
"anyhow",
|
|
1435
1436
|
"bytes",
|
|
@@ -1531,9 +1532,9 @@ dependencies = [
|
|
|
1531
1532
|
|
|
1532
1533
|
[[package]]
|
|
1533
1534
|
name = "mcp-methods"
|
|
1534
|
-
version = "0.4.
|
|
1535
|
+
version = "0.4.2"
|
|
1535
1536
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1536
|
-
checksum = "
|
|
1537
|
+
checksum = "04c1063113c3a885d5967e45da2ea1cbb235938a9ba02333f15e81149b451b23"
|
|
1537
1538
|
dependencies = [
|
|
1538
1539
|
"anyhow",
|
|
1539
1540
|
"clap",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[workspace]
|
|
2
|
+
members = ["crates/codingest", "crates/codingest-cli", "crates/codingest-mcp", "crates/codingest-py"]
|
|
3
|
+
resolver = "2"
|
|
4
|
+
|
|
5
|
+
[workspace.package]
|
|
6
|
+
version = "0.1.4"
|
|
7
|
+
edition = "2021"
|
|
8
|
+
rust-version = "1.88"
|
|
9
|
+
license = "MIT"
|
|
10
|
+
repository = "https://github.com/kkollsga/codingest"
|
|
11
|
+
|
|
12
|
+
[workspace.dependencies]
|
|
13
|
+
base64 = "0.22"
|
|
14
|
+
sha2 = "0.11"
|
|
15
|
+
# kglite engine + MCP server — imported as crates.io libraries. The 0.15.3
|
|
16
|
+
# floor brings tri-state `durable` levels with an on-demand `sync()` barrier,
|
|
17
|
+
# typed constraint/conflict exceptions carrying a stable `.code`, and the
|
|
18
|
+
# columnar-REMOVE correctness fix, on top of the generic workspace-graph hooks
|
|
19
|
+
# and deterministic Postcard persistence stack inherited from 0.14.5. The
|
|
20
|
+
# 0.15.1-0.15.3 patches add a declared `rust-version = 1.88.0` on both crates we
|
|
21
|
+
# consume (matching our own floor), fix a `storage="disk"` save that could emit
|
|
22
|
+
# a directory the same build could not load, widen the unknown-label diagnostic
|
|
23
|
+
# to subqueries, and correct understated dependency floors — with no change to
|
|
24
|
+
# `kglite::api`, graph output, property encoding, or `.kgl` serialization.
|
|
25
|
+
# Keep both libraries aligned so the graph and server share one engine release.
|
|
26
|
+
kglite = { version = "0.15.3", default-features = false }
|
|
27
|
+
kglite-mcp-server = { version = "0.15.3", default-features = false }
|
|
28
|
+
|
|
29
|
+
# Mirror KGLite's release codegen so the standalone builder has
|
|
30
|
+
# performance parity with the in-tree component (see KGLite
|
|
31
|
+
# workspace Cargo.toml).
|
|
32
|
+
[profile.release]
|
|
33
|
+
lto = "thin"
|
|
34
|
+
codegen-units = 1
|
|
35
|
+
strip = "symbols"
|
|
36
|
+
|
|
37
|
+
[profile.profiling]
|
|
38
|
+
inherits = "release"
|
|
39
|
+
debug = "line-tables-only"
|
|
40
|
+
strip = "none"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codingest
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
12
12
|
Classifier: Programming Language :: Rust
|
|
13
13
|
Classifier: Topic :: Software Development
|
|
14
14
|
Classifier: Topic :: Software Development :: Libraries
|
|
15
|
-
Requires-Dist: kglite>=0.
|
|
15
|
+
Requires-Dist: kglite>=0.15.3,<0.16
|
|
16
16
|
License-File: LICENSE
|
|
17
17
|
Summary: Build fast, queryable code graphs for AI agents, MCP code review, and local or GitHub repository analysis.
|
|
18
18
|
Keywords: ai-agents,mcp,code-review,knowledge-graph,code-analysis,github,tree-sitter,cypher
|
|
@@ -70,10 +70,13 @@ a real `kglite.KnowledgeGraph`, ready for Cypher queries.
|
|
|
70
70
|
- **Revision-aware:** analyse a tag, branch, commit, or multiple revisions in
|
|
71
71
|
one graph and query what changed.
|
|
72
72
|
|
|
73
|
+
AGC assembly receives architecture-aware control and data relationships rather
|
|
74
|
+
than a generic text-level call graph; see the [AGC graph model](https://codingest.readthedocs.io/en/latest/agc-assembly.html).
|
|
75
|
+
|
|
73
76
|
## Give your agent a code-review MCP server
|
|
74
77
|
|
|
75
78
|
```bash
|
|
76
|
-
|
|
79
|
+
pip install codingest
|
|
77
80
|
codingest-mcp --watch /absolute/path/to/repo
|
|
78
81
|
```
|
|
79
82
|
|
|
@@ -172,21 +175,25 @@ for direct Cypher analysis.
|
|
|
172
175
|
## Install options
|
|
173
176
|
|
|
174
177
|
```bash
|
|
175
|
-
pip install codingest # Python API +
|
|
176
|
-
cargo install codingest-cli # pure-Rust builder CLI
|
|
177
|
-
cargo install codingest-mcp # MCP server for agents
|
|
178
|
+
pip install codingest # Python API + CLI + MCP server + KGLite runtime
|
|
178
179
|
codingest skill install # code-review Agent Skill
|
|
179
180
|
```
|
|
180
181
|
|
|
181
|
-
The wheel bundles every grammar
|
|
182
|
-
installed automatically as the query/storage
|
|
182
|
+
The wheel bundles every grammar plus the `codingest` and `codingest-mcp`
|
|
183
|
+
commands. KGLite ≥0.15.3 is installed automatically as the query/storage
|
|
184
|
+
engine; its MCP server and the transitive `mcp-methods` framework power the
|
|
185
|
+
builder-aware Codingest server. Nothing else needs to be installed.
|
|
186
|
+
|
|
187
|
+
Rust-only environments can instead use `cargo install codingest-cli
|
|
188
|
+
codingest-mcp`; these are alternative distributions of the same commands, not
|
|
189
|
+
Python prerequisites.
|
|
183
190
|
|
|
184
191
|
### Rust crate
|
|
185
192
|
|
|
186
193
|
```toml
|
|
187
194
|
[dependencies]
|
|
188
195
|
codingest = "0.1"
|
|
189
|
-
kglite = "0.
|
|
196
|
+
kglite = "0.15.3"
|
|
190
197
|
```
|
|
191
198
|
|
|
192
199
|
```rust
|
|
@@ -230,23 +237,35 @@ cargo test --workspace
|
|
|
230
237
|
cargo test -p codingest --test parity
|
|
231
238
|
```
|
|
232
239
|
|
|
233
|
-
### The golden-digest oracle
|
|
240
|
+
### The golden-digest oracle (also the determinism gate)
|
|
234
241
|
|
|
235
242
|
KGLite deleted its in-tree `code_tree` builder on 2026-07-16, so the old
|
|
236
243
|
two-builder parity sweep is gone. The authority it enforced was **frozen** while
|
|
237
244
|
the builders were still verified identical, into per-corpus SHA-256 digests
|
|
238
245
|
under `crates/codingest/tests/goldens/` (committed fixtures — no network). The
|
|
239
|
-
`golden_parity` test builds each corpus with only the codingest
|
|
240
|
-
a canonical exhaustive graph rendering, and
|
|
241
|
-
|
|
242
|
-
|
|
246
|
+
`golden_parity` test builds each corpus **three times** with only the codingest
|
|
247
|
+
builder, digests a canonical exhaustive graph rendering, and requires every
|
|
248
|
+
build to match every other build *and* the frozen golden. Repeating the build is
|
|
249
|
+
what makes this the determinism gate: hash iteration order is randomized per
|
|
250
|
+
`HashMap`, so an order-dependent builder fails it (the
|
|
251
|
+
`tests/corpus/dup_minified_assets` fixture reproduces the original DEFINES-edge
|
|
252
|
+
nondeterminism bug). Disagreement between builds is reported as NONDETERMINISM;
|
|
253
|
+
agreement between builds but not with the golden is reported as a behaviour
|
|
254
|
+
change. The multi-rev fixture is guarded instead by `rev_self_consistency`.
|
|
255
|
+
Regenerate goldens only for deliberate builder-behavior changes:
|
|
243
256
|
`cargo test -p codingest --test parity -- --ignored capture_goldens` (details in
|
|
244
257
|
`crates/codingest/tests/goldens/README.md`).
|
|
245
258
|
|
|
259
|
+
Everything the gate needs is committed to this repository, so it is hermetic and
|
|
260
|
+
runs in CI. It replaced a local-only `make` step that asserted an exact edge
|
|
261
|
+
count against a *sibling* checkout — a verdict this project did not control.
|
|
262
|
+
`make determinism-soak REPO=…` keeps the large-repo reproducer available as a
|
|
263
|
+
diagnostic.
|
|
264
|
+
|
|
246
265
|
## Dependency policy
|
|
247
266
|
|
|
248
267
|
`kglite` and `kglite-mcp-server` use matching crates.io requirements with a
|
|
249
|
-
0.
|
|
268
|
+
0.15.3 minimum and a shared lockfile. This keeps the builder, persistence
|
|
250
269
|
handoff, and embedded MCP server on one compatible engine patch line.
|
|
251
270
|
|
|
252
271
|
## Parity with the (now-removed) in-tree component
|
|
@@ -43,10 +43,13 @@ a real `kglite.KnowledgeGraph`, ready for Cypher queries.
|
|
|
43
43
|
- **Revision-aware:** analyse a tag, branch, commit, or multiple revisions in
|
|
44
44
|
one graph and query what changed.
|
|
45
45
|
|
|
46
|
+
AGC assembly receives architecture-aware control and data relationships rather
|
|
47
|
+
than a generic text-level call graph; see the [AGC graph model](https://codingest.readthedocs.io/en/latest/agc-assembly.html).
|
|
48
|
+
|
|
46
49
|
## Give your agent a code-review MCP server
|
|
47
50
|
|
|
48
51
|
```bash
|
|
49
|
-
|
|
52
|
+
pip install codingest
|
|
50
53
|
codingest-mcp --watch /absolute/path/to/repo
|
|
51
54
|
```
|
|
52
55
|
|
|
@@ -145,21 +148,25 @@ for direct Cypher analysis.
|
|
|
145
148
|
## Install options
|
|
146
149
|
|
|
147
150
|
```bash
|
|
148
|
-
pip install codingest # Python API +
|
|
149
|
-
cargo install codingest-cli # pure-Rust builder CLI
|
|
150
|
-
cargo install codingest-mcp # MCP server for agents
|
|
151
|
+
pip install codingest # Python API + CLI + MCP server + KGLite runtime
|
|
151
152
|
codingest skill install # code-review Agent Skill
|
|
152
153
|
```
|
|
153
154
|
|
|
154
|
-
The wheel bundles every grammar
|
|
155
|
-
installed automatically as the query/storage
|
|
155
|
+
The wheel bundles every grammar plus the `codingest` and `codingest-mcp`
|
|
156
|
+
commands. KGLite ≥0.15.3 is installed automatically as the query/storage
|
|
157
|
+
engine; its MCP server and the transitive `mcp-methods` framework power the
|
|
158
|
+
builder-aware Codingest server. Nothing else needs to be installed.
|
|
159
|
+
|
|
160
|
+
Rust-only environments can instead use `cargo install codingest-cli
|
|
161
|
+
codingest-mcp`; these are alternative distributions of the same commands, not
|
|
162
|
+
Python prerequisites.
|
|
156
163
|
|
|
157
164
|
### Rust crate
|
|
158
165
|
|
|
159
166
|
```toml
|
|
160
167
|
[dependencies]
|
|
161
168
|
codingest = "0.1"
|
|
162
|
-
kglite = "0.
|
|
169
|
+
kglite = "0.15.3"
|
|
163
170
|
```
|
|
164
171
|
|
|
165
172
|
```rust
|
|
@@ -203,23 +210,35 @@ cargo test --workspace
|
|
|
203
210
|
cargo test -p codingest --test parity
|
|
204
211
|
```
|
|
205
212
|
|
|
206
|
-
### The golden-digest oracle
|
|
213
|
+
### The golden-digest oracle (also the determinism gate)
|
|
207
214
|
|
|
208
215
|
KGLite deleted its in-tree `code_tree` builder on 2026-07-16, so the old
|
|
209
216
|
two-builder parity sweep is gone. The authority it enforced was **frozen** while
|
|
210
217
|
the builders were still verified identical, into per-corpus SHA-256 digests
|
|
211
218
|
under `crates/codingest/tests/goldens/` (committed fixtures — no network). The
|
|
212
|
-
`golden_parity` test builds each corpus with only the codingest
|
|
213
|
-
a canonical exhaustive graph rendering, and
|
|
214
|
-
|
|
215
|
-
|
|
219
|
+
`golden_parity` test builds each corpus **three times** with only the codingest
|
|
220
|
+
builder, digests a canonical exhaustive graph rendering, and requires every
|
|
221
|
+
build to match every other build *and* the frozen golden. Repeating the build is
|
|
222
|
+
what makes this the determinism gate: hash iteration order is randomized per
|
|
223
|
+
`HashMap`, so an order-dependent builder fails it (the
|
|
224
|
+
`tests/corpus/dup_minified_assets` fixture reproduces the original DEFINES-edge
|
|
225
|
+
nondeterminism bug). Disagreement between builds is reported as NONDETERMINISM;
|
|
226
|
+
agreement between builds but not with the golden is reported as a behaviour
|
|
227
|
+
change. The multi-rev fixture is guarded instead by `rev_self_consistency`.
|
|
228
|
+
Regenerate goldens only for deliberate builder-behavior changes:
|
|
216
229
|
`cargo test -p codingest --test parity -- --ignored capture_goldens` (details in
|
|
217
230
|
`crates/codingest/tests/goldens/README.md`).
|
|
218
231
|
|
|
232
|
+
Everything the gate needs is committed to this repository, so it is hermetic and
|
|
233
|
+
runs in CI. It replaced a local-only `make` step that asserted an exact edge
|
|
234
|
+
count against a *sibling* checkout — a verdict this project did not control.
|
|
235
|
+
`make determinism-soak REPO=…` keeps the large-repo reproducer available as a
|
|
236
|
+
diagnostic.
|
|
237
|
+
|
|
219
238
|
## Dependency policy
|
|
220
239
|
|
|
221
240
|
`kglite` and `kglite-mcp-server` use matching crates.io requirements with a
|
|
222
|
-
0.
|
|
241
|
+
0.15.3 minimum and a shared lockfile. This keeps the builder, persistence
|
|
223
242
|
handoff, and embedded MCP server on one compatible engine patch line.
|
|
224
243
|
|
|
225
244
|
## Parity with the (now-removed) in-tree component
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Console-script shim for the Rust ``codingest-mcp`` server in the wheel.
|
|
2
|
+
|
|
3
|
+
Codingest supplies the source-builder hooks; KGlite supplies graph/Cypher
|
|
4
|
+
serving and pulls the generic MCP lifecycle from ``mcp-methods``. All three are
|
|
5
|
+
linked into the wheel's existing native extension, so users need only
|
|
6
|
+
``pip install codingest``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main(argv: list[str] | None = None) -> int:
|
|
17
|
+
"""Run the bundled MCP server with ``argv`` or ``sys.argv[1:]``."""
|
|
18
|
+
from codingest.codingest import _run_mcp_server
|
|
19
|
+
|
|
20
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
21
|
+
# In a wheel console script, current_exe() is Python rather than the Rust
|
|
22
|
+
# server. Give KGlite's selftest the exact command needed to re-enter this
|
|
23
|
+
# module with the same interpreter and environment.
|
|
24
|
+
os.environ["KGLITE_MCP_RESPAWN"] = json.dumps(
|
|
25
|
+
[sys.executable, "-m", "codingest.mcp_server"]
|
|
26
|
+
)
|
|
27
|
+
try:
|
|
28
|
+
_run_mcp_server(args)
|
|
29
|
+
except KeyboardInterrupt:
|
|
30
|
+
return 130
|
|
31
|
+
except RuntimeError as exc:
|
|
32
|
+
print(f"codingest-mcp: {exc}", file=sys.stderr)
|
|
33
|
+
return 1
|
|
34
|
+
return 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
raise SystemExit(main())
|
|
@@ -9,8 +9,8 @@ description = "codingest — standalone extraction of KGLite's code-tree compone
|
|
|
9
9
|
keywords = ["knowledge-graph", "code-analysis", "tree-sitter", "cypher", "kglite"]
|
|
10
10
|
categories = ["development-tools", "parser-implementations"]
|
|
11
11
|
|
|
12
|
-
# Requires the kglite 0.
|
|
13
|
-
# workspace pins the current 0.
|
|
12
|
+
# Requires the kglite 0.15 engine API (`kglite::api::code_entities`); the
|
|
13
|
+
# workspace pins the current 0.15 patch floor (0.15.3).
|
|
14
14
|
|
|
15
15
|
[lib]
|
|
16
16
|
name = "codingest"
|
|
@@ -16,16 +16,41 @@
|
|
|
16
16
|
//! MISMATCH is a query-result determinism regression, and the A/B delta is
|
|
17
17
|
//! run-to-run timing variance. The release gate fails on any MISMATCH.
|
|
18
18
|
//!
|
|
19
|
+
//! # The corpus is defined by the harness, not by the working directory
|
|
20
|
+
//!
|
|
21
|
+
//! A benchmark number is comparable to another only if both were measured on
|
|
22
|
+
//! the same input. The builder walks whatever is on disk: it skips dot-dirs,
|
|
23
|
+
//! `target/`, `node_modules/`, `venv/` and `__pycache__/` by name, but it has
|
|
24
|
+
//! no notion of `.gitignore`, so a repository's *untracked* working state
|
|
25
|
+
//! (scratch folders, local notes, generated markdown) is ingested — notably
|
|
26
|
+
//! through the docs pass. Benchmarking a working tree therefore measures a
|
|
27
|
+
//! corpus nobody else can reconstruct. Measured on this workspace: a clean
|
|
28
|
+
//! `git worktree` of a commit scored 1,115 nodes / 3,692 edges while the
|
|
29
|
+
//! working tree at that same commit scored 1,170 / 3,759 — a ~5% swing with no
|
|
30
|
+
//! builder or engine involvement, which silently invalidated published numbers.
|
|
31
|
+
//!
|
|
32
|
+
//! So this harness defines its own corpus. By default it materializes the
|
|
33
|
+
//! target's **git-tracked** files into a temporary directory and builds that:
|
|
34
|
+
//! the input is then a function of the committed content plus any uncommitted
|
|
35
|
+
//! edits to tracked files, reproducible on any machine at the same revision.
|
|
36
|
+
//! Every run prints `corpus_sha256`, so two numbers can be *checked* for
|
|
37
|
+
//! comparability rather than assumed comparable. `--include-untracked` restores
|
|
38
|
+
//! the old build-the-directory-as-is behaviour for a one-off measurement of a
|
|
39
|
+
//! non-git tree, and prints a NOT-REPRODUCIBLE banner.
|
|
40
|
+
//!
|
|
19
41
|
//! Usage:
|
|
20
42
|
//! cargo run -p codingest --bin codingest_bench --release -- <path>
|
|
21
43
|
//! cargo run -p codingest --bin codingest_bench --release -- <path> --json
|
|
44
|
+
//! cargo run -p codingest --bin codingest_bench --release -- <path> --include-untracked
|
|
22
45
|
|
|
23
46
|
use kglite::api::session::{execute_read, ExecuteOptions, ExecuteOutcome};
|
|
24
47
|
use kglite::api::{DirGraph, GraphRead, Value};
|
|
48
|
+
use sha2::{Digest, Sha256};
|
|
25
49
|
use std::collections::hash_map::DefaultHasher;
|
|
26
50
|
use std::collections::HashMap;
|
|
27
51
|
use std::hash::{Hash, Hasher};
|
|
28
|
-
use std::path::Path;
|
|
52
|
+
use std::path::{Path, PathBuf};
|
|
53
|
+
use std::process::Command;
|
|
29
54
|
use std::time::Instant;
|
|
30
55
|
|
|
31
56
|
const WARMUP: usize = 3;
|
|
@@ -92,6 +117,153 @@ fn min(v: &[f64]) -> f64 {
|
|
|
92
117
|
v.iter().cloned().fold(f64::INFINITY, f64::min)
|
|
93
118
|
}
|
|
94
119
|
|
|
120
|
+
// ── corpus selection ─────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/// How the benchmark input was assembled. See the module docs.
|
|
123
|
+
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
124
|
+
enum CorpusMode {
|
|
125
|
+
/// Git-tracked files of the target, copied into a tempdir. Reproducible.
|
|
126
|
+
TrackedOnly,
|
|
127
|
+
/// The target directory exactly as it sits on disk. Not reproducible if
|
|
128
|
+
/// the tree carries untracked or git-ignored content.
|
|
129
|
+
WorkingTree,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
impl CorpusMode {
|
|
133
|
+
fn as_str(self) -> &'static str {
|
|
134
|
+
match self {
|
|
135
|
+
CorpusMode::TrackedOnly => "tracked-only",
|
|
136
|
+
CorpusMode::WorkingTree => "working-tree",
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// The resolved benchmark input: the directory to build, plus the identity of
|
|
142
|
+
/// the content in it.
|
|
143
|
+
struct Corpus {
|
|
144
|
+
build_dir: PathBuf,
|
|
145
|
+
mode: CorpusMode,
|
|
146
|
+
/// Why we ended up in `mode` — printed so the choice is never silent.
|
|
147
|
+
reason: String,
|
|
148
|
+
files: usize,
|
|
149
|
+
bytes: u64,
|
|
150
|
+
/// `None` in `WorkingTree` mode: the ingested set is not enumerable
|
|
151
|
+
/// without duplicating the builder's walk, and an approximate identity is
|
|
152
|
+
/// worse than an explicit absence.
|
|
153
|
+
sha256: Option<String>,
|
|
154
|
+
/// Owns the tempdir for the process lifetime; dropping it deletes the copy.
|
|
155
|
+
_tmp: Option<tempfile::TempDir>,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn hex(bytes: &[u8]) -> String {
|
|
159
|
+
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// Git-tracked paths under `target`, relative to it. `None` when `target` is
|
|
163
|
+
/// not inside a git work tree (or git is unavailable).
|
|
164
|
+
fn git_tracked_files(target: &Path) -> Option<Vec<String>> {
|
|
165
|
+
let out = Command::new("git")
|
|
166
|
+
.arg("-C")
|
|
167
|
+
.arg(target)
|
|
168
|
+
.args(["ls-files", "-z", "--cached"])
|
|
169
|
+
.output()
|
|
170
|
+
.ok()?;
|
|
171
|
+
if !out.status.success() {
|
|
172
|
+
return None;
|
|
173
|
+
}
|
|
174
|
+
let stdout = String::from_utf8(out.stdout).ok()?;
|
|
175
|
+
Some(
|
|
176
|
+
stdout
|
|
177
|
+
.split('\0')
|
|
178
|
+
.filter(|s| !s.is_empty())
|
|
179
|
+
.map(str::to_string)
|
|
180
|
+
.collect(),
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// Copy `rels` (relative to `target`) into a fresh tempdir and return the
|
|
185
|
+
/// corpus, including a content digest over the sorted `(path, sha256(bytes))`
|
|
186
|
+
/// pairs. Two runs with the same digest measured the same input.
|
|
187
|
+
fn materialize_tracked(target: &Path, mut rels: Vec<String>) -> std::io::Result<Corpus> {
|
|
188
|
+
rels.sort();
|
|
189
|
+
let tmp = tempfile::Builder::new()
|
|
190
|
+
.prefix("codingest-bench-corpus-")
|
|
191
|
+
.tempdir()?;
|
|
192
|
+
let root = tmp.path().to_path_buf();
|
|
193
|
+
|
|
194
|
+
let mut manifest = Sha256::new();
|
|
195
|
+
let mut files = 0usize;
|
|
196
|
+
let mut bytes = 0u64;
|
|
197
|
+
|
|
198
|
+
for rel in &rels {
|
|
199
|
+
let src = target.join(rel);
|
|
200
|
+
// `ls-files` lists index entries; a tracked-but-deleted file, or a
|
|
201
|
+
// gitlink (submodule) directory, has nothing to copy.
|
|
202
|
+
let Ok(data) = std::fs::read(&src) else {
|
|
203
|
+
continue;
|
|
204
|
+
};
|
|
205
|
+
let dst = root.join(rel);
|
|
206
|
+
if let Some(parent) = dst.parent() {
|
|
207
|
+
std::fs::create_dir_all(parent)?;
|
|
208
|
+
}
|
|
209
|
+
std::fs::write(&dst, &data)?;
|
|
210
|
+
|
|
211
|
+
let mut file_hash = Sha256::new();
|
|
212
|
+
file_hash.update(&data);
|
|
213
|
+
manifest.update(rel.as_bytes());
|
|
214
|
+
manifest.update(b"\0");
|
|
215
|
+
manifest.update(hex(&file_hash.finalize()).as_bytes());
|
|
216
|
+
manifest.update(b"\n");
|
|
217
|
+
|
|
218
|
+
files += 1;
|
|
219
|
+
bytes += data.len() as u64;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
Ok(Corpus {
|
|
223
|
+
build_dir: root,
|
|
224
|
+
mode: CorpusMode::TrackedOnly,
|
|
225
|
+
reason: format!("git-tracked content of {}", target.display()),
|
|
226
|
+
files,
|
|
227
|
+
bytes,
|
|
228
|
+
sha256: Some(hex(&manifest.finalize())),
|
|
229
|
+
_tmp: Some(tmp),
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// Resolve the benchmark input for `target`.
|
|
234
|
+
///
|
|
235
|
+
/// Default is `TrackedOnly`; it degrades to `WorkingTree` only when the target
|
|
236
|
+
/// is not in a git work tree, and says so. `--include-untracked` forces
|
|
237
|
+
/// `WorkingTree`.
|
|
238
|
+
fn resolve_corpus(target: &Path, include_untracked: bool) -> Corpus {
|
|
239
|
+
let working_tree = |reason: String| Corpus {
|
|
240
|
+
build_dir: target.to_path_buf(),
|
|
241
|
+
mode: CorpusMode::WorkingTree,
|
|
242
|
+
reason,
|
|
243
|
+
files: 0,
|
|
244
|
+
bytes: 0,
|
|
245
|
+
sha256: None,
|
|
246
|
+
_tmp: None,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
if include_untracked {
|
|
250
|
+
return working_tree("--include-untracked requested".to_string());
|
|
251
|
+
}
|
|
252
|
+
let Some(rels) = git_tracked_files(target) else {
|
|
253
|
+
return working_tree(format!("{} is not in a git work tree", target.display()));
|
|
254
|
+
};
|
|
255
|
+
if rels.is_empty() {
|
|
256
|
+
return working_tree(format!("git tracks no files under {}", target.display()));
|
|
257
|
+
}
|
|
258
|
+
match materialize_tracked(target, rels) {
|
|
259
|
+
Ok(corpus) => corpus,
|
|
260
|
+
Err(e) => {
|
|
261
|
+
eprintln!("warning: could not materialize the tracked-only corpus: {e}");
|
|
262
|
+
working_tree(format!("tracked-only materialization failed: {e}"))
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
95
267
|
struct QueryResult {
|
|
96
268
|
name: String,
|
|
97
269
|
rows: usize,
|
|
@@ -114,12 +286,30 @@ fn main() {
|
|
|
114
286
|
};
|
|
115
287
|
let rest: Vec<String> = args.collect();
|
|
116
288
|
let json = rest.iter().any(|a| a == "--json");
|
|
289
|
+
let include_untracked = rest.iter().any(|a| a == "--include-untracked");
|
|
290
|
+
// Reject unknown flags rather than silently ignoring them: a typo'd
|
|
291
|
+
// `--include-untraked` would otherwise change nothing and be reported as a
|
|
292
|
+
// measurement taken under a mode it never ran in.
|
|
293
|
+
if let Some(bad) = rest
|
|
294
|
+
.iter()
|
|
295
|
+
.find(|a| !matches!(a.as_str(), "--json" | "--include-untracked"))
|
|
296
|
+
{
|
|
297
|
+
eprintln!("unknown argument `{bad}`");
|
|
298
|
+
eprintln!("usage: codingest_bench <path> [--json] [--include-untracked]");
|
|
299
|
+
std::process::exit(2);
|
|
300
|
+
}
|
|
117
301
|
let target = Path::new(&path);
|
|
118
302
|
|
|
303
|
+
// Resolve the corpus BEFORE any timing: by default this copies the
|
|
304
|
+
// target's git-tracked files into a tempdir so the measured input is
|
|
305
|
+
// reproducible from the revision alone (see the module docs).
|
|
306
|
+
let corpus = resolve_corpus(target, include_untracked);
|
|
307
|
+
let build_dir = corpus.build_dir.as_path();
|
|
308
|
+
|
|
119
309
|
// Two independent codingest builds with identical arguments:
|
|
120
310
|
// verbose=false, include_tests=false, save_to=None, max_loc=None, docs=true.
|
|
121
311
|
let t = Instant::now();
|
|
122
|
-
let graph_a = codingest::builder::run_with_options(
|
|
312
|
+
let graph_a = codingest::builder::run_with_options(build_dir, false, false, None, None, true)
|
|
123
313
|
.unwrap_or_else(|e| {
|
|
124
314
|
eprintln!("build A failed: {e}");
|
|
125
315
|
std::process::exit(1);
|
|
@@ -127,7 +317,7 @@ fn main() {
|
|
|
127
317
|
let build_a_secs = t.elapsed().as_secs_f64();
|
|
128
318
|
|
|
129
319
|
let t = Instant::now();
|
|
130
|
-
let graph_b = codingest::builder::run_with_options(
|
|
320
|
+
let graph_b = codingest::builder::run_with_options(build_dir, false, false, None, None, true)
|
|
131
321
|
.unwrap_or_else(|e| {
|
|
132
322
|
eprintln!("build B failed: {e}");
|
|
133
323
|
std::process::exit(1);
|
|
@@ -291,6 +481,11 @@ fn main() {
|
|
|
291
481
|
if json {
|
|
292
482
|
let out = serde_json::json!({
|
|
293
483
|
"path": path,
|
|
484
|
+
"corpus_mode": corpus.mode.as_str(),
|
|
485
|
+
"corpus_reason": corpus.reason,
|
|
486
|
+
"corpus_files": corpus.sha256.as_ref().map(|_| corpus.files),
|
|
487
|
+
"corpus_bytes": corpus.sha256.as_ref().map(|_| corpus.bytes),
|
|
488
|
+
"corpus_sha256": corpus.sha256,
|
|
294
489
|
"nodes": nodes,
|
|
295
490
|
"edges": edges,
|
|
296
491
|
"build_a_secs": (build_a_secs * 1000.0).round() / 1000.0,
|
|
@@ -316,6 +511,22 @@ fn main() {
|
|
|
316
511
|
|
|
317
512
|
println!("codingest Cypher benchmark — query-parity across two independent builds");
|
|
318
513
|
println!("target : {path}");
|
|
514
|
+
match &corpus.sha256 {
|
|
515
|
+
Some(digest) => println!(
|
|
516
|
+
"corpus : {} — {} files, {} bytes, sha256 {}\n ({})",
|
|
517
|
+
corpus.mode.as_str(),
|
|
518
|
+
corpus.files,
|
|
519
|
+
corpus.bytes,
|
|
520
|
+
digest,
|
|
521
|
+
corpus.reason,
|
|
522
|
+
),
|
|
523
|
+
None => println!(
|
|
524
|
+
"corpus : {} — NOT REPRODUCIBLE: untracked and git-ignored content \
|
|
525
|
+
is ingested\n ({})",
|
|
526
|
+
corpus.mode.as_str(),
|
|
527
|
+
corpus.reason,
|
|
528
|
+
),
|
|
529
|
+
}
|
|
319
530
|
println!("graph : {nodes} nodes / {edges} edges (identical across both builds)");
|
|
320
531
|
println!("build : A {build_a_secs:.3}s | B {build_b_secs:.3}s (one-off, context)");
|
|
321
532
|
println!("anchor : in-hub = {hot_in}");
|
|
@@ -19,6 +19,12 @@ pub struct CallEdge {
|
|
|
19
19
|
/// Comma-separated sorted unique line numbers.
|
|
20
20
|
pub call_lines: String,
|
|
21
21
|
pub call_count: i64,
|
|
22
|
+
/// Optional parser-preserved transfer spelling/mechanism. Empty for the
|
|
23
|
+
/// ordinary language call resolver, populated by typed semantic sites.
|
|
24
|
+
pub raw_targets: Option<String>,
|
|
25
|
+
pub offsets: Option<String>,
|
|
26
|
+
pub via: Option<String>,
|
|
27
|
+
pub address_lines: Option<String>,
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
/// Aggregate counters describing how the resolver classified every call
|
|
@@ -48,6 +54,18 @@ pub struct CallResolutionStats {
|
|
|
48
54
|
pub resolved_via_inheritance: u64,
|
|
49
55
|
}
|
|
50
56
|
|
|
57
|
+
impl CallResolutionStats {
|
|
58
|
+
pub(crate) fn merge(&mut self, other: Self) {
|
|
59
|
+
self.total_calls += other.total_calls;
|
|
60
|
+
self.excluded_noise += other.excluded_noise;
|
|
61
|
+
self.no_candidate += other.no_candidate;
|
|
62
|
+
self.ambiguous_dropped += other.ambiguous_dropped;
|
|
63
|
+
self.resolved_call_sites += other.resolved_call_sites;
|
|
64
|
+
self.resolved_edges += other.resolved_edges;
|
|
65
|
+
self.resolved_via_inheritance += other.resolved_via_inheritance;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
51
69
|
/// Per-function scratch counters, summed into [`CallResolutionStats`] after
|
|
52
70
|
/// the parallel match loop. Kept `Copy` so the rayon reduce stays alloc-free.
|
|
53
71
|
#[derive(Debug, Clone, Copy, Default)]
|
|
@@ -495,6 +513,10 @@ pub fn build_call_edges(
|
|
|
495
513
|
callee: callee.to_string(),
|
|
496
514
|
call_lines,
|
|
497
515
|
call_count: count,
|
|
516
|
+
raw_targets: None,
|
|
517
|
+
offsets: None,
|
|
518
|
+
via: None,
|
|
519
|
+
address_lines: None,
|
|
498
520
|
}
|
|
499
521
|
})
|
|
500
522
|
.collect();
|