codingest 0.1.3__tar.gz → 0.1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. {codingest-0.1.3 → codingest-0.1.5}/Cargo.lock +10 -10
  2. codingest-0.1.5/Cargo.toml +42 -0
  3. {codingest-0.1.3 → codingest-0.1.5}/PKG-INFO +29 -10
  4. {codingest-0.1.3 → codingest-0.1.5}/README.md +27 -8
  5. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/Cargo.toml +2 -2
  6. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/bin/codingest_bench.rs +214 -3
  7. codingest-0.1.5/crates/codingest/src/bin/codingest_stats.rs +464 -0
  8. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/call_edges.rs +390 -17
  9. codingest-0.1.5/crates/codingest/src/builder/js_workspace.rs +595 -0
  10. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/load/edge_frames.rs +24 -0
  11. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/load.rs +37 -5
  12. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/mod.rs +40 -12
  13. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/other_edges.rs +406 -5
  14. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/semantic_edges.rs +7 -0
  15. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/agc.rs +12 -2
  16. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/registry.rs +15 -0
  17. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/typescript.rs +161 -10
  18. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/rev.rs +8 -2
  19. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/tests/goldens/README.md +17 -0
  20. codingest-0.1.5/crates/codingest/tests/goldens/dup_minified_assets.sha256 +1 -0
  21. codingest-0.1.5/crates/codingest/tests/goldens/py_basic.sha256 +1 -0
  22. codingest-0.1.5/crates/codingest/tests/goldens/py_inheritance.sha256 +1 -0
  23. codingest-0.1.5/crates/codingest/tests/goldens/rust_xfile.sha256 +1 -0
  24. codingest-0.1.5/crates/codingest/tests/goldens/ts_callback.sha256 +1 -0
  25. codingest-0.1.5/crates/codingest/tests/goldens/ts_monorepo.sha256 +1 -0
  26. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/tests/parity.rs +173 -20
  27. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-cli/Cargo.toml +1 -1
  28. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-cli/src/code_tree_cli.rs +7 -3
  29. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-cli/src/lib.rs +20 -0
  30. codingest-0.1.5/crates/codingest-cli/src/main.rs +10 -0
  31. codingest-0.1.5/crates/codingest-cli/src/query.rs +644 -0
  32. codingest-0.1.5/crates/codingest-cli/tests/exit_codes.rs +209 -0
  33. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-mcp/Cargo.toml +1 -1
  34. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-py/Cargo.toml +3 -3
  35. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-py/src/lib.rs +1 -1
  36. {codingest-0.1.3 → codingest-0.1.5}/pyproject.toml +10 -1
  37. codingest-0.1.3/Cargo.toml +0 -33
  38. codingest-0.1.3/crates/codingest/src/bin/codingest_stats.rs +0 -150
  39. codingest-0.1.3/crates/codingest/tests/goldens/dup_minified_assets.sha256 +0 -1
  40. codingest-0.1.3/crates/codingest/tests/goldens/py_basic.sha256 +0 -1
  41. codingest-0.1.3/crates/codingest/tests/goldens/py_inheritance.sha256 +0 -1
  42. codingest-0.1.3/crates/codingest/tests/goldens/rust_xfile.sha256 +0 -1
  43. codingest-0.1.3/crates/codingest/tests/goldens/ts_callback.sha256 +0 -1
  44. codingest-0.1.3/crates/codingest-cli/src/main.rs +0 -5
  45. {codingest-0.1.3 → codingest-0.1.5}/LICENSE +0 -0
  46. {codingest-0.1.3 → codingest-0.1.5}/codingest/__init__.py +0 -0
  47. {codingest-0.1.3 → codingest-0.1.5}/codingest/__init__.pyi +0 -0
  48. {codingest-0.1.3 → codingest-0.1.5}/codingest/cli.py +0 -0
  49. {codingest-0.1.3 → codingest-0.1.5}/codingest/mcp_server.py +0 -0
  50. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/load/entity_frames.rs +0 -0
  51. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/routes/django.rs +0 -0
  52. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/routes/fastapi.rs +0 -0
  53. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/routes/flask.rs +0 -0
  54. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/routes/mod.rs +0 -0
  55. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/builder/type_edges.rs +0 -0
  56. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/cross_lang.rs +0 -0
  57. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/docs/mod.rs +0 -0
  58. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/docs/rst.rs +0 -0
  59. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/lib.rs +0 -0
  60. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/manifest/mod.rs +0 -0
  61. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/models.rs +0 -0
  62. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/cpp.rs +0 -0
  63. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/csharp.rs +0 -0
  64. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/css.rs +0 -0
  65. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/dart.rs +0 -0
  66. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/go.rs +0 -0
  67. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/html.rs +0 -0
  68. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/java.rs +0 -0
  69. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/mod.rs +0 -0
  70. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/php.rs +0 -0
  71. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/python.rs +0 -0
  72. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/rust_lang.rs +0 -0
  73. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/shared.rs +0 -0
  74. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/parsers/swift.rs +0 -0
  75. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/src/repo.rs +0 -0
  76. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/tests/agc_apollo.rs +0 -0
  77. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/tests/goldens/agc_basic.sha256 +0 -0
  78. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest/tests/goldens/cross_ts_py.sha256 +0 -0
  79. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-cli/skills/codingest-code-review/SKILL.md +0 -0
  80. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-cli/skills/codingest-code-review/references/mcp-upgrade.md +0 -0
  81. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-cli/skills/codingest-code-review/references/public-repositories.md +0 -0
  82. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-cli/skills/codingest-code-review/references/queries.md +0 -0
  83. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-cli/src/skill.rs +0 -0
  84. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-cli/src/skill_assets.rs +0 -0
  85. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-mcp/src/lib.rs +0 -0
  86. {codingest-0.1.3 → codingest-0.1.5}/crates/codingest-mcp/src/main.rs +0 -0
