codingest 0.2.18__tar.gz → 0.2.20__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 (110) hide show
  1. {codingest-0.2.18 → codingest-0.2.20}/Cargo.lock +12 -10
  2. {codingest-0.2.18 → codingest-0.2.20}/Cargo.toml +12 -4
  3. {codingest-0.2.18 → codingest-0.2.20}/PKG-INFO +5 -5
  4. {codingest-0.2.18 → codingest-0.2.20}/README.md +3 -3
  5. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/Cargo.toml +1 -1
  6. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/builder/other_edges.rs +7 -5
  7. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/rev.rs +132 -18
  8. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest-cli/Cargo.toml +1 -1
  9. codingest-0.2.20/crates/codingest-cli/skills/codingest-code-review/SKILL.md +166 -0
  10. codingest-0.2.20/crates/codingest-cli/skills/codingest-code-review/references/mcp-upgrade.md +88 -0
  11. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest-cli/skills/codingest-code-review/references/public-repositories.md +10 -0
  12. codingest-0.2.20/crates/codingest-cli/skills/codingest-code-review/references/queries.md +186 -0
  13. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest-cli/src/query.rs +62 -6
  14. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest-cli/src/skill.rs +57 -33
  15. codingest-0.2.20/crates/codingest-cli/tests/skill_recipes.rs +754 -0
  16. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest-mcp/Cargo.toml +5 -1
  17. codingest-0.2.20/crates/codingest-mcp/tests/response_contract.rs +800 -0
  18. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest-py/Cargo.toml +3 -3
  19. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest-py/src/lib.rs +1 -1
  20. {codingest-0.2.18 → codingest-0.2.20}/pyproject.toml +2 -2
  21. codingest-0.2.18/crates/codingest-cli/skills/codingest-code-review/SKILL.md +0 -95
  22. codingest-0.2.18/crates/codingest-cli/skills/codingest-code-review/references/mcp-upgrade.md +0 -18
  23. codingest-0.2.18/crates/codingest-cli/skills/codingest-code-review/references/queries.md +0 -85
  24. {codingest-0.2.18 → codingest-0.2.20}/LICENSE +0 -0
  25. {codingest-0.2.18 → codingest-0.2.20}/codingest/__init__.py +0 -0
  26. {codingest-0.2.18 → codingest-0.2.20}/codingest/__init__.pyi +0 -0
  27. {codingest-0.2.18 → codingest-0.2.20}/codingest/cli.py +0 -0
  28. {codingest-0.2.18 → codingest-0.2.20}/codingest/mcp_server.py +0 -0
  29. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/bin/codingest_bench.rs +0 -0
  30. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/bin/codingest_stats.rs +0 -0
  31. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/builder/call_edges.rs +0 -0
  32. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/builder/js_workspace.rs +0 -0
  33. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/builder/load/edge_frames.rs +0 -0
  34. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/builder/load/entity_frames.rs +0 -0
  35. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/builder/load.rs +0 -0
  36. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/builder/mod.rs +0 -0
  37. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/builder/routes/django.rs +0 -0
  38. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/builder/routes/fastapi.rs +0 -0
  39. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/builder/routes/flask.rs +0 -0
  40. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/builder/routes/mod.rs +0 -0
  41. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/builder/semantic_edges.rs +0 -0
  42. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/builder/type_edges.rs +0 -0
  43. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/cross_lang.rs +0 -0
  44. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/docs/mod.rs +0 -0
  45. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/docs/rst.rs +0 -0
  46. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/lib.rs +0 -0
  47. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/manifest/mod.rs +0 -0
  48. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/models.rs +0 -0
  49. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/parsers/agc.rs +0 -0
  50. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/parsers/cpp.rs +0 -0
  51. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/parsers/csharp.rs +0 -0
  52. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/parsers/css.rs +0 -0
  53. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/parsers/dart.rs +0 -0
  54. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/parsers/go.rs +0 -0
  55. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/parsers/html.rs +0 -0
  56. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/parsers/java.rs +0 -0
  57. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/parsers/julia.rs +0 -0
  58. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/parsers/mod.rs +0 -0
  59. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/parsers/php.rs +0 -0
  60. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/parsers/python.rs +0 -0
  61. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/parsers/r.rs +0 -0
  62. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/parsers/registry.rs +0 -0
  63. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/parsers/rust_lang.rs +0 -0
  64. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/parsers/shared.rs +0 -0
  65. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/parsers/swift.rs +0 -0
  66. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/parsers/typescript.rs +0 -0
  67. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/src/repo.rs +0 -0
  68. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/agc_apollo.rs +0 -0
  69. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/README.md +0 -0
  70. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/agc_basic.sha256 +0 -0
  71. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/cpp_extern_c.sha256 +0 -0
  72. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/cpp_include.sha256 +0 -0
  73. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/cross_ts_py.sha256 +0 -0
  74. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/csharp_using_alias.sha256 +0 -0
  75. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/dart_import.sha256 +0 -0
  76. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/dart_part_of.sha256 +0 -0
  77. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/docs_ext_collide.sha256 +0 -0
  78. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/docs_mdx.sha256 +0 -0
  79. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/dup_minified_assets.sha256 +0 -0
  80. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/go_interface.sha256 +0 -0
  81. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/html_js_lang_group.sha256 +0 -0
  82. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/java_javadoc.sha256 +0 -0
  83. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/julia_basic.sha256 +0 -0
  84. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/php_group_use.sha256 +0 -0
  85. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/py_basic.sha256 +0 -0
  86. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/py_import.sha256 +0 -0
  87. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/py_inheritance.sha256 +0 -0
  88. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/py_nested_defs.sha256 +0 -0
  89. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/py_routes_dup.sha256 +0 -0
  90. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/py_src_layout.sha256 +0 -0
  91. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/r_basic.sha256 +0 -0
  92. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/rust_import.sha256 +0 -0
  93. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/rust_inline_mod.sha256 +0 -0
  94. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/rust_xfile.sha256 +0 -0
  95. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/swift_basic.sha256 +0 -0
  96. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/ts_callback.sha256 +0 -0
  97. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/ts_closure_scope.sha256 +0 -0
  98. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/ts_hof_binding.sha256 +0 -0
  99. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/ts_monorepo.sha256 +0 -0
  100. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/goldens/web_served_root.sha256 +0 -0
  101. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/grammar_kinds.rs +0 -0
  102. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/parity.rs +0 -0
  103. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest/tests/traversal_semantics.rs +0 -0
  104. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest-cli/src/code_tree_cli.rs +0 -0
  105. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest-cli/src/lib.rs +0 -0
  106. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest-cli/src/main.rs +0 -0
  107. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest-cli/src/skill_assets.rs +0 -0
  108. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest-cli/tests/exit_codes.rs +0 -0
  109. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest-mcp/src/lib.rs +0 -0
  110. {codingest-0.2.18 → codingest-0.2.20}/crates/codingest-mcp/src/main.rs +0 -0
