codingest 0.1.3__tar.gz → 0.1.5__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.3 → codingest-0.1.5}/Cargo.lock +10 -10
- codingest-0.1.5/Cargo.toml +42 -0
- {codingest-0.1.3 → codingest-0.1.5}/PKG-INFO +29 -10
- {codingest-0.1.3 → codingest-0.1.5}/README.md +27 -8
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/Cargo.toml +2 -2
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/bin/codingest_bench.rs +214 -3
- codingest-0.1.5/crates/codingest/src/bin/codingest_stats.rs +464 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/call_edges.rs +390 -17
- codingest-0.1.5/crates/codingest/src/builder/js_workspace.rs +595 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/load/edge_frames.rs +24 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/load.rs +37 -5
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/mod.rs +40 -12
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/other_edges.rs +406 -5
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/semantic_edges.rs +7 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/agc.rs +12 -2
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/registry.rs +15 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/typescript.rs +161 -10
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/rev.rs +8 -2
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/tests/goldens/README.md +17 -0
- codingest-0.1.5/crates/codingest/tests/goldens/dup_minified_assets.sha256 +1 -0
- codingest-0.1.5/crates/codingest/tests/goldens/py_basic.sha256 +1 -0
- codingest-0.1.5/crates/codingest/tests/goldens/py_inheritance.sha256 +1 -0
- codingest-0.1.5/crates/codingest/tests/goldens/rust_xfile.sha256 +1 -0
- codingest-0.1.5/crates/codingest/tests/goldens/ts_callback.sha256 +1 -0
- codingest-0.1.5/crates/codingest/tests/goldens/ts_monorepo.sha256 +1 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/tests/parity.rs +173 -20
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-cli/Cargo.toml +1 -1
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-cli/src/code_tree_cli.rs +7 -3
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-cli/src/lib.rs +20 -0
- codingest-0.1.5/crates/codingest-cli/src/main.rs +10 -0
- codingest-0.1.5/crates/codingest-cli/src/query.rs +644 -0
- codingest-0.1.5/crates/codingest-cli/tests/exit_codes.rs +209 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-mcp/Cargo.toml +1 -1
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-py/Cargo.toml +3 -3
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-py/src/lib.rs +1 -1
- {codingest-0.1.3 → codingest-0.1.5}/pyproject.toml +10 -1
- codingest-0.1.3/Cargo.toml +0 -33
- codingest-0.1.3/crates/codingest/src/bin/codingest_stats.rs +0 -150
- codingest-0.1.3/crates/codingest/tests/goldens/dup_minified_assets.sha256 +0 -1
- codingest-0.1.3/crates/codingest/tests/goldens/py_basic.sha256 +0 -1
- codingest-0.1.3/crates/codingest/tests/goldens/py_inheritance.sha256 +0 -1
- codingest-0.1.3/crates/codingest/tests/goldens/rust_xfile.sha256 +0 -1
- codingest-0.1.3/crates/codingest/tests/goldens/ts_callback.sha256 +0 -1
- codingest-0.1.3/crates/codingest-cli/src/main.rs +0 -5
- {codingest-0.1.3 → codingest-0.1.5}/LICENSE +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/codingest/__init__.py +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/codingest/__init__.pyi +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/codingest/cli.py +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/codingest/mcp_server.py +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/load/entity_frames.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/routes/django.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/routes/fastapi.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/routes/flask.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/routes/mod.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/type_edges.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/cross_lang.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/docs/mod.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/docs/rst.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/lib.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/manifest/mod.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/models.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/cpp.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/csharp.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/css.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/dart.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/go.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/html.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/java.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/mod.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/php.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/python.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/rust_lang.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/shared.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/swift.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/repo.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/tests/agc_apollo.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/tests/goldens/agc_basic.sha256 +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/tests/goldens/cross_ts_py.sha256 +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-cli/skills/codingest-code-review/SKILL.md +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-cli/skills/codingest-code-review/references/mcp-upgrade.md +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-cli/skills/codingest-code-review/references/public-repositories.md +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-cli/skills/codingest-code-review/references/queries.md +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-cli/src/skill.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-cli/src/skill_assets.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-mcp/src/lib.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-mcp/src/main.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.5"
|
|
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.5"
|
|
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.5"
|
|
361
361
|
dependencies = [
|
|
362
362
|
"anyhow",
|
|
363
363
|
"codingest",
|
|
@@ -366,7 +366,7 @@ dependencies = [
|
|
|
366
366
|
|
|
367
367
|
[[package]]
|
|
368
368
|
name = "codingest-py"
|
|
369
|
-
version = "0.1.
|
|
369
|
+
version = "0.1.5"
|
|
370
370
|
dependencies = [
|
|
371
371
|
"codingest",
|
|
372
372
|
"codingest-cli",
|
|
@@ -1393,9 +1393,9 @@ dependencies = [
|
|
|
1393
1393
|
|
|
1394
1394
|
[[package]]
|
|
1395
1395
|
name = "kglite"
|
|
1396
|
-
version = "0.
|
|
1396
|
+
version = "0.15.5"
|
|
1397
1397
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1398
|
-
checksum = "
|
|
1398
|
+
checksum = "4664fc457e5dd762f5775977f06eb5377634556fffae45e4a86e2eb1e1eb0bfb"
|
|
1399
1399
|
dependencies = [
|
|
1400
1400
|
"bzip2",
|
|
1401
1401
|
"chrono",
|
|
@@ -1428,9 +1428,9 @@ dependencies = [
|
|
|
1428
1428
|
|
|
1429
1429
|
[[package]]
|
|
1430
1430
|
name = "kglite-mcp-server"
|
|
1431
|
-
version = "0.
|
|
1431
|
+
version = "0.15.5"
|
|
1432
1432
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1433
|
-
checksum = "
|
|
1433
|
+
checksum = "e5e5d2207a45eb77c90e60a772bc8cc60ceac3d7c6f3ad2b948e8079355e1df4"
|
|
1434
1434
|
dependencies = [
|
|
1435
1435
|
"anyhow",
|
|
1436
1436
|
"bytes",
|
|
@@ -1532,9 +1532,9 @@ dependencies = [
|
|
|
1532
1532
|
|
|
1533
1533
|
[[package]]
|
|
1534
1534
|
name = "mcp-methods"
|
|
1535
|
-
version = "0.4.
|
|
1535
|
+
version = "0.4.3"
|
|
1536
1536
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1537
|
-
checksum = "
|
|
1537
|
+
checksum = "caa2079c12e642cd4b9a20543c85305eb5ffee04b1d565feab47b084db167a99"
|
|
1538
1538
|
dependencies = [
|
|
1539
1539
|
"anyhow",
|
|
1540
1540
|
"clap",
|
|
@@ -0,0 +1,42 @@
|
|
|
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.5"
|
|
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.5
|
|
16
|
+
# floor adds the two workspace controls codingest asked upstream for:
|
|
17
|
+
# `workspace.sandbox_root`, a real containment boundary that confines
|
|
18
|
+
# `set_root_dir` to a subtree instead of trusting the caller's path, and
|
|
19
|
+
# `workspace.adopt_client_roots`. Both land on top of the generic
|
|
20
|
+
# workspace-graph hooks, tri-state `durable` levels, typed
|
|
21
|
+
# constraint/conflict exceptions carrying a stable `.code`, and the
|
|
22
|
+
# deterministic Postcard persistence stack inherited from 0.14.5/0.15.0-0.15.3.
|
|
23
|
+
# 0.15.4 is deliberately skipped: it was WAL/durability and mapped-graph work
|
|
24
|
+
# we consume none of — codingest uses no `durable=` level, no `MappedGraph`,
|
|
25
|
+
# and no disk storage. Neither patch changes `kglite::api`, graph output,
|
|
26
|
+
# property encoding, or `.kgl` serialization.
|
|
27
|
+
# Keep both libraries aligned so the graph and server share one engine release.
|
|
28
|
+
kglite = { version = "0.15.5", default-features = false }
|
|
29
|
+
kglite-mcp-server = { version = "0.15.5", default-features = false }
|
|
30
|
+
|
|
31
|
+
# Mirror KGLite's release codegen so the standalone builder has
|
|
32
|
+
# performance parity with the in-tree component (see KGLite
|
|
33
|
+
# workspace Cargo.toml).
|
|
34
|
+
[profile.release]
|
|
35
|
+
lto = "thin"
|
|
36
|
+
codegen-units = 1
|
|
37
|
+
strip = "symbols"
|
|
38
|
+
|
|
39
|
+
[profile.profiling]
|
|
40
|
+
inherits = "release"
|
|
41
|
+
debug = "line-tables-only"
|
|
42
|
+
strip = "none"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codingest
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
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.5,<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
|
|
@@ -136,9 +136,16 @@ The bundled terminal command offers the same workflow:
|
|
|
136
136
|
```bash
|
|
137
137
|
codingest build /path/to/repo
|
|
138
138
|
codingest status --output /path/to/repo/.kglite/code-review.kgl
|
|
139
|
+
codingest query -g /path/to/repo/.kglite/code-review.kgl \
|
|
140
|
+
"MATCH (f:Function)-[:CALLS]->(g:Function) RETURN f.name, g.name LIMIT 20"
|
|
139
141
|
kglite describe /path/to/repo/.kglite/code-review.kgl --connections
|
|
140
142
|
```
|
|
141
143
|
|
|
144
|
+
`codingest query` prints unbounded TSV to stdout (use `--format csv|json`, or
|
|
145
|
+
Cypher `LIMIT` to bound rows), warns on stderr when the graph has gone stale,
|
|
146
|
+
and with `--require-fresh` refuses a stale graph with exit code 3 — see
|
|
147
|
+
[docs/cli.md](docs/cli.md).
|
|
148
|
+
|
|
142
149
|
## Analyse an open-source repository
|
|
143
150
|
|
|
144
151
|
```python
|
|
@@ -180,7 +187,7 @@ codingest skill install # code-review Agent Skill
|
|
|
180
187
|
```
|
|
181
188
|
|
|
182
189
|
The wheel bundles every grammar plus the `codingest` and `codingest-mcp`
|
|
183
|
-
commands. KGLite ≥0.
|
|
190
|
+
commands. KGLite ≥0.15.5 is installed automatically as the query/storage
|
|
184
191
|
engine; its MCP server and the transitive `mcp-methods` framework power the
|
|
185
192
|
builder-aware Codingest server. Nothing else needs to be installed.
|
|
186
193
|
|
|
@@ -193,7 +200,7 @@ Python prerequisites.
|
|
|
193
200
|
```toml
|
|
194
201
|
[dependencies]
|
|
195
202
|
codingest = "0.1"
|
|
196
|
-
kglite = "0.
|
|
203
|
+
kglite = "0.15.5"
|
|
197
204
|
```
|
|
198
205
|
|
|
199
206
|
```rust
|
|
@@ -237,23 +244,35 @@ cargo test --workspace
|
|
|
237
244
|
cargo test -p codingest --test parity
|
|
238
245
|
```
|
|
239
246
|
|
|
240
|
-
### The golden-digest oracle
|
|
247
|
+
### The golden-digest oracle (also the determinism gate)
|
|
241
248
|
|
|
242
249
|
KGLite deleted its in-tree `code_tree` builder on 2026-07-16, so the old
|
|
243
250
|
two-builder parity sweep is gone. The authority it enforced was **frozen** while
|
|
244
251
|
the builders were still verified identical, into per-corpus SHA-256 digests
|
|
245
252
|
under `crates/codingest/tests/goldens/` (committed fixtures — no network). The
|
|
246
|
-
`golden_parity` test builds each corpus with only the codingest
|
|
247
|
-
a canonical exhaustive graph rendering, and
|
|
248
|
-
|
|
249
|
-
|
|
253
|
+
`golden_parity` test builds each corpus **three times** with only the codingest
|
|
254
|
+
builder, digests a canonical exhaustive graph rendering, and requires every
|
|
255
|
+
build to match every other build *and* the frozen golden. Repeating the build is
|
|
256
|
+
what makes this the determinism gate: hash iteration order is randomized per
|
|
257
|
+
`HashMap`, so an order-dependent builder fails it (the
|
|
258
|
+
`tests/corpus/dup_minified_assets` fixture reproduces the original DEFINES-edge
|
|
259
|
+
nondeterminism bug). Disagreement between builds is reported as NONDETERMINISM;
|
|
260
|
+
agreement between builds but not with the golden is reported as a behaviour
|
|
261
|
+
change. The multi-rev fixture is guarded instead by `rev_self_consistency`.
|
|
262
|
+
Regenerate goldens only for deliberate builder-behavior changes:
|
|
250
263
|
`cargo test -p codingest --test parity -- --ignored capture_goldens` (details in
|
|
251
264
|
`crates/codingest/tests/goldens/README.md`).
|
|
252
265
|
|
|
266
|
+
Everything the gate needs is committed to this repository, so it is hermetic and
|
|
267
|
+
runs in CI. It replaced a local-only `make` step that asserted an exact edge
|
|
268
|
+
count against a *sibling* checkout — a verdict this project did not control.
|
|
269
|
+
`make determinism-soak REPO=…` keeps the large-repo reproducer available as a
|
|
270
|
+
diagnostic.
|
|
271
|
+
|
|
253
272
|
## Dependency policy
|
|
254
273
|
|
|
255
274
|
`kglite` and `kglite-mcp-server` use matching crates.io requirements with a
|
|
256
|
-
0.
|
|
275
|
+
0.15.5 minimum and a shared lockfile. This keeps the builder, persistence
|
|
257
276
|
handoff, and embedded MCP server on one compatible engine patch line.
|
|
258
277
|
|
|
259
278
|
## Parity with the (now-removed) in-tree component
|
|
@@ -109,9 +109,16 @@ The bundled terminal command offers the same workflow:
|
|
|
109
109
|
```bash
|
|
110
110
|
codingest build /path/to/repo
|
|
111
111
|
codingest status --output /path/to/repo/.kglite/code-review.kgl
|
|
112
|
+
codingest query -g /path/to/repo/.kglite/code-review.kgl \
|
|
113
|
+
"MATCH (f:Function)-[:CALLS]->(g:Function) RETURN f.name, g.name LIMIT 20"
|
|
112
114
|
kglite describe /path/to/repo/.kglite/code-review.kgl --connections
|
|
113
115
|
```
|
|
114
116
|
|
|
117
|
+
`codingest query` prints unbounded TSV to stdout (use `--format csv|json`, or
|
|
118
|
+
Cypher `LIMIT` to bound rows), warns on stderr when the graph has gone stale,
|
|
119
|
+
and with `--require-fresh` refuses a stale graph with exit code 3 — see
|
|
120
|
+
[docs/cli.md](docs/cli.md).
|
|
121
|
+
|
|
115
122
|
## Analyse an open-source repository
|
|
116
123
|
|
|
117
124
|
```python
|
|
@@ -153,7 +160,7 @@ codingest skill install # code-review Agent Skill
|
|
|
153
160
|
```
|
|
154
161
|
|
|
155
162
|
The wheel bundles every grammar plus the `codingest` and `codingest-mcp`
|
|
156
|
-
commands. KGLite ≥0.
|
|
163
|
+
commands. KGLite ≥0.15.5 is installed automatically as the query/storage
|
|
157
164
|
engine; its MCP server and the transitive `mcp-methods` framework power the
|
|
158
165
|
builder-aware Codingest server. Nothing else needs to be installed.
|
|
159
166
|
|
|
@@ -166,7 +173,7 @@ Python prerequisites.
|
|
|
166
173
|
```toml
|
|
167
174
|
[dependencies]
|
|
168
175
|
codingest = "0.1"
|
|
169
|
-
kglite = "0.
|
|
176
|
+
kglite = "0.15.5"
|
|
170
177
|
```
|
|
171
178
|
|
|
172
179
|
```rust
|
|
@@ -210,23 +217,35 @@ cargo test --workspace
|
|
|
210
217
|
cargo test -p codingest --test parity
|
|
211
218
|
```
|
|
212
219
|
|
|
213
|
-
### The golden-digest oracle
|
|
220
|
+
### The golden-digest oracle (also the determinism gate)
|
|
214
221
|
|
|
215
222
|
KGLite deleted its in-tree `code_tree` builder on 2026-07-16, so the old
|
|
216
223
|
two-builder parity sweep is gone. The authority it enforced was **frozen** while
|
|
217
224
|
the builders were still verified identical, into per-corpus SHA-256 digests
|
|
218
225
|
under `crates/codingest/tests/goldens/` (committed fixtures — no network). The
|
|
219
|
-
`golden_parity` test builds each corpus with only the codingest
|
|
220
|
-
a canonical exhaustive graph rendering, and
|
|
221
|
-
|
|
222
|
-
|
|
226
|
+
`golden_parity` test builds each corpus **three times** with only the codingest
|
|
227
|
+
builder, digests a canonical exhaustive graph rendering, and requires every
|
|
228
|
+
build to match every other build *and* the frozen golden. Repeating the build is
|
|
229
|
+
what makes this the determinism gate: hash iteration order is randomized per
|
|
230
|
+
`HashMap`, so an order-dependent builder fails it (the
|
|
231
|
+
`tests/corpus/dup_minified_assets` fixture reproduces the original DEFINES-edge
|
|
232
|
+
nondeterminism bug). Disagreement between builds is reported as NONDETERMINISM;
|
|
233
|
+
agreement between builds but not with the golden is reported as a behaviour
|
|
234
|
+
change. The multi-rev fixture is guarded instead by `rev_self_consistency`.
|
|
235
|
+
Regenerate goldens only for deliberate builder-behavior changes:
|
|
223
236
|
`cargo test -p codingest --test parity -- --ignored capture_goldens` (details in
|
|
224
237
|
`crates/codingest/tests/goldens/README.md`).
|
|
225
238
|
|
|
239
|
+
Everything the gate needs is committed to this repository, so it is hermetic and
|
|
240
|
+
runs in CI. It replaced a local-only `make` step that asserted an exact edge
|
|
241
|
+
count against a *sibling* checkout — a verdict this project did not control.
|
|
242
|
+
`make determinism-soak REPO=…` keeps the large-repo reproducer available as a
|
|
243
|
+
diagnostic.
|
|
244
|
+
|
|
226
245
|
## Dependency policy
|
|
227
246
|
|
|
228
247
|
`kglite` and `kglite-mcp-server` use matching crates.io requirements with a
|
|
229
|
-
0.
|
|
248
|
+
0.15.5 minimum and a shared lockfile. This keeps the builder, persistence
|
|
230
249
|
handoff, and embedded MCP server on one compatible engine patch line.
|
|
231
250
|
|
|
232
251
|
## Parity with the (now-removed) in-tree component
|
|
@@ -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.5).
|
|
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}");
|