codingest 0.1.3__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.3 → codingest-0.1.4}/Cargo.lock +10 -10
- codingest-0.1.4/Cargo.toml +40 -0
- {codingest-0.1.3 → codingest-0.1.4}/PKG-INFO +22 -10
- {codingest-0.1.3 → codingest-0.1.4}/README.md +20 -8
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/Cargo.toml +2 -2
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/bin/codingest_bench.rs +214 -3
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/rev.rs +8 -2
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/tests/goldens/README.md +8 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/tests/parity.rs +75 -20
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-cli/Cargo.toml +1 -1
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-mcp/Cargo.toml +1 -1
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-py/Cargo.toml +3 -3
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-py/src/lib.rs +1 -1
- {codingest-0.1.3 → codingest-0.1.4}/pyproject.toml +10 -1
- codingest-0.1.3/Cargo.toml +0 -33
- {codingest-0.1.3 → codingest-0.1.4}/LICENSE +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/codingest/__init__.py +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/codingest/__init__.pyi +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/codingest/cli.py +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/codingest/mcp_server.py +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/bin/codingest_stats.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/call_edges.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/load/edge_frames.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/load/entity_frames.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/load.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/mod.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/other_edges.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/routes/django.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/routes/fastapi.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/routes/flask.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/routes/mod.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/semantic_edges.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/type_edges.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/cross_lang.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/docs/mod.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/docs/rst.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/lib.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/manifest/mod.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/models.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/agc.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/cpp.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/csharp.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/css.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/dart.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/go.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/html.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/java.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/mod.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/php.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/python.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/registry.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/rust_lang.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/shared.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/swift.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/typescript.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/repo.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/tests/agc_apollo.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/tests/goldens/agc_basic.sha256 +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/tests/goldens/cross_ts_py.sha256 +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/tests/goldens/dup_minified_assets.sha256 +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/tests/goldens/py_basic.sha256 +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/tests/goldens/py_inheritance.sha256 +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/tests/goldens/rust_xfile.sha256 +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/tests/goldens/ts_callback.sha256 +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-cli/skills/codingest-code-review/SKILL.md +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-cli/skills/codingest-code-review/references/mcp-upgrade.md +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-cli/skills/codingest-code-review/references/public-repositories.md +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-cli/skills/codingest-code-review/references/queries.md +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-cli/src/code_tree_cli.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-cli/src/lib.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-cli/src/main.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-cli/src/skill.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-cli/src/skill_assets.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-mcp/src/lib.rs +0 -0
- {codingest-0.1.3 → codingest-0.1.4}/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.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,7 +366,7 @@ 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",
|
|
@@ -1393,9 +1393,9 @@ dependencies = [
|
|
|
1393
1393
|
|
|
1394
1394
|
[[package]]
|
|
1395
1395
|
name = "kglite"
|
|
1396
|
-
version = "0.
|
|
1396
|
+
version = "0.15.3"
|
|
1397
1397
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1398
|
-
checksum = "
|
|
1398
|
+
checksum = "4ec07586e3dac4fa5dd1e216ac057f41ffe056f95eefc8ddd866f09fd7e48394"
|
|
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.3"
|
|
1432
1432
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1433
|
-
checksum = "
|
|
1433
|
+
checksum = "f9dae485f96add5453ef56518cd14e57c3f20727c8ab151500cc6f1afb70deef"
|
|
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.2"
|
|
1536
1536
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1537
|
-
checksum = "
|
|
1537
|
+
checksum = "04c1063113c3a885d5967e45da2ea1cbb235938a9ba02333f15e81149b451b23"
|
|
1538
1538
|
dependencies = [
|
|
1539
1539
|
"anyhow",
|
|
1540
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
|
|
@@ -180,7 +180,7 @@ codingest skill install # code-review Agent Skill
|
|
|
180
180
|
```
|
|
181
181
|
|
|
182
182
|
The wheel bundles every grammar plus the `codingest` and `codingest-mcp`
|
|
183
|
-
commands. KGLite ≥0.
|
|
183
|
+
commands. KGLite ≥0.15.3 is installed automatically as the query/storage
|
|
184
184
|
engine; its MCP server and the transitive `mcp-methods` framework power the
|
|
185
185
|
builder-aware Codingest server. Nothing else needs to be installed.
|
|
186
186
|
|
|
@@ -193,7 +193,7 @@ Python prerequisites.
|
|
|
193
193
|
```toml
|
|
194
194
|
[dependencies]
|
|
195
195
|
codingest = "0.1"
|
|
196
|
-
kglite = "0.
|
|
196
|
+
kglite = "0.15.3"
|
|
197
197
|
```
|
|
198
198
|
|
|
199
199
|
```rust
|
|
@@ -237,23 +237,35 @@ cargo test --workspace
|
|
|
237
237
|
cargo test -p codingest --test parity
|
|
238
238
|
```
|
|
239
239
|
|
|
240
|
-
### The golden-digest oracle
|
|
240
|
+
### The golden-digest oracle (also the determinism gate)
|
|
241
241
|
|
|
242
242
|
KGLite deleted its in-tree `code_tree` builder on 2026-07-16, so the old
|
|
243
243
|
two-builder parity sweep is gone. The authority it enforced was **frozen** while
|
|
244
244
|
the builders were still verified identical, into per-corpus SHA-256 digests
|
|
245
245
|
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
|
-
|
|
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:
|
|
250
256
|
`cargo test -p codingest --test parity -- --ignored capture_goldens` (details in
|
|
251
257
|
`crates/codingest/tests/goldens/README.md`).
|
|
252
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
|
+
|
|
253
265
|
## Dependency policy
|
|
254
266
|
|
|
255
267
|
`kglite` and `kglite-mcp-server` use matching crates.io requirements with a
|
|
256
|
-
0.
|
|
268
|
+
0.15.3 minimum and a shared lockfile. This keeps the builder, persistence
|
|
257
269
|
handoff, and embedded MCP server on one compatible engine patch line.
|
|
258
270
|
|
|
259
271
|
## Parity with the (now-removed) in-tree component
|
|
@@ -153,7 +153,7 @@ codingest skill install # code-review Agent Skill
|
|
|
153
153
|
```
|
|
154
154
|
|
|
155
155
|
The wheel bundles every grammar plus the `codingest` and `codingest-mcp`
|
|
156
|
-
commands. KGLite ≥0.
|
|
156
|
+
commands. KGLite ≥0.15.3 is installed automatically as the query/storage
|
|
157
157
|
engine; its MCP server and the transitive `mcp-methods` framework power the
|
|
158
158
|
builder-aware Codingest server. Nothing else needs to be installed.
|
|
159
159
|
|
|
@@ -166,7 +166,7 @@ Python prerequisites.
|
|
|
166
166
|
```toml
|
|
167
167
|
[dependencies]
|
|
168
168
|
codingest = "0.1"
|
|
169
|
-
kglite = "0.
|
|
169
|
+
kglite = "0.15.3"
|
|
170
170
|
```
|
|
171
171
|
|
|
172
172
|
```rust
|
|
@@ -210,23 +210,35 @@ cargo test --workspace
|
|
|
210
210
|
cargo test -p codingest --test parity
|
|
211
211
|
```
|
|
212
212
|
|
|
213
|
-
### The golden-digest oracle
|
|
213
|
+
### The golden-digest oracle (also the determinism gate)
|
|
214
214
|
|
|
215
215
|
KGLite deleted its in-tree `code_tree` builder on 2026-07-16, so the old
|
|
216
216
|
two-builder parity sweep is gone. The authority it enforced was **frozen** while
|
|
217
217
|
the builders were still verified identical, into per-corpus SHA-256 digests
|
|
218
218
|
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
|
-
|
|
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:
|
|
223
229
|
`cargo test -p codingest --test parity -- --ignored capture_goldens` (details in
|
|
224
230
|
`crates/codingest/tests/goldens/README.md`).
|
|
225
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
|
+
|
|
226
238
|
## Dependency policy
|
|
227
239
|
|
|
228
240
|
`kglite` and `kglite-mcp-server` use matching crates.io requirements with a
|
|
229
|
-
0.
|
|
241
|
+
0.15.3 minimum and a shared lockfile. This keeps the builder, persistence
|
|
230
242
|
handoff, and embedded MCP server on one compatible engine patch line.
|
|
231
243
|
|
|
232
244
|
## 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.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}");
|
|
@@ -278,8 +278,14 @@ fn stamp_rev_provenance(
|
|
|
278
278
|
// One graph holding N revisions via shared identity + rev-sets: one node per
|
|
279
279
|
// entity, native list props `revs: [str]` / `rev_fp: [int]` on nodes and
|
|
280
280
|
// `revs: [str]` on edges. Unchanged entities are stored once, so the merged
|
|
281
|
-
// graph is ≈ base + deltas.
|
|
282
|
-
//
|
|
281
|
+
// graph is ≈ base + deltas.
|
|
282
|
+
//
|
|
283
|
+
// This comment is deliberately self-contained. It used to defer to a
|
|
284
|
+
// `dev-docs/plans/` design doc for the settled decisions — but `dev-docs/` is
|
|
285
|
+
// gitignored working state, so the citation outlived the file it pointed at and
|
|
286
|
+
// became a dead reference for every reader who cloned this repo. Committed code
|
|
287
|
+
// does not cite `dev-docs/` paths; anything a reader needs belongs here, and
|
|
288
|
+
// anything durable enough to survive belongs in a committed doc.
|
|
283
289
|
|
|
284
290
|
/// Collapse a requested rev-label list to its order-preserving unique form
|
|
285
291
|
/// (first occurrence wins). A duplicate label resolves to the same tree, so
|
|
@@ -25,6 +25,14 @@ in-tree vs codingest check) is gone. `golden_parity` — which builds each corpu
|
|
|
25
25
|
with only the `codingest` builder and compares to these frozen digests — is now
|
|
26
26
|
the sole guardian that codingest still produces the historically-correct graph.
|
|
27
27
|
|
|
28
|
+
`golden_parity` builds each corpus **three times** (`BUILDS_PER_CORPUS`), so it
|
|
29
|
+
is also the builder-determinism gate: hash iteration order is randomized per
|
|
30
|
+
`HashMap` instance, and `dup_minified_assets` reproduces the DEFINES-edge
|
|
31
|
+
ordering bug that once flapped edge totals run-to-run. A disagreement *between
|
|
32
|
+
builds* is nondeterminism and is never a reason to regenerate; agreement between
|
|
33
|
+
builds that differs from the golden is a behaviour change and may be. The test
|
|
34
|
+
reports the two cases distinctly.
|
|
35
|
+
|
|
28
36
|
## Regenerating (deliberate builder-behavior changes only)
|
|
29
37
|
|
|
30
38
|
Do **not** regenerate to make a red `golden_parity` go green. A digest change
|
|
@@ -9,9 +9,18 @@
|
|
|
9
9
|
//! builders were still verified-identical) into per-corpus SHA-256 golden
|
|
10
10
|
//! digests, and the surviving tests carry it forward:
|
|
11
11
|
//!
|
|
12
|
-
//! * `golden_parity` builds each corpus
|
|
13
|
-
//!
|
|
14
|
-
//! `tests/goldens/README.md`).
|
|
12
|
+
//! * `golden_parity` builds each corpus `BUILDS_PER_CORPUS` times with
|
|
13
|
+
//! `codingest` and asserts every build's canonical digest matches the
|
|
14
|
+
//! frozen golden (see `tests/goldens/README.md`). Repeating the build is
|
|
15
|
+
//! what makes this the project's **determinism** gate as well: hash
|
|
16
|
+
//! iteration order is randomized per `HashMap` instance, so an
|
|
17
|
+
//! order-dependent builder produces digests that disagree with each other
|
|
18
|
+
//! (reported as NONDETERMINISM) or agree with each other but not the
|
|
19
|
+
//! golden (reported as a behaviour change). This replaced a Makefile-only
|
|
20
|
+
//! step that ran three builds of an *external sibling checkout* and pinned
|
|
21
|
+
//! its exact edge count — a gate whose verdict depended on a repository
|
|
22
|
+
//! this project does not own, which never ran in CI, and which skipped
|
|
23
|
+
//! silently when the checkout was absent.
|
|
15
24
|
//! * `rev_self_consistency` builds the same 2-commit tempdir repo twice with
|
|
16
25
|
//! `codingest` and asserts the graphs are equivalent — INCLUDING the
|
|
17
26
|
//! `revs` / `rev_fp` list properties stamped onto every node and edge — a
|
|
@@ -45,6 +54,20 @@ const CORPORA: &[&str] = &[
|
|
|
45
54
|
"agc_basic",
|
|
46
55
|
];
|
|
47
56
|
|
|
57
|
+
/// Independent builds of each corpus per `golden_parity` run.
|
|
58
|
+
///
|
|
59
|
+
/// One build proves the output still matches the frozen golden; repeating it
|
|
60
|
+
/// also proves the output does not depend on hash iteration order. The
|
|
61
|
+
/// original nondeterminism bug (randomized `HashMap` iteration over DEFINES
|
|
62
|
+
/// (source_type, target_type) pairs — whichever pair went first got
|
|
63
|
+
/// skip-existence-check dedup semantics) is reproduced by the
|
|
64
|
+
/// `dup_minified_assets` corpus, which defines the same selector/element id
|
|
65
|
+
/// from one file more than once. Three builds is enough to make an
|
|
66
|
+
/// order-dependent builder fail loudly and quickly; the digest comparison
|
|
67
|
+
/// against the golden catches the residual case where all three happen to
|
|
68
|
+
/// agree on a wrong order.
|
|
69
|
+
const BUILDS_PER_CORPUS: usize = 3;
|
|
70
|
+
|
|
48
71
|
fn corpus_root() -> PathBuf {
|
|
49
72
|
// tests/parity.rs lives in crates/code-tree; corpus is at the workspace root.
|
|
50
73
|
Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
@@ -229,62 +252,62 @@ fn read_golden(corpus: &str) -> String {
|
|
|
229
252
|
|
|
230
253
|
/// Assert the two graphs are equivalent along every dimension, with
|
|
231
254
|
/// pinpointed diffs on failure.
|
|
232
|
-
fn assert_graphs_equiv(label: &str,
|
|
255
|
+
fn assert_graphs_equiv(label: &str, left: &DirGraph, right: &DirGraph) {
|
|
233
256
|
// 1. node-type counts
|
|
234
|
-
let (a, b) = (node_type_counts(
|
|
257
|
+
let (a, b) = (node_type_counts(left), node_type_counts(right));
|
|
235
258
|
assert_eq!(
|
|
236
259
|
a, b,
|
|
237
|
-
"[{label}] node-type counts differ\
|
|
260
|
+
"[{label}] node-type counts differ\nleft={a:?}\nright={b:?}"
|
|
238
261
|
);
|
|
239
262
|
|
|
240
263
|
// 2. edge-type counts
|
|
241
|
-
let (a, b) = (edge_type_counts(
|
|
264
|
+
let (a, b) = (edge_type_counts(left), edge_type_counts(right));
|
|
242
265
|
assert_eq!(
|
|
243
266
|
a, b,
|
|
244
|
-
"[{label}] edge-type counts differ\
|
|
267
|
+
"[{label}] edge-type counts differ\nleft={a:?}\nright={b:?}"
|
|
245
268
|
);
|
|
246
269
|
|
|
247
270
|
// 3. node identity set
|
|
248
|
-
let (a, b) = (node_identities(
|
|
271
|
+
let (a, b) = (node_identities(left), node_identities(right));
|
|
249
272
|
if a != b {
|
|
250
273
|
let only_in: Vec<_> = a.iter().filter(|x| !b.contains(x)).take(10).collect();
|
|
251
274
|
let only_st: Vec<_> = b.iter().filter(|x| !a.contains(x)).take(10).collect();
|
|
252
275
|
panic!(
|
|
253
|
-
"[{label}] node identity sets differ\nonly
|
|
276
|
+
"[{label}] node identity sets differ\nonly left (<=10): {only_in:?}\nonly right (<=10): {only_st:?}"
|
|
254
277
|
);
|
|
255
278
|
}
|
|
256
279
|
|
|
257
280
|
// 4. per-node property sweep
|
|
258
|
-
let (a, b) = (node_props(
|
|
281
|
+
let (a, b) = (node_props(left), node_props(right));
|
|
259
282
|
if a != b {
|
|
260
283
|
for (x, y) in a.iter().zip(b.iter()) {
|
|
261
284
|
if x != y {
|
|
262
285
|
panic!(
|
|
263
|
-
"[{label}] node properties differ\nfirst mismatch:\n
|
|
286
|
+
"[{label}] node properties differ\nfirst mismatch:\n left=({}, {}) {:?}\n right=({}, {}) {:?}",
|
|
264
287
|
x.0, x.1, x.2, y.0, y.1, y.2
|
|
265
288
|
);
|
|
266
289
|
}
|
|
267
290
|
}
|
|
268
291
|
panic!(
|
|
269
|
-
"[{label}] node property lists differ in length:
|
|
292
|
+
"[{label}] node property lists differ in length: left={} right={}",
|
|
270
293
|
a.len(),
|
|
271
294
|
b.len()
|
|
272
295
|
);
|
|
273
296
|
}
|
|
274
297
|
|
|
275
298
|
// 5. per-edge property sweep
|
|
276
|
-
let (a, b) = (edge_props(
|
|
299
|
+
let (a, b) = (edge_props(left), edge_props(right));
|
|
277
300
|
if a != b {
|
|
278
301
|
for (x, y) in a.iter().zip(b.iter()) {
|
|
279
302
|
if x != y {
|
|
280
303
|
panic!(
|
|
281
|
-
"[{label}] edge properties differ\nfirst mismatch:\n
|
|
304
|
+
"[{label}] edge properties differ\nfirst mismatch:\n left=({}, {}->{}) {:?}\n right=({}, {}->{}) {:?}",
|
|
282
305
|
x.0, x.1, x.2, x.3, y.0, y.1, y.2, y.3
|
|
283
306
|
);
|
|
284
307
|
}
|
|
285
308
|
}
|
|
286
309
|
panic!(
|
|
287
|
-
"[{label}] edge property lists differ in length:
|
|
310
|
+
"[{label}] edge property lists differ in length: left={} right={}",
|
|
288
311
|
a.len(),
|
|
289
312
|
b.len()
|
|
290
313
|
);
|
|
@@ -298,6 +321,12 @@ fn assert_graphs_equiv(label: &str, in_tree: &DirGraph, standalone: &DirGraph) {
|
|
|
298
321
|
// (see `tests/goldens/README.md`). This is the test that carries the
|
|
299
322
|
// authority forward now that KGLite has deleted its in-tree builder: it keeps
|
|
300
323
|
// proving that codingest reproduces the frozen graph byte-for-byte.
|
|
324
|
+
//
|
|
325
|
+
// It is also the determinism gate: each corpus is built `BUILDS_PER_CORPUS`
|
|
326
|
+
// times and every build must produce the same digest as every other *and* as
|
|
327
|
+
// the golden. The two failure modes are reported separately, because they call
|
|
328
|
+
// for opposite responses — a behaviour change may be legitimate and
|
|
329
|
+
// regenerated, while nondeterminism is always a bug.
|
|
301
330
|
#[test]
|
|
302
331
|
fn golden_parity() {
|
|
303
332
|
let root = corpus_root();
|
|
@@ -305,14 +334,40 @@ fn golden_parity() {
|
|
|
305
334
|
let dir = root.join(name);
|
|
306
335
|
assert!(dir.is_dir(), "missing corpus dir: {}", dir.display());
|
|
307
336
|
|
|
308
|
-
let
|
|
309
|
-
|
|
310
|
-
|
|
337
|
+
let build = || {
|
|
338
|
+
codingest::builder::run_with_options(&dir, false, true, None, None, true)
|
|
339
|
+
.unwrap_or_else(|e| panic!("[{name}] codingest build failed: {e}"))
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
let first = build();
|
|
343
|
+
let got = graph_digest(&first);
|
|
344
|
+
|
|
345
|
+
// 1. Determinism: repeat builds must be identical to the first.
|
|
346
|
+
for run in 2..=BUILDS_PER_CORPUS {
|
|
347
|
+
let again = build();
|
|
348
|
+
let again_digest = graph_digest(&again);
|
|
349
|
+
if again_digest != got {
|
|
350
|
+
// Pinpoint the divergence before reporting it.
|
|
351
|
+
assert_graphs_equiv(
|
|
352
|
+
&format!("{name} nondeterminism run 1 vs {run}"),
|
|
353
|
+
&first,
|
|
354
|
+
&again,
|
|
355
|
+
);
|
|
356
|
+
panic!(
|
|
357
|
+
"[{name}] NONDETERMINISM: build 1 and build {run} of the same corpus \
|
|
358
|
+
produced different digests ({got} != {again_digest}) but compared equal \
|
|
359
|
+
dimension-by-dimension — widen the canonical rendering."
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// 2. Frozen authority: the (now proven stable) digest must be the golden.
|
|
311
365
|
let want = read_golden(name);
|
|
312
366
|
assert_eq!(
|
|
313
367
|
got, want,
|
|
314
368
|
"[{name}] golden digest mismatch\n golden (frozen authority) = {want}\n codingest build = {got}\n\
|
|
315
|
-
|
|
369
|
+
All {BUILDS_PER_CORPUS} builds agreed, so this is a builder BEHAVIOUR change, not nondeterminism.\n\
|
|
370
|
+
If it is deliberate, regenerate with\n\
|
|
316
371
|
`cargo test -p codingest --test parity -- --ignored capture_goldens`."
|
|
317
372
|
);
|
|
318
373
|
}
|
|
@@ -14,7 +14,7 @@ name = "codingest"
|
|
|
14
14
|
path = "src/main.rs"
|
|
15
15
|
|
|
16
16
|
[dependencies]
|
|
17
|
-
codingest = { version = "0.1.
|
|
17
|
+
codingest = { version = "0.1.4", path = "../codingest" }
|
|
18
18
|
kglite = { workspace = true }
|
|
19
19
|
anyhow = "1"
|
|
20
20
|
clap = { version = "4", features = ["derive"] }
|
|
@@ -42,7 +42,7 @@ doctest = false
|
|
|
42
42
|
pyo3 = { version = "0.29" }
|
|
43
43
|
# The engine-free builder (parsers, call/type/route edges, rev merge, repo
|
|
44
44
|
# clone, manifest reader). Docs pass gated by this crate's `docs` feature.
|
|
45
|
-
codingest = { version = "0.1.
|
|
45
|
+
codingest = { version = "0.1.4", path = "../codingest", default-features = false }
|
|
46
46
|
# `.kgl` persistence (`kglite::api::io::save_graph`) for the serialize half of
|
|
47
47
|
# the build-then-load handoff, plus the `DirGraph` type the builder returns.
|
|
48
48
|
kglite = { workspace = true }
|
|
@@ -53,9 +53,9 @@ tempfile = "3"
|
|
|
53
53
|
# through `_run_cli`, so `pip install codingest` also installs the `codingest`
|
|
54
54
|
# command without a second wheel or a duplicated builder build. Mirrors
|
|
55
55
|
# kglite-py's `kglite-cli` bundling.
|
|
56
|
-
codingest-cli = { version = "0.1.
|
|
56
|
+
codingest-cli = { version = "0.1.4", path = "../codingest-cli" }
|
|
57
57
|
# Source-building MCP composition. The graph server and generic MCP lifecycle
|
|
58
58
|
# remain owned by its existing kglite-mcp-server -> mcp-methods Rust dependency
|
|
59
59
|
# chain; linking this thin library into the extension gives pip users the same
|
|
60
60
|
# builder-aware server as the standalone `codingest-mcp` binary.
|
|
61
|
-
codingest-mcp = { version = "0.1.
|
|
61
|
+
codingest-mcp = { version = "0.1.4", path = "../codingest-mcp" }
|
|
@@ -67,7 +67,7 @@ fn load_via_kglite(py: Python<'_>, path: &Path) -> PyResult<Py<PyAny>> {
|
|
|
67
67
|
let kglite = py.import("kglite").map_err(|e| {
|
|
68
68
|
PyRuntimeError::new_err(format!(
|
|
69
69
|
"codingest.build() returns a kglite.KnowledgeGraph, but importing \
|
|
70
|
-
`kglite` failed ({e}). Install it: `pip install kglite>=0.
|
|
70
|
+
`kglite` failed ({e}). Install it: `pip install kglite>=0.15.3`."
|
|
71
71
|
))
|
|
72
72
|
})?;
|
|
73
73
|
let graph = kglite.call_method1("load", (path.to_string_lossy().as_ref(),))?;
|
|
@@ -34,8 +34,17 @@ dynamic = ["version"]
|
|
|
34
34
|
# `load()`. So the returned object is a real `kglite.KnowledgeGraph`, and the
|
|
35
35
|
# wheel depends on kglite at runtime. Keep the Python engine aligned with the
|
|
36
36
|
# Rust crates so both halves use the same persistence and MCP fixes.
|
|
37
|
+
#
|
|
38
|
+
# The upper bound is a `.kgl` format guard, not stylistic. The two halves of
|
|
39
|
+
# this handoff are versioned independently: the *Rust* kglite compiled into
|
|
40
|
+
# this wheel writes the bytes, and the separately-installed *Python* kglite
|
|
41
|
+
# wheel reads them. KGLite deliberately hard-breaks the `.kgl` format across
|
|
42
|
+
# minor releases (e.g. the v3->v4 break, which refuses old files outright), so
|
|
43
|
+
# pip must never be free to pair our writer with a reader from a different
|
|
44
|
+
# minor. This range therefore mirrors the Cargo semver range for the pin above
|
|
45
|
+
# (`version = "0.15.3"` == `>=0.15.3,<0.16.0`) exactly — bump both together.
|
|
37
46
|
dependencies = [
|
|
38
|
-
"kglite>=0.
|
|
47
|
+
"kglite>=0.15.3,<0.16",
|
|
39
48
|
]
|
|
40
49
|
|
|
41
50
|
[project.urls]
|
codingest-0.1.3/Cargo.toml
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
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.3"
|
|
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.14.5
|
|
16
|
-
# floor brings generic workspace-graph hooks and generation-safe MCP activation
|
|
17
|
-
# on top of the current deterministic Postcard persistence stack.
|
|
18
|
-
# Keep both libraries aligned so the graph and server share one engine release.
|
|
19
|
-
kglite = { version = "0.14.5", default-features = false }
|
|
20
|
-
kglite-mcp-server = { version = "0.14.5", default-features = false }
|
|
21
|
-
|
|
22
|
-
# Mirror KGLite's release codegen so the standalone builder has
|
|
23
|
-
# performance parity with the in-tree component (see KGLite
|
|
24
|
-
# workspace Cargo.toml).
|
|
25
|
-
[profile.release]
|
|
26
|
-
lto = "thin"
|
|
27
|
-
codegen-units = 1
|
|
28
|
-
strip = "symbols"
|
|
29
|
-
|
|
30
|
-
[profile.profiling]
|
|
31
|
-
inherits = "release"
|
|
32
|
-
debug = "line-tables-only"
|
|
33
|
-
strip = "none"
|
|
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
|
{codingest-0.1.3 → codingest-0.1.4}/crates/codingest/tests/goldens/dup_minified_assets.sha256
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codingest-0.1.3 → codingest-0.1.4}/crates/codingest-cli/skills/codingest-code-review/SKILL.md
RENAMED
|
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
|