codingest 0.2.19__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 (109) hide show
  1. {codingest-0.2.19 → codingest-0.2.20}/Cargo.lock +12 -10
  2. {codingest-0.2.19 → codingest-0.2.20}/Cargo.toml +12 -4
  3. {codingest-0.2.19 → codingest-0.2.20}/PKG-INFO +5 -5
  4. {codingest-0.2.19 → codingest-0.2.20}/README.md +3 -3
  5. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/Cargo.toml +1 -1
  6. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/builder/other_edges.rs +7 -5
  7. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/rev.rs +132 -18
  8. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest-cli/Cargo.toml +1 -1
  9. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest-cli/skills/codingest-code-review/SKILL.md +74 -21
  10. codingest-0.2.20/crates/codingest-cli/skills/codingest-code-review/references/mcp-upgrade.md +88 -0
  11. codingest-0.2.20/crates/codingest-cli/skills/codingest-code-review/references/queries.md +186 -0
  12. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest-cli/src/query.rs +62 -6
  13. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest-cli/src/skill.rs +57 -33
  14. codingest-0.2.20/crates/codingest-cli/tests/skill_recipes.rs +754 -0
  15. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest-mcp/Cargo.toml +5 -1
  16. codingest-0.2.20/crates/codingest-mcp/tests/response_contract.rs +800 -0
  17. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest-py/Cargo.toml +3 -3
  18. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest-py/src/lib.rs +1 -1
  19. {codingest-0.2.19 → codingest-0.2.20}/pyproject.toml +2 -2
  20. codingest-0.2.19/crates/codingest-cli/skills/codingest-code-review/references/mcp-upgrade.md +0 -18
  21. codingest-0.2.19/crates/codingest-cli/skills/codingest-code-review/references/queries.md +0 -87
  22. {codingest-0.2.19 → codingest-0.2.20}/LICENSE +0 -0
  23. {codingest-0.2.19 → codingest-0.2.20}/codingest/__init__.py +0 -0
  24. {codingest-0.2.19 → codingest-0.2.20}/codingest/__init__.pyi +0 -0
  25. {codingest-0.2.19 → codingest-0.2.20}/codingest/cli.py +0 -0
  26. {codingest-0.2.19 → codingest-0.2.20}/codingest/mcp_server.py +0 -0
  27. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/bin/codingest_bench.rs +0 -0
  28. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/bin/codingest_stats.rs +0 -0
  29. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/builder/call_edges.rs +0 -0
  30. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/builder/js_workspace.rs +0 -0
  31. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/builder/load/edge_frames.rs +0 -0
  32. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/builder/load/entity_frames.rs +0 -0
  33. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/builder/load.rs +0 -0
  34. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/builder/mod.rs +0 -0
  35. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/builder/routes/django.rs +0 -0
  36. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/builder/routes/fastapi.rs +0 -0
  37. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/builder/routes/flask.rs +0 -0
  38. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/builder/routes/mod.rs +0 -0
  39. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/builder/semantic_edges.rs +0 -0
  40. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/builder/type_edges.rs +0 -0
  41. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/cross_lang.rs +0 -0
  42. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/docs/mod.rs +0 -0
  43. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/docs/rst.rs +0 -0
  44. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/lib.rs +0 -0
  45. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/manifest/mod.rs +0 -0
  46. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/models.rs +0 -0
  47. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/parsers/agc.rs +0 -0
  48. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/parsers/cpp.rs +0 -0
  49. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/parsers/csharp.rs +0 -0
  50. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/parsers/css.rs +0 -0
  51. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/parsers/dart.rs +0 -0
  52. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/parsers/go.rs +0 -0
  53. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/parsers/html.rs +0 -0
  54. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/parsers/java.rs +0 -0
  55. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/parsers/julia.rs +0 -0
  56. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/parsers/mod.rs +0 -0
  57. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/parsers/php.rs +0 -0
  58. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/parsers/python.rs +0 -0
  59. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/parsers/r.rs +0 -0
  60. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/parsers/registry.rs +0 -0
  61. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/parsers/rust_lang.rs +0 -0
  62. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/parsers/shared.rs +0 -0
  63. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/parsers/swift.rs +0 -0
  64. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/parsers/typescript.rs +0 -0
  65. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/src/repo.rs +0 -0
  66. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/agc_apollo.rs +0 -0
  67. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/README.md +0 -0
  68. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/agc_basic.sha256 +0 -0
  69. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/cpp_extern_c.sha256 +0 -0
  70. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/cpp_include.sha256 +0 -0
  71. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/cross_ts_py.sha256 +0 -0
  72. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/csharp_using_alias.sha256 +0 -0
  73. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/dart_import.sha256 +0 -0
  74. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/dart_part_of.sha256 +0 -0
  75. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/docs_ext_collide.sha256 +0 -0
  76. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/docs_mdx.sha256 +0 -0
  77. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/dup_minified_assets.sha256 +0 -0
  78. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/go_interface.sha256 +0 -0
  79. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/html_js_lang_group.sha256 +0 -0
  80. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/java_javadoc.sha256 +0 -0
  81. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/julia_basic.sha256 +0 -0
  82. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/php_group_use.sha256 +0 -0
  83. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/py_basic.sha256 +0 -0
  84. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/py_import.sha256 +0 -0
  85. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/py_inheritance.sha256 +0 -0
  86. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/py_nested_defs.sha256 +0 -0
  87. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/py_routes_dup.sha256 +0 -0
  88. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/py_src_layout.sha256 +0 -0
  89. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/r_basic.sha256 +0 -0
  90. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/rust_import.sha256 +0 -0
  91. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/rust_inline_mod.sha256 +0 -0
  92. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/rust_xfile.sha256 +0 -0
  93. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/swift_basic.sha256 +0 -0
  94. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/ts_callback.sha256 +0 -0
  95. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/ts_closure_scope.sha256 +0 -0
  96. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/ts_hof_binding.sha256 +0 -0
  97. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/ts_monorepo.sha256 +0 -0
  98. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/goldens/web_served_root.sha256 +0 -0
  99. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/grammar_kinds.rs +0 -0
  100. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/parity.rs +0 -0
  101. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest/tests/traversal_semantics.rs +0 -0
  102. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest-cli/skills/codingest-code-review/references/public-repositories.md +0 -0
  103. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest-cli/src/code_tree_cli.rs +0 -0
  104. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest-cli/src/lib.rs +0 -0
  105. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest-cli/src/main.rs +0 -0
  106. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest-cli/src/skill_assets.rs +0 -0
  107. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest-cli/tests/exit_codes.rs +0 -0
  108. {codingest-0.2.19 → codingest-0.2.20}/crates/codingest-mcp/src/lib.rs +0 -0
  109. {codingest-0.2.19 → 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.19"
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.19"
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.19"
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.19"
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.19"
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.19
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.19", 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"] }
@@ -10,9 +10,12 @@ git diff, source reading, and literal-text search; it does not replace them.
10
10
 
11
11
  ## Review workflow
12
12
 
13
- > **Prerequisites:** the `codingest` and `kglite` CLIs must be on PATH.
14
- > `pip install codingest` provides both plus the builder-aware
15
- > `codingest-mcp` server. Rust-only environments can alternatively use Cargo.
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.
16
19
 
17
20
  Inspect the diff and repository guidance first. Identify changed symbols and
18
21
  the base/head revisions, then build or refresh the graph without executing
@@ -35,21 +38,43 @@ Before reusing an artifact, check freshness:
35
38
  codingest status --output .kglite/code-review.kgl --format json
36
39
  ```
37
40
 
38
- ## Retrieve only what answers the question
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.
39
63
 
40
64
  Choose the narrowest route for the uncertainty in front of you; these are
41
65
  alternatives, not a compulsory sequence. Reuse tool signatures already known
42
- in the current session; if a needed tool is not visible, discover that tool
66
+ in the current session. If a needed tool is not visible, discover that tool
43
67
  rather than loading a broad catalog.
44
68
 
45
69
  - **Known symbol or location:** read it directly. With MCP, use
46
70
  `read_code_source` for a qualified name or `read_source` for a path, applying
47
71
  `start_line`, `end_line`, and `max_chars` when a full body or file is not
48
72
  needed.
49
- - **Exact structural question:** use `cypher_query`, or `kglite query` at the
50
- CLI. Reuse a schema already observed for the same unchanged graph. Otherwise
51
- inspect only the needed node or connection shape with `graph_overview` or
52
- `kglite describe`; do not retrieve the whole schema by habit.
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.
53
78
  - **Broad explanation or unfamiliar subsystem:** use bounded `explore`, starting
54
79
  with a short topic, a few `max_entities`, shallow `max_depth`, and
55
80
  `include_source: false` unless bodies are needed. Narrow to selected symbols
@@ -60,14 +85,41 @@ rather than loading a broad catalog.
60
85
  Every additional read should resolve a concrete uncertainty needed for the
61
86
  answer. Project only useful Cypher properties, return a few relevant candidates,
62
87
  and exclude unrelated fixtures, generated code, vendors, or examples when the
63
- question does not cover them. If output truncates, narrow the query or source
64
- range instead of repeatedly increasing output. Reuse evidence already seen
65
- unless its source or active graph changed.
66
-
67
- Open implicated code at exact lines before claiming behavior; an edge alone is
68
- not runtime proof. Stop when the requested conclusions are supported. Expand
69
- only for a contradiction, missing evidence, or a correctness risk that could
70
- change the answer.
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.
71
123
 
72
124
  See [queries.md](references/queries.md) for query patterns,
73
125
  [public-repositories.md](references/public-repositories.md) for safe public-repo
@@ -77,7 +129,7 @@ workflow.
77
129
  ## What counts as a finding
78
130
 
79
131
  The workflow above verifies a finding against exact source lines. This is the
80
- prior question — what is eligible to be a finding at all.
132
+ prior question: what is eligible to be a finding at all.
81
133
 
82
134
  - **A finding names a concrete failure**: the input, state, or sequence, and the
83
135
  wrong outcome it produces. A wrong result, a crash, data loss or corruption, a
@@ -86,7 +138,7 @@ prior question — what is eligible to be a finding at all.
86
138
  code contradicts. **"No findings" is a valid review**, and a good one.
87
139
  - **Design, structure, naming, "consider using X", and "this won't scale" are
88
140
  not findings** — they are mis-staged. Their venue is planning, where "I would
89
- have designed this differently" is invited and settled before the code exists.
141
+ have designed this differently" is invited and settled before code exists.
90
142
  After a plan is approved, review measures the implementation against that plan
91
143
  and against correctness, never against a design the reviewer would have
92
144
  preferred. A design opinion formed while reading a diff is input to the *next*
@@ -105,8 +157,9 @@ prior question — what is eligible to be a finding at all.
105
157
 
106
158
  ## Honesty rules
107
159
 
108
- - Never invent labels, properties, or connection types. Discover an unfamiliar
109
- shape before querying it; reuse a known shape while the graph is unchanged.
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.
110
163
  - Treat unresolved or missing graph edges as absence of evidence, not proof.
111
164
  - Quote paths and revisions passed through the shell.
112
165
  - Never build, import, or execute code from a repository merely to review it.
@@ -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.