codingest 0.1.2__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. {codingest-0.1.2 → codingest-0.1.4}/Cargo.lock +11 -10
  2. codingest-0.1.4/Cargo.toml +40 -0
  3. {codingest-0.1.2 → codingest-0.1.4}/PKG-INFO +34 -15
  4. {codingest-0.1.2 → codingest-0.1.4}/README.md +32 -13
  5. codingest-0.1.4/codingest/mcp_server.py +38 -0
  6. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/Cargo.toml +2 -2
  7. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/bin/codingest_bench.rs +214 -3
  8. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/builder/call_edges.rs +22 -0
  9. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/builder/load/edge_frames.rs +176 -2
  10. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/builder/load/entity_frames.rs +58 -4
  11. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/builder/load.rs +71 -2
  12. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/builder/mod.rs +46 -0
  13. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/builder/other_edges.rs +16 -0
  14. codingest-0.1.4/crates/codingest/src/builder/semantic_edges.rs +530 -0
  15. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/models.rs +91 -0
  16. codingest-0.1.4/crates/codingest/src/parsers/agc.rs +984 -0
  17. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/rev.rs +8 -2
  18. codingest-0.1.4/crates/codingest/tests/agc_apollo.rs +116 -0
  19. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/tests/goldens/README.md +11 -2
  20. codingest-0.1.4/crates/codingest/tests/goldens/agc_basic.sha256 +1 -0
  21. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/tests/parity.rs +75 -20
  22. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-cli/Cargo.toml +1 -1
  23. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-cli/skills/codingest-code-review/SKILL.md +2 -2
  24. codingest-0.1.4/crates/codingest-mcp/Cargo.toml +19 -0
  25. codingest-0.1.4/crates/codingest-mcp/src/lib.rs +99 -0
  26. codingest-0.1.4/crates/codingest-mcp/src/main.rs +5 -0
  27. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-py/Cargo.toml +7 -2
  28. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-py/src/lib.rs +16 -1
  29. {codingest-0.1.2 → codingest-0.1.4}/pyproject.toml +16 -6
  30. codingest-0.1.2/Cargo.toml +0 -33
  31. codingest-0.1.2/crates/codingest/src/parsers/agc.rs +0 -522
  32. codingest-0.1.2/crates/codingest/tests/goldens/agc_basic.sha256 +0 -1
  33. {codingest-0.1.2 → codingest-0.1.4}/LICENSE +0 -0
  34. {codingest-0.1.2 → codingest-0.1.4}/codingest/__init__.py +0 -0
  35. {codingest-0.1.2 → codingest-0.1.4}/codingest/__init__.pyi +0 -0
  36. {codingest-0.1.2 → codingest-0.1.4}/codingest/cli.py +0 -0
  37. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/bin/codingest_stats.rs +0 -0
  38. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/builder/routes/django.rs +0 -0
  39. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/builder/routes/fastapi.rs +0 -0
  40. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/builder/routes/flask.rs +0 -0
  41. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/builder/routes/mod.rs +0 -0
  42. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/builder/type_edges.rs +0 -0
  43. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/cross_lang.rs +0 -0
  44. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/docs/mod.rs +0 -0
  45. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/docs/rst.rs +0 -0
  46. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/lib.rs +0 -0
  47. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/manifest/mod.rs +0 -0
  48. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/cpp.rs +0 -0
  49. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/csharp.rs +0 -0
  50. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/css.rs +0 -0
  51. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/dart.rs +0 -0
  52. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/go.rs +0 -0
  53. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/html.rs +0 -0
  54. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/java.rs +0 -0
  55. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/mod.rs +0 -0
  56. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/php.rs +0 -0
  57. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/python.rs +0 -0
  58. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/registry.rs +0 -0
  59. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/rust_lang.rs +0 -0
  60. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/shared.rs +0 -0
  61. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/swift.rs +0 -0
  62. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/parsers/typescript.rs +0 -0
  63. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/src/repo.rs +0 -0
  64. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/tests/goldens/cross_ts_py.sha256 +0 -0
  65. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/tests/goldens/dup_minified_assets.sha256 +0 -0
  66. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/tests/goldens/py_basic.sha256 +0 -0
  67. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/tests/goldens/py_inheritance.sha256 +0 -0
  68. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/tests/goldens/rust_xfile.sha256 +0 -0
  69. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest/tests/goldens/ts_callback.sha256 +0 -0
  70. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-cli/skills/codingest-code-review/references/mcp-upgrade.md +0 -0
  71. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-cli/skills/codingest-code-review/references/public-repositories.md +0 -0
  72. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-cli/skills/codingest-code-review/references/queries.md +0 -0
  73. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-cli/src/code_tree_cli.rs +0 -0
  74. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-cli/src/lib.rs +0 -0
  75. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-cli/src/main.rs +0 -0
  76. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-cli/src/skill.rs +0 -0
  77. {codingest-0.1.2 → codingest-0.1.4}/crates/codingest-cli/src/skill_assets.rs +0 -0
@@ -313,7 +313,7 @@ dependencies = [
313
313
 
314
314
  [[package]]
315
315
  name = "codingest"
316
- version = "0.1.2"
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.2"
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.2"
360
+ version = "0.1.4"
361
361
  dependencies = [
362
362
  "anyhow",
363
363
  "codingest",
@@ -366,10 +366,11 @@ dependencies = [
366
366
 
367
367
  [[package]]
368
368
  name = "codingest-py"
369
- version = "0.1.2"
369
+ version = "0.1.4"
370
370
  dependencies = [
371
371
  "codingest",
372
372
  "codingest-cli",
373
+ "codingest-mcp",
373
374
  "kglite",
374
375
  "pyo3",
375
376
  "tempfile",
@@ -1392,9 +1393,9 @@ dependencies = [
1392
1393
 
1393
1394
  [[package]]
1394
1395
  name = "kglite"
1395
- version = "0.14.5"
1396
+ version = "0.15.3"
1396
1397
  source = "registry+https://github.com/rust-lang/crates.io-index"
1397
- checksum = "e03ddfab585aa45e1f8149d78477b677fd253b9c9b0a8d04bdcd3829c50c8caf"
1398
+ checksum = "4ec07586e3dac4fa5dd1e216ac057f41ffe056f95eefc8ddd866f09fd7e48394"
1398
1399
  dependencies = [
1399
1400
  "bzip2",
1400
1401
  "chrono",
@@ -1427,9 +1428,9 @@ dependencies = [
1427
1428
 
1428
1429
  [[package]]
1429
1430
  name = "kglite-mcp-server"
1430
- version = "0.14.5"
1431
+ version = "0.15.3"
1431
1432
  source = "registry+https://github.com/rust-lang/crates.io-index"
1432
- checksum = "67bcf469c57b9c6bdc51493ec362a90c98c085f79a0de75c203dc42e3612b6ea"
1433
+ checksum = "f9dae485f96add5453ef56518cd14e57c3f20727c8ab151500cc6f1afb70deef"
1433
1434
  dependencies = [
1434
1435
  "anyhow",
1435
1436
  "bytes",
@@ -1531,9 +1532,9 @@ dependencies = [
1531
1532
 
1532
1533
  [[package]]
1533
1534
  name = "mcp-methods"
1534
- version = "0.4.1"
1535
+ version = "0.4.2"
1535
1536
  source = "registry+https://github.com/rust-lang/crates.io-index"
1536
- checksum = "ccd3c31a95d7775d6d930a174aeffdc416c2ad6ffce0eee1cad262a007340175"
1537
+ checksum = "04c1063113c3a885d5967e45da2ea1cbb235938a9ba02333f15e81149b451b23"
1537
1538
  dependencies = [
1538
1539
  "anyhow",
1539
1540
  "clap",
@@ -0,0 +1,40 @@
1
+ [workspace]
2
+ members = ["crates/codingest", "crates/codingest-cli", "crates/codingest-mcp", "crates/codingest-py"]
3
+ resolver = "2"
4
+
5
+ [workspace.package]
6
+ version = "0.1.4"
7
+ edition = "2021"
8
+ rust-version = "1.88"
9
+ license = "MIT"
10
+ repository = "https://github.com/kkollsga/codingest"
11
+
12
+ [workspace.dependencies]
13
+ base64 = "0.22"
14
+ sha2 = "0.11"
15
+ # kglite engine + MCP server — imported as crates.io libraries. The 0.15.3
16
+ # floor brings tri-state `durable` levels with an on-demand `sync()` barrier,
17
+ # typed constraint/conflict exceptions carrying a stable `.code`, and the
18
+ # columnar-REMOVE correctness fix, on top of the generic workspace-graph hooks
19
+ # and deterministic Postcard persistence stack inherited from 0.14.5. The
20
+ # 0.15.1-0.15.3 patches add a declared `rust-version = 1.88.0` on both crates we
21
+ # consume (matching our own floor), fix a `storage="disk"` save that could emit
22
+ # a directory the same build could not load, widen the unknown-label diagnostic
23
+ # to subqueries, and correct understated dependency floors — with no change to
24
+ # `kglite::api`, graph output, property encoding, or `.kgl` serialization.
25
+ # Keep both libraries aligned so the graph and server share one engine release.
26
+ kglite = { version = "0.15.3", default-features = false }
27
+ kglite-mcp-server = { version = "0.15.3", default-features = false }
28
+
29
+ # Mirror KGLite's release codegen so the standalone builder has
30
+ # performance parity with the in-tree component (see KGLite
31
+ # workspace Cargo.toml).
32
+ [profile.release]
33
+ lto = "thin"
34
+ codegen-units = 1
35
+ strip = "symbols"
36
+
37
+ [profile.profiling]
38
+ inherits = "release"
39
+ debug = "line-tables-only"
40
+ strip = "none"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codingest
3
- Version: 0.1.2
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
@@ -70,10 +70,13 @@ a real `kglite.KnowledgeGraph`, ready for Cypher queries.
70
70
  - **Revision-aware:** analyse a tag, branch, commit, or multiple revisions in
71
71
  one graph and query what changed.
72
72
 
73
+ AGC assembly receives architecture-aware control and data relationships rather
74
+ than a generic text-level call graph; see the [AGC graph model](https://codingest.readthedocs.io/en/latest/agc-assembly.html).
75
+
73
76
  ## Give your agent a code-review MCP server
74
77
 
75
78
  ```bash
76
- cargo install codingest-mcp
79
+ pip install codingest
77
80
  codingest-mcp --watch /absolute/path/to/repo
78
81
  ```
79
82
 
@@ -172,21 +175,25 @@ for direct Cypher analysis.
172
175
  ## Install options
173
176
 
174
177
  ```bash
175
- pip install codingest # Python API + codingest CLI + KGLite runtime
176
- cargo install codingest-cli # pure-Rust builder CLI
177
- cargo install codingest-mcp # MCP server for agents
178
+ pip install codingest # Python API + CLI + MCP server + KGLite runtime
178
179
  codingest skill install # code-review Agent Skill
179
180
  ```
180
181
 
181
- The wheel bundles every grammar and the `codingest` command. KGLite ≥0.14.5 is
182
- installed automatically as the query/storage engine.
182
+ The wheel bundles every grammar plus the `codingest` and `codingest-mcp`
183
+ commands. KGLite ≥0.15.3 is installed automatically as the query/storage
184
+ engine; its MCP server and the transitive `mcp-methods` framework power the
185
+ builder-aware Codingest server. Nothing else needs to be installed.
186
+
187
+ Rust-only environments can instead use `cargo install codingest-cli
188
+ codingest-mcp`; these are alternative distributions of the same commands, not
189
+ Python prerequisites.
183
190
 
184
191
  ### Rust crate
185
192
 
186
193
  ```toml
187
194
  [dependencies]
188
195
  codingest = "0.1"
189
- kglite = "0.14.5"
196
+ kglite = "0.15.3"
190
197
  ```
191
198
 
192
199
  ```rust
@@ -230,23 +237,35 @@ cargo test --workspace
230
237
  cargo test -p codingest --test parity
231
238
  ```
232
239
 
233
- ### The golden-digest oracle
240
+ ### The golden-digest oracle (also the determinism gate)
234
241
 
235
242
  KGLite deleted its in-tree `code_tree` builder on 2026-07-16, so the old
236
243
  two-builder parity sweep is gone. The authority it enforced was **frozen** while
237
244
  the builders were still verified identical, into per-corpus SHA-256 digests
238
245
  under `crates/codingest/tests/goldens/` (committed fixtures — no network). The
239
- `golden_parity` test builds each corpus with only the codingest builder, digests
240
- a canonical exhaustive graph rendering, and compares to the frozen golden. The
241
- multi-rev fixture is guarded instead by `rev_self_consistency`. Regenerate
242
- 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:
243
256
  `cargo test -p codingest --test parity -- --ignored capture_goldens` (details in
244
257
  `crates/codingest/tests/goldens/README.md`).
245
258
 
259
+ Everything the gate needs is committed to this repository, so it is hermetic and
260
+ runs in CI. It replaced a local-only `make` step that asserted an exact edge
261
+ count against a *sibling* checkout — a verdict this project did not control.
262
+ `make determinism-soak REPO=…` keeps the large-repo reproducer available as a
263
+ diagnostic.
264
+
246
265
  ## Dependency policy
247
266
 
248
267
  `kglite` and `kglite-mcp-server` use matching crates.io requirements with a
249
- 0.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
250
269
  handoff, and embedded MCP server on one compatible engine patch line.
251
270
 
252
271
  ## Parity with the (now-removed) in-tree component
@@ -43,10 +43,13 @@ a real `kglite.KnowledgeGraph`, ready for Cypher queries.
43
43
  - **Revision-aware:** analyse a tag, branch, commit, or multiple revisions in
44
44
  one graph and query what changed.
45
45
 
46
+ AGC assembly receives architecture-aware control and data relationships rather
47
+ than a generic text-level call graph; see the [AGC graph model](https://codingest.readthedocs.io/en/latest/agc-assembly.html).
48
+
46
49
  ## Give your agent a code-review MCP server
47
50
 
48
51
  ```bash
49
- cargo install codingest-mcp
52
+ pip install codingest
50
53
  codingest-mcp --watch /absolute/path/to/repo
51
54
  ```
52
55
 
@@ -145,21 +148,25 @@ for direct Cypher analysis.
145
148
  ## Install options
146
149
 
147
150
  ```bash
148
- pip install codingest # Python API + codingest CLI + KGLite runtime
149
- cargo install codingest-cli # pure-Rust builder CLI
150
- cargo install codingest-mcp # MCP server for agents
151
+ pip install codingest # Python API + CLI + MCP server + KGLite runtime
151
152
  codingest skill install # code-review Agent Skill
152
153
  ```
153
154
 
154
- The wheel bundles every grammar and the `codingest` command. KGLite ≥0.14.5 is
155
- installed automatically as the query/storage engine.
155
+ The wheel bundles every grammar plus the `codingest` and `codingest-mcp`
156
+ commands. KGLite ≥0.15.3 is installed automatically as the query/storage
157
+ engine; its MCP server and the transitive `mcp-methods` framework power the
158
+ builder-aware Codingest server. Nothing else needs to be installed.
159
+
160
+ Rust-only environments can instead use `cargo install codingest-cli
161
+ codingest-mcp`; these are alternative distributions of the same commands, not
162
+ Python prerequisites.
156
163
 
157
164
  ### Rust crate
158
165
 
159
166
  ```toml
160
167
  [dependencies]
161
168
  codingest = "0.1"
162
- kglite = "0.14.5"
169
+ kglite = "0.15.3"
163
170
  ```
164
171
 
165
172
  ```rust
@@ -203,23 +210,35 @@ cargo test --workspace
203
210
  cargo test -p codingest --test parity
204
211
  ```
205
212
 
206
- ### The golden-digest oracle
213
+ ### The golden-digest oracle (also the determinism gate)
207
214
 
208
215
  KGLite deleted its in-tree `code_tree` builder on 2026-07-16, so the old
209
216
  two-builder parity sweep is gone. The authority it enforced was **frozen** while
210
217
  the builders were still verified identical, into per-corpus SHA-256 digests
211
218
  under `crates/codingest/tests/goldens/` (committed fixtures — no network). The
212
- `golden_parity` test builds each corpus with only the codingest builder, digests
213
- a canonical exhaustive graph rendering, and compares to the frozen golden. The
214
- multi-rev fixture is guarded instead by `rev_self_consistency`. Regenerate
215
- 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:
216
229
  `cargo test -p codingest --test parity -- --ignored capture_goldens` (details in
217
230
  `crates/codingest/tests/goldens/README.md`).
218
231
 
232
+ Everything the gate needs is committed to this repository, so it is hermetic and
233
+ runs in CI. It replaced a local-only `make` step that asserted an exact edge
234
+ count against a *sibling* checkout — a verdict this project did not control.
235
+ `make determinism-soak REPO=…` keeps the large-repo reproducer available as a
236
+ diagnostic.
237
+
219
238
  ## Dependency policy
220
239
 
221
240
  `kglite` and `kglite-mcp-server` use matching crates.io requirements with a
222
- 0.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
223
242
  handoff, and embedded MCP server on one compatible engine patch line.
224
243
 
225
244
  ## Parity with the (now-removed) in-tree component
@@ -0,0 +1,38 @@
1
+ """Console-script shim for the Rust ``codingest-mcp`` server in the wheel.
2
+
3
+ Codingest supplies the source-builder hooks; KGlite supplies graph/Cypher
4
+ serving and pulls the generic MCP lifecycle from ``mcp-methods``. All three are
5
+ linked into the wheel's existing native extension, so users need only
6
+ ``pip install codingest``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import sys
14
+
15
+
16
+ def main(argv: list[str] | None = None) -> int:
17
+ """Run the bundled MCP server with ``argv`` or ``sys.argv[1:]``."""
18
+ from codingest.codingest import _run_mcp_server
19
+
20
+ args = list(sys.argv[1:] if argv is None else argv)
21
+ # In a wheel console script, current_exe() is Python rather than the Rust
22
+ # server. Give KGlite's selftest the exact command needed to re-enter this
23
+ # module with the same interpreter and environment.
24
+ os.environ["KGLITE_MCP_RESPAWN"] = json.dumps(
25
+ [sys.executable, "-m", "codingest.mcp_server"]
26
+ )
27
+ try:
28
+ _run_mcp_server(args)
29
+ except KeyboardInterrupt:
30
+ return 130
31
+ except RuntimeError as exc:
32
+ print(f"codingest-mcp: {exc}", file=sys.stderr)
33
+ return 1
34
+ return 0
35
+
36
+
37
+ if __name__ == "__main__":
38
+ raise SystemExit(main())
@@ -9,8 +9,8 @@ description = "codingest — standalone extraction of KGLite's code-tree compone
9
9
  keywords = ["knowledge-graph", "code-analysis", "tree-sitter", "cypher", "kglite"]
10
10
  categories = ["development-tools", "parser-implementations"]
11
11
 
12
- # Requires the kglite 0.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}");
@@ -19,6 +19,12 @@ pub struct CallEdge {
19
19
  /// Comma-separated sorted unique line numbers.
20
20
  pub call_lines: String,
21
21
  pub call_count: i64,
22
+ /// Optional parser-preserved transfer spelling/mechanism. Empty for the
23
+ /// ordinary language call resolver, populated by typed semantic sites.
24
+ pub raw_targets: Option<String>,
25
+ pub offsets: Option<String>,
26
+ pub via: Option<String>,
27
+ pub address_lines: Option<String>,
22
28
  }
23
29
 
24
30
  /// Aggregate counters describing how the resolver classified every call
@@ -48,6 +54,18 @@ pub struct CallResolutionStats {
48
54
  pub resolved_via_inheritance: u64,
49
55
  }
50
56
 
57
+ impl CallResolutionStats {
58
+ pub(crate) fn merge(&mut self, other: Self) {
59
+ self.total_calls += other.total_calls;
60
+ self.excluded_noise += other.excluded_noise;
61
+ self.no_candidate += other.no_candidate;
62
+ self.ambiguous_dropped += other.ambiguous_dropped;
63
+ self.resolved_call_sites += other.resolved_call_sites;
64
+ self.resolved_edges += other.resolved_edges;
65
+ self.resolved_via_inheritance += other.resolved_via_inheritance;
66
+ }
67
+ }
68
+
51
69
  /// Per-function scratch counters, summed into [`CallResolutionStats`] after
52
70
  /// the parallel match loop. Kept `Copy` so the rayon reduce stays alloc-free.
53
71
  #[derive(Debug, Clone, Copy, Default)]
@@ -495,6 +513,10 @@ pub fn build_call_edges(
495
513
  callee: callee.to_string(),
496
514
  call_lines,
497
515
  call_count: count,
516
+ raw_targets: None,
517
+ offsets: None,
518
+ via: None,
519
+ address_lines: None,
498
520
  }
499
521
  })
500
522
  .collect();