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.
Files changed (75) hide show
  1. {codingest-0.1.3 → codingest-0.1.4}/Cargo.lock +10 -10
  2. codingest-0.1.4/Cargo.toml +40 -0
  3. {codingest-0.1.3 → codingest-0.1.4}/PKG-INFO +22 -10
  4. {codingest-0.1.3 → codingest-0.1.4}/README.md +20 -8
  5. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/Cargo.toml +2 -2
  6. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/bin/codingest_bench.rs +214 -3
  7. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/rev.rs +8 -2
  8. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/tests/goldens/README.md +8 -0
  9. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/tests/parity.rs +75 -20
  10. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-cli/Cargo.toml +1 -1
  11. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-mcp/Cargo.toml +1 -1
  12. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-py/Cargo.toml +3 -3
  13. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-py/src/lib.rs +1 -1
  14. {codingest-0.1.3 → codingest-0.1.4}/pyproject.toml +10 -1
  15. codingest-0.1.3/Cargo.toml +0 -33
  16. {codingest-0.1.3 → codingest-0.1.4}/LICENSE +0 -0
  17. {codingest-0.1.3 → codingest-0.1.4}/codingest/__init__.py +0 -0
  18. {codingest-0.1.3 → codingest-0.1.4}/codingest/__init__.pyi +0 -0
  19. {codingest-0.1.3 → codingest-0.1.4}/codingest/cli.py +0 -0
  20. {codingest-0.1.3 → codingest-0.1.4}/codingest/mcp_server.py +0 -0
  21. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/bin/codingest_stats.rs +0 -0
  22. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/call_edges.rs +0 -0
  23. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/load/edge_frames.rs +0 -0
  24. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/load/entity_frames.rs +0 -0
  25. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/load.rs +0 -0
  26. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/mod.rs +0 -0
  27. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/other_edges.rs +0 -0
  28. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/routes/django.rs +0 -0
  29. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/routes/fastapi.rs +0 -0
  30. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/routes/flask.rs +0 -0
  31. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/routes/mod.rs +0 -0
  32. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/semantic_edges.rs +0 -0
  33. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/builder/type_edges.rs +0 -0
  34. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/cross_lang.rs +0 -0
  35. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/docs/mod.rs +0 -0
  36. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/docs/rst.rs +0 -0
  37. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/lib.rs +0 -0
  38. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/manifest/mod.rs +0 -0
  39. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/models.rs +0 -0
  40. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/agc.rs +0 -0
  41. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/cpp.rs +0 -0
  42. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/csharp.rs +0 -0
  43. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/css.rs +0 -0
  44. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/dart.rs +0 -0
  45. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/go.rs +0 -0
  46. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/html.rs +0 -0
  47. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/java.rs +0 -0
  48. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/mod.rs +0 -0
  49. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/php.rs +0 -0
  50. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/python.rs +0 -0
  51. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/registry.rs +0 -0
  52. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/rust_lang.rs +0 -0
  53. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/shared.rs +0 -0
  54. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/swift.rs +0 -0
  55. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/parsers/typescript.rs +0 -0
  56. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/src/repo.rs +0 -0
  57. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/tests/agc_apollo.rs +0 -0
  58. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/tests/goldens/agc_basic.sha256 +0 -0
  59. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/tests/goldens/cross_ts_py.sha256 +0 -0
  60. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/tests/goldens/dup_minified_assets.sha256 +0 -0
  61. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/tests/goldens/py_basic.sha256 +0 -0
  62. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/tests/goldens/py_inheritance.sha256 +0 -0
  63. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/tests/goldens/rust_xfile.sha256 +0 -0
  64. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest/tests/goldens/ts_callback.sha256 +0 -0
  65. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-cli/skills/codingest-code-review/SKILL.md +0 -0
  66. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-cli/skills/codingest-code-review/references/mcp-upgrade.md +0 -0
  67. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-cli/skills/codingest-code-review/references/public-repositories.md +0 -0
  68. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-cli/skills/codingest-code-review/references/queries.md +0 -0
  69. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-cli/src/code_tree_cli.rs +0 -0
  70. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-cli/src/lib.rs +0 -0
  71. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-cli/src/main.rs +0 -0
  72. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-cli/src/skill.rs +0 -0
  73. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-cli/src/skill_assets.rs +0 -0
  74. {codingest-0.1.3 → codingest-0.1.4}/crates/codingest-mcp/src/lib.rs +0 -0
  75. {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.3"
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.3"
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.3"
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.3"
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.14.5"
1396
+ version = "0.15.3"
1397
1397
  source = "registry+https://github.com/rust-lang/crates.io-index"
1398
- checksum = "e03ddfab585aa45e1f8149d78477b677fd253b9c9b0a8d04bdcd3829c50c8caf"
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.14.5"
1431
+ version = "0.15.3"
1432
1432
  source = "registry+https://github.com/rust-lang/crates.io-index"
1433
- checksum = "67bcf469c57b9c6bdc51493ec362a90c98c085f79a0de75c203dc42e3612b6ea"
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.1"
1535
+ version = "0.4.2"
1536
1536
  source = "registry+https://github.com/rust-lang/crates.io-index"
1537
- checksum = "ccd3c31a95d7775d6d930a174aeffdc416c2ad6ffce0eee1cad262a007340175"
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
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.14.5,<0.15
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.14.5 is installed automatically as the query/storage
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.14.5"
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 builder, digests
247
- a canonical exhaustive graph rendering, and compares to the frozen golden. The
248
- multi-rev fixture is guarded instead by `rev_self_consistency`. Regenerate
249
- goldens only for deliberate builder-behavior changes:
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.14.5 minimum and a shared lockfile. This keeps the builder, persistence
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.14.5 is installed automatically as the query/storage
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.14.5"
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 builder, digests
220
- a canonical exhaustive graph rendering, and compares to the frozen golden. The
221
- multi-rev fixture is guarded instead by `rev_self_consistency`. Regenerate
222
- goldens only for deliberate builder-behavior changes:
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.14.5 minimum and a shared lockfile. This keeps the builder, persistence
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.14 engine API (`kglite::api::code_entities`); the
13
- # workspace pins the current 0.14 patch floor.
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(target, false, false, None, None, true)
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(target, false, false, None, None, true)
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. See `dev-docs/plans/rev-aware-code-graphs.md`
282
- // "B.2 design" for the eight settled decisions.
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 once with `codingest` and asserts
13
- //! its canonical digest matches the frozen golden (see
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, in_tree: &DirGraph, standalone: &DirGraph) {
255
+ fn assert_graphs_equiv(label: &str, left: &DirGraph, right: &DirGraph) {
233
256
  // 1. node-type counts
234
- let (a, b) = (node_type_counts(in_tree), node_type_counts(standalone));
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\nin-tree={a:?}\nstandalone={b:?}"
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(in_tree), edge_type_counts(standalone));
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\nin-tree={a:?}\nstandalone={b:?}"
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(in_tree), node_identities(standalone));
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 in-tree (<=10): {only_in:?}\nonly standalone (<=10): {only_st:?}"
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(in_tree), node_props(standalone));
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 in-tree=({}, {}) {:?}\n standalone=({}, {}) {:?}",
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: in-tree={} standalone={}",
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(in_tree), edge_props(standalone));
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 in-tree=({}, {}->{}) {:?}\n standalone=({}, {}->{}) {:?}",
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: in-tree={} standalone={}",
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 g = codingest::builder::run_with_options(&dir, false, true, None, None, true)
309
- .unwrap_or_else(|e| panic!("[{name}] codingest build failed: {e}"));
310
- let got = graph_digest(&g);
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
- If this change to builder behavior is deliberate, regenerate with\n\
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.0", path = "../codingest" }
17
+ codingest = { version = "0.1.4", path = "../codingest" }
18
18
  kglite = { workspace = true }
19
19
  anyhow = "1"
20
20
  clap = { version = "4", features = ["derive"] }
@@ -14,6 +14,6 @@ name = "codingest-mcp"
14
14
  path = "src/main.rs"
15
15
 
16
16
  [dependencies]
17
- codingest = { version = "0.1.0", path = "../codingest" }
17
+ codingest = { version = "0.1.4", path = "../codingest" }
18
18
  kglite-mcp-server = { workspace = true }
19
19
  anyhow = "1"
@@ -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.0", path = "../codingest", default-features = false }
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.0", path = "../codingest-cli" }
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.0", path = "../codingest-mcp" }
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.14.5`."
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.14.5,<0.15",
47
+ "kglite>=0.15.3,<0.16",
39
48
  ]
40
49
 
41
50
  [project.urls]
@@ -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