@@ -302,7 +302,7 @@ dependencies = [
302
302
 
303
303
  [[package]]
304
304
  name = "codingest"
305
- version = "0.2.18"
305
+ version = "0.2.20"
306
306
  dependencies = [
307
307
  "aho-corasick",
308
308
  "base64 0.22.1",
@@ -338,7 +338,7 @@ dependencies = [
338
338
 
339
339
  [[package]]
340
340
  name = "codingest-cli"
341
- version = "0.2.18"
341
+ version = "0.2.20"
342
342
  dependencies = [
343
343
  "anyhow",
344
344
  "clap",
@@ -350,16 +350,18 @@ dependencies = [
350
350
 
351
351
  [[package]]
352
352
  name = "codingest-mcp"
353
- version = "0.2.18"
353
+ version = "0.2.20"
354
354
  dependencies = [
355
355
  "anyhow",
356
356
  "codingest",
357
357
  "kglite-mcp-server",
358
+ "serde_json",
359
+ "tempfile",
358
360
  ]
359
361
 
360
362
  [[package]]
361
363
  name = "codingest-py"
362
- version = "0.2.18"
364
+ version = "0.2.20"
363
365
  dependencies = [
364
366
  "codingest",
365
367
  "codingest-cli",
@@ -1366,9 +1368,9 @@ dependencies = [
1366
1368
 
1367
1369
  [[package]]
1368
1370
  name = "kglite"
1369
- version = "0.17.3"
1371
+ version = "0.17.4"
1370
1372
  source = "registry+https://github.com/rust-lang/crates.io-index"
1371
- checksum = "c414d1f4ae2d7d0af3ce088b843fbc3622cac2c29467cc81bc2dc639006136e0"
1373
+ checksum = "ffc03d38445c638a848109cb8f9b22fca8a5e7fdbe571b686b9ae11c671fb3b7"
1372
1374
  dependencies = [
1373
1375
  "bzip2",
1374
1376
  "chrono",
@@ -1402,9 +1404,9 @@ dependencies = [
1402
1404
 
1403
1405
  [[package]]
1404
1406
  name = "kglite-mcp-server"
1405
- version = "0.17.3"
1407
+ version = "0.17.4"
1406
1408
  source = "registry+https://github.com/rust-lang/crates.io-index"
1407
- checksum = "764efb5d817e481d9c96302c185a71bceba6ed09a79b3c6ad2f131688c7384c9"
1409
+ checksum = "6bce52afae0ad63db082643cf33ca89476eb70c02f6c84dc8f5e360f8b4b6bac"
1408
1410
  dependencies = [
1409
1411
  "anyhow",
1410
1412
  "bytes",
@@ -1508,9 +1510,9 @@ dependencies = [
1508
1510
 
1509
1511
  [[package]]
1510
1512
  name = "mcp-methods"
1511
- version = "0.4.8"
1513
+ version = "0.4.9"
1512
1514
  source = "registry+https://github.com/rust-lang/crates.io-index"
1513
- checksum = "bff1a460997aa714ab45ad88fc5dbfcbcaee4983e84b2512d2461a6d281c2386"
1515
+ checksum = "140792f5b2f41ef005ea26c180869d05afda7174a22d0aedec22708fadfa879f"
1514
1516
  dependencies = [
1515
1517
  "anyhow",
1516
1518
  "clap",
@@ -3,7 +3,7 @@ members = ["crates/codingest", "crates/codingest-cli", "crates/codingest-mcp", "
3
3
  resolver = "2"
4
4
 
5
5
  [workspace.package]
6
- version = "0.2.18"
6
+ version = "0.2.20"
7
7
  edition = "2021"
8
8
  rust-version = "1.88"
9
9
  license = "MIT"
@@ -12,7 +12,15 @@ repository = "https://github.com/kkollsga/codingest"
12
12
  [workspace.dependencies]
13
13
  base64 = "0.22"
14
14
  sha2 = "0.11"
15
- # kglite engine + MCP server — imported as crates.io libraries. The 0.17.3
15
+ # kglite engine + MCP server — imported as crates.io libraries. The 0.17.4
16
+ # floor rejects every unbound Cypher parameter before candidate selection, so
17
+ # an empty label scan, inline map, WHERE or subquery cannot hide a missing
18
+ # binding behind zero rows. Installing or clearing a declared schema now
19
+ # invalidates cached diagnostics, including on the Python graph returned by
20
+ # `codingest.build()`. The embedded MCP server gains bounded, navigable query
21
+ # responses backed by retained complete results; the standalone codingest CLI
22
+ # keeps complete JSON and CSV output.
23
+ # The preceding 0.17.3
16
24
  # floor spans the 0.17.2 and 0.17.3 query releases. Nothing in codingest's
17
25
  # builder source moved for either release; the changes reach users through
18
26
  # `codingest query` and the embedded `codingest-mcp` server:
@@ -499,8 +507,8 @@ sha2 = "0.11"
499
507
  # and no disk storage. Neither 0.15.7 nor 0.15.8 changes the `kglite::api` we
500
508
  # call, graph output, property encoding, or `.kgl` serialization.
501
509
  # Keep both libraries aligned so the graph and server share one engine release.
502
- kglite = { version = "0.17.3", default-features = false }
503
- kglite-mcp-server = { version = "0.17.3", default-features = false }
510
+ kglite = { version = "0.17.4", default-features = false }
511
+ kglite-mcp-server = { version = "0.17.4", default-features = false }
504
512
 
505
513
  # Dependency debuginfo capped at line tables (kglite coordination, 2026-08-31,
506
514
  # after unbounded debug trees filled the build-cache disk to zero): a
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codingest
3
- Version: 0.2.18
3
+ Version: 0.2.20
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.17.3,<0.18
15
+ Requires-Dist: kglite>=0.17.4,<0.18
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
@@ -192,7 +192,7 @@ codingest skill install # code-review Agent Skill
192
192
  ```
193
193
 
194
194
  The wheel bundles every grammar plus the `codingest` and `codingest-mcp`
195
- commands. KGLite ≥0.17.3 is installed automatically as the query/storage
195
+ commands. KGLite ≥0.17.4 is installed automatically as the query/storage
196
196
  engine; its MCP server and the transitive `mcp-methods` framework power the
197
197
  builder-aware Codingest server. Nothing else needs to be installed.
198
198
 
@@ -205,7 +205,7 @@ Python prerequisites.
205
205
  ```toml
206
206
  [dependencies]
207
207
  codingest = "0.2"
208
- kglite = "0.17.3"
208
+ kglite = "0.17.4"
209
209
  ```
210
210
 
211
211
  ```rust
@@ -277,7 +277,7 @@ diagnostic.
277
277
  ## Dependency policy
278
278
 
279
279
  `kglite` and `kglite-mcp-server` use matching crates.io requirements with a
280
- 0.17.3 minimum and a shared lockfile. This keeps the builder, persistence
280
+ 0.17.4 minimum and a shared lockfile. This keeps the builder, persistence
281
281
  handoff, and embedded MCP server on one compatible engine patch line.
282
282
 
283
283
  ## Parity with the (now-removed) in-tree component
@@ -165,7 +165,7 @@ codingest skill install # code-review Agent Skill
165
165
  ```
166
166
 
167
167
  The wheel bundles every grammar plus the `codingest` and `codingest-mcp`
168
- commands. KGLite ≥0.17.3 is installed automatically as the query/storage
168
+ commands. KGLite ≥0.17.4 is installed automatically as the query/storage
169
169
  engine; its MCP server and the transitive `mcp-methods` framework power the
170
170
  builder-aware Codingest server. Nothing else needs to be installed.
171
171
 
@@ -178,7 +178,7 @@ Python prerequisites.
178
178
  ```toml
179
179
  [dependencies]
180
180
  codingest = "0.2"
181
- kglite = "0.17.3"
181
+ kglite = "0.17.4"
182
182
  ```
183
183
 
184
184
  ```rust
@@ -250,7 +250,7 @@ diagnostic.
250
250
  ## Dependency policy
251
251
 
252
252
  `kglite` and `kglite-mcp-server` use matching crates.io requirements with a
253
- 0.17.3 minimum and a shared lockfile. This keeps the builder, persistence
253
+ 0.17.4 minimum and a shared lockfile. This keeps the builder, persistence
254
254
  handoff, and embedded MCP server on one compatible engine patch line.
255
255
 
256
256
  ## Parity with the (now-removed) in-tree component
@@ -10,7 +10,7 @@ keywords = ["knowledge-graph", "code-analysis", "tree-sitter", "cypher", "kglite
10
10
  categories = ["development-tools", "parser-implementations"]
11
11
 
12
12
  # Requires the kglite 0.15 engine API (`kglite::api::code_entities`); the
13
- # workspace pins the current engine floor (0.17.3).
13
+ # workspace pins the current engine floor (0.17.4).
14
14
 
15
15
  [lib]
16
16
  name = "codingest"
@@ -45,12 +45,14 @@ pub struct UsesTypeEdge {
45
45
  pub target_node_type: &'static str,
46
46
  /// Where in the function signature this type appears. Aggregates across
47
47
  /// all sites in the same function — a type used as both a parameter and
48
- /// a return value yields `"both"`. Values: `"parameter"` | `"return"` |
49
- /// `"both"` | `"signature"`. `"signature"` is the fallback when the
50
- /// parser couldn't extract structured parameters (typically the AC
51
- /// scanner found the type embedded in the signature string).
48
+ /// a return value yields `"both"`. Values: `"parameter"` | `"receiver"` |
49
+ /// `"return"` | `"both"` | `"signature"`. `"receiver"` is the implicit
50
+ /// input of a method, kept distinct from an explicit parameter;
51
+ /// `"signature"` is the fallback when the parser couldn't extract
52
+ /// structured parameters (typically the AC scanner found the type embedded
53
+ /// in the signature string).
52
54
  ///
53
- /// Cypher: `WHERE r.position IN ['parameter','both']` for "consumes T",
55
+ /// Cypher: `WHERE r.position IN ['parameter','receiver','both']` for "consumes T",
54
56
  /// `WHERE r.position IN ['return','both']` for "produces T".
55
57
  pub position: &'static str,
56
58
  }
@@ -221,9 +221,28 @@ fn verify_rev(repo_root: &Path, rev: &str) -> Result<String, String> {
221
221
  Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
222
222
  }
223
223
 
224
+ fn spawn_archive_extractor(
225
+ archive_stdout: impl Into<Stdio>,
226
+ dest: &Path,
227
+ ) -> Result<std::process::Child, String> {
228
+ Command::new("tar")
229
+ // Both bsdtar and GNU tar otherwise stop at the first end-of-archive
230
+ // blocks. Git can still be writing record padding at that point and
231
+ // receive SIGPIPE even though extraction completed successfully.
232
+ .arg("--ignore-zeros")
233
+ .arg("-x")
234
+ .arg("-C")
235
+ .arg(dest)
236
+ .stdin(archive_stdout)
237
+ .stderr(Stdio::piped())
238
+ .spawn()
239
+ .map_err(|e| format!("tar failed to start: {}", e))
240
+ }
241
+
224
242
  /// Stream `git archive --format=tar <rev>` into `tar -x -C <dest>`. Both run
225
243
  /// concurrently over a pipe (bounded memory — the tar is never buffered
226
- /// whole); list args, no shell.
244
+ /// whole); list args, no shell. The extractor reads through tar end markers to
245
+ /// EOF so Git can finish writing record padding without receiving SIGPIPE.
227
246
  fn archive_into(repo_root: &Path, rev: &str, dest: &Path) -> Result<(), String> {
228
247
  let mut archive = Command::new("git")
229
248
  .arg("-C")
@@ -240,14 +259,7 @@ fn archive_into(repo_root: &Path, rev: &str, dest: &Path) -> Result<(), String>
240
259
  .take()
241
260
  .ok_or_else(|| "git archive produced no stdout".to_string())?;
242
261
 
243
- let tar = Command::new("tar")
244
- .arg("-x")
245
- .arg("-C")
246
- .arg(dest)
247
- .stdin(Stdio::from(archive_stdout))
248
- .stderr(Stdio::piped())
249
- .spawn()
250
- .map_err(|e| format!("tar failed to start: {}", e))?;
262
+ let tar = spawn_archive_extractor(Stdio::from(archive_stdout), dest)?;
251
263
 
252
264
  // tar reads the pipe as git writes; wait for the reader first, then the
253
265
  // writer, so neither blocks on a full pipe.
@@ -258,16 +270,14 @@ fn archive_into(repo_root: &Path, rev: &str, dest: &Path) -> Result<(), String>
258
270
  .wait_with_output()
259
271
  .map_err(|e| format!("git archive wait failed: {}", e))?;
260
272
 
261
- if !archive_out.status.success() {
273
+ if !archive_out.status.success() || !tar_out.status.success() {
262
274
  return Err(format!(
263
- "git archive failed: {}",
264
- String::from_utf8_lossy(&archive_out.stderr).trim()
265
- ));
266
- }
267
- if !tar_out.status.success() {
268
- return Err(format!(
269
- "tar extract failed: {}",
270
- String::from_utf8_lossy(&tar_out.stderr).trim()
275
+ "archive pipeline failed: git archive status {}; stderr {:?}; \
276
+ tar extract status {}; stderr {:?}",
277
+ archive_out.status,
278
+ String::from_utf8_lossy(&archive_out.stderr).trim(),
279
+ tar_out.status,
280
+ String::from_utf8_lossy(&tar_out.stderr).trim(),
271
281
  ));
272
282
  }
273
283
  Ok(())
@@ -867,6 +877,110 @@ mod tests {
867
877
  assert!(error.contains("outside repository root"), "{error}");
868
878
  }
869
879
 
880
+ #[test]
881
+ fn archive_pipeline_reports_both_children_for_invalid_revision() {
882
+ let (repo, _) = commit_files(&[("app.py", "def compute():\n return 1\n")]);
883
+ let destination = tempfile::tempdir().unwrap();
884
+ let error = archive_into(
885
+ repo.path(),
886
+ "revision-that-does-not-exist",
887
+ destination.path(),
888
+ )
889
+ .expect_err("invalid revision must fail");
890
+
891
+ assert!(error.contains("git archive status"), "{error}");
892
+ assert!(error.contains("tar extract status"), "{error}");
893
+ assert_eq!(error.matches("stderr").count(), 2, "{error}");
894
+ assert!(error.contains("revision-that-does-not-exist"), "{error}");
895
+ assert!(
896
+ !error.contains("git archive status exit status: 0"),
897
+ "{error}"
898
+ );
899
+ }
900
+
901
+ #[test]
902
+ fn archive_pipeline_reports_both_children_after_early_consumer_exit() {
903
+ let body = "x".repeat(4 * 1024 * 1024);
904
+ let (repo, revision) = commit_files(&[("large.bin", &body)]);
905
+ let missing_destination = repo.path().join("missing").join("destination");
906
+ let error = archive_into(repo.path(), &revision, &missing_destination)
907
+ .expect_err("missing extraction destination must fail");
908
+
909
+ assert!(error.contains("git archive status"), "{error}");
910
+ assert!(error.contains("tar extract status"), "{error}");
911
+ assert_eq!(error.matches("stderr").count(), 2, "{error}");
912
+ assert!(
913
+ error.contains(&missing_destination.to_string_lossy().to_string()),
914
+ "{error}"
915
+ );
916
+ assert!(
917
+ !error.contains("git archive status exit status: 0"),
918
+ "{error}"
919
+ );
920
+ assert!(
921
+ !error.contains("tar extract status exit status: 0"),
922
+ "{error}"
923
+ );
924
+ }
925
+
926
+ #[cfg(unix)]
927
+ #[test]
928
+ fn archive_extractor_drains_padding_after_end_marker() {
929
+ let expected = "def compute():\n return 1\n";
930
+ let (repo, revision) = commit_files(&[("app.py", expected)]);
931
+ let archive_path = repo.path().join("fixture.tar");
932
+ let archive_file = std::fs::File::create(&archive_path).unwrap();
933
+ let archived = Command::new("git")
934
+ .arg("-C")
935
+ .arg(repo.path())
936
+ .args(["archive", "--format=tar"])
937
+ .arg(&revision)
938
+ .stdout(archive_file)
939
+ .stderr(Stdio::piped())
940
+ .output()
941
+ .unwrap();
942
+ assert!(
943
+ archived.status.success(),
944
+ "git archive fixture failed: {}",
945
+ String::from_utf8_lossy(&archived.stderr)
946
+ );
947
+
948
+ let destination = tempfile::tempdir().unwrap();
949
+ let mut producer = Command::new("sh")
950
+ .args([
951
+ "-c",
952
+ "cat \"$1\"; sleep 0.25; dd if=/dev/zero bs=1048576 count=4 2>/dev/null",
953
+ "padding-producer",
954
+ ])
955
+ .arg(&archive_path)
956
+ .stdout(Stdio::piped())
957
+ .stderr(Stdio::piped())
958
+ .spawn()
959
+ .unwrap();
960
+ let producer_stdout = producer.stdout.take().unwrap();
961
+ let extractor = spawn_archive_extractor(Stdio::from(producer_stdout), destination.path())
962
+ .expect("spawn archive extractor");
963
+ let extracted = extractor.wait_with_output().unwrap();
964
+ let produced = producer.wait_with_output().unwrap();
965
+
966
+ assert!(
967
+ extracted.status.success(),
968
+ "tar status {}; stderr {:?}",
969
+ extracted.status,
970
+ String::from_utf8_lossy(&extracted.stderr).trim()
971
+ );
972
+ assert!(
973
+ produced.status.success(),
974
+ "producer status {}; stderr {:?}",
975
+ produced.status,
976
+ String::from_utf8_lossy(&produced.stderr).trim()
977
+ );
978
+ assert_eq!(
979
+ std::fs::read_to_string(destination.path().join("app.py")).unwrap(),
980
+ expected
981
+ );
982
+ }
983
+
870
984
  fn build(dir: &Path, revs: &[String]) -> Arc<DirGraph> {
871
985
  build_code_tree_revs(dir, revs, Some(dir), false, false, None, None, false)
872
986
  .expect("build_code_tree_revs")
@@ -14,7 +14,7 @@ name = "codingest"
14
14
  path = "src/main.rs"
15
15
 
16
16
  [dependencies]
17
- codingest = { version = "0.2.18", path = "../codingest" }
17
+ codingest = { version = "0.2.20", path = "../codingest" }
18
18
  kglite = { workspace = true }
19
19
  anyhow = "1.0.104"
20
20
  clap = { version = "4.6.4", features = ["derive"] }
@@ -0,0 +1,166 @@
1
+ ---
2
+ name: codingest-code-review
3
+ description: Use when reviewing a code change or answering structural questions about a codebase, including definitions, callers, dependencies, routes, affected tests, and history across git revisions. Builds a local Cypher-queryable code graph, uses it alongside the diff and literal search, and verifies every finding against exact source lines.
4
+ ---
5
+
6
+ # Codingest code review
7
+
8
+ Use Codingest for structural evidence during review. The graph complements the
9
+ git diff, source reading, and literal-text search; it does not replace them.
10
+
11
+ ## Review workflow
12
+
13
+ > **Prerequisites:** the `codingest` CLI must be on PATH. `pip install
14
+ > codingest` also provides the builder-aware `codingest-mcp` server and a
15
+ > compatible graph engine. Rust-only environments can alternatively use Cargo.
16
+ > Examples that invoke the external `kglite` CLI require a compatible
17
+ > `kglite>=0.17.4,<0.18`; verify `kglite --version` before using its
18
+ > agent-response features.
19
+
20
+ Inspect the diff and repository guidance first. Identify changed symbols and
21
+ the base/head revisions, then build or refresh the graph without executing
22
+ repository code:
23
+
24
+ ```bash
25
+ codingest build . --output .kglite/code-review.kgl --format json
26
+ ```
27
+
28
+ For a committed comparison, use one graph spanning both revisions:
29
+
30
+ ```bash
31
+ codingest build . --revs '<base>' '<head>' \
32
+ --output .kglite/code-review.kgl --format json
33
+ ```
34
+
35
+ Before reusing an artifact, check freshness:
36
+
37
+ ```bash
38
+ codingest status --output .kglite/code-review.kgl --format json
39
+ ```
40
+
41
+ ## Work from the question
42
+
43
+ Use this loop only as far as the question requires. It has no required map set
44
+ or fixed query count.
45
+
46
+ 1. **Establish target and coverage.** Record the exact repository, revision,
47
+ symbol, path, or subsystem in scope. For a completeness or absence claim,
48
+ identify the relevant groups before retrieving their rows and record every
49
+ query, executor, or presentation boundary.
50
+ 2. **Retrieve a relevant relation.** Ask the smallest structural question that
51
+ can change the answer: callers, callees, implementers, imports, dependencies,
52
+ routes, tests, or revision identity.
53
+ 3. **Inspect selected source.** Open the implicated definitions and call sites
54
+ at exact lines. Graph edges identify candidates; source and executed tests
55
+ establish behavior.
56
+ 4. **Retain an evidence map.** Keep each working claim with its graph revision,
57
+ exact query or reference, concrete example or input state, what an assertion
58
+ actually guarantees, and any unresolved question. These fields can be a
59
+ compact note rather than separate documents.
60
+ 5. **Resolve consequential uncertainty.** Expand only when a contradiction,
61
+ missing precondition, or correctness risk could change the answer. Stop when
62
+ the requested conclusions have direct support.
63
+
64
+ Choose the narrowest route for the uncertainty in front of you; these are
65
+ alternatives, not a compulsory sequence. Reuse tool signatures already known
66
+ in the current session. If a needed tool is not visible, discover that tool
67
+ rather than loading a broad catalog.
68
+
69
+ - **Known symbol or location:** read it directly. With MCP, use
70
+ `read_code_source` for a qualified name or `read_source` for a path, applying
71
+ `start_line`, `end_line`, and `max_chars` when a full body or file is not
72
+ needed.
73
+ - **Exact structural question:** use `cypher_query`, or `kglite query` when the
74
+ compatible external CLI is installed. Reuse a schema already observed for
75
+ the same unchanged graph. Otherwise inspect only the needed node or
76
+ connection shape with `graph_overview` or `kglite describe`; do not retrieve
77
+ the whole schema by habit.
78
+ - **Broad explanation or unfamiliar subsystem:** use bounded `explore`, starting
79
+ with a short topic, a few `max_entities`, shallow `max_depth`, and
80
+ `include_source: false` unless bodies are needed. Narrow to selected symbols
81
+ before reading source.
82
+ - **Literal text:** use grep/ripgrep for error strings, comments, and config
83
+ keys, then open only the relevant matches.
84
+
85
+ Every additional read should resolve a concrete uncertainty needed for the
86
+ answer. Project only useful Cypher properties, return a few relevant candidates,
87
+ and exclude unrelated fixtures, generated code, vendors, or examples when the
88
+ question does not cover them. Reuse evidence already seen unless its source or
89
+ active graph changed.
90
+
91
+ Treat response presentation separately from query coverage. When an MCP
92
+ response says the completed result was retained, follow its advertised action
93
+ to expand the needed value or page within the authorized read-only
94
+ investigation; this does not require separate human approval. Expansion cannot
95
+ recover rows excluded by a Cypher `LIMIT`, an executor row limit, or tool-level
96
+ omission. Broaden or regroup the query when those boundaries matter. Narrow a
97
+ source range when only a source read was clipped. See
98
+ [mcp-upgrade.md](references/mcp-upgrade.md) for the expansion procedure.
99
+
100
+ Macro-generated structure may not exist as source-level graph nodes, and
101
+ heuristic relationships are candidates rather than runtime proof. Inspect the
102
+ source or an executed test before turning either limitation into a behavioral
103
+ claim.
104
+
105
+ ## Verify the answer
106
+
107
+ Before reporting a finding or a consequential conclusion, use focused reads to
108
+ check:
109
+
110
+ - each required precondition holds at the reviewed revision;
111
+ - the stated input, state, or sequence reaches the claimed behavior;
112
+ - a returned value or error observed below the claimed API boundary is traced
113
+ through catches, fallbacks, retries, and active options to that boundary;
114
+ - one observed path is used as evidence for that case, not as a universal
115
+ outcome;
116
+ - each cited assertion runs on that input and guarantees what the answer says;
117
+ - every file and line reference still points to the supporting code; and
118
+ - completeness, absence, ownership, and reachability claims are limited to the
119
+ coverage actually established.
120
+
121
+ Label source-composed examples that were not executed. Leave unresolved
122
+ questions explicit rather than converting them into findings.
123
+
124
+ See [queries.md](references/queries.md) for query patterns,
125
+ [public-repositories.md](references/public-repositories.md) for safe public-repo
126
+ review, and [mcp-upgrade.md](references/mcp-upgrade.md) for the persistent MCP
127
+ workflow.
128
+
129
+ ## What counts as a finding
130
+
131
+ The workflow above verifies a finding against exact source lines. This is the
132
+ prior question: what is eligible to be a finding at all.
133
+
134
+ - **A finding names a concrete failure**: the input, state, or sequence, and the
135
+ wrong outcome it produces. A wrong result, a crash, data loss or corruption, a
136
+ broken contract with a caller or a persisted format, a security hole, a
137
+ *measured* performance regression, a check that cannot fail, or a claim the
138
+ code contradicts. **"No findings" is a valid review**, and a good one.
139
+ - **Design, structure, naming, "consider using X", and "this won't scale" are
140
+ not findings** — they are mis-staged. Their venue is planning, where "I would
141
+ have designed this differently" is invited and settled before code exists.
142
+ After a plan is approved, review measures the implementation against that plan
143
+ and against correctness, never against a design the reviewer would have
144
+ preferred. A design opinion formed while reading a diff is input to the *next*
145
+ plan.
146
+ - **A finding that cannot state its failure case is removed, not downgraded.**
147
+ Severity labels are how a preference gets laundered into a report: "Minor:
148
+ consider extracting this" is a preference wearing a label.
149
+ - **One narrow exception**: citing a rule the project declared *before* the diff
150
+ existed — a documented ceiling, a boundary rule, a checklist — naming both the
151
+ rule and the violating line. That is enforcement, not taste.
152
+ - **A review tool's effort or confidence level is orthogonal.** A higher level
153
+ buys more *speculative bugs*; it never buys permission to report preferences.
154
+ - **A graph edge showing coupling is a fact, not a defect.** Structural evidence
155
+ answers "what would this change reach"; it does not by itself establish that
156
+ anything is wrong.
157
+
158
+ ## Honesty rules
159
+
160
+ - Never invent labels, properties, connection types, response-control fields,
161
+ expansion-tool names, or retained-result IDs. Discover unfamiliar shapes and
162
+ controls; reuse them only while the graph and session remain unchanged.
163
+ - Treat unresolved or missing graph edges as absence of evidence, not proof.
164
+ - Quote paths and revisions passed through the shell.
165
+ - Never build, import, or execute code from a repository merely to review it.
166
+ - Use grep/ripgrep for exact tokens and the graph for relationships and impact.
@@ -0,0 +1,88 @@
1
+ # When to use the MCP server
2
+
3
+ The CLI skill is the low-friction path: no agent configuration, one process per
4
+ command, and a review artifact that can be rebuilt explicitly.
5
+
6
+ Upgrade to `codingest-mcp` when the work benefits from:
7
+
8
+ - a graph kept warm across many queries;
9
+ - watch mode and automatic refresh after file changes;
10
+ - typed tool input/output schemas rather than shell quoting;
11
+ - switching among several repository roots;
12
+ - cached public-repository lifecycle and GitHub/source tools; or
13
+ - long collaborative sessions where process startup becomes noticeable.
14
+
15
+ The local-code-review MCP workspace covers a changing checkout. The
16
+ open-source workspace covers cached public repositories and adds constrained
17
+ source and GitHub tooling. Codingest builds the code graph; both workflows use
18
+ KGLite's graph engine, query language, and read-tool surface.
19
+
20
+ ## Expand retained MCP evidence
21
+
22
+ A bounded presentation is a view of a completed retained result. It does not
23
+ change query semantics and expansion does not rerun the original tool. A
24
+ Cypher `LIMIT`, an executor row limit, or a tool-level omission is different:
25
+ excluded rows require another reviewed query.
26
+
27
+ Use the response itself as the protocol contract:
28
+
29
+ 1. Discover the query tool's response-control property and the expansion tool
30
+ through the current session's tool listing. Names may be changed to avoid a
31
+ collision, so do not assume `_response` or `expand_response`.
32
+ 2. When the result is bounded, copy its complete advertised
33
+ `next.selected_value` action. Keep the action's `name` and
34
+ `arguments.result_id` unchanged.
35
+ 3. Choose a target actually advertised by the response. Apply its patch by
36
+ copying `json_pointer`, `offset`, and `response` to the action arguments
37
+ named `path`, `offset`, and `response`. Do not infer paths or semantic groups
38
+ that are absent from the returned navigation data.
39
+ 4. Dispatch the patched action and use the returned value as evidence from the
40
+ original completed result. Follow an advertised page action when more of the
41
+ same selected value is required.
42
+
43
+ Within an already authorized read-only investigation, selecting an advertised
44
+ page, requesting a larger per-call budget, or requesting an advertised full
45
+ response needs no additional human approval. Copy the discovered control field
46
+ and supported shape into that individual call. Do not turn expansion into a
47
+ replay of the original query, especially when that tool may mutate state.
48
+
49
+ Retained MCP results are short-lived, held in memory, and scoped to the current
50
+ MCP session. Honor the retention and eviction facts in the response. If a
51
+ handle is unavailable, rerun only a read that remains authorized and whose
52
+ preconditions are still current.
53
+
54
+ ## External CLI agent output is optional
55
+
56
+ The examples below require an external `kglite` CLI satisfying
57
+ `>=0.17.4,<0.18`:
58
+
59
+ ```console
60
+ kglite --version
61
+ ```
62
+
63
+ The following source-composed example is exercised against a generated graph by
64
+ the packaged-consumer test:
65
+
66
+ <!-- example: kglite-agent-query -->
67
+ ```console
68
+ kglite query <graph> "RETURN 'agent-evidence' AS evidence" --format agent
69
+ ```
70
+
71
+ Agent output advertises executable expansion commands. Copy the returned
72
+ command and its result ID; do not construct either. KGLite's CLI retention uses
73
+ a private disk cache, so expansion can run in a later process. This lifecycle
74
+ is separate from the MCP server's in-memory, session-scoped retention.
75
+
76
+ If the compatible external CLI is unavailable, report the agent example as
77
+ untested. Codingest's own query command remains the portable complete-output
78
+ path and does not require response retention:
79
+
80
+ ```console
81
+ codingest query "MATCH (n:Evidence) RETURN n.id ORDER BY n.id" \
82
+ --graph app.kgl --format json
83
+ codingest query "MATCH (n:Evidence) RETURN n.id ORDER BY n.id" \
84
+ --graph app.kgl --format csv
85
+ ```
86
+
87
+ Codingest JSON and CSV contain every row produced by the query and executor.
88
+ They do not make a literal `LIMIT` or another execution boundary broader.