@@ -313,7 +313,7 @@ dependencies = [
313
313
 
314
314
  [[package]]
315
315
  name = "codingest"
316
- version = "0.1.3"
316
+ version = "0.1.5"
317
317
  dependencies = [
318
318
  "aho-corasick",
319
319
  "base64",
@@ -345,7 +345,7 @@ dependencies = [
345
345
 
346
346
  [[package]]
347
347
  name = "codingest-cli"
348
- version = "0.1.3"
348
+ version = "0.1.5"
349
349
  dependencies = [
350
350
  "anyhow",
351
351
  "clap",
@@ -357,7 +357,7 @@ dependencies = [
357
357
 
358
358
  [[package]]
359
359
  name = "codingest-mcp"
360
- version = "0.1.3"
360
+ version = "0.1.5"
361
361
  dependencies = [
362
362
  "anyhow",
363
363
  "codingest",
@@ -366,7 +366,7 @@ dependencies = [
366
366
 
367
367
  [[package]]
368
368
  name = "codingest-py"
369
- version = "0.1.3"
369
+ version = "0.1.5"
370
370
  dependencies = [
371
371
  "codingest",
372
372
  "codingest-cli",
@@ -1393,9 +1393,9 @@ dependencies = [
1393
1393
 
1394
1394
  [[package]]
1395
1395
  name = "kglite"
1396
- version = "0.14.5"
1396
+ version = "0.15.5"
1397
1397
  source = "registry+https://github.com/rust-lang/crates.io-index"
1398
- checksum = "e03ddfab585aa45e1f8149d78477b677fd253b9c9b0a8d04bdcd3829c50c8caf"
1398
+ checksum = "4664fc457e5dd762f5775977f06eb5377634556fffae45e4a86e2eb1e1eb0bfb"
1399
1399
  dependencies = [
1400
1400
  "bzip2",
1401
1401
  "chrono",
@@ -1428,9 +1428,9 @@ dependencies = [
1428
1428
 
1429
1429
  [[package]]
1430
1430
  name = "kglite-mcp-server"
1431
- version = "0.14.5"
1431
+ version = "0.15.5"
1432
1432
  source = "registry+https://github.com/rust-lang/crates.io-index"
1433
- checksum = "67bcf469c57b9c6bdc51493ec362a90c98c085f79a0de75c203dc42e3612b6ea"
1433
+ checksum = "e5e5d2207a45eb77c90e60a772bc8cc60ceac3d7c6f3ad2b948e8079355e1df4"
1434
1434
  dependencies = [
1435
1435
  "anyhow",
1436
1436
  "bytes",
@@ -1532,9 +1532,9 @@ dependencies = [
1532
1532
 
1533
1533
  [[package]]
1534
1534
  name = "mcp-methods"
1535
- version = "0.4.1"
1535
+ version = "0.4.3"
1536
1536
  source = "registry+https://github.com/rust-lang/crates.io-index"
1537
- checksum = "ccd3c31a95d7775d6d930a174aeffdc416c2ad6ffce0eee1cad262a007340175"
1537
+ checksum = "caa2079c12e642cd4b9a20543c85305eb5ffee04b1d565feab47b084db167a99"
1538
1538
  dependencies = [
1539
1539
  "anyhow",
1540
1540
  "clap",
@@ -0,0 +1,42 @@
1
+ [workspace]
2
+ members = ["crates/codingest", "crates/codingest-cli", "crates/codingest-mcp", "crates/codingest-py"]
3
+ resolver = "2"
4
+
5
+ [workspace.package]
6
+ version = "0.1.5"
7
+ edition = "2021"
8
+ rust-version = "1.88"
9
+ license = "MIT"
10
+ repository = "https://github.com/kkollsga/codingest"
11
+
12
+ [workspace.dependencies]
13
+ base64 = "0.22"
14
+ sha2 = "0.11"
15
+ # kglite engine + MCP server — imported as crates.io libraries. The 0.15.5
16
+ # floor adds the two workspace controls codingest asked upstream for:
17
+ # `workspace.sandbox_root`, a real containment boundary that confines
18
+ # `set_root_dir` to a subtree instead of trusting the caller's path, and
19
+ # `workspace.adopt_client_roots`. Both land on top of the generic
20
+ # workspace-graph hooks, tri-state `durable` levels, typed
21
+ # constraint/conflict exceptions carrying a stable `.code`, and the
22
+ # deterministic Postcard persistence stack inherited from 0.14.5/0.15.0-0.15.3.
23
+ # 0.15.4 is deliberately skipped: it was WAL/durability and mapped-graph work
24
+ # we consume none of — codingest uses no `durable=` level, no `MappedGraph`,
25
+ # and no disk storage. Neither patch changes `kglite::api`, graph output,
26
+ # property encoding, or `.kgl` serialization.
27
+ # Keep both libraries aligned so the graph and server share one engine release.
28
+ kglite = { version = "0.15.5", default-features = false }
29
+ kglite-mcp-server = { version = "0.15.5", default-features = false }
30
+
31
+ # Mirror KGLite's release codegen so the standalone builder has
32
+ # performance parity with the in-tree component (see KGLite
33
+ # workspace Cargo.toml).
34
+ [profile.release]
35
+ lto = "thin"
36
+ codegen-units = 1
37
+ strip = "symbols"
38
+
39
+ [profile.profiling]
40
+ inherits = "release"
41
+ debug = "line-tables-only"
42
+ strip = "none"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codingest
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Programming Language :: Python :: 3
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3.14
12
12
  Classifier: Programming Language :: Rust
13
13
  Classifier: Topic :: Software Development
14
14
  Classifier: Topic :: Software Development :: Libraries
15
- Requires-Dist: kglite>=0.14.5,<0.15
15
+ Requires-Dist: kglite>=0.15.5,<0.16
16
16
  License-File: LICENSE
17
17
  Summary: Build fast, queryable code graphs for AI agents, MCP code review, and local or GitHub repository analysis.
18
18
  Keywords: ai-agents,mcp,code-review,knowledge-graph,code-analysis,github,tree-sitter,cypher
@@ -136,9 +136,16 @@ The bundled terminal command offers the same workflow:
136
136
  ```bash
137
137
  codingest build /path/to/repo
138
138
  codingest status --output /path/to/repo/.kglite/code-review.kgl
139
+ codingest query -g /path/to/repo/.kglite/code-review.kgl \
140
+ "MATCH (f:Function)-[:CALLS]->(g:Function) RETURN f.name, g.name LIMIT 20"
139
141
  kglite describe /path/to/repo/.kglite/code-review.kgl --connections
140
142
  ```
141
143
 
144
+ `codingest query` prints unbounded TSV to stdout (use `--format csv|json`, or
145
+ Cypher `LIMIT` to bound rows), warns on stderr when the graph has gone stale,
146
+ and with `--require-fresh` refuses a stale graph with exit code 3 — see
147
+ [docs/cli.md](docs/cli.md).
148
+
142
149
  ## Analyse an open-source repository
143
150
 
144
151
  ```python
@@ -180,7 +187,7 @@ codingest skill install # code-review Agent Skill
180
187
  ```
181
188
 
182
189
  The wheel bundles every grammar plus the `codingest` and `codingest-mcp`
183
- commands. KGLite ≥0.14.5 is installed automatically as the query/storage
190
+ commands. KGLite ≥0.15.5 is installed automatically as the query/storage
184
191
  engine; its MCP server and the transitive `mcp-methods` framework power the
185
192
  builder-aware Codingest server. Nothing else needs to be installed.
186
193
 
@@ -193,7 +200,7 @@ Python prerequisites.
193
200
  ```toml
194
201
  [dependencies]
195
202
  codingest = "0.1"
196
- kglite = "0.14.5"
203
+ kglite = "0.15.5"
197
204
  ```
198
205
 
199
206
  ```rust
@@ -237,23 +244,35 @@ cargo test --workspace
237
244
  cargo test -p codingest --test parity
238
245
  ```
239
246
 
240
- ### The golden-digest oracle
247
+ ### The golden-digest oracle (also the determinism gate)
241
248
 
242
249
  KGLite deleted its in-tree `code_tree` builder on 2026-07-16, so the old
243
250
  two-builder parity sweep is gone. The authority it enforced was **frozen** while
244
251
  the builders were still verified identical, into per-corpus SHA-256 digests
245
252
  under `crates/codingest/tests/goldens/` (committed fixtures — no network). The
246
- `golden_parity` test builds each corpus with only the codingest 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:
253
+ `golden_parity` test builds each corpus **three times** with only the codingest
254
+ builder, digests a canonical exhaustive graph rendering, and requires every
255
+ build to match every other build *and* the frozen golden. Repeating the build is
256
+ what makes this the determinism gate: hash iteration order is randomized per
257
+ `HashMap`, so an order-dependent builder fails it (the
258
+ `tests/corpus/dup_minified_assets` fixture reproduces the original DEFINES-edge
259
+ nondeterminism bug). Disagreement between builds is reported as NONDETERMINISM;
260
+ agreement between builds but not with the golden is reported as a behaviour
261
+ change. The multi-rev fixture is guarded instead by `rev_self_consistency`.
262
+ Regenerate goldens only for deliberate builder-behavior changes:
250
263
  `cargo test -p codingest --test parity -- --ignored capture_goldens` (details in
251
264
  `crates/codingest/tests/goldens/README.md`).
252
265
 
266
+ Everything the gate needs is committed to this repository, so it is hermetic and
267
+ runs in CI. It replaced a local-only `make` step that asserted an exact edge
268
+ count against a *sibling* checkout — a verdict this project did not control.
269
+ `make determinism-soak REPO=…` keeps the large-repo reproducer available as a
270
+ diagnostic.
271
+
253
272
  ## Dependency policy
254
273
 
255
274
  `kglite` and `kglite-mcp-server` use matching crates.io requirements with a
256
- 0.14.5 minimum and a shared lockfile. This keeps the builder, persistence
275
+ 0.15.5 minimum and a shared lockfile. This keeps the builder, persistence
257
276
  handoff, and embedded MCP server on one compatible engine patch line.
258
277
 
259
278
  ## Parity with the (now-removed) in-tree component
@@ -109,9 +109,16 @@ The bundled terminal command offers the same workflow:
109
109
  ```bash
110
110
  codingest build /path/to/repo
111
111
  codingest status --output /path/to/repo/.kglite/code-review.kgl
112
+ codingest query -g /path/to/repo/.kglite/code-review.kgl \
113
+ "MATCH (f:Function)-[:CALLS]->(g:Function) RETURN f.name, g.name LIMIT 20"
112
114
  kglite describe /path/to/repo/.kglite/code-review.kgl --connections
113
115
  ```
114
116
 
117
+ `codingest query` prints unbounded TSV to stdout (use `--format csv|json`, or
118
+ Cypher `LIMIT` to bound rows), warns on stderr when the graph has gone stale,
119
+ and with `--require-fresh` refuses a stale graph with exit code 3 — see
120
+ [docs/cli.md](docs/cli.md).
121
+
115
122
  ## Analyse an open-source repository
116
123
 
117
124
  ```python
@@ -153,7 +160,7 @@ codingest skill install # code-review Agent Skill
153
160
  ```
154
161
 
155
162
  The wheel bundles every grammar plus the `codingest` and `codingest-mcp`
156
- commands. KGLite ≥0.14.5 is installed automatically as the query/storage
163
+ commands. KGLite ≥0.15.5 is installed automatically as the query/storage
157
164
  engine; its MCP server and the transitive `mcp-methods` framework power the
158
165
  builder-aware Codingest server. Nothing else needs to be installed.
159
166
 
@@ -166,7 +173,7 @@ Python prerequisites.
166
173
  ```toml
167
174
  [dependencies]
168
175
  codingest = "0.1"
169
- kglite = "0.14.5"
176
+ kglite = "0.15.5"
170
177
  ```
171
178
 
172
179
  ```rust
@@ -210,23 +217,35 @@ cargo test --workspace
210
217
  cargo test -p codingest --test parity
211
218
  ```
212
219
 
213
- ### The golden-digest oracle
220
+ ### The golden-digest oracle (also the determinism gate)
214
221
 
215
222
  KGLite deleted its in-tree `code_tree` builder on 2026-07-16, so the old
216
223
  two-builder parity sweep is gone. The authority it enforced was **frozen** while
217
224
  the builders were still verified identical, into per-corpus SHA-256 digests
218
225
  under `crates/codingest/tests/goldens/` (committed fixtures — no network). The
219
- `golden_parity` test builds each corpus with only the codingest 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:
226
+ `golden_parity` test builds each corpus **three times** with only the codingest
227
+ builder, digests a canonical exhaustive graph rendering, and requires every
228
+ build to match every other build *and* the frozen golden. Repeating the build is
229
+ what makes this the determinism gate: hash iteration order is randomized per
230
+ `HashMap`, so an order-dependent builder fails it (the
231
+ `tests/corpus/dup_minified_assets` fixture reproduces the original DEFINES-edge
232
+ nondeterminism bug). Disagreement between builds is reported as NONDETERMINISM;
233
+ agreement between builds but not with the golden is reported as a behaviour
234
+ change. The multi-rev fixture is guarded instead by `rev_self_consistency`.
235
+ Regenerate goldens only for deliberate builder-behavior changes:
223
236
  `cargo test -p codingest --test parity -- --ignored capture_goldens` (details in
224
237
  `crates/codingest/tests/goldens/README.md`).
225
238
 
239
+ Everything the gate needs is committed to this repository, so it is hermetic and
240
+ runs in CI. It replaced a local-only `make` step that asserted an exact edge
241
+ count against a *sibling* checkout — a verdict this project did not control.
242
+ `make determinism-soak REPO=…` keeps the large-repo reproducer available as a
243
+ diagnostic.
244
+
226
245
  ## Dependency policy
227
246
 
228
247
  `kglite` and `kglite-mcp-server` use matching crates.io requirements with a
229
- 0.14.5 minimum and a shared lockfile. This keeps the builder, persistence
248
+ 0.15.5 minimum and a shared lockfile. This keeps the builder, persistence
230
249
  handoff, and embedded MCP server on one compatible engine patch line.
231
250
 
232
251
  ## Parity with the (now-removed) in-tree component
@@ -9,8 +9,8 @@ description = "codingest — standalone extraction of KGLite's code-tree compone
9
9
  keywords = ["knowledge-graph", "code-analysis", "tree-sitter", "cypher", "kglite"]
10
10
  categories = ["development-tools", "parser-implementations"]
11
11
 
12
- # Requires the kglite 0.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.5).
14
14
 
15
15
  [lib]
16
16
  name = "codingest"
@@ -16,16 +16,41 @@
16
16
  //! MISMATCH is a query-result determinism regression, and the A/B delta is
17
17
  //! run-to-run timing variance. The release gate fails on any MISMATCH.
18
18
  //!
19
+ //! # The corpus is defined by the harness, not by the working directory
20
+ //!
21
+ //! A benchmark number is comparable to another only if both were measured on
22
+ //! the same input. The builder walks whatever is on disk: it skips dot-dirs,
23
+ //! `target/`, `node_modules/`, `venv/` and `__pycache__/` by name, but it has
24
+ //! no notion of `.gitignore`, so a repository's *untracked* working state
25
+ //! (scratch folders, local notes, generated markdown) is ingested — notably
26
+ //! through the docs pass. Benchmarking a working tree therefore measures a
27
+ //! corpus nobody else can reconstruct. Measured on this workspace: a clean
28
+ //! `git worktree` of a commit scored 1,115 nodes / 3,692 edges while the
29
+ //! working tree at that same commit scored 1,170 / 3,759 — a ~5% swing with no
30
+ //! builder or engine involvement, which silently invalidated published numbers.
31
+ //!
32
+ //! So this harness defines its own corpus. By default it materializes the
33
+ //! target's **git-tracked** files into a temporary directory and builds that:
34
+ //! the input is then a function of the committed content plus any uncommitted
35
+ //! edits to tracked files, reproducible on any machine at the same revision.
36
+ //! Every run prints `corpus_sha256`, so two numbers can be *checked* for
37
+ //! comparability rather than assumed comparable. `--include-untracked` restores
38
+ //! the old build-the-directory-as-is behaviour for a one-off measurement of a
39
+ //! non-git tree, and prints a NOT-REPRODUCIBLE banner.
40
+ //!
19
41
  //! Usage:
20
42
  //! cargo run -p codingest --bin codingest_bench --release -- <path>
21
43
  //! cargo run -p codingest --bin codingest_bench --release -- <path> --json
44
+ //! cargo run -p codingest --bin codingest_bench --release -- <path> --include-untracked
22
45
 
23
46
  use kglite::api::session::{execute_read, ExecuteOptions, ExecuteOutcome};
24
47
  use kglite::api::{DirGraph, GraphRead, Value};
48
+ use sha2::{Digest, Sha256};
25
49
  use std::collections::hash_map::DefaultHasher;
26
50
  use std::collections::HashMap;
27
51
  use std::hash::{Hash, Hasher};
28
- use std::path::Path;
52
+ use std::path::{Path, PathBuf};
53
+ use std::process::Command;
29
54
  use std::time::Instant;
30
55
 
31
56
  const WARMUP: usize = 3;
@@ -92,6 +117,153 @@ fn min(v: &[f64]) -> f64 {
92
117
  v.iter().cloned().fold(f64::INFINITY, f64::min)
93
118
  }
94
119
 
120
+ // ── corpus selection ─────────────────────────────────────────────────────
121
+
122
+ /// How the benchmark input was assembled. See the module docs.
123
+ #[derive(Clone, Copy, PartialEq, Eq)]
124
+ enum CorpusMode {
125
+ /// Git-tracked files of the target, copied into a tempdir. Reproducible.
126
+ TrackedOnly,
127
+ /// The target directory exactly as it sits on disk. Not reproducible if
128
+ /// the tree carries untracked or git-ignored content.
129
+ WorkingTree,
130
+ }
131
+
132
+ impl CorpusMode {
133
+ fn as_str(self) -> &'static str {
134
+ match self {
135
+ CorpusMode::TrackedOnly => "tracked-only",
136
+ CorpusMode::WorkingTree => "working-tree",
137
+ }
138
+ }
139
+ }
140
+
141
+ /// The resolved benchmark input: the directory to build, plus the identity of
142
+ /// the content in it.
143
+ struct Corpus {
144
+ build_dir: PathBuf,
145
+ mode: CorpusMode,
146
+ /// Why we ended up in `mode` — printed so the choice is never silent.
147
+ reason: String,
148
+ files: usize,
149
+ bytes: u64,
150
+ /// `None` in `WorkingTree` mode: the ingested set is not enumerable
151
+ /// without duplicating the builder's walk, and an approximate identity is
152
+ /// worse than an explicit absence.
153
+ sha256: Option<String>,
154
+ /// Owns the tempdir for the process lifetime; dropping it deletes the copy.
155
+ _tmp: Option<tempfile::TempDir>,
156
+ }
157
+
158
+ fn hex(bytes: &[u8]) -> String {
159
+ bytes.iter().map(|b| format!("{b:02x}")).collect()
160
+ }
161
+
162
+ /// Git-tracked paths under `target`, relative to it. `None` when `target` is
163
+ /// not inside a git work tree (or git is unavailable).
164
+ fn git_tracked_files(target: &Path) -> Option<Vec<String>> {
165
+ let out = Command::new("git")
166
+ .arg("-C")
167
+ .arg(target)
168
+ .args(["ls-files", "-z", "--cached"])
169
+ .output()
170
+ .ok()?;
171
+ if !out.status.success() {
172
+ return None;
173
+ }
174
+ let stdout = String::from_utf8(out.stdout).ok()?;
175
+ Some(
176
+ stdout
177
+ .split('\0')
178
+ .filter(|s| !s.is_empty())
179
+ .map(str::to_string)
180
+ .collect(),
181
+ )
182
+ }
183
+
184
+ /// Copy `rels` (relative to `target`) into a fresh tempdir and return the
185
+ /// corpus, including a content digest over the sorted `(path, sha256(bytes))`
186
+ /// pairs. Two runs with the same digest measured the same input.
187
+ fn materialize_tracked(target: &Path, mut rels: Vec<String>) -> std::io::Result<Corpus> {
188
+ rels.sort();
189
+ let tmp = tempfile::Builder::new()
190
+ .prefix("codingest-bench-corpus-")
191
+ .tempdir()?;
192
+ let root = tmp.path().to_path_buf();
193
+
194
+ let mut manifest = Sha256::new();
195
+ let mut files = 0usize;
196
+ let mut bytes = 0u64;
197
+
198
+ for rel in &rels {
199
+ let src = target.join(rel);
200
+ // `ls-files` lists index entries; a tracked-but-deleted file, or a
201
+ // gitlink (submodule) directory, has nothing to copy.
202
+ let Ok(data) = std::fs::read(&src) else {
203
+ continue;
204
+ };
205
+ let dst = root.join(rel);
206
+ if let Some(parent) = dst.parent() {
207
+ std::fs::create_dir_all(parent)?;
208
+ }
209
+ std::fs::write(&dst, &data)?;
210
+
211
+ let mut file_hash = Sha256::new();
212
+ file_hash.update(&data);
213
+ manifest.update(rel.as_bytes());
214
+ manifest.update(b"\0");
215
+ manifest.update(hex(&file_hash.finalize()).as_bytes());
216
+ manifest.update(b"\n");
217
+
218
+ files += 1;
219
+ bytes += data.len() as u64;
220
+ }
221
+
222
+ Ok(Corpus {
223
+ build_dir: root,
224
+ mode: CorpusMode::TrackedOnly,
225
+ reason: format!("git-tracked content of {}", target.display()),
226
+ files,
227
+ bytes,
228
+ sha256: Some(hex(&manifest.finalize())),
229
+ _tmp: Some(tmp),
230
+ })
231
+ }
232
+
233
+ /// Resolve the benchmark input for `target`.
234
+ ///
235
+ /// Default is `TrackedOnly`; it degrades to `WorkingTree` only when the target
236
+ /// is not in a git work tree, and says so. `--include-untracked` forces
237
+ /// `WorkingTree`.
238
+ fn resolve_corpus(target: &Path, include_untracked: bool) -> Corpus {
239
+ let working_tree = |reason: String| Corpus {
240
+ build_dir: target.to_path_buf(),
241
+ mode: CorpusMode::WorkingTree,
242
+ reason,
243
+ files: 0,
244
+ bytes: 0,
245
+ sha256: None,
246
+ _tmp: None,
247
+ };
248
+
249
+ if include_untracked {
250
+ return working_tree("--include-untracked requested".to_string());
251
+ }
252
+ let Some(rels) = git_tracked_files(target) else {
253
+ return working_tree(format!("{} is not in a git work tree", target.display()));
254
+ };
255
+ if rels.is_empty() {
256
+ return working_tree(format!("git tracks no files under {}", target.display()));
257
+ }
258
+ match materialize_tracked(target, rels) {
259
+ Ok(corpus) => corpus,
260
+ Err(e) => {
261
+ eprintln!("warning: could not materialize the tracked-only corpus: {e}");
262
+ working_tree(format!("tracked-only materialization failed: {e}"))
263
+ }
264
+ }
265
+ }
266
+
95
267
  struct QueryResult {
96
268
  name: String,
97
269
  rows: usize,
@@ -114,12 +286,30 @@ fn main() {
114
286
  };
115
287
  let rest: Vec<String> = args.collect();
116
288
  let json = rest.iter().any(|a| a == "--json");
289
+ let include_untracked = rest.iter().any(|a| a == "--include-untracked");
290
+ // Reject unknown flags rather than silently ignoring them: a typo'd
291
+ // `--include-untraked` would otherwise change nothing and be reported as a
292
+ // measurement taken under a mode it never ran in.
293
+ if let Some(bad) = rest
294
+ .iter()
295
+ .find(|a| !matches!(a.as_str(), "--json" | "--include-untracked"))
296
+ {
297
+ eprintln!("unknown argument `{bad}`");
298
+ eprintln!("usage: codingest_bench <path> [--json] [--include-untracked]");
299
+ std::process::exit(2);
300
+ }
117
301
  let target = Path::new(&path);
118
302
 
303
+ // Resolve the corpus BEFORE any timing: by default this copies the
304
+ // target's git-tracked files into a tempdir so the measured input is
305
+ // reproducible from the revision alone (see the module docs).
306
+ let corpus = resolve_corpus(target, include_untracked);
307
+ let build_dir = corpus.build_dir.as_path();
308
+
119
309
  // Two independent codingest builds with identical arguments:
120
310
  // verbose=false, include_tests=false, save_to=None, max_loc=None, docs=true.
121
311
  let t = Instant::now();
122
- let graph_a = codingest::builder::run_with_options(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}");