diff --git a/.gitleaks.toml b/.gitleaks.toml index d34c014..bac44d2 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -4,6 +4,11 @@ title = "Code System Graph Gitleaks configuration" description = "Ignore the synthetic Terraform password fixture used to prove secret-free persistence." paths = [ '''^fixtures/platform-demo/infra/terraform/main\.tf$''', + '''^\.git/''', + '''^\.cursor/''', + '''^\.codegraph/''', + '''^dist/''', + '''^target/''', ] regexes = [ '''ghp_012345678901234567890123456789012345''', diff --git a/CHANGELOG.md b/CHANGELOG.md index b137a84..8d4a413 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,52 @@ All notable public changes to Code System Graph are documented in this file. Code System Graph follows Semantic Versioning. +## [1.1.0] - 2026-08-11 + +### Breaking changes + +- MCP tools now return one bounded Markdown text block and omit `structuredContent` and result + `outputSchema`; all MCP resources use `text/markdown`, with fenced JSON in the schema catalog. +- HTTP and CLI tool envelopes use delivery schema v2. Explore returns `ExploreReport` and places + ephemeral source in `source_markdown`. +- Direct MCP mode requires `--config`; binding mode loads its recorded global manifest. Generated + plugin bindings and ownership receipts use version 2. +- SQLite schema version 2 deliberately rejects 1.0.x databases. A fresh database and complete scan + are required; no legacy serializer, cache, plugin, or database compatibility path is provided. + +### Added + +- Added global Explore, Query, MCP-tool, MCP-resource, and schema-catalog limits to + `executionPolicy`, including checked capacity relationships and immutable policy sharing across + long-lived servers. +- Added independent scan and agent-delivery fingerprints so presentation-only limits do not + invalidate snapshots, batches, or checkpoints. +- Explore now reports repository context, source Markdown, resolved symbols, callers/callees, + evidence-correlated federated handoffs, coverage gaps, truncations, grounded next actions, and + exact provider execution accounting. +- Query now returns grounded next actions and directs zero-hit source questions toward Explore when + a real registered repository alias is recognized. +- Added bounded Markdown rendering with UTF-8-safe, block-stable truncation and centralized escaping + for headings, inline values, paths, controls, and untrusted source blocks. + +### Fixed + +- Explore now enforces one deadline across snapshot loading, provider traversal, and correlation; + timed-out or abandoned work is cooperatively cancelled while partial context remains available. +- Snapshot and candidate identity now includes the complete scan fingerprint while excluding + agent-delivery-only settings, so scan-facing changes cannot reuse stale persisted results. +- Explore reports observed provider concurrency and counts an anchor as traversed only after both + caller and callee directions complete successfully. +- MCP rendering now preserves compact status, freshness, warnings, coverage, and paths at the + 256-byte minimum, and reports collection retention after both item and byte limits are applied. + +### Release engineering + +- Added exhaustive typed Markdown fixtures and golden coverage for all 15 MCP tools and every MCP + resource, including low-budget, UTF-8, fenced-source, error, and nested-collection cases. +- Added direct deadline and cancellation tests for Explore snapshot loading and correlation, plus + synchronized Markdown-only MCP instructions and CLI configuration examples. + ## [1.0.3] - 2026-08-09 ### Added diff --git a/Cargo.lock b/Cargo.lock index 44d97df..ba5d368 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,3844 +1,2574 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. version = 4 [[package]] +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = ["cfg-if", "getrandom 0.3.4", "once_cell", "serde", "version_check", "zerocopy"] name = "ahash" -version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.4", - "once_cell", - "serde", - "version_check", - "zerocopy", -] +version = "0.8.12" [[package]] +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = ["memchr"] name = "aho-corasick" -version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] +version = "1.1.4" [[package]] +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" name = "allocator-api2" -version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +version = "0.2.21" [[package]] +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = ["libc"] name = "android_system_properties" -version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] +version = "0.1.5" [[package]] +checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" +dependencies = ["anstyle", "memchr", "unicode-width"] name = "annotate-snippets" -version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" -dependencies = [ - "anstyle", - "memchr", - "unicode-width", -] +version = "0.12.16" [[package]] +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = ["anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse"] name = "anstream" -version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] +version = "1.0.0" [[package]] +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" name = "anstyle" -version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" +version = "1.0.14" [[package]] +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = ["utf8parse"] name = "anstyle-parse" -version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] +version = "1.0.0" [[package]] +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = ["windows-sys 0.61.2"] name = "anstyle-query" -version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] +version = "1.1.5" [[package]] +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = ["anstyle", "once_cell_polyfill", "windows-sys 0.61.2"] name = "anstyle-wincon" -version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] +version = "3.0.11" [[package]] +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" name = "anyhow" -version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +version = "1.0.104" [[package]] +checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6" +dependencies = ["object"] name = "ar_archive_writer" -version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6" -dependencies = [ - "object", -] +version = "0.5.3" [[package]] +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" name = "arbitrary" -version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +version = "1.4.2" [[package]] +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" name = "arraydeque" -version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" +version = "0.5.1" [[package]] +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" name = "arrayref" -version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" +version = "0.3.9" [[package]] +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" name = "arrayvec" -version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +version = "0.7.8" [[package]] +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = ["proc-macro2", "quote", "syn 3.0.3"] name = "async-trait" -version = "0.1.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] +version = "0.1.91" [[package]] +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" name = "atomic-waker" -version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +version = "1.1.2" [[package]] +checksum = "84790c55b5704b0d35130bf16a4ce22a8e70eb0ea773522557524d9a4852663d" +dependencies = ["nix 0.30.1", "rand 0.9.5"] name = "atomic-write-file" -version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84790c55b5704b0d35130bf16a4ce22a8e70eb0ea773522557524d9a4852663d" -dependencies = [ - "nix 0.30.1", - "rand 0.9.5", -] +version = "0.3.0" [[package]] +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" name = "autocfg" -version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +version = "1.5.1" [[package]] +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +dependencies = ["aws-lc-sys", "zeroize"] name = "aws-lc-rs" -version = "1.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" -dependencies = [ - "aws-lc-sys", - "zeroize", -] +version = "1.18.0" [[package]] +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +dependencies = ["cc", "cmake", "dunce", "fs_extra", "pkg-config"] name = "aws-lc-sys" -version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", - "pkg-config", -] +version = "0.44.0" [[package]] +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = ["axum-core", "bytes", "form_urlencoded", "futures-util", "http", "http-body", "http-body-util", "hyper", "hyper-util", "itoa", "matchit", "memchr", "mime", "percent-encoding", "pin-project-lite", "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", "sync_wrapper", "tokio", "tower", "tower-layer", "tower-service", "tracing"] name = "axum" -version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" -dependencies = [ - "axum-core", - "bytes", - "form_urlencoded", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "serde_core", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower", - "tower-layer", - "tower-service", - "tracing", -] +version = "0.8.9" [[package]] +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = ["bytes", "futures-core", "http", "http-body", "http-body-util", "mime", "pin-project-lite", "sync_wrapper", "tower-layer", "tower-service", "tracing"] name = "axum-core" -version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "sync_wrapper", - "tower-layer", - "tower-service", - "tracing", -] +version = "0.5.6" [[package]] +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" name = "base64" -version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +version = "0.22.1" [[package]] +checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9" name = "base64" -version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9" +version = "0.23.0" [[package]] +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = ["bit-vec"] name = "bit-set" -version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] +version = "0.8.0" [[package]] +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" name = "bit-vec" -version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +version = "0.8.0" [[package]] +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" name = "bitflags" -version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +version = "2.13.1" [[package]] +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = ["arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", "cpufeatures"] name = "blake3" -version = "1.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", - "cpufeatures", -] +version = "1.8.5" [[package]] +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" name = "borrow-or-share" -version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" +version = "0.2.4" [[package]] +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" +dependencies = ["memchr", "serde_core"] name = "bstr" -version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" -dependencies = [ - "memchr", - "serde_core", -] +version = "1.13.0" [[package]] +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" name = "bumpalo" -version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +version = "3.20.3" [[package]] +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" name = "bytecount" -version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" +version = "0.6.9" [[package]] +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" name = "bytes" -version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +version = "1.12.1" [[package]] +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" +dependencies = ["serde_core"] name = "camino" -version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" -dependencies = [ - "serde_core", -] +version = "1.2.5" [[package]] +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = ["find-msvc-tools", "jobserver", "libc", "shlex"] name = "cc" -version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] +version = "1.4.0" [[package]] +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" name = "cfg-if" -version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +version = "1.0.4" [[package]] +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" name = "cfg_aliases" -version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" +version = "0.2.2" [[package]] +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = ["cfg-if", "cpufeatures", "rand_core 0.10.1"] name = "chacha20" -version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" -dependencies = [ - "cfg-if", - "cpufeatures", - "rand_core 0.10.1", -] +version = "0.10.1" [[package]] +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = ["iana-time-zone", "num-traits", "serde", "windows-link"] name = "chrono" -version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" -dependencies = [ - "iana-time-zone", - "num-traits", - "serde", - "windows-link", -] +version = "0.4.45" [[package]] +checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" +dependencies = ["clap_builder", "clap_derive"] name = "clap" -version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" -dependencies = [ - "clap_builder", - "clap_derive", -] +version = "4.6.5" [[package]] +checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" +dependencies = ["anstream", "anstyle", "clap_lex", "strsim"] name = "clap_builder" -version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] +version = "4.6.5" [[package]] +checksum = "b1f84a88507dbd05c695f2cb5e8558e747179134005e9893882dec964190ed89" +dependencies = ["clap"] name = "clap_complete" -version = "4.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f84a88507dbd05c695f2cb5e8558e747179134005e9893882dec964190ed89" -dependencies = [ - "clap", -] +version = "4.6.8" [[package]] +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = ["heck", "proc-macro2", "quote", "syn 3.0.3"] name = "clap_derive" -version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 3.0.3", -] +version = "4.6.4" [[package]] +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" name = "clap_lex" -version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +version = "1.1.0" [[package]] +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = ["cc"] name = "cmake" -version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] +version = "0.1.58" [[package]] +dependencies = ["anyhow", "atomic-write-file", "axum", "blake3", "clap", "clap_complete", "code-system-graph-core", "code-system-graph-hooks", "code-system-graph-model", "code-system-graph-store-sqlite", "jsonschema", "nix 0.31.3", "notify", "reqwest", "rmcp", "rusqlite", "schemars", "serde", "serde-saphyr", "serde_json", "signal-hook", "subtle", "sysinfo", "tempfile", "thiserror 2.0.19", "tokio", "tokio-util", "tower", "tower-http 0.7.0", "windows-sys 0.61.2"] name = "code-system-graph" -version = "1.0.3" -dependencies = [ - "anyhow", - "atomic-write-file", - "axum", - "blake3", - "clap", - "clap_complete", - "code-system-graph-core", - "code-system-graph-hooks", - "code-system-graph-model", - "code-system-graph-store-sqlite", - "jsonschema", - "nix 0.31.3", - "notify", - "reqwest", - "rmcp", - "rusqlite", - "schemars", - "serde", - "serde-saphyr", - "serde_json", - "signal-hook", - "subtle", - "sysinfo", - "tempfile", - "thiserror 2.0.19", - "tokio", - "tokio-util", - "tower", - "tower-http 0.7.0", - "windows-sys 0.61.2", -] +version = "1.1.0" [[package]] +dependencies = ["async-trait", "atomic-write-file", "blake3", "code-system-graph-model", "globset", "graphql-parser", "hcl-rs", "ignore", "libc", "nix 0.31.3", "proto-parser", "pulldown-cmark", "reqwest", "rmcp", "schemars", "semver", "serde", "serde-saphyr", "serde_json", "sqlparser", "tempfile", "thiserror 2.0.19", "tokio", "tokio-util", "tree-sitter", "tree-sitter-go", "tree-sitter-java", "tree-sitter-javascript", "tree-sitter-python", "tree-sitter-rust", "tree-sitter-typescript", "url"] name = "code-system-graph-core" -version = "1.0.3" -dependencies = [ - "async-trait", - "atomic-write-file", - "blake3", - "code-system-graph-model", - "globset", - "graphql-parser", - "hcl-rs", - "ignore", - "libc", - "nix 0.31.3", - "proto-parser", - "pulldown-cmark", - "reqwest", - "rmcp", - "schemars", - "semver", - "serde", - "serde-saphyr", - "serde_json", - "sqlparser", - "tempfile", - "thiserror 2.0.19", - "tokio", - "tokio-util", - "tree-sitter", - "tree-sitter-go", - "tree-sitter-java", - "tree-sitter-javascript", - "tree-sitter-python", - "tree-sitter-rust", - "tree-sitter-typescript", - "url", -] +version = "1.1.0" [[package]] +dependencies = ["code-system-graph-core", "code-system-graph-model", "libfuzzer-sys", "serde_json"] name = "code-system-graph-fuzz" version = "0.0.0" -dependencies = [ - "code-system-graph-core", - "code-system-graph-model", - "libfuzzer-sys", - "serde_json", -] [[package]] +dependencies = ["atomic-write-file", "blake3", "libc", "nix 0.31.3", "schemars", "serde", "serde_json", "tempfile", "thiserror 2.0.19"] name = "code-system-graph-hooks" -version = "1.0.3" -dependencies = [ - "atomic-write-file", - "blake3", - "libc", - "nix 0.31.3", - "schemars", - "serde", - "serde_json", - "tempfile", - "thiserror 2.0.19", -] +version = "1.1.0" [[package]] +dependencies = ["blake3", "camino", "schemars", "semver", "serde", "time"] name = "code-system-graph-model" -version = "1.0.3" -dependencies = [ - "blake3", - "camino", - "schemars", - "semver", - "serde", - "time", -] +version = "1.1.0" [[package]] +dependencies = ["blake3", "code-system-graph-model", "libc", "rusqlite", "same-file", "serde_json", "sysinfo", "tempfile", "thiserror 2.0.19", "windows-sys 0.61.2"] name = "code-system-graph-store-sqlite" -version = "1.0.3" -dependencies = [ - "blake3", - "code-system-graph-model", - "libc", - "rusqlite", - "same-file", - "serde_json", - "sysinfo", - "tempfile", - "thiserror 2.0.19", - "windows-sys 0.61.2", -] +version = "1.1.0" [[package]] +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" name = "colorchoice" -version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +version = "1.0.5" [[package]] +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = ["bytes", "memchr"] name = "combine" -version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] +version = "4.6.7" [[package]] +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" name = "constant_time_eq" -version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +version = "0.4.2" [[package]] +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = ["core-foundation-sys", "libc"] name = "core-foundation" -version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] +version = "0.10.1" [[package]] +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" name = "core-foundation-sys" -version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +version = "0.8.7" [[package]] +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = ["libc"] name = "cpufeatures" -version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] +version = "0.3.0" [[package]] +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = ["crossbeam-epoch", "crossbeam-utils"] name = "crossbeam-deque" -version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] +version = "0.8.7" [[package]] +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = ["crossbeam-utils"] name = "crossbeam-epoch" -version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" -dependencies = [ - "crossbeam-utils", -] +version = "0.9.20" [[package]] +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" name = "crossbeam-utils" -version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +version = "0.8.22" [[package]] +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = ["darling_core", "darling_macro"] name = "darling" -version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core", - "darling_macro", -] +version = "0.23.0" [[package]] +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = ["ident_case", "proc-macro2", "quote", "strsim", "syn 2.0.119"] name = "darling_core" -version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.119", -] +version = "0.23.0" [[package]] +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = ["darling_core", "quote", "syn 2.0.119"] name = "darling_macro" -version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.119", -] +version = "0.23.0" [[package]] +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" name = "data-encoding" -version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" +version = "2.11.1" [[package]] +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = ["serde_core"] name = "deranged" -version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "serde_core", -] +version = "0.5.8" [[package]] +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = ["proc-macro2", "quote", "syn 3.0.3"] name = "displaydoc" -version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] +version = "0.2.7" [[package]] +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" name = "dunce" -version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +version = "1.0.5" [[package]] +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" name = "dyn-clone" -version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +version = "1.0.20" [[package]] +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = ["serde"] name = "email_address" -version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" -dependencies = [ - "serde", -] +version = "0.2.9" [[package]] +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = ["cfg-if"] name = "encoding_rs" -version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] +version = "0.8.35" [[package]] +checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" +dependencies = ["encoding_rs"] name = "encoding_rs_io" -version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" -dependencies = [ - "encoding_rs", -] +version = "0.1.7" [[package]] +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" name = "equivalent" -version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +version = "1.0.2" [[package]] +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = ["libc", "windows-sys 0.61.2"] name = "errno" -version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] +version = "0.3.14" [[package]] +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" name = "fallible-iterator" -version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" +version = "0.3.0" [[package]] +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" name = "fallible-streaming-iterator" -version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +version = "0.1.9" [[package]] +checksum = "476de73bddf2ef8490aa4ee8f1cf40b430bf1d56c48c22080e5186952cd580e6" +dependencies = ["bit-set", "regex-automata", "regex-syntax"] name = "fancy-regex" -version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "476de73bddf2ef8490aa4ee8f1cf40b430bf1d56c48c22080e5186952cd580e6" -dependencies = [ - "bit-set", - "regex-automata", - "regex-syntax", -] +version = "0.19.0" [[package]] +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" name = "fastrand" -version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +version = "2.5.0" [[package]] +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" name = "find-msvc-tools" -version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +version = "0.1.9" [[package]] +checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" +dependencies = ["borrow-or-share", "ref-cast", "serde"] name = "fluent-uri" -version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" -dependencies = [ - "borrow-or-share", - "ref-cast", - "serde", -] +version = "0.4.1" [[package]] +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" name = "fnv" -version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +version = "1.0.7" [[package]] +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" name = "foldhash" -version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +version = "0.2.0" [[package]] +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = ["percent-encoding"] name = "form_urlencoded" -version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] +version = "1.2.2" [[package]] +checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872" +dependencies = ["lazy_static", "num"] name = "fraction" -version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872" -dependencies = [ - "lazy_static", - "num", -] +version = "0.15.4" [[package]] +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" name = "fs_extra" -version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +version = "1.3.0" [[package]] +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = ["libc"] name = "fsevent-sys" -version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" -dependencies = [ - "libc", -] +version = "4.1.0" [[package]] +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = ["futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util"] name = "futures" -version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] +version = "0.3.33" [[package]] +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = ["futures-core", "futures-sink"] name = "futures-channel" -version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" -dependencies = [ - "futures-core", - "futures-sink", -] +version = "0.3.33" [[package]] +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" name = "futures-core" -version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +version = "0.3.33" [[package]] +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = ["futures-core", "futures-task", "futures-util"] name = "futures-executor" -version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] +version = "0.3.33" [[package]] +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" name = "futures-io" -version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" +version = "0.3.33" [[package]] +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = ["proc-macro2", "quote", "syn 2.0.119"] name = "futures-macro" -version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] +version = "0.3.33" [[package]] +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" name = "futures-sink" -version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" +version = "0.3.33" [[package]] +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" name = "futures-task" -version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" +version = "0.3.33" [[package]] +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = ["futures-channel", "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "slab"] name = "futures-util" -version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] +version = "0.3.33" [[package]] +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = ["unicode-width"] name = "getopts" -version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" -dependencies = [ - "unicode-width", -] +version = "0.2.24" [[package]] +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = ["cfg-if", "js-sys", "libc", "wasi", "wasm-bindgen"] name = "getrandom" -version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] +version = "0.2.17" [[package]] +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = ["cfg-if", "js-sys", "libc", "r-efi 5.3.0", "wasip2", "wasm-bindgen"] name = "getrandom" -version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 5.3.0", - "wasip2", - "wasm-bindgen", -] +version = "0.3.4" [[package]] +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = ["cfg-if", "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", "wasm-bindgen"] name = "getrandom" -version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 6.0.0", - "rand_core 0.10.1", - "wasm-bindgen", -] +version = "0.4.3" [[package]] +checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd" +dependencies = ["aho-corasick", "bstr", "log", "regex-automata", "regex-syntax"] name = "globset" -version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd" -dependencies = [ - "aho-corasick", - "bstr", - "log", - "regex-automata", - "regex-syntax", -] +version = "0.4.19" [[package]] +checksum = "c60388b03522b86e24b6b45952255279936b08a871775156fc89c94e792d21d8" +dependencies = ["arraydeque", "smallvec"] name = "granit-parser" -version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c60388b03522b86e24b6b45952255279936b08a871775156fc89c94e792d21d8" -dependencies = [ - "arraydeque", - "smallvec", -] +version = "1.0.0" [[package]] +checksum = "7a818c0d883d7c0801df27be910917750932be279c7bc82dc541b8769425f409" +dependencies = ["combine", "thiserror 1.0.69"] name = "graphql-parser" -version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a818c0d883d7c0801df27be910917750932be279c7bc82dc541b8769425f409" -dependencies = [ - "combine", - "thiserror 1.0.69", -] +version = "0.4.1" [[package]] +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = ["foldhash"] name = "hashbrown" -version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "foldhash", -] +version = "0.16.1" [[package]] +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = ["allocator-api2", "equivalent", "foldhash"] name = "hashbrown" -version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] +version = "0.17.1" [[package]] +checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" +dependencies = ["hashbrown 0.17.1"] name = "hashlink" -version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" -dependencies = [ - "hashbrown 0.17.1", -] +version = "0.12.1" [[package]] +checksum = "f641e65979f9c567246f13f9c64f7af41b3476e3171a63b10b36778af95b267b" +dependencies = ["fnv", "hcl-primitives", "pratt", "vecmap-rs", "winnow"] name = "hcl-edit" -version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f641e65979f9c567246f13f9c64f7af41b3476e3171a63b10b36778af95b267b" -dependencies = [ - "fnv", - "hcl-primitives", - "pratt", - "vecmap-rs", - "winnow", -] +version = "0.9.6" [[package]] +checksum = "829a11d304c89e2cfe0dbb494a686bbe2b48ade17705c62cd1957b04aa4630f6" +dependencies = ["itoa", "kstring", "ryu", "serde", "unicode-ident"] name = "hcl-primitives" -version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829a11d304c89e2cfe0dbb494a686bbe2b48ade17705c62cd1957b04aa4630f6" -dependencies = [ - "itoa", - "kstring", - "ryu", - "serde", - "unicode-ident", -] +version = "0.1.11" [[package]] +checksum = "153fc72c1037f6933efe01c1c769235479e995fb82a799c5716dabad7375b671" +dependencies = ["hcl-edit", "hcl-primitives", "indexmap", "itoa", "serde", "vecmap-rs"] name = "hcl-rs" -version = "0.19.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "153fc72c1037f6933efe01c1c769235479e995fb82a799c5716dabad7375b671" -dependencies = [ - "hcl-edit", - "hcl-primitives", - "indexmap", - "itoa", - "serde", - "vecmap-rs", -] +version = "0.19.7" [[package]] +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" name = "heck" -version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +version = "0.5.0" [[package]] +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = ["bytes", "itoa"] name = "http" -version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" -dependencies = [ - "bytes", - "itoa", -] +version = "1.5.0" [[package]] +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = ["bytes", "http"] name = "http-body" -version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" -dependencies = [ - "bytes", - "http", -] +version = "1.1.0" [[package]] +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = ["bytes", "futures-core", "http", "http-body", "pin-project-lite"] name = "http-body-util" -version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] +version = "0.1.4" [[package]] +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" name = "httparse" -version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +version = "1.10.1" [[package]] +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" name = "httpdate" -version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +version = "1.0.3" [[package]] +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = ["atomic-waker", "bytes", "futures-channel", "futures-core", "http", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", "smallvec", "tokio", "want"] name = "hyper" -version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] +version = "1.11.0" [[package]] +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = ["http", "hyper", "hyper-util", "rustls", "tokio", "tokio-rustls", "tower-service"] name = "hyper-rustls" -version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "tokio", - "tokio-rustls", - "tower-service", -] +version = "0.27.9" [[package]] +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = ["base64 0.22.1", "bytes", "futures-channel", "futures-util", "http", "http-body", "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", "socket2", "tokio", "tower-service", "tracing"] name = "hyper-util" -version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] +version = "0.1.20" [[package]] +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = ["android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "log", "wasm-bindgen", "windows-core"] name = "iana-time-zone" -version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] +version = "0.1.65" [[package]] +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = ["cc"] name = "iana-time-zone-haiku" -version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] +version = "0.1.2" [[package]] +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = ["displaydoc", "potential_utf", "utf8_iter", "yoke", "zerofrom", "zerovec"] name = "icu_collections" -version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] +version = "2.2.0" [[package]] +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = ["displaydoc", "litemap", "tinystr", "writeable", "zerovec"] name = "icu_locale_core" -version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] +version = "2.2.0" [[package]] +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = ["icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "zerovec"] name = "icu_normalizer" -version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] +version = "2.2.0" [[package]] +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" name = "icu_normalizer_data" -version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +version = "2.2.0" [[package]] +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = ["icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", "zerotrie", "zerovec"] name = "icu_properties" -version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] +version = "2.2.0" [[package]] +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" name = "icu_properties_data" -version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +version = "2.2.0" [[package]] +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = ["displaydoc", "icu_locale_core", "writeable", "yoke", "zerofrom", "zerotrie", "zerovec"] name = "icu_provider" -version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] +version = "2.2.0" [[package]] +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" name = "ident_case" -version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +version = "1.0.1" [[package]] +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = ["idna_adapter", "smallvec", "utf8_iter"] name = "idna" -version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] +version = "1.1.0" [[package]] +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = ["icu_normalizer", "icu_properties"] name = "idna_adapter" -version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] +version = "1.2.2" [[package]] +checksum = "00b69833ed729dc5aa7d19541d96d6cf8e9137194207a04916d658e43168402f" +dependencies = ["crossbeam-deque", "globset", "log", "memchr", "regex-automata", "same-file", "walkdir", "winapi-util"] name = "ignore" -version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b69833ed729dc5aa7d19541d96d6cf8e9137194207a04916d658e43168402f" -dependencies = [ - "crossbeam-deque", - "globset", - "log", - "memchr", - "regex-automata", - "same-file", - "walkdir", - "winapi-util", -] +version = "0.4.33" [[package]] +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = ["equivalent", "hashbrown 0.17.1", "serde", "serde_core"] name = "indexmap" -version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] +version = "2.14.0" [[package]] +checksum = "153be1941a183ec9ccd095ddbe17a8b8d435ef6c76e9e02451b933c3999af2c8" +dependencies = ["bitflags", "inotify-sys", "libc"] name = "inotify" -version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "153be1941a183ec9ccd095ddbe17a8b8d435ef6c76e9e02451b933c3999af2c8" -dependencies = [ - "bitflags", - "inotify-sys", - "libc", -] +version = "0.11.4" [[package]] +checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" +dependencies = ["libc"] name = "inotify-sys" -version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" -dependencies = [ - "libc", -] +version = "0.1.8" [[package]] +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" name = "ipnet" -version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +version = "2.12.0" [[package]] +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" name = "is_terminal_polyfill" -version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +version = "1.70.2" [[package]] +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" name = "itoa" -version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +version = "1.0.18" [[package]] +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = ["cfg-if", "combine", "jni-macros", "jni-sys", "log", "simd_cesu8", "thiserror 2.0.19", "walkdir", "windows-link"] name = "jni" -version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" -dependencies = [ - "cfg-if", - "combine", - "jni-macros", - "jni-sys", - "log", - "simd_cesu8", - "thiserror 2.0.19", - "walkdir", - "windows-link", -] +version = "0.22.4" [[package]] +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = ["proc-macro2", "quote", "rustc_version", "simd_cesu8", "syn 2.0.119"] name = "jni-macros" -version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "simd_cesu8", - "syn 2.0.119", -] +version = "0.22.4" [[package]] +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = ["jni-sys-macros"] name = "jni-sys" -version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] +version = "0.4.1" [[package]] +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = ["quote", "syn 2.0.119"] name = "jni-sys-macros" -version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.119", -] +version = "0.4.1" [[package]] +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = ["getrandom 0.4.3", "libc"] name = "jobserver" -version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" -dependencies = [ - "getrandom 0.4.3", - "libc", -] +version = "0.1.35" [[package]] +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = ["cfg-if", "futures-util", "wasm-bindgen"] name = "js-sys" -version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] +version = "0.3.103" [[package]] +checksum = "ce93912abc8220a3fdb768b2c4a826a7a9a4b1599cfb4d760d422eaa1faf88c7" +dependencies = ["ahash", "bytecount", "data-encoding", "email_address", "fancy-regex", "fraction", "getrandom 0.3.4", "idna", "itoa", "jsonschema-regex", "jsonschema-value", "num-cmp", "num-traits", "percent-encoding", "referencing", "regex", "serde", "serde_json", "strum", "unicode-general-category", "uuid-simd"] name = "jsonschema" -version = "0.49.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce93912abc8220a3fdb768b2c4a826a7a9a4b1599cfb4d760d422eaa1faf88c7" -dependencies = [ - "ahash", - "bytecount", - "data-encoding", - "email_address", - "fancy-regex", - "fraction", - "getrandom 0.3.4", - "idna", - "itoa", - "jsonschema-regex", - "jsonschema-value", - "num-cmp", - "num-traits", - "percent-encoding", - "referencing", - "regex", - "serde", - "serde_json", - "strum", - "unicode-general-category", - "uuid-simd", -] +version = "0.49.8" [[package]] +checksum = "8165657ebed4d32c50f3c250c1986d8428b16fbfeac355222e8fec50aa26eb1d" +dependencies = ["regex-syntax"] name = "jsonschema-regex" -version = "0.49.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8165657ebed4d32c50f3c250c1986d8428b16fbfeac355222e8fec50aa26eb1d" -dependencies = [ - "regex-syntax", -] +version = "0.49.8" [[package]] +checksum = "b069c5fdda3e9c2242bba49d811d5bbdb488abd8d234ed44a6312fd2891113e1" +dependencies = ["ahash", "bytecount", "fraction", "num-cmp", "num-traits", "serde_json"] name = "jsonschema-value" -version = "0.49.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b069c5fdda3e9c2242bba49d811d5bbdb488abd8d234ed44a6312fd2891113e1" -dependencies = [ - "ahash", - "bytecount", - "fraction", - "num-cmp", - "num-traits", - "serde_json", -] +version = "0.49.8" [[package]] +checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" +dependencies = ["kqueue-sys", "libc"] name = "kqueue" -version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" -dependencies = [ - "kqueue-sys", - "libc", -] +version = "1.2.0" [[package]] +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = ["bitflags", "libc"] name = "kqueue-sys" -version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" -dependencies = [ - "bitflags", - "libc", -] +version = "1.1.2" [[package]] +checksum = "b609e7ca5ea38f093c20a4a102335b247221c9643b7a6bc3510f196f99499a9e" +dependencies = ["serde", "static_assertions"] name = "kstring" -version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b609e7ca5ea38f093c20a4a102335b247221c9643b7a6bc3510f196f99499a9e" -dependencies = [ - "serde", - "static_assertions", -] +version = "2.0.4" [[package]] +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" name = "lazy_static" -version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +version = "1.5.0" [[package]] +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" name = "libc" -version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +version = "0.2.189" [[package]] +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = ["arbitrary", "cc"] name = "libfuzzer-sys" -version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" -dependencies = [ - "arbitrary", - "cc", -] +version = "0.4.13" [[package]] +checksum = "f6c19a05435c21ac299d71b6a9c13db3e3f47c520517d58990a462a1397a61db" +dependencies = ["cc", "pkg-config", "vcpkg"] name = "libsqlite3-sys" -version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6c19a05435c21ac299d71b6a9c13db3e3f47c520517d58990a462a1397a61db" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] +version = "0.38.1" [[package]] +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" name = "linux-raw-sys" -version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +version = "0.12.1" [[package]] +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" name = "litemap" -version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +version = "0.8.2" [[package]] +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = ["scopeguard"] name = "lock_api" -version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] +version = "0.4.14" [[package]] +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" name = "log" -version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +version = "0.4.33" [[package]] +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" name = "lru-slab" -version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +version = "0.1.2" [[package]] +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" name = "matchit" -version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +version = "0.8.4" [[package]] +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" name = "memchr" -version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +version = "2.8.3" [[package]] +checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74" name = "micromap" -version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74" +version = "0.3.0" [[package]] +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" name = "mime" -version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +version = "0.3.17" [[package]] +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = ["libc", "log", "wasi", "windows-sys 0.61.2"] name = "mio" -version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.61.2", -] +version = "1.2.2" [[package]] +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = ["bitflags", "cfg-if", "cfg_aliases", "libc"] name = "nix" -version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", -] +version = "0.30.1" [[package]] +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = ["bitflags", "cfg-if", "cfg_aliases", "libc"] name = "nix" -version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" -dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", -] +version = "0.31.3" [[package]] +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" name = "nohash-hasher" -version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +version = "0.2.0" [[package]] +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = ["bitflags", "fsevent-sys", "inotify", "kqueue", "libc", "log", "mio", "notify-types", "walkdir", "windows-sys 0.60.2"] name = "notify" -version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" -dependencies = [ - "bitflags", - "fsevent-sys", - "inotify", - "kqueue", - "libc", - "log", - "mio", - "notify-types", - "walkdir", - "windows-sys 0.60.2", -] +version = "8.2.0" [[package]] +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = ["bitflags"] name = "notify-types" -version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" -dependencies = [ - "bitflags", -] +version = "2.1.0" [[package]] +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = ["winapi"] name = "ntapi" -version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" -dependencies = [ - "winapi", -] +version = "0.4.3" [[package]] +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = ["num-bigint", "num-complex", "num-integer", "num-iter", "num-rational", "num-traits"] name = "num" -version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] +version = "0.4.3" [[package]] +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = ["num-integer", "num-traits"] name = "num-bigint" -version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" -dependencies = [ - "num-integer", - "num-traits", -] +version = "0.4.8" [[package]] +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" name = "num-cmp" -version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" +version = "0.1.0" [[package]] +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = ["num-traits"] name = "num-complex" -version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] +version = "0.4.6" [[package]] +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" name = "num-conv" -version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" +version = "0.2.2" [[package]] +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = ["num-traits"] name = "num-integer" -version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] +version = "0.1.46" [[package]] +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = ["num-integer", "num-traits"] name = "num-iter" -version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" -dependencies = [ - "num-integer", - "num-traits", -] +version = "0.1.46" [[package]] +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = ["num-bigint", "num-integer", "num-traits"] name = "num-rational" -version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] +version = "0.4.2" [[package]] +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = ["autocfg"] name = "num-traits" -version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] +version = "0.2.19" [[package]] +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = ["bitflags"] name = "objc2-core-foundation" -version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags", -] +version = "0.3.2" [[package]] +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = ["libc", "objc2-core-foundation"] name = "objc2-io-kit" -version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" -dependencies = [ - "libc", - "objc2-core-foundation", -] +version = "0.3.2" [[package]] +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = ["memchr"] name = "object" -version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" -dependencies = [ - "memchr", -] +version = "0.39.1" [[package]] +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" name = "once_cell" -version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +version = "1.21.4" [[package]] +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" name = "once_cell_polyfill" -version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +version = "1.70.2" [[package]] +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" name = "openssl-probe" -version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +version = "0.2.1" [[package]] +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" name = "outref" -version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" +version = "0.5.2" [[package]] +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = ["lock_api", "parking_lot_core"] name = "parking_lot" -version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] +version = "0.12.5" [[package]] +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = ["cfg-if", "libc", "redox_syscall", "smallvec", "windows-link"] name = "parking_lot_core" -version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] +version = "0.9.12" [[package]] +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" name = "pastey" -version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" +version = "0.2.3" [[package]] +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" name = "percent-encoding" -version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +version = "2.3.2" [[package]] +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" name = "pin-project-lite" -version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +version = "0.2.17" [[package]] +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" name = "pkg-config" -version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +version = "0.3.33" [[package]] +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = ["zerovec"] name = "potential_utf" -version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] +version = "0.1.5" [[package]] +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" name = "powerfmt" -version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +version = "0.2.0" [[package]] +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = ["zerocopy"] name = "ppv-lite86" -version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] +version = "0.2.21" [[package]] +checksum = "17e0a4425d076f0718b820673a38fbf3747080c61017eeb0dd79bc7e472b8bb8" name = "pratt" -version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17e0a4425d076f0718b820673a38fbf3747080c61017eeb0dd79bc7e472b8bb8" +version = "0.4.0" [[package]] +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = ["unicode-ident"] name = "proc-macro2" -version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] +version = "1.0.107" [[package]] +checksum = "2e842efad9119158434d193c6682e2ebee4b44d6ad801d7b349623b3f57cdf55" +dependencies = ["futures", "indexmap", "nix 0.31.3", "tokio", "tracing", "windows"] name = "process-wrap" -version = "9.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e842efad9119158434d193c6682e2ebee4b44d6ad801d7b349623b3f57cdf55" -dependencies = [ - "futures", - "indexmap", - "nix 0.31.3", - "tokio", - "tracing", - "windows", -] +version = "9.1.0" [[package]] +checksum = "c10e0ce3022eceb3fd55e68577249e530fb2d2cfed8a585eb52fd90ab994e734" name = "proto-parser" -version = "1.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c10e0ce3022eceb3fd55e68577249e530fb2d2cfed8a585eb52fd90ab994e734" +version = "1.14.3" [[package]] +checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" +dependencies = ["ar_archive_writer", "cc"] name = "psm" -version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" -dependencies = [ - "ar_archive_writer", - "cc", -] +version = "0.1.31" [[package]] +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" +dependencies = ["bitflags", "getopts", "memchr", "pulldown-cmark-escape", "unicase"] name = "pulldown-cmark" -version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" -dependencies = [ - "bitflags", - "getopts", - "memchr", - "pulldown-cmark-escape", - "unicase", -] +version = "0.13.4" [[package]] +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" name = "pulldown-cmark-escape" -version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" +version = "0.11.0" [[package]] +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = ["bytes", "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", "rustc-hash", "rustls", "socket2", "thiserror 2.0.19", "tokio", "tracing", "web-time"] name = "quinn" -version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.19", - "tokio", - "tracing", - "web-time", -] +version = "0.11.11" [[package]] +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = ["aws-lc-rs", "bytes", "getrandom 0.4.3", "lru-slab", "rand 0.10.2", "rand_pcg", "ring", "rustc-hash", "rustls", "rustls-pki-types", "slab", "thiserror 2.0.19", "tinyvec", "tracing", "web-time"] name = "quinn-proto" -version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.4.3", - "lru-slab", - "rand 0.10.2", - "rand_pcg", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.19", - "tinyvec", - "tracing", - "web-time", -] +version = "0.11.16" [[package]] +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = ["cfg_aliases", "libc", "once_cell", "socket2", "tracing", "windows-sys 0.61.2"] name = "quinn-udp" -version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.61.2", -] +version = "0.5.15" [[package]] +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = ["proc-macro2"] name = "quote" -version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] +version = "1.0.47" [[package]] +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" name = "r-efi" -version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +version = "5.3.0" [[package]] +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" name = "r-efi" -version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +version = "6.0.0" [[package]] +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = ["rand_chacha", "rand_core 0.9.5"] name = "rand" -version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" -dependencies = [ - "rand_chacha", - "rand_core 0.9.5", -] +version = "0.9.5" [[package]] +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = ["chacha20", "getrandom 0.4.3", "rand_core 0.10.1"] name = "rand" -version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" -dependencies = [ - "chacha20", - "getrandom 0.4.3", - "rand_core 0.10.1", -] +version = "0.10.2" [[package]] +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = ["ppv-lite86", "rand_core 0.9.5"] name = "rand_chacha" -version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] +version = "0.9.0" [[package]] +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = ["getrandom 0.3.4"] name = "rand_core" -version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] +version = "0.9.5" [[package]] +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" name = "rand_core" -version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +version = "0.10.1" [[package]] +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = ["rand_core 0.10.1"] name = "rand_pcg" -version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" -dependencies = [ - "rand_core 0.10.1", -] +version = "0.10.2" [[package]] +checksum = "0786a43debb760f491b1bc0269fe5e84155353c67482b9e60d0cfb596054b43e" +dependencies = ["recursive-proc-macro-impl", "stacker"] name = "recursive" -version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0786a43debb760f491b1bc0269fe5e84155353c67482b9e60d0cfb596054b43e" -dependencies = [ - "recursive-proc-macro-impl", - "stacker", -] +version = "0.1.1" [[package]] +checksum = "76009fbe0614077fc1a2ce255e3a1881a2e3a3527097d5dc6d8212c585e7e38b" +dependencies = ["quote", "syn 2.0.119"] name = "recursive-proc-macro-impl" -version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76009fbe0614077fc1a2ce255e3a1881a2e3a3527097d5dc6d8212c585e7e38b" -dependencies = [ - "quote", - "syn 2.0.119", -] +version = "0.1.1" [[package]] +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = ["bitflags"] name = "redox_syscall" -version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] +version = "0.5.18" [[package]] +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = ["ref-cast-impl"] name = "ref-cast" -version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" -dependencies = [ - "ref-cast-impl", -] +version = "1.0.26" [[package]] +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = ["proc-macro2", "quote", "syn 3.0.3"] name = "ref-cast-impl" -version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] +version = "1.0.26" [[package]] +checksum = "f39f4c36ce0f50e96fb740d895f1cad34cfa76c4ab5c36934d6590bcd7029087" +dependencies = ["ahash", "fluent-uri", "getrandom 0.3.4", "hashbrown 0.17.1", "itoa", "micromap", "parking_lot", "percent-encoding", "serde_json"] name = "referencing" -version = "0.49.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f39f4c36ce0f50e96fb740d895f1cad34cfa76c4ab5c36934d6590bcd7029087" -dependencies = [ - "ahash", - "fluent-uri", - "getrandom 0.3.4", - "hashbrown 0.17.1", - "itoa", - "micromap", - "parking_lot", - "percent-encoding", - "serde_json", -] +version = "0.49.8" [[package]] +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = ["aho-corasick", "memchr", "regex-automata", "regex-syntax"] name = "regex" -version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] +version = "1.13.1" [[package]] +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = ["aho-corasick", "memchr", "regex-syntax"] name = "regex-automata" -version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] +version = "0.4.18" [[package]] +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" name = "regex-syntax" -version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +version = "0.8.11" [[package]] +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = ["base64 0.22.1", "bytes", "futures-core", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", "quinn", "rustls", "rustls-pki-types", "rustls-platform-verifier", "serde", "serde_json", "sync_wrapper", "tokio", "tokio-rustls", "tower", "tower-http 0.6.11", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys"] name = "reqwest" -version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "serde", - "serde_json", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http 0.6.11", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] +version = "0.13.4" [[package]] +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = ["cc", "cfg-if", "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0"] name = "ring" -version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] +version = "0.17.14" [[package]] +checksum = "ad26b216c966e987e80e86daf784a455c039c43d98575ceed57b8faa259e5695" +dependencies = ["async-trait", "base64 0.23.0", "chrono", "futures", "pastey", "pin-project-lite", "process-wrap", "rmcp-macros", "schemars", "serde", "serde_json", "thiserror 2.0.19", "tokio", "tokio-stream", "tokio-util", "tracing", "uuid", "which"] name = "rmcp" -version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad26b216c966e987e80e86daf784a455c039c43d98575ceed57b8faa259e5695" -dependencies = [ - "async-trait", - "base64 0.23.0", - "chrono", - "futures", - "pastey", - "pin-project-lite", - "process-wrap", - "rmcp-macros", - "schemars", - "serde", - "serde_json", - "thiserror 2.0.19", - "tokio", - "tokio-stream", - "tokio-util", - "tracing", - "uuid", - "which", -] +version = "3.1.0" [[package]] +checksum = "41bc748630c2be2a71b614c2f40d27bc0df0060696d224e1692c72345b7e0b79" +dependencies = ["darling", "proc-macro2", "quote", "serde_json", "syn 2.0.119"] name = "rmcp-macros" -version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41bc748630c2be2a71b614c2f40d27bc0df0060696d224e1692c72345b7e0b79" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "serde_json", - "syn 2.0.119", -] +version = "3.1.0" [[package]] +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" +dependencies = ["hashbrown 0.16.1", "thiserror 2.0.19"] name = "rsqlite-vfs" -version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" -dependencies = [ - "hashbrown 0.16.1", - "thiserror 2.0.19", -] +version = "0.1.1" [[package]] +checksum = "11438310b19e3109b6446c33d1ed5e889428cf2e278407bc7896bc4aaea43323" +dependencies = ["bitflags", "fallible-iterator", "fallible-streaming-iterator", "hashlink", "libsqlite3-sys", "smallvec", "sqlite-wasm-rs"] name = "rusqlite" -version = "0.40.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11438310b19e3109b6446c33d1ed5e889428cf2e278407bc7896bc4aaea43323" -dependencies = [ - "bitflags", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "smallvec", - "sqlite-wasm-rs", -] +version = "0.40.1" [[package]] +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" name = "rustc-hash" -version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" +version = "2.1.3" [[package]] +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = ["semver"] name = "rustc_version" -version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] +version = "0.4.1" [[package]] +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = ["bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.61.2"] name = "rustix" -version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] +version = "1.1.4" [[package]] +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = ["aws-lc-rs", "once_cell", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize"] name = "rustls" -version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" -dependencies = [ - "aws-lc-rs", - "once_cell", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] +version = "0.23.43" [[package]] +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = ["openssl-probe", "rustls-pki-types", "schannel", "security-framework"] name = "rustls-native-certs" -version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] +version = "0.8.4" [[package]] +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = ["web-time", "zeroize"] name = "rustls-pki-types" -version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" -dependencies = [ - "web-time", - "zeroize", -] +version = "1.15.1" [[package]] +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = ["core-foundation", "core-foundation-sys", "jni", "log", "once_cell", "rustls", "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki", "security-framework", "security-framework-sys", "webpki-root-certs", "windows-sys 0.61.2"] name = "rustls-platform-verifier" -version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" -dependencies = [ - "core-foundation", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.61.2", -] +version = "0.7.0" [[package]] +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" name = "rustls-platform-verifier-android" -version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" +version = "0.1.1" [[package]] +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = ["aws-lc-rs", "ring", "rustls-pki-types", "untrusted"] name = "rustls-webpki" -version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -dependencies = [ - "aws-lc-rs", - "ring", - "rustls-pki-types", - "untrusted", -] +version = "0.103.13" [[package]] +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" name = "rustversion" -version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +version = "1.0.23" [[package]] +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" name = "ryu" -version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +version = "1.0.23" [[package]] +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = ["winapi-util"] name = "same-file" -version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] +version = "1.0.6" [[package]] +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = ["windows-sys 0.61.2"] name = "schannel" -version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" -dependencies = [ - "windows-sys 0.61.2", -] +version = "0.1.29" [[package]] +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = ["chrono", "dyn-clone", "ref-cast", "schemars_derive", "serde", "serde_json"] name = "schemars" -version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" -dependencies = [ - "chrono", - "dyn-clone", - "ref-cast", - "schemars_derive", - "serde", - "serde_json", -] +version = "1.2.2" [[package]] +checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" +dependencies = ["proc-macro2", "quote", "serde_derive_internals", "syn 3.0.3"] name = "schemars_derive" -version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 3.0.3", -] +version = "1.2.2" [[package]] +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" name = "scopeguard" -version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +version = "1.2.0" [[package]] +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = ["bitflags", "core-foundation", "core-foundation-sys", "libc", "security-framework-sys"] name = "security-framework" -version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] +version = "3.7.0" [[package]] +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = ["core-foundation-sys", "libc"] name = "security-framework-sys" -version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" -dependencies = [ - "core-foundation-sys", - "libc", -] +version = "2.17.0" [[package]] +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = ["serde", "serde_core"] name = "semver" -version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" -dependencies = [ - "serde", - "serde_core", -] +version = "1.0.28" [[package]] +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = ["serde_core", "serde_derive"] name = "serde" -version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", - "serde_derive", -] +version = "1.0.229" [[package]] +checksum = "5bb49c0aa8a5bb88c00b833c11bae50d1611fb89e01336f53b05f7c643b1c883" +dependencies = ["annotate-snippets", "base64 0.22.1", "encoding_rs_io", "granit-parser", "nohash-hasher", "num-traits", "serde_core", "smallvec", "zmij"] name = "serde-saphyr" -version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb49c0aa8a5bb88c00b833c11bae50d1611fb89e01336f53b05f7c643b1c883" -dependencies = [ - "annotate-snippets", - "base64 0.23.0", - "encoding_rs_io", - "granit-parser", - "nohash-hasher", - "num-traits", - "serde_core", - "smallvec", - "zmij", -] +version = "1.0.0" [[package]] +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = ["serde_derive"] name = "serde_core" -version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] +version = "1.0.229" [[package]] +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = ["proc-macro2", "quote", "syn 3.0.3"] name = "serde_derive" -version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] +version = "1.0.229" [[package]] +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" +dependencies = ["proc-macro2", "quote", "syn 3.0.3"] name = "serde_derive_internals" -version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] +version = "0.30.0" [[package]] +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = ["indexmap", "itoa", "memchr", "serde", "serde_core", "zmij"] name = "serde_json" -version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "indexmap", - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] +version = "1.0.151" [[package]] +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = ["itoa", "serde", "serde_core"] name = "serde_path_to_error" -version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" -dependencies = [ - "itoa", - "serde", - "serde_core", -] +version = "0.1.20" [[package]] +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = ["form_urlencoded", "itoa", "ryu", "serde"] name = "serde_urlencoded" -version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] +version = "0.7.1" [[package]] +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" name = "shlex" -version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +version = "2.0.1" [[package]] +checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d" +dependencies = ["libc", "signal-hook-registry"] name = "signal-hook" -version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d" -dependencies = [ - "libc", - "signal-hook-registry", -] +version = "0.4.4" [[package]] +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = ["errno", "libc"] name = "signal-hook-registry" -version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] +version = "1.4.8" [[package]] +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = ["rustc_version", "simdutf8"] name = "simd_cesu8" -version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" -dependencies = [ - "rustc_version", - "simdutf8", -] +version = "1.2.0" [[package]] +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" name = "simdutf8" -version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +version = "0.1.5" [[package]] +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" name = "slab" -version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +version = "0.4.12" [[package]] +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" name = "smallvec" -version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +version = "1.15.2" [[package]] +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = ["libc", "windows-sys 0.61.2"] name = "socket2" -version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] +version = "0.6.5" [[package]] +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" +dependencies = ["cc", "js-sys", "rsqlite-vfs", "wasm-bindgen"] name = "sqlite-wasm-rs" -version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" -dependencies = [ - "cc", - "js-sys", - "rsqlite-vfs", - "wasm-bindgen", -] +version = "0.5.5" [[package]] +checksum = "13c6d1b651dc4edf07eead2a0c6c78016ce971bc2c10da5266861b13f25e7cec" +dependencies = ["log", "recursive"] name = "sqlparser" -version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c6d1b651dc4edf07eead2a0c6c78016ce971bc2c10da5266861b13f25e7cec" -dependencies = [ - "log", - "recursive", -] +version = "0.62.0" [[package]] +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" name = "stable_deref_trait" -version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +version = "1.2.1" [[package]] +checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190" +dependencies = ["cc", "cfg-if", "libc", "psm", "windows-sys 0.61.2"] name = "stacker" -version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys 0.61.2", -] +version = "0.1.24" [[package]] +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" name = "static_assertions" -version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +version = "1.1.0" [[package]] +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" name = "streaming-iterator" -version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" +version = "0.1.9" [[package]] +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" name = "strsim" -version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +version = "0.11.1" [[package]] +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = ["strum_macros"] name = "strum" -version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" -dependencies = [ - "strum_macros", -] +version = "0.28.0" [[package]] +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = ["heck", "proc-macro2", "quote", "syn 2.0.119"] name = "strum_macros" -version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.119", -] +version = "0.28.0" [[package]] +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" name = "subtle" -version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +version = "2.6.1" [[package]] +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = ["proc-macro2", "quote", "unicode-ident"] name = "syn" -version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] +version = "2.0.119" [[package]] +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = ["proc-macro2", "quote", "unicode-ident"] name = "syn" -version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] +version = "3.0.3" [[package]] +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = ["futures-core"] name = "sync_wrapper" -version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] +version = "1.0.2" [[package]] +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = ["proc-macro2", "quote", "syn 2.0.119"] name = "synstructure" -version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] +version = "0.13.2" [[package]] +checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6" +dependencies = ["libc", "memchr", "ntapi", "objc2-core-foundation", "objc2-io-kit", "windows"] name = "sysinfo" -version = "0.39.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6" -dependencies = [ - "libc", - "memchr", - "ntapi", - "objc2-core-foundation", - "objc2-io-kit", - "windows", -] +version = "0.39.6" [[package]] +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = ["fastrand", "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2"] name = "tempfile" -version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.3", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] +version = "3.27.0" [[package]] +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = ["thiserror-impl 1.0.69"] name = "thiserror" -version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] +version = "1.0.69" [[package]] +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = ["thiserror-impl 2.0.19"] name = "thiserror" -version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" -dependencies = [ - "thiserror-impl 2.0.19", -] +version = "2.0.19" [[package]] +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = ["proc-macro2", "quote", "syn 2.0.119"] name = "thiserror-impl" -version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] +version = "1.0.69" [[package]] +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = ["proc-macro2", "quote", "syn 3.0.3"] name = "thiserror-impl" -version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] +version = "2.0.19" [[package]] +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = ["deranged", "num-conv", "powerfmt", "serde_core", "time-core", "time-macros"] name = "time" -version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] +version = "0.3.55" [[package]] +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" name = "time-core" -version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" +version = "0.1.9" [[package]] +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = ["num-conv", "time-core"] name = "time-macros" -version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" -dependencies = [ - "num-conv", - "time-core", -] +version = "0.2.32" [[package]] +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = ["displaydoc", "zerovec"] name = "tinystr" -version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] +version = "0.8.3" [[package]] +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = ["tinyvec_macros"] name = "tinyvec" -version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" -dependencies = [ - "tinyvec_macros", -] +version = "1.12.0" [[package]] +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" name = "tinyvec_macros" -version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +version = "0.1.1" [[package]] +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = ["bytes", "libc", "mio", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2"] name = "tokio" -version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", -] +version = "1.53.1" [[package]] +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = ["proc-macro2", "quote", "syn 3.0.3"] name = "tokio-macros" -version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] +version = "2.7.2" [[package]] +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = ["rustls", "tokio"] name = "tokio-rustls" -version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] +version = "0.26.4" [[package]] +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = ["futures-core", "pin-project-lite", "tokio"] name = "tokio-stream" -version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] +version = "0.1.19" [[package]] +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = ["bytes", "futures-core", "futures-sink", "futures-util", "libc", "pin-project-lite", "tokio"] name = "tokio-util" -version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "futures-util", - "libc", - "pin-project-lite", - "tokio", -] +version = "0.7.19" [[package]] +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = ["futures-core", "futures-util", "pin-project-lite", "sync_wrapper", "tokio", "tokio-util", "tower-layer", "tower-service", "tracing"] name = "tower" -version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] +version = "0.5.3" [[package]] +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = ["bitflags", "bytes", "futures-util", "http", "http-body", "pin-project-lite", "tower", "tower-layer", "tower-service", "url"] name = "tower-http" -version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", - "url", -] +version = "0.6.11" [[package]] +checksum = "b11f75e912b0c2be01b63d8cf8057b8c3f97cf34abb3d431a3a4c8675498e233" +dependencies = ["bitflags", "bytes", "futures-util", "http", "http-body", "http-body-util", "percent-encoding", "pin-project-lite", "tokio", "tower-layer", "tower-service", "tracing"] name = "tower-http" -version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b11f75e912b0c2be01b63d8cf8057b8c3f97cf34abb3d431a3a4c8675498e233" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "percent-encoding", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] +version = "0.7.0" [[package]] +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" name = "tower-layer" -version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" +version = "0.3.3" [[package]] +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" name = "tower-service" -version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +version = "0.3.3" [[package]] +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = ["log", "pin-project-lite", "tracing-attributes", "tracing-core"] name = "tracing" -version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] +version = "0.1.44" [[package]] +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = ["proc-macro2", "quote", "syn 2.0.119"] name = "tracing-attributes" -version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] +version = "0.1.31" [[package]] +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = ["once_cell"] name = "tracing-core" -version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] +version = "0.1.36" [[package]] +checksum = "af1c71c1c4cc0920b20d6b0f6572e7682cd07a6a2faec71067a31fa394c586df" +dependencies = ["cc", "regex", "regex-syntax", "serde_json", "streaming-iterator", "tree-sitter-language"] name = "tree-sitter" -version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1c71c1c4cc0920b20d6b0f6572e7682cd07a6a2faec71067a31fa394c586df" -dependencies = [ - "cc", - "regex", - "regex-syntax", - "serde_json", - "streaming-iterator", - "tree-sitter-language", -] +version = "0.26.11" [[package]] +checksum = "c8560a4d2f835cc0d4d2c2e03cbd0dde2f6114b43bc491164238d333e28b16ea" +dependencies = ["cc", "tree-sitter-language"] name = "tree-sitter-go" -version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8560a4d2f835cc0d4d2c2e03cbd0dde2f6114b43bc491164238d333e28b16ea" -dependencies = [ - "cc", - "tree-sitter-language", -] +version = "0.25.0" [[package]] +checksum = "0aa6cbcdc8c679b214e616fd3300da67da0e492e066df01bcf5a5921a71e90d6" +dependencies = ["cc", "tree-sitter-language"] name = "tree-sitter-java" -version = "0.23.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa6cbcdc8c679b214e616fd3300da67da0e492e066df01bcf5a5921a71e90d6" -dependencies = [ - "cc", - "tree-sitter-language", -] +version = "0.23.5" [[package]] +checksum = "68204f2abc0627a90bdf06e605f5c470aa26fdcb2081ea553a04bdad756693f5" +dependencies = ["cc", "tree-sitter-language"] name = "tree-sitter-javascript" -version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68204f2abc0627a90bdf06e605f5c470aa26fdcb2081ea553a04bdad756693f5" -dependencies = [ - "cc", - "tree-sitter-language", -] +version = "0.25.0" [[package]] +checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782" name = "tree-sitter-language" -version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782" +version = "0.1.7" [[package]] +checksum = "6bf85fd39652e740bf60f46f4cda9492c3a9ad75880575bf14960f775cb74a1c" +dependencies = ["cc", "tree-sitter-language"] name = "tree-sitter-python" -version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf85fd39652e740bf60f46f4cda9492c3a9ad75880575bf14960f775cb74a1c" -dependencies = [ - "cc", - "tree-sitter-language", -] +version = "0.25.0" [[package]] +checksum = "439e577dbe07423ec2582ac62c7531120dbfccfa6e5f92406f93dd271a120e45" +dependencies = ["cc", "tree-sitter-language"] name = "tree-sitter-rust" -version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439e577dbe07423ec2582ac62c7531120dbfccfa6e5f92406f93dd271a120e45" -dependencies = [ - "cc", - "tree-sitter-language", -] +version = "0.24.2" [[package]] +checksum = "6c5f76ed8d947a75cc446d5fccd8b602ebf0cde64ccf2ffa434d873d7a575eff" +dependencies = ["cc", "tree-sitter-language"] name = "tree-sitter-typescript" -version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5f76ed8d947a75cc446d5fccd8b602ebf0cde64ccf2ffa434d873d7a575eff" -dependencies = [ - "cc", - "tree-sitter-language", -] +version = "0.23.2" [[package]] +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" name = "try-lock" -version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +version = "0.2.5" [[package]] +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" name = "unicase" -version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +version = "2.9.0" [[package]] +checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" name = "unicode-general-category" -version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" +version = "1.1.0" [[package]] +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" name = "unicode-ident" -version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +version = "1.0.24" [[package]] +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" name = "unicode-width" -version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +version = "0.2.2" [[package]] +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" name = "untrusted" -version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +version = "0.9.0" [[package]] +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = ["form_urlencoded", "idna", "percent-encoding", "serde"] name = "url" -version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] +version = "2.5.8" [[package]] +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" name = "utf8_iter" -version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +version = "1.0.4" [[package]] +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" name = "utf8parse" -version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +version = "0.2.2" [[package]] +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = ["getrandom 0.4.3", "js-sys", "wasm-bindgen"] name = "uuid" -version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" -dependencies = [ - "getrandom 0.4.3", - "js-sys", - "wasm-bindgen", -] +version = "1.24.0" [[package]] +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = ["outref", "vsimd"] name = "uuid-simd" -version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" -dependencies = [ - "outref", - "vsimd", -] +version = "0.8.0" [[package]] +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" name = "vcpkg" -version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +version = "0.2.15" [[package]] +checksum = "58c1dc449b873236909c7f325adf395071c209047737d831183e95c692413adf" +dependencies = ["serde"] name = "vecmap-rs" -version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58c1dc449b873236909c7f325adf395071c209047737d831183e95c692413adf" -dependencies = [ - "serde", -] +version = "0.2.5" [[package]] +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" name = "version_check" -version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +version = "0.9.5" [[package]] +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" name = "vsimd" -version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" +version = "0.8.0" [[package]] +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = ["same-file", "winapi-util"] name = "walkdir" -version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] +version = "2.5.0" [[package]] +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = ["try-lock"] name = "want" -version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] +version = "0.3.1" [[package]] +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +version = "0.11.1+wasi-snapshot-preview1" [[package]] +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = ["wit-bindgen"] name = "wasip2" -version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] +version = "1.0.4+wasi-0.2.12" [[package]] +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = ["cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared"] name = "wasm-bindgen" -version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] +version = "0.2.126" [[package]] +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = ["js-sys", "wasm-bindgen"] name = "wasm-bindgen-futures" -version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" -dependencies = [ - "js-sys", - "wasm-bindgen", -] +version = "0.4.76" [[package]] +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = ["quote", "wasm-bindgen-macro-support"] name = "wasm-bindgen-macro" -version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] +version = "0.2.126" [[package]] +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = ["bumpalo", "proc-macro2", "quote", "syn 2.0.119", "wasm-bindgen-shared"] name = "wasm-bindgen-macro-support" -version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.119", - "wasm-bindgen-shared", -] +version = "0.2.126" [[package]] +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = ["unicode-ident"] name = "wasm-bindgen-shared" -version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" -dependencies = [ - "unicode-ident", -] +version = "0.2.126" [[package]] +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = ["js-sys", "wasm-bindgen"] name = "web-sys" -version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" -dependencies = [ - "js-sys", - "wasm-bindgen", -] +version = "0.3.103" [[package]] +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = ["js-sys", "wasm-bindgen"] name = "web-time" -version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] +version = "1.1.0" [[package]] +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = ["rustls-pki-types"] name = "webpki-root-certs" -version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" -dependencies = [ - "rustls-pki-types", -] +version = "1.0.9" [[package]] +checksum = "8f3ef584124b911bcc3875c2f1472e80f24361ceb789bd1c62b3e9a3df9ff43c" +dependencies = ["libc"] name = "which" -version = "8.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3ef584124b911bcc3875c2f1472e80f24361ceb789bd1c62b3e9a3df9ff43c" -dependencies = [ - "libc", -] +version = "8.0.5" [[package]] +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = ["winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu"] name = "winapi" -version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] +version = "0.3.9" [[package]] +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +version = "0.4.0" [[package]] +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = ["windows-sys 0.61.2"] name = "winapi-util" -version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] +version = "0.1.11" [[package]] +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +version = "0.4.0" [[package]] +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = ["windows-collections", "windows-core", "windows-future", "windows-numerics"] name = "windows" -version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-numerics", -] +version = "0.62.2" [[package]] +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = ["windows-core"] name = "windows-collections" -version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core", -] +version = "0.3.2" [[package]] +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = ["windows-implement", "windows-interface", "windows-link", "windows-result", "windows-strings"] name = "windows-core" -version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] +version = "0.62.2" [[package]] +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = ["windows-core", "windows-link", "windows-threading"] name = "windows-future" -version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core", - "windows-link", - "windows-threading", -] +version = "0.3.2" [[package]] +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = ["proc-macro2", "quote", "syn 2.0.119"] name = "windows-implement" -version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] +version = "0.60.2" [[package]] +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = ["proc-macro2", "quote", "syn 2.0.119"] name = "windows-interface" -version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] +version = "0.59.3" [[package]] +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" name = "windows-link" -version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +version = "0.2.1" [[package]] +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = ["windows-core", "windows-link"] name = "windows-numerics" -version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core", - "windows-link", -] +version = "0.3.1" [[package]] +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = ["windows-link"] name = "windows-result" -version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] +version = "0.4.1" [[package]] +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = ["windows-link"] name = "windows-strings" -version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] +version = "0.5.1" [[package]] +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = ["windows-targets 0.52.6"] name = "windows-sys" -version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] +version = "0.52.0" [[package]] +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = ["windows-targets 0.53.5"] name = "windows-sys" -version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] +version = "0.60.2" [[package]] +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = ["windows-link"] name = "windows-sys" -version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] +version = "0.61.2" [[package]] +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = ["windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6"] name = "windows-targets" -version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] +version = "0.52.6" [[package]] +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = ["windows-link", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", "windows_i686_gnullvm 0.53.1", "windows_i686_msvc 0.53.1", "windows_x86_64_gnu 0.53.1", "windows_x86_64_gnullvm 0.53.1", "windows_x86_64_msvc 0.53.1"] name = "windows-targets" -version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] +version = "0.53.5" [[package]] +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = ["windows-link"] name = "windows-threading" -version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link", -] +version = "0.2.1" [[package]] +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" name = "windows_aarch64_gnullvm" -version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +version = "0.52.6" [[package]] +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" name = "windows_aarch64_gnullvm" -version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +version = "0.53.1" [[package]] +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" name = "windows_aarch64_msvc" -version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +version = "0.52.6" [[package]] +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" name = "windows_aarch64_msvc" -version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +version = "0.53.1" [[package]] +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" name = "windows_i686_gnu" -version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +version = "0.52.6" [[package]] +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" name = "windows_i686_gnu" -version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +version = "0.53.1" [[package]] +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" name = "windows_i686_gnullvm" -version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +version = "0.52.6" [[package]] +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" name = "windows_i686_gnullvm" -version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +version = "0.53.1" [[package]] +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" name = "windows_i686_msvc" -version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +version = "0.52.6" [[package]] +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" name = "windows_i686_msvc" -version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +version = "0.53.1" [[package]] +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" name = "windows_x86_64_gnu" -version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +version = "0.52.6" [[package]] +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" name = "windows_x86_64_gnu" -version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +version = "0.53.1" [[package]] +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" name = "windows_x86_64_gnullvm" -version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +version = "0.52.6" [[package]] +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" name = "windows_x86_64_gnullvm" -version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +version = "0.53.1" [[package]] +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" name = "windows_x86_64_msvc" -version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +version = "0.52.6" [[package]] +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" name = "windows_x86_64_msvc" -version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +version = "0.53.1" [[package]] +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = ["memchr"] name = "winnow" -version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" -dependencies = [ - "memchr", -] +version = "1.0.4" [[package]] +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" name = "wit-bindgen" -version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +version = "0.57.1" [[package]] +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" name = "writeable" -version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +version = "0.6.3" [[package]] +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = ["stable_deref_trait", "yoke-derive", "zerofrom"] name = "yoke" -version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] +version = "0.8.3" [[package]] +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = ["proc-macro2", "quote", "syn 2.0.119", "synstructure"] name = "yoke-derive" -version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "synstructure", -] +version = "0.8.2" [[package]] +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = ["zerocopy-derive"] name = "zerocopy" -version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" -dependencies = [ - "zerocopy-derive", -] +version = "0.8.55" [[package]] +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = ["proc-macro2", "quote", "syn 2.0.119"] name = "zerocopy-derive" -version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] +version = "0.8.55" [[package]] +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = ["zerofrom-derive"] name = "zerofrom" -version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -dependencies = [ - "zerofrom-derive", -] +version = "0.1.8" [[package]] +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = ["proc-macro2", "quote", "syn 2.0.119", "synstructure"] name = "zerofrom-derive" -version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "synstructure", -] +version = "0.1.7" [[package]] +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" name = "zeroize" -version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +version = "1.9.0" [[package]] +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = ["displaydoc", "yoke", "zerofrom"] name = "zerotrie" -version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] +version = "0.2.4" [[package]] +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = ["yoke", "zerofrom", "zerovec-derive"] name = "zerovec" -version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] +version = "0.11.6" [[package]] +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = ["proc-macro2", "quote", "syn 2.0.119"] name = "zerovec-derive" -version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] +version = "0.11.3" [[package]] +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" name = "zmij" -version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" +version = "1.0.23" diff --git a/Cargo.toml b/Cargo.toml index c9e4f41..aaac2ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ default-members = ["crates/*"] resolver = "3" [workspace.package] -version = "1.0.3" +version = "1.1.0" edition = "2024" rust-version = "1.97.1" description = "Local cross-repository code intelligence and dependency graph for impact analysis and AI coding agents." diff --git a/README.md b/README.md index 8e197c0..c1d3e86 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Map APIs, events, schemas, packages, databases, and ownership across repositorie breaks another service. [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE) -![Source version](https://img.shields.io/badge/source-v1.0.3-orange.svg) +![Source version](https://img.shields.io/badge/source-v1.1.0-orange.svg) [![crates.io](https://img.shields.io/crates/v/code-system-graph.svg)](https://crates.io/crates/code-system-graph) ![Platforms](https://img.shields.io/badge/validated-Linux%20%7C%20macOS%20%7C%20Windows-1793d1.svg) ![Privacy](https://img.shields.io/badge/privacy-local%20%7C%20no%20telemetry-2ea44f.svg) @@ -268,11 +268,18 @@ Code System Graph exposes a local MCP server. For Codex: ```bash codex mcp add code-system-graph -- \ csgraph mcp \ + --config /absolute/path/to/my-project/code-system-graph.yaml \ --codegraph \ --workspace my-project \ --database /absolute/path/to/my-project/.code-system-graph/code-system-graph.db ``` +MCP tools return one bounded Markdown text block and resources use `text/markdown`. HTTP and CLI +retain typed schema-v2 JSON. Explore is the only source-bearing response and combines ephemeral +source, symbols, callers/callees, federated evidence handoffs, coverage, and grounded next actions. +Final MCP tool, resource, and schema-catalog budgets must each be at least 256 bytes so status and +truncation controls remain representable. + For clients that support [Agent Plugins](https://agent-plugins.org/), generate one portable, workspace-bound package plus its ignored local binding instead of configuring MCP and routing guidance separately: @@ -321,7 +328,7 @@ plugin root. Re-run with `--replace-generated` after local paths change; only a Code System Graph's recognized ownership identity can be replaced. Until local binding creation runs, that MCP entry fails visibly while independent plugin skills and servers remain usable. -The generated MCP is read-only and requires `csgraph 1.0.3` in `PATH`; it does not bundle binaries. +The generated MCP is read-only and requires `csgraph 1.1.0` in `PATH`; it does not bundle binaries. Its plugin, server, and skill share a stable name derived from the declared workspace name, so clones produce the same versioned files. Install it project-locally for workspace-only activation; the generated skill also requires the nearest manifest and MCP `status` to report that workspace. @@ -411,7 +418,7 @@ discovery rules without opening a database. Search for the contract, select its stable node ID, and inspect upstream impact: ```bash -csgraph query "POST /orders" --workspace my-project --database .code-system-graph/code-system-graph.db +csgraph query "POST /orders" --config code-system-graph.yaml --workspace my-project --database .code-system-graph/code-system-graph.db csgraph impact --target --workspace my-project --database .code-system-graph/code-system-graph.db ``` @@ -437,6 +444,8 @@ For a complete first workspace, including multi-repository layout and expected o - Local workflows run without telemetry or required network access. - Source bodies and secret values are not persisted. +- An MCP host may retain requests and responses, including ephemeral Explore source, in its own + conversation history; configure host retention separately. - SQLite snapshots are local and atomically replaced. - Remote pull-request access is disabled by default and requires explicit enablement and consent. - MCP is read-only by default; administrative tools require an explicit server flag. diff --git a/crates/code-system-graph-cli/Cargo.toml b/crates/code-system-graph-cli/Cargo.toml index 50742f4..84af0f4 100644 --- a/crates/code-system-graph-cli/Cargo.toml +++ b/crates/code-system-graph-cli/Cargo.toml @@ -31,10 +31,10 @@ axum = "0.8.9" blake3 = "1.8.5" clap = { version = "4.6.4", features = ["derive"] } clap_complete = "4.6.8" -code-system-graph-core = { version = "1.0.3", path = "../code-system-graph-core" } -code-system-graph-hooks = { version = "1.0.3", path = "../code-system-graph-hooks" } -code-system-graph-model = { version = "1.0.3", path = "../code-system-graph-model" } -code-system-graph-store-sqlite = { version = "1.0.3", path = "../code-system-graph-store-sqlite" } +code-system-graph-core = { version = "1.1.0", path = "../code-system-graph-core" } +code-system-graph-hooks = { version = "1.1.0", path = "../code-system-graph-hooks" } +code-system-graph-model = { version = "1.1.0", path = "../code-system-graph-model" } +code-system-graph-store-sqlite = { version = "1.1.0", path = "../code-system-graph-store-sqlite" } rmcp = { version = "3.1.0", features = ["transport-io"] } notify = "8.2.0" rusqlite = { version = "0.40.1", features = ["bundled"] } diff --git a/crates/code-system-graph-cli/src/agent_markdown.rs b/crates/code-system-graph-cli/src/agent_markdown.rs new file mode 100644 index 0000000..cb7a820 --- /dev/null +++ b/crates/code-system-graph-cli/src/agent_markdown.rs @@ -0,0 +1,816 @@ +//! Typed, bounded Markdown construction for agent-facing MCP delivery. + +use std::fmt::{Debug, Display}; + +use code_system_graph_model::{FreshnessSummary, ToolStatus}; +use serde_json::{Map, Value}; + +const TRUNCATION_NOTICE: &str = "\n## Truncation\n\nAdditional result content was omitted by the effective MCP response-byte limit.\n"; + +enum DocumentBlock { + Control(String), + Atomic(String), + Collection { + heading: String, + total: usize, + source_truncated: bool, + items: Vec, + }, + Source { + heading: String, + value: String, + }, +} + +impl DocumentBlock { + fn complete(&self) -> String { + match self { + Self::Control(value) | Self::Atomic(value) => value.clone(), + Self::Collection { + heading, + total, + source_truncated, + items, + } => collection_block(heading, *total, items.len(), *source_truncated, items), + Self::Source { + heading: source_heading, + value, + } => { + format!( + "\n## {}\n{}", + heading(source_heading), + fenced("text", value) + ) + } + } + } +} + +/// Internal typed Markdown document. Callers select concrete report fields before adding them; +/// this builder never reflects over serialized report objects. +pub(crate) struct MarkdownDocument { + blocks: Vec, + compact: String, +} + +impl MarkdownDocument { + pub(crate) fn fragment() -> Self { + Self { + blocks: Vec::new(), + compact: String::new(), + } + } + + pub(crate) fn tool( + name: &str, + schema_version: u32, + status: ToolStatus, + freshness: &FreshnessSummary, + warnings: &[String], + coverage_present: bool, + path: Option<&str>, + ) -> Self { + let status_text = tool_status(status); + let freshness_text = format!("{:?}", freshness.overall).to_lowercase(); + let warning = warnings.first().map_or_else( + || "none".to_owned(), + |value| compact_text(&inline(value), 18), + ); + let path = path.map_or_else( + || "absent".to_owned(), + |value| compact_text(&inline(value), 18), + ); + let compact = format!( + "# {}\nstatus={status_text} freshness={freshness_text} truncated=true\nwarning={warning}\ncoverage={} path={path}\n", + compact_text(&heading(name), 24), + if coverage_present { + "present" + } else { + "absent" + }, + ); + let mut document = Self { + blocks: vec![DocumentBlock::Control(format!("# {}\n", heading(name)))], + compact, + }; + document.blocks.push(DocumentBlock::Control(format!( + "\n## Status\n\n- State: `{status_text}`\n- Delivery schema: `{schema_version}`\n" + ))); + document.control_string_collection("Warnings", warnings); + document.blocks.push(DocumentBlock::Control(format!( + "\n## Freshness\n\n- Overall: `{freshness_text}`\n" + ))); + document.control_debug_collection("Stale repositories", &freshness.stale_repositories); + document.control_string_collection("Freshness reasons", &freshness.reasons); + document + } + + pub(crate) fn resource(name: &str, schema_version: u32) -> Self { + Self { + blocks: vec![ + DocumentBlock::Control(format!("# {}\n", heading(name))), + DocumentBlock::Control(format!( + "\n## Contract\n\n- Delivery schema: `{schema_version}`\n" + )), + ], + compact: format!( + "# {}\nschema={schema_version} truncated=true\n", + compact_text(&heading(name), 32) + ), + } + } + + pub(crate) fn scalar(&mut self, name: &str, value: impl Display) { + self.blocks.push(DocumentBlock::Atomic(format!( + "\n## {}\n\n`{}`\n", + heading(name), + inline(&value.to_string()) + ))); + } + + pub(crate) fn text(&mut self, name: &str, value: &str) { + self.blocks.push(DocumentBlock::Atomic(format!( + "\n## {}\n\n{}\n", + heading(name), + inline(value) + ))); + } + + pub(crate) fn debug(&mut self, name: &str, value: &impl Debug) { + self.text(name, &format!("{value:?}")); + } + + pub(crate) fn debug_collection(&mut self, name: &str, items: &[T]) { + self.bounded_fragments( + name, + items.len(), + false, + items + .iter() + .map(|item| format!("\n- {}\n", inline(&format!("{item:?}")))) + .collect(), + ); + } + + pub(crate) fn string_collection(&mut self, name: &str, items: &[String]) { + self.bounded_fragments( + name, + items.len(), + false, + items + .iter() + .map(|item| format!("\n- {}\n", inline(item))) + .collect(), + ); + } + + fn control_debug_collection(&mut self, name: &str, items: &[T]) { + self.control_collection_metadata(name, items.len(), items.len(), false); + for item in items { + self.blocks.push(DocumentBlock::Control(format!( + "\n- {}\n", + inline(&format!("{item:?}")) + ))); + } + } + + fn control_string_collection(&mut self, name: &str, items: &[String]) { + self.control_collection_metadata(name, items.len(), items.len(), false); + for item in items { + self.blocks + .push(DocumentBlock::Control(format!("\n- {}\n", inline(item)))); + } + } + + pub(crate) fn bounded_collection( + &mut self, + name: &str, + total: usize, + retained: usize, + truncated: bool, + items: &[T], + ) { + debug_assert_eq!(retained, items.len()); + self.bounded_fragments( + name, + total, + truncated, + items + .iter() + .map(|item| format!("\n- {}\n", inline(&format!("{item:?}")))) + .collect(), + ); + } + + pub(crate) fn bounded_fragments( + &mut self, + name: &str, + total: usize, + source_truncated: bool, + items: Vec, + ) { + self.blocks.push(DocumentBlock::Collection { + heading: name.to_owned(), + total, + source_truncated, + items, + }); + } + + fn control_collection_metadata( + &mut self, + name: &str, + total: usize, + retained: usize, + truncated: bool, + ) { + self.blocks.push(DocumentBlock::Control(format!( + "\n## {}\n\n- Total: `{total}`\n- Retained: `{retained}`\n- Truncated: `{truncated}`\n", + heading(name) + ))); + } + + pub(crate) fn source(&mut self, name: &str, value: &str) { + self.blocks.push(DocumentBlock::Source { + heading: name.to_owned(), + value: safe_multiline(value), + }); + } + + pub(crate) fn render(self, maximum: usize) -> String { + fit_document(self.blocks, maximum, &self.compact) + } + + pub(crate) fn into_complete(self) -> String { + self.blocks.iter().map(DocumentBlock::complete).collect() + } +} + +/// Renders the heterogeneous schema catalog with atomic fenced JSON entries. +pub(crate) fn render_schema_catalog(value: &Value, maximum: usize) -> String { + if let Some(object) = value.as_object() + && let Some(schemas) = object.get("schemas").and_then(Value::as_object) + { + return render_nested_schema_catalog(object, schemas, maximum); + } + let mut blocks = vec!["# Schema Catalog\n".to_owned()]; + if let Some(object) = value.as_object() { + for (name, schema) in object { + let encoded = + serde_json::to_string_pretty(schema).unwrap_or_else(|_| "null".to_owned()); + blocks.push(format!( + "\n## {}\n{}", + heading(name), + fenced("json", &encoded) + )); + } + } else { + let text = serde_json::to_string_pretty(value).unwrap_or_else(|_| "null".to_owned()); + blocks.push(fenced("json", &text)); + } + fit_atomic_blocks(blocks, maximum) +} + +fn fit_document(blocks: Vec, maximum: usize, compact: &str) -> String { + let complete = blocks + .iter() + .map(DocumentBlock::complete) + .collect::(); + if complete.len() <= maximum { + return complete; + } + let available = maximum.saturating_sub(TRUNCATION_NOTICE.len()); + let mut output = if compact.len() <= available { + compact.to_owned() + } else { + String::new() + }; + for block in blocks { + match block { + DocumentBlock::Control(_) => {} + DocumentBlock::Atomic(value) => { + if output.len().saturating_add(value.len()) <= available { + output.push_str(&value); + } + } + DocumentBlock::Collection { + heading, + total, + source_truncated, + items, + } => { + let remaining = available.saturating_sub(output.len()); + if let Some(value) = + bounded_collection_block(&heading, total, source_truncated, &items, remaining) + { + output.push_str(&value); + } + } + DocumentBlock::Source { heading, value } => { + let remaining = available.saturating_sub(output.len()); + if let Some(value) = bounded_source_block(&heading, &value, remaining) { + output.push_str(&value); + } + } + } + } + if output.len().saturating_add(TRUNCATION_NOTICE.len()) <= maximum { + output.push_str(TRUNCATION_NOTICE); + } + output +} + +fn collection_block( + name: &str, + total: usize, + retained: usize, + truncated: bool, + items: &[String], +) -> String { + format!( + "\n## {}\n\n- Total: `{total}`\n- Retained: `{retained}`\n- Truncated: `{truncated}`\n{}", + heading(name), + items.concat() + ) +} + +fn bounded_collection_block( + name: &str, + total: usize, + source_truncated: bool, + items: &[String], + maximum: usize, +) -> Option { + let mut retained = 0_usize; + let mut item_bytes = 0_usize; + for item in items { + let candidate_retained = retained + 1; + let metadata = collection_block( + name, + total, + candidate_retained, + source_truncated || candidate_retained < total, + &[], + ); + if metadata + .len() + .saturating_add(item_bytes) + .saturating_add(item.len()) + > maximum + { + break; + } + retained = candidate_retained; + item_bytes = item_bytes.saturating_add(item.len()); + } + let mut output = collection_block( + name, + total, + retained, + source_truncated || retained < total, + &[], + ); + if output.len().saturating_add(item_bytes) > maximum { + return None; + } + for item in items.iter().take(retained) { + output.push_str(item); + } + Some(output) +} + +fn fit_atomic_blocks(blocks: Vec, maximum: usize) -> String { + let complete = blocks.concat(); + if complete.len() <= maximum { + return complete; + } + let available = maximum.saturating_sub(TRUNCATION_NOTICE.len()); + let mut output = String::new(); + for block in blocks { + if output.len().saturating_add(block.len()) <= available { + output.push_str(&block); + } + } + if output.len().saturating_add(TRUNCATION_NOTICE.len()) <= maximum { + output.push_str(TRUNCATION_NOTICE); + } + output +} + +fn bounded_source_block(name: &str, value: &str, maximum: usize) -> Option { + let heading = format!("\n## {}\n\n", heading(name)); + let minimum = heading.len().saturating_add("```text\n\n```\n".len()); + if minimum > maximum { + return None; + } + + let mut boundary = 0_usize; + let mut current_backticks = 0_usize; + let mut longest_backticks = 0_usize; + for (offset, character) in value.char_indices() { + current_backticks = if character == '`' { + current_backticks + 1 + } else { + 0 + }; + longest_backticks = longest_backticks.max(current_backticks); + let next_boundary = offset + character.len_utf8(); + let fence_bytes = longest_backticks.max(2) + 1; + let candidate_bytes = heading + .len() + .saturating_add(next_boundary) + .saturating_add(fence_bytes.saturating_mul(2)) + .saturating_add("text\n\n\n".len()); + if candidate_bytes > maximum { + break; + } + boundary = next_boundary; + } + + let retained = &value[..boundary]; + let fence = fence_marker(retained); + Some(format!("{heading}{fence}text\n{retained}\n{fence}\n")) +} + +fn render_nested_schema_catalog( + object: &Map, + schemas: &Map, + maximum: usize, +) -> String { + let header = "# Schema Catalog\n".to_owned(); + let total = object + .get("schema_total") + .and_then(Value::as_u64) + .and_then(|value| usize::try_from(value).ok()) + .unwrap_or(schemas.len()) + .max(schemas.len()); + let source_truncated = object + .get("schemas_truncated") + .and_then(Value::as_bool) + .unwrap_or(total > schemas.len()); + let version = object + .get("schema_version") + .map_or_else(|| "_none_".to_owned(), compact_json); + let media_type = object + .get("media_type") + .map_or_else(|| "_none_".to_owned(), compact_json); + let schema_blocks = schemas + .iter() + .map(|(name, schema)| schema_catalog_entry(name, schema)) + .collect::>(); + let optional_blocks = object + .iter() + .filter(|(name, _)| { + !matches!( + name.as_str(), + "schema_version" + | "media_type" + | "schemas" + | "schema_total" + | "schema_retained" + | "schemas_truncated" + ) + }) + .map(|(name, value)| schema_catalog_entry(name, value)) + .collect::>(); + let complete_preamble = schema_catalog_preamble( + &version, + &media_type, + total, + schemas.len(), + source_truncated || total > schemas.len(), + ); + let complete_length = std::iter::once(&header) + .chain(std::iter::once(&complete_preamble)) + .chain(schema_blocks.iter()) + .chain(optional_blocks.iter()) + .fold(0_usize, |length, block| length.saturating_add(block.len())); + if complete_length <= maximum { + return std::iter::once(header) + .chain(std::iter::once(complete_preamble)) + .chain(schema_blocks) + .chain(optional_blocks) + .collect(); + } + + let available = maximum.saturating_sub(TRUNCATION_NOTICE.len()); + let mut retained_blocks = Vec::new(); + let mut retained_bytes = 0_usize; + for block in schema_blocks { + let candidate_count = retained_blocks.len() + 1; + let candidate_preamble = schema_catalog_preamble( + &version, + &media_type, + total, + candidate_count, + source_truncated || total > candidate_count, + ); + let candidate_length = header + .len() + .saturating_add(candidate_preamble.len()) + .saturating_add(retained_bytes) + .saturating_add(block.len()); + if candidate_length <= available { + retained_bytes = retained_bytes.saturating_add(block.len()); + retained_blocks.push(block); + } + } + let retained = retained_blocks.len(); + let preamble = schema_catalog_preamble( + &version, + &media_type, + total, + retained, + source_truncated || total > retained, + ); + let mut output = header; + output.push_str(&preamble); + for block in retained_blocks { + output.push_str(&block); + } + for block in optional_blocks { + if output.len().saturating_add(block.len()) <= available { + output.push_str(&block); + } + } + if output.len().saturating_add(TRUNCATION_NOTICE.len()) <= maximum { + output.push_str(TRUNCATION_NOTICE); + } + output +} + +fn schema_catalog_preamble( + version: &str, + media_type: &str, + total: usize, + retained: usize, + truncated: bool, +) -> String { + format!( + "\n## Catalog\n\n- Schema version: {version}\n- Media type: {media_type}\n- Schemas: total `{total}`, retained `{retained}`, truncated `{truncated}`\n" + ) +} + +fn schema_catalog_entry(name: &str, value: &Value) -> String { + let encoded = serde_json::to_string_pretty(value).unwrap_or_else(|_| "null".to_owned()); + format!("\n## {}\n{}", heading(name), fenced("json", &encoded)) +} + +fn compact_json(value: &Value) -> String { + match value { + Value::Null => "_none_".to_owned(), + Value::Bool(value) => format!("`{value}`"), + Value::Number(value) => format!("`{value}`"), + Value::String(value) => inline(value), + _ => "_structured item_".to_owned(), + } +} + +fn tool_status(status: ToolStatus) -> &'static str { + match status { + ToolStatus::Ok => "ok", + ToolStatus::Degraded => "degraded", + ToolStatus::Error => "error", + } +} + +fn compact_text(value: &str, maximum: usize) -> String { + let value = value.replace(['\n', '\r', '\t'], " "); + let boundary = utf8_boundary_at_or_before(&value, maximum); + value[..boundary].to_owned() +} + +fn utf8_boundary_at_or_before(value: &str, maximum: usize) -> usize { + let mut boundary = maximum.min(value.len()); + while boundary > 0 && !value.is_char_boundary(boundary) { + boundary -= 1; + } + boundary +} + +fn fenced(language: &str, value: &str) -> String { + let value = safe_multiline(value); + let fence = fence_marker(&value); + format!("\n{fence}{language}\n{value}\n{fence}\n") +} + +fn fence_marker(value: &str) -> String { + let longest = value + .split(|character| character != '`') + .map(str::len) + .max() + .unwrap_or(0); + "`".repeat(longest.max(2) + 1) +} + +fn heading(value: &str) -> String { + value + .chars() + .map(|character| match character { + '_' | '-' | '\n' | '\r' | '\t' | '\0' => ' ', + character => safe_character(character), + }) + .collect() +} + +fn inline(value: &str) -> String { + let mut output = String::with_capacity(value.len()); + for character in value.chars() { + let character = match character { + '\n' | '\r' | '\t' | '\0' => ' ', + '|' => '¦', + '`' => 'ˋ', + '<' => '‹', + '>' => '›', + '\\' | '[' | ']' | '(' | ')' | '*' | '_' | '#' | '!' => { + output.push('\\'); + output.push(character); + continue; + } + character => safe_character(character), + }; + output.push(character); + } + output +} + +fn safe_multiline(value: &str) -> String { + value + .chars() + .map(|character| match character { + '\n' | '\t' => character, + '\r' => '\n', + character => safe_character(character), + }) + .collect() +} + +fn safe_character(character: char) -> char { + if character.is_control() + || matches!( + character, + '\u{061c}' + | '\u{200b}'..='\u{200f}' + | '\u{202a}'..='\u{202e}' + | '\u{2060}'..='\u{2069}' + | '\u{feff}' + ) + { + '�' + } else { + character + } +} + +#[cfg(test)] +mod tests { + use code_system_graph_model::{FreshnessSummary, OverallFreshness, ToolStatus}; + + use super::{MarkdownDocument, render_schema_catalog}; + + fn freshness() -> FreshnessSummary { + FreshnessSummary { + overall: OverallFreshness::Fresh, + stale_repositories: Vec::new(), + reasons: Vec::new(), + } + } + + #[test] + fn complete_document_that_exactly_fits_should_not_claim_truncation() { + let document = MarkdownDocument::tool( + "query", + 2, + ToolStatus::Ok, + &freshness(), + &[], + true, + Some("src/lib.rs"), + ); + let complete = document.render(4_096); + let exact = complete.len(); + let document = MarkdownDocument::tool( + "query", + 2, + ToolStatus::Ok, + &freshness(), + &[], + true, + Some("src/lib.rs"), + ); + assert_eq!(document.render(exact), complete); + assert!(!complete.contains("## Truncation")); + } + + #[test] + fn minimum_budget_should_keep_mandatory_tool_control() { + let minimum = + usize::try_from(code_system_graph_core::MIN_MCP_MARKDOWN_BYTES).expect("MCP minimum"); + let rendered = MarkdownDocument::tool( + "query", + 2, + ToolStatus::Degraded, + &freshness(), + &["provider timed out".to_owned()], + true, + Some("src/💡.rs"), + ) + .render(minimum); + assert!(rendered.len() <= minimum); + for required in [ + "status=degraded", + "freshness=fresh", + "warning=provider timed out", + "coverage=present", + "path=src/💡.rs", + "## Truncation", + ] { + assert!(rendered.contains(required), "{rendered}"); + } + } + + #[test] + fn oversized_source_should_keep_utf8_prefix_and_close_fence() { + let mut document = MarkdownDocument::tool( + "explore", + 2, + ToolStatus::Degraded, + &freshness(), + &[], + true, + Some("src/lib.rs"), + ); + document.source("Source", &"💡 source line\n".repeat(128)); + let rendered = document.render(512); + assert!(rendered.contains("💡 source"), "{rendered}"); + assert_eq!(rendered.matches("```text").count(), 1); + assert_eq!(rendered.matches("\n```\n").count(), 1); + assert!(rendered.contains("## Truncation")); + assert!(std::str::from_utf8(rendered.as_bytes()).is_ok()); + } + + #[test] + fn source_fitting_should_ignore_backticks_outside_the_retained_prefix() { + let mut document = MarkdownDocument::tool( + "explore", + 2, + ToolStatus::Degraded, + &freshness(), + &[], + true, + Some("src/lib.rs"), + ); + document.source("Source", &format!("useful source\n{}", "`".repeat(2_048))); + let rendered = document.render(512); + + assert!(rendered.contains("useful source"), "{rendered}"); + assert!(rendered.contains("## Source"), "{rendered}"); + assert!(rendered.contains("## Truncation"), "{rendered}"); + } + + #[test] + fn byte_fitting_should_recompute_delivered_collection_metadata() { + let mut document = MarkdownDocument::resource("bounded", 2); + document.bounded_collection("Items", 2, 2, false, &["a".repeat(128), "b".repeat(128)]); + let rendered = document.render(256); + + assert!(rendered.contains("- Total: `2`"), "{rendered}"); + assert!(rendered.contains("- Retained: `0`"), "{rendered}"); + assert!(rendered.contains("- Truncated: `true`"), "{rendered}"); + assert!(!rendered.contains(&"a".repeat(32)), "{rendered}"); + } + + #[test] + fn large_collection_fitting_should_remain_linear_and_exact() { + let items = (0..10_000) + .map(|index| format!("item-{index}")) + .collect::>(); + let mut document = MarkdownDocument::resource("bounded", 2); + document.bounded_collection("Items", items.len(), items.len(), false, &items); + let rendered = document.render(1_048_576); + + assert!(rendered.contains("- Retained: `10000`"), "{rendered}"); + assert!(rendered.contains("- Truncated: `false`"), "{rendered}"); + assert!(rendered.contains("item-9999"), "{rendered}"); + } + + #[test] + fn bounded_nested_catalog_should_retain_individual_schemas() { + let rendered = render_schema_catalog( + &serde_json::json!({ + "schema_version": 2, + "media_type": "application/schema+json", + "schema_total": 2, + "schemas": { + "alpha": {"type": "string"}, + "beta": {"description": "x".repeat(1_024)} + } + }), + 512, + ); + assert!(rendered.contains("## alpha"), "{rendered}"); + assert!(!rendered.contains("## beta"), "{rendered}"); + assert!(rendered.contains("retained `1`"), "{rendered}"); + assert!(rendered.contains("truncated `true`"), "{rendered}"); + } +} diff --git a/crates/code-system-graph-cli/src/agent_plugin.rs b/crates/code-system-graph-cli/src/agent_plugin.rs index 020746c..5cc647a 100644 --- a/crates/code-system-graph-cli/src/agent_plugin.rs +++ b/crates/code-system-graph-cli/src/agent_plugin.rs @@ -343,7 +343,7 @@ pub fn create_agent_plugin( ), }; let binding = AgentPluginMcpBinding { - schema_version: 1, + schema_version: 2, generator: BINDING_GENERATOR.to_owned(), generator_build: Some(generator_build_metadata()?), base_plugin_name: plugin_name.clone(), @@ -377,7 +377,7 @@ pub fn create_agent_plugin( }; Ok(AgentPluginCreateReport { - schema_version: 1, + schema_version: 2, mode, workspace: status.workspace, plugin_name, @@ -449,7 +449,7 @@ pub fn load_agent_plugin_mcp_binding( file: "mcp-binding.json", source, })?; - if binding.schema_version != 1 || !recognized_binding_generator(&binding.generator) { + if binding.schema_version != 2 || !recognized_binding_generator(&binding.generator) { return Err(conflict( &path, "binding ownership identity is not recognized", diff --git a/crates/code-system-graph-cli/src/agent_plugin/composition.rs b/crates/code-system-graph-cli/src/agent_plugin/composition.rs index a0c69a3..120a66e 100644 --- a/crates/code-system-graph-cli/src/agent_plugin/composition.rs +++ b/crates/code-system-graph-cli/src/agent_plugin/composition.rs @@ -11,7 +11,7 @@ use super::filesystem::{ }; use super::render::render_existing_integration; use super::{ - AgentPluginError, AgentPluginGeneratorBuild, AgentPluginMcpBinding, AgentPluginUninstallReport, AgentPluginUninstallRequest, BINDING_RELATIVE_PATH, INTEGRATION_RECEIPT_RELATIVE_PATH, canonicalize_directory, conflict, pretty_json, read_json_file, recognized_binding_generator, required_json_string, unicode_path, validate_component_name, write_local_binding + AgentPluginError, AgentPluginGeneratorBuild, AgentPluginMcpBinding, AgentPluginUninstallReport, AgentPluginUninstallRequest, BINDING_RELATIVE_PATH, INTEGRATION_RECEIPT_RELATIVE_PATH, canonicalize_directory, conflict, pretty_json, read_json_file, required_json_string, unicode_path, validate_component_name, write_local_binding }; const RECEIPT_GENERATOR: &str = "csgraph plugin integration"; @@ -91,7 +91,7 @@ pub(super) fn install_composed_integration( )?; let mut local_files = super::binding_files(binding)?; let receipt = IntegrationReceipt { - schema_version: 1, + schema_version: 2, generator: RECEIPT_GENERATOR.to_owned(), generator_build: binding.generator_build.clone(), base_plugin_name: plugin_name.to_owned(), @@ -226,7 +226,7 @@ pub fn uninstall_composed_integration( } removed.sort(); Ok(AgentPluginUninstallReport { - schema_version: 1, + schema_version: 2, plugin_name, mcp_server_name: request.mcp_server_name.clone(), skill_name: request.routing_skill.clone(), @@ -392,82 +392,29 @@ fn load_owned_receipt( let plugin = read_json_file(&base.join("plugin.json"), "plugin.json")?; let plugin_name = required_json_string(&plugin, "plugin.json", "name")?.to_owned(); let receipt_path = base.join(INTEGRATION_RECEIPT_RELATIVE_PATH); - let mut receipt: IntegrationReceipt = serde_json::from_slice(&read_bytes(&receipt_path)?) - .map_err(|source| AgentPluginError::Json { - file: "plugin-integration.json", - source, + let receipt: IntegrationReceipt = + serde_json::from_slice(&read_bytes(&receipt_path)?).map_err(|source| { + AgentPluginError::Json { + file: "plugin-integration.json", + source, + } })?; - if receipt.schema_version != 1 + if receipt.schema_version != 2 || receipt.generator != RECEIPT_GENERATOR || receipt.base_plugin_name != plugin_name || receipt.mcp_server_name != request.mcp_server_name || receipt.routing_skill != request.routing_skill + || receipt.managed_documents.is_empty() + || receipt.managed_local_files.is_empty() { return Err(conflict( &receipt_path, "integration receipt does not own the requested plugin components", )); } - upgrade_legacy_receipt(base, &mut receipt)?; Ok((plugin_name, receipt)) } -fn upgrade_legacy_receipt( - base: &Path, - receipt: &mut IntegrationReceipt, -) -> Result<(), AgentPluginError> { - if receipt.managed_documents.is_empty() { - receipt.managed_documents.push("mcp.json".to_owned()); - let codex_path = base.join(".codex-plugin/plugin.json"); - if codex_path.is_file() { - let codex = read_json_file(&codex_path, ".codex-plugin/plugin.json")?; - let (server, _) = render_existing_integration( - &receipt.workspace, - &receipt.mcp_server_name, - &receipt.routing_skill, - false, - )?; - if codex - .get("mcpServers") - .and_then(serde_json::Value::as_object) - .and_then(|servers| servers.get(&receipt.mcp_server_name)) - == Some(&server) - { - receipt - .managed_documents - .push(".codex-plugin/plugin.json".to_owned()); - } - } - } - if receipt.managed_local_files.is_empty() { - let binding_path = base.join(BINDING_RELATIVE_PATH); - let binding = read_bytes(&binding_path)?; - let identity: AgentPluginMcpBinding = serde_json::from_slice(&binding).map_err(|_| { - conflict( - &binding_path, - "legacy integration receipt has a malformed local binding", - ) - })?; - if identity.schema_version != 1 - || !recognized_binding_generator(&identity.generator) - || identity.generator_build != receipt.generator_build - || identity.base_plugin_name != receipt.base_plugin_name - || identity.base != unicode_path(base)? - || identity.workspace != receipt.workspace - { - return Err(conflict( - &binding_path, - "legacy integration receipt does not match the local binding identity", - )); - } - receipt.managed_local_files.insert( - "mcp-binding.json".to_owned(), - stable_id_bytes(MANAGED_FILE_HASH_NAMESPACE, &binding), - ); - } - Ok(()) -} - fn stage_managed_removal( base: &Path, routing_skill: &str, diff --git a/crates/code-system-graph-cli/src/agent_plugin/filesystem.rs b/crates/code-system-graph-cli/src/agent_plugin/filesystem.rs index 934479d..f4b45d7 100644 --- a/crates/code-system-graph-cli/src/agent_plugin/filesystem.rs +++ b/crates/code-system-graph-cli/src/agent_plugin/filesystem.rs @@ -174,7 +174,7 @@ pub(super) fn verify_binding_ownership(output: &Path) -> Result<(), AgentPluginE } })?) .map_err(|_| conflict(output, "binding ownership identity is malformed"))?; - if binding.schema_version != 1 || !recognized_binding_generator(&binding.generator) { + if binding.schema_version != 2 || !recognized_binding_generator(&binding.generator) { return Err(conflict( output, "binding ownership identity is unrecognized", diff --git a/crates/code-system-graph-cli/src/explore.rs b/crates/code-system-graph-cli/src/explore.rs new file mode 100644 index 0000000..d22e7a8 --- /dev/null +++ b/crates/code-system-graph-cli/src/explore.rs @@ -0,0 +1,561 @@ +//! Bounded Explore orchestration. + +mod correlation; +mod model; +mod provider; +mod runtime; + +use code_system_graph_core::{ExecutionPolicy, ResolvedSymbol}; +use correlation::{ + ExploreCorrelationInput, ExploreCorrelationOutput, correlate_explore_handoffs, explore_repository_context, explore_repository_contexts +}; +pub use model::{ + ExploreCoverage, ExploreEvidenceLocation, ExploreExecution, ExploreFederatedHandoff, ExploreInput, ExploreLocalRelationship, ExploreReport, ExploreRepositoryContext +}; +use provider::{ + ExploreProviderInput, ExploreProviderOutcome, create_explore_provider, run_explore_provider_stages, validate_explore_input +}; +use runtime::{ + ExploreBlockingError, ExploreBudgetLedger, ExploreExecutionContext, ExploreProviderData, ExploreSnapshotData, load_explore_snapshot, run_bounded_explore_blocking +}; +use tokio_util::sync::CancellationToken; + +use crate::{ + AgentNextAction, BTreeMap, BTreeSet, Duration, FreshnessSummary, LocalCodeIntelligenceProvider, OverallFreshness, Path, ProviderBudget, ProviderRequest, RepoFreshness, RepositoryRecord, ToolEnvelope, ToolStatus, WorkspaceRecord, freshness_summary, native_relative_path +}; + +fn explore_provider_request( + repository: &RepositoryRecord, + project_path: &Path, + max_output_bytes: usize, + max_items: usize, + deadline: tokio::time::Instant, + cancellation: &CancellationToken, +) -> ProviderRequest { + ProviderRequest { + repo_id: repository.id.clone(), + project_path: project_path.to_path_buf(), + budget: ProviderBudget { + timeout: deadline + .saturating_duration_since(tokio::time::Instant::now()) + .max(Duration::from_millis(1)), + max_output_bytes: max_output_bytes.max(1), + max_items: max_items.max(1), + }, + cancellation: cancellation.clone(), + } +} + +fn truncate_utf8_owned(mut value: String, maximum: usize) -> String { + if value.len() <= maximum { + return value; + } + let mut boundary = maximum; + while boundary > 0 && !value.is_char_boundary(boundary) { + boundary -= 1; + } + value.truncate(boundary); + value +} + +fn fallback_explore_anchors(source_markdown: &str, maximum: usize) -> Vec { + let mut in_blast_radius = false; + let mut anchors = Vec::new(); + for line in source_markdown.lines() { + if line.starts_with("**Blast radius") && line.ends_with("**") { + in_blast_radius = true; + continue; + } + if in_blast_radius && line.starts_with("**") && line.ends_with("**") { + break; + } + if !in_blast_radius || anchors.len() == maximum { + continue; + } + let Some(entry) = line.strip_prefix("- `") else { + continue; + }; + let Some((name, location)) = entry.split_once("` (") else { + continue; + }; + let Some((location, suffix)) = location.split_once(')') else { + continue; + }; + if !suffix.is_empty() && !suffix.starts_with(" — ") { + continue; + } + let Some((path, line)) = location.rsplit_once(':') else { + continue; + }; + let Ok(start_line) = line.parse::() else { + continue; + }; + let path = path.trim_start_matches("./"); + let safe_path = !path.is_empty() + && Path::new(path).is_relative() + && Path::new(path).components().all(|component| { + matches!( + component, + std::path::Component::Normal(_) | std::path::Component::CurDir + ) + }); + if name.is_empty() + || start_line == 0 + || !safe_path + || name.chars().any(char::is_control) + || path.chars().any(char::is_control) + { + continue; + } + anchors.push(ResolvedSymbol { + local_id: None, + name: name.to_owned(), + qualified_name: None, + kind: "unknown".to_owned(), + file_path: path.to_owned(), + start_line, + score: None, + }); + } + anchors +} + +fn select_explore_anchors(symbols: &[ResolvedSymbol], maximum: usize) -> Vec { + let mut ranked = symbols.to_vec(); + ranked.sort_by(|left, right| { + right + .score + .unwrap_or(f64::NEG_INFINITY) + .total_cmp(&left.score.unwrap_or(f64::NEG_INFINITY)) + .then_with(|| left.file_path.cmp(&right.file_path)) + .then_with(|| left.start_line.cmp(&right.start_line)) + .then_with(|| left.name.cmp(&right.name)) + }); + let mut selected = Vec::new(); + let mut files = BTreeSet::new(); + for symbol in &ranked { + if selected.len() == maximum { + break; + } + if files.insert(symbol.file_path.clone()) { + selected.push(symbol.clone()); + } + } + for symbol in ranked { + if selected.len() == maximum { + break; + } + if !selected.iter().any(|item| { + item.file_path == symbol.file_path + && item.start_line == symbol.start_line + && item.name == symbol.name + }) { + selected.push(symbol); + } + } + selected +} + +fn explore_next_actions( + workspace: &str, + repository: &RepositoryRecord, + query: &str, + handoffs: &[ExploreFederatedHandoff], + policy: &ExecutionPolicy, +) -> Vec { + let maximum = usize::try_from(policy.max_agent_next_actions_per_response) + .expect("validated policy count is usize-representable"); + let mut actions = handoffs + .iter() + .map(|handoff| AgentNextAction { + tool: "source_context".to_owned(), + arguments: BTreeMap::from([ + ("workspace".to_owned(), workspace.to_owned()), + ("node_id".to_owned(), handoff.node_id.as_str().to_owned()), + ]), + rationale: format!( + "Inspect persisted evidence for the federated entity linked from `{}`.", + handoff.anchor + ), + }) + .collect::>(); + if actions.len() < maximum { + actions.push(AgentNextAction { + tool: "explore".to_owned(), + arguments: BTreeMap::from([ + ("workspace".to_owned(), workspace.to_owned()), + ("repository".to_owned(), repository.alias.clone()), + ("query".to_owned(), query.to_owned()), + ]), + rationale: + "Refine the local source question while retaining the selected repository scope." + .to_owned(), + }); + } + actions.truncate(maximum); + actions +} + +fn select_explore_repository<'a>( + registry: &'a WorkspaceRecord, + requested_alias: Option<&str>, + workspace: &str, +) -> Result<&'a RepositoryRecord, String> { + if let Some(alias) = requested_alias { + return registry + .repositories + .iter() + .find(|repository| repository.alias == alias) + .ok_or_else(|| { + format!("repository alias `{alias}` is not registered in workspace `{workspace}`") + }); + } + if registry.repositories.len() == 1 { + return registry.repositories.first().ok_or_else(|| { + format!("workspace `{workspace}` unexpectedly has no registered repository") + }); + } + Err(format!( + "repository is required because workspace `{workspace}` contains {} repositories", + registry.repositories.len() + )) +} + +fn explore_scoped_error_envelope( + freshness: FreshnessSummary, + status: ToolStatus, + message: String, +) -> ToolEnvelope { + ToolEnvelope { + schema_version: 2, + status, + data: None, + freshness, + warnings: vec![message], + } +} + +fn explore_error_envelope(message: String) -> ToolEnvelope { + ToolEnvelope { + schema_version: 2, + status: ToolStatus::Error, + data: None, + freshness: FreshnessSummary { + overall: OverallFreshness::Unknown, + stale_repositories: Vec::new(), + reasons: vec!["Local repository exploration could not start.".to_owned()], + }, + warnings: vec![message], + } +} + +fn explore_deadline_without_context(stage: &str) -> ToolEnvelope { + ToolEnvelope { + schema_version: 2, + status: ToolStatus::Degraded, + data: None, + freshness: FreshnessSummary { + overall: OverallFreshness::Unknown, + stale_repositories: Vec::new(), + reasons: vec!["Explore exhausted its global wall-time budget.".to_owned()], + }, + warnings: vec![format!( + "{stage} exceeded maxExploreWallTimeMs before repository context was available" + )], + } +} + +async fn correlate_explore_with_deadline( + input: ExploreCorrelationInput, + context: &ExploreExecutionContext, + ledger: &mut ExploreBudgetLedger, +) -> Vec { + let cancellation = context.cancellation.clone(); + let deadline = context.deadline; + let result = run_bounded_explore_blocking(context, move || { + correlate_explore_handoffs(&input, || { + cancellation.is_cancelled() || tokio::time::Instant::now() >= deadline + }) + .map_err(|()| "federated handoff correlation was cancelled".to_owned()) + }) + .await; + match result { + Ok(ExploreCorrelationOutput { + handoffs, + truncations, + }) => { + ledger.truncations.extend(truncations); + handoffs + } + Err(ExploreBlockingError::Failed(_)) + if context.expired() || context.cancellation.is_cancelled() => + { + ledger + .gaps + .push("federated handoff correlation exceeded maxExploreWallTimeMs".to_owned()); + Vec::new() + } + Err(ExploreBlockingError::Failed(error)) => { + ledger + .gaps + .push(format!("federated handoff correlation failed: {error}")); + Vec::new() + } + Err(ExploreBlockingError::Deadline) => { + ledger + .gaps + .push("federated handoff correlation exceeded maxExploreWallTimeMs".to_owned()); + Vec::new() + } + } +} + +struct ExploreEnvelopeInput { + repository: RepositoryRecord, + persisted_freshness: Vec, + freshness: FreshnessSummary, + provider_data: ExploreProviderData, + federated_handoffs: Vec, + next_actions: Vec, + policy: ExecutionPolicy, + maximum_concurrency_observed: usize, + ledger: ExploreBudgetLedger, +} + +fn finish_explore_envelope(mut input: ExploreEnvelopeInput) -> ToolEnvelope { + input.ledger.normalize(); + let status = if input.ledger.gaps.is_empty() + && input.ledger.degradations.is_empty() + && input.freshness.overall == OverallFreshness::Fresh + { + ToolStatus::Ok + } else { + ToolStatus::Degraded + }; + let retained_bytes = input + .ledger + .operations + .iter() + .map(|item| item.output_bytes) + .sum(); + ToolEnvelope { + schema_version: 2, + status, + data: Some(ExploreReport { + repository: explore_repository_context(&input.repository, &input.persisted_freshness), + source_markdown: input.provider_data.source_markdown, + resolved_symbols: input.provider_data.resolved_symbols, + local_relationships: input.provider_data.local_relationships, + federated_handoffs: input.federated_handoffs, + coverage: ExploreCoverage { + source_context: input.provider_data.source_context, + symbol_resolution: input.provider_data.symbol_resolution, + anchors_traversed: input.provider_data.anchors_traversed, + gaps: input.ledger.gaps, + truncations: input.ledger.truncations, + }, + next_actions: input.next_actions, + execution: ExploreExecution { + effective_policy: input.policy, + provider_operations: input.ledger.provider_operations, + maximum_concurrency_observed: input.maximum_concurrency_observed, + retained_bytes, + operations: input.ledger.operations, + degradations: input.ledger.degradations.clone(), + }, + }), + freshness: input.freshness, + warnings: input.ledger.degradations, + } +} + +fn snapshot_freshness(snapshot: &ExploreSnapshotData) -> FreshnessSummary { + if snapshot.freshness_loaded { + freshness_summary(&snapshot.freshness) + } else { + FreshnessSummary { + overall: OverallFreshness::Unknown, + stale_repositories: Vec::new(), + reasons: vec!["Persisted freshness was not loaded before Explore degraded.".to_owned()], + } + } +} + +fn select_requested_repository( + snapshot: &ExploreSnapshotData, + requested: Option<&str>, + workspace: &str, +) -> Result { + let requested = requested.map(str::trim).filter(|alias| !alias.is_empty()); + select_explore_repository(&snapshot.registry, requested, workspace).cloned() +} + +fn partial_snapshot_envelope( + workspace: &str, + input: &ExploreInput, + repository: RepositoryRecord, + snapshot: ExploreSnapshotData, + policy: &ExecutionPolicy, +) -> ToolEnvelope { + let stage = snapshot.incomplete_stage.unwrap_or("snapshot loading"); + let freshness = snapshot_freshness(&snapshot); + let mut ledger = ExploreBudgetLedger::default(); + ledger.gaps.extend(snapshot.gaps); + ledger.gaps.push(format!( + "{stage} exceeded maxExploreWallTimeMs; no later Explore stages were started" + )); + finish_explore_envelope(ExploreEnvelopeInput { + next_actions: explore_next_actions(workspace, &repository, &input.query, &[], policy), + repository, + persisted_freshness: snapshot.freshness, + freshness, + provider_data: ExploreProviderData::default(), + federated_handoffs: Vec::new(), + policy: policy.clone(), + maximum_concurrency_observed: 0, + ledger, + }) +} + +fn provider_unavailable_envelope( + workspace: &str, + query: &str, + repository: RepositoryRecord, + snapshot: ExploreSnapshotData, + freshness: FreshnessSummary, + policy: &ExecutionPolicy, + message: &str, +) -> ToolEnvelope { + let mut ledger = ExploreBudgetLedger::default(); + ledger.gaps.extend(snapshot.gaps); + ledger + .gaps + .push(format!("CodeGraph provider unavailable: {message}")); + finish_explore_envelope(ExploreEnvelopeInput { + next_actions: explore_next_actions(workspace, &repository, query, &[], policy), + repository, + persisted_freshness: snapshot.freshness, + freshness, + provider_data: ExploreProviderData::default(), + federated_handoffs: Vec::new(), + policy: policy.clone(), + maximum_concurrency_observed: 0, + ledger, + }) +} + +/// Explores one registered checkout through bounded, ephemeral local code intelligence. +/// +/// The returned [`ExploreReport::source_markdown`] may contain source code and must never be +/// persisted, logged, cached, or included in audit records. +/// +/// # Panics +/// +/// Panics if `policy` was manually constructed with a count or byte limit that cannot be +/// represented by the current platform. Server entry points always pass a validated, resolved +/// policy. +pub async fn explore_repository( + database_path: &Path, + workspace: &str, + input: &ExploreInput, + binary: Option, + policy: &ExecutionPolicy, +) -> ToolEnvelope { + let execution_context = ExploreExecutionContext::new(policy); + let max_files = match validate_explore_input(workspace, input, policy) { + Ok(max_files) => max_files, + Err(error) => return explore_error_envelope(error), + }; + let snapshot = match load_explore_snapshot(database_path, workspace, &execution_context).await { + Ok(snapshot) => snapshot, + Err(ExploreBlockingError::Failed(error)) => return explore_error_envelope(error), + Err(ExploreBlockingError::Deadline) => { + execution_context.cancellation.cancel(); + return explore_deadline_without_context("workspace registry loading"); + } + }; + let freshness = snapshot_freshness(&snapshot); + let repository = + match select_requested_repository(&snapshot, input.repository.as_deref(), workspace) { + Ok(repository) => repository, + Err(message) => { + return explore_scoped_error_envelope(freshness, ToolStatus::Error, message); + } + }; + if snapshot.incomplete_stage.is_some() { + return partial_snapshot_envelope(workspace, input, repository, snapshot, policy); + } + let provider = match create_explore_provider(binary, policy) { + Ok(provider) => provider, + Err(message) => { + return provider_unavailable_envelope( + workspace, + &input.query, + repository, + snapshot, + freshness, + policy, + &message, + ); + } + }; + let project_path = native_relative_path(&repository.canonical_path); + let outcome = run_explore_provider_stages(ExploreProviderInput { + provider: &provider, + repository: &repository, + project_path: &project_path, + query: &input.query, + policy, + context: &execution_context, + max_files, + }) + .await; + let ExploreProviderOutcome { + data: provider_data, + mut ledger, + anchors, + } = outcome; + ledger.gaps.extend(snapshot.gaps); + + let repository_contexts = explore_repository_contexts(&snapshot.registry, &snapshot.freshness); + let federated_handoffs = correlate_explore_with_deadline( + ExploreCorrelationInput { + repository: repository.clone(), + anchors, + nodes: snapshot.nodes, + edges: snapshot.edges, + evidence: snapshot.evidence, + repositories: repository_contexts, + policy: policy.clone(), + }, + &execution_context, + &mut ledger, + ) + .await; + let next_actions = explore_next_actions( + workspace, + &repository, + &input.query, + &federated_handoffs, + policy, + ); + if let Err(error) = provider.shutdown().await { + ledger + .degradations + .push(format!("CodeGraph shutdown degraded: {error}")); + } + finish_explore_envelope(ExploreEnvelopeInput { + repository, + persisted_freshness: snapshot.freshness, + freshness, + provider_data, + federated_handoffs, + next_actions, + policy: policy.clone(), + maximum_concurrency_observed: provider.maximum_concurrency_observed(), + ledger, + }) +} + +#[cfg(test)] +mod tests; diff --git a/crates/code-system-graph-cli/src/explore/correlation.rs b/crates/code-system-graph-cli/src/explore/correlation.rs new file mode 100644 index 0000000..46ed953 --- /dev/null +++ b/crates/code-system-graph-cli/src/explore/correlation.rs @@ -0,0 +1,368 @@ +//! Pure persisted-graph correlation for Explore. + +use std::collections::{BTreeMap, BTreeSet}; + +use code_system_graph_core::{ExecutionPolicy, ResolvedSymbol}; +use code_system_graph_model::{ + Edge, Evidence, EvidenceId, Node, NodeId, RepoFreshness, RepoFreshnessState, RepoId, RepositoryRecord, WorkspaceRecord +}; + +use super::{ExploreEvidenceLocation, ExploreFederatedHandoff, ExploreRepositoryContext}; + +pub(super) struct ExploreCorrelationInput { + pub(super) repository: RepositoryRecord, + pub(super) anchors: Vec, + pub(super) nodes: Vec, + pub(super) edges: Vec, + pub(super) evidence: Vec, + pub(super) repositories: BTreeMap, + pub(super) policy: ExecutionPolicy, +} + +pub(super) struct ExploreCorrelationOutput { + pub(super) handoffs: Vec, + pub(super) truncations: Vec, +} + +pub(super) fn explore_repository_context( + repository: &RepositoryRecord, + freshness: &[RepoFreshness], +) -> ExploreRepositoryContext { + ExploreRepositoryContext { + alias: repository.alias.clone(), + repo_id: repository.id.clone(), + root: repository.canonical_path.display.clone(), + revision: repository.head_commit.clone(), + freshness: freshness + .iter() + .find(|item| item.repo_id == repository.id) + .map_or(RepoFreshnessState::Unknown, |item| item.state), + } +} + +pub(super) fn explore_repository_contexts( + registry: &WorkspaceRecord, + freshness: &[RepoFreshness], +) -> BTreeMap { + registry + .repositories + .iter() + .map(|repository| { + ( + repository.id.clone(), + explore_repository_context(repository, freshness), + ) + }) + .collect() +} + +pub(super) fn correlate_explore_handoffs( + input: &ExploreCorrelationInput, + mut should_stop: impl FnMut() -> bool, +) -> Result { + let per_anchor = usize::try_from(input.policy.max_explore_federated_handoffs_per_anchor) + .expect("validated policy count is usize-representable"); + let total_limit = usize::try_from(input.policy.max_explore_federated_handoffs) + .expect("validated policy count is usize-representable"); + let evidence_limit = usize::try_from(input.policy.max_explore_evidence_locations_per_handoff) + .expect("validated policy count is usize-representable"); + let (node_by_id, evidence_by_id) = correlation_indexes(input, &mut should_stop)?; + let mut output = Vec::new(); + let mut truncations = Vec::new(); + 'anchors: for anchor in &input.anchors { + if should_stop() { + return Err(()); + } + let matching_evidence = matching_anchor_evidence(input, anchor, &mut should_stop)?; + if matching_evidence.is_empty() { + continue; + } + let mut emitted_for_anchor = 0_usize; + let mut seen = BTreeSet::new(); + for (index, edge) in input.edges.iter().enumerate() { + if index % 256 == 0 && should_stop() { + return Err(()); + } + if !edge + .evidence + .iter() + .any(|id| matching_evidence.contains(id)) + { + continue; + } + let remote = [&edge.source, &edge.target] + .into_iter() + .filter_map(|id| node_by_id.get(id).copied()) + .find(|node| { + node.repo_id + .as_ref() + .is_some_and(|id| id != &input.repository.id) + }); + let Some(remote) = remote else { + continue; + }; + if !seen.insert(remote.id.clone()) { + continue; + } + if output.len() == total_limit { + truncations.push("maxExploreFederatedHandoffs".to_owned()); + break 'anchors; + } + if emitted_for_anchor == per_anchor { + truncations.push("maxExploreFederatedHandoffsPerAnchor".to_owned()); + break; + } + let locations = + explore_handoff_locations(edge, &evidence_by_id, evidence_limit, &mut truncations); + output.push(ExploreFederatedHandoff { + anchor: anchor + .qualified_name + .clone() + .unwrap_or_else(|| anchor.name.clone()), + node_id: remote.id.clone(), + label: remote.label.clone(), + remote_repository: remote + .repo_id + .as_ref() + .and_then(|id| input.repositories.get(id).cloned()), + status: edge.status, + confidence: edge.confidence, + evidence: locations, + }); + emitted_for_anchor += 1; + } + } + Ok(ExploreCorrelationOutput { + handoffs: output, + truncations, + }) +} + +type CorrelationIndexes<'a> = ( + BTreeMap<&'a NodeId, &'a Node>, + BTreeMap<&'a EvidenceId, &'a Evidence>, +); + +fn correlation_indexes<'a>( + input: &'a ExploreCorrelationInput, + should_stop: &mut impl FnMut() -> bool, +) -> Result, ()> { + let mut node_by_id = BTreeMap::new(); + for (index, node) in input.nodes.iter().enumerate() { + if index % 256 == 0 && should_stop() { + return Err(()); + } + node_by_id.insert(&node.id, node); + } + let mut evidence_by_id = BTreeMap::new(); + for (index, item) in input.evidence.iter().enumerate() { + if index % 256 == 0 && should_stop() { + return Err(()); + } + evidence_by_id.insert(&item.id, item); + } + Ok((node_by_id, evidence_by_id)) +} + +fn matching_anchor_evidence<'a>( + input: &'a ExploreCorrelationInput, + anchor: &ResolvedSymbol, + should_stop: &mut impl FnMut() -> bool, +) -> Result, ()> { + let anchor_path = anchor.file_path.trim_start_matches("./"); + let mut matching = BTreeSet::new(); + for (index, item) in input.evidence.iter().enumerate() { + if index % 256 == 0 && should_stop() { + return Err(()); + } + let matches_anchor = item.repo_id.as_ref() == Some(&input.repository.id) + && item + .file_path + .as_deref() + .is_some_and(|path| path.trim_start_matches("./") == anchor_path) + && item.start_line.is_none_or(|start| { + usize::try_from(start).is_ok_and(|start| start <= anchor.start_line) + }) + && item + .end_line + .is_none_or(|end| usize::try_from(end).is_ok_and(|end| end >= anchor.start_line)); + if matches_anchor { + matching.insert(&item.id); + } + } + Ok(matching) +} + +fn explore_handoff_locations( + edge: &Edge, + evidence_by_id: &BTreeMap<&EvidenceId, &Evidence>, + evidence_limit: usize, + truncations: &mut Vec, +) -> Vec { + let mut locations = edge + .evidence + .iter() + .filter_map(|id| evidence_by_id.get(id).copied()) + .filter_map(|item| { + Some(ExploreEvidenceLocation { + repo_id: item.repo_id.clone()?, + path: item.file_path.clone()?, + start_line: item.start_line, + end_line: item.end_line, + }) + }) + .collect::>(); + locations.sort_by(|left, right| { + (&left.repo_id, &left.path, left.start_line, left.end_line).cmp(&( + &right.repo_id, + &right.path, + right.start_line, + right.end_line, + )) + }); + locations.dedup(); + if locations.len() > evidence_limit { + locations.truncate(evidence_limit); + truncations.push("maxExploreEvidenceLocationsPerHandoff".to_owned()); + } + locations +} + +#[cfg(test)] +mod tests { + use code_system_graph_core::{ExecutionPolicy, ResolvedSymbol}; + use code_system_graph_model::{ + CheckoutId, Edge, EdgeId, EdgeKind, EpistemicStatus, Evidence, EvidenceId, NativePath, NativePathEncoding, Node, NodeId, NodeKind, Provenance, RepoId, RepositoryRecord + }; + + use super::{ExploreCorrelationInput, correlate_explore_handoffs}; + + fn repository() -> RepositoryRecord { + RepositoryRecord { + id: RepoId::new("repo:api"), + checkout_id: CheckoutId::new("checkout:api"), + alias: "api".to_owned(), + canonical_path: NativePath { + encoding: NativePathEncoding::Utf8, + bytes: b"/api".to_vec(), + display: "/api".to_owned(), + }, + git_common_dir: None, + normalized_remote: None, + head_commit: None, + is_linked_worktree: false, + working_tree_dirty: false, + } + } + + fn anchor(name: &str) -> ResolvedSymbol { + ResolvedSymbol { + local_id: None, + name: name.to_owned(), + qualified_name: None, + kind: "function".to_owned(), + file_path: format!("src/{name}.rs"), + start_line: 1, + score: None, + } + } + + fn remote_node(name: &str) -> Node { + Node { + id: NodeId::new(format!("node:remote:{name}")), + kind: NodeKind::Service, + repo_id: Some(RepoId::new("repo:remote")), + stable_key: format!("remote:{name}"), + label: format!("{name} remote"), + } + } + + fn anchor_evidence(name: &str) -> Evidence { + Evidence { + id: EvidenceId::new(format!("evidence:{name}")), + repo_id: Some(RepoId::new("repo:api")), + file_path: Some(format!("src/{name}.rs")), + start_line: Some(1), + end_line: Some(1), + extractor: "fixture".to_owned(), + extractor_version: "1".to_owned(), + provenance: Provenance::Extracted, + confidence: 1.0, + observed_at_commit: None, + content_hash: None, + note: None, + } + } + + fn handoff_edge(name: &str) -> Edge { + Edge { + id: EdgeId::new(format!("edge:{name}")), + source: NodeId::new(format!("node:local:{name}")), + target: NodeId::new(format!("node:remote:{name}")), + kind: EdgeKind::CallsRemote, + confidence: 1.0, + status: EpistemicStatus::Confirmed, + evidence: vec![EvidenceId::new(format!("evidence:{name}"))], + } + } + + fn correlation_input(include_second_handoff: bool) -> ExploreCorrelationInput { + let mut names = vec!["first"]; + if include_second_handoff { + names.push("second"); + } + ExploreCorrelationInput { + repository: repository(), + anchors: names.iter().map(|name| anchor(name)).collect(), + nodes: names.iter().map(|name| remote_node(name)).collect(), + edges: names.iter().map(|name| handoff_edge(name)).collect(), + evidence: names.iter().map(|name| anchor_evidence(name)).collect(), + repositories: std::collections::BTreeMap::new(), + policy: ExecutionPolicy { + max_explore_federated_handoffs: 1, + ..ExecutionPolicy::default() + }, + } + } + + #[test] + fn correlation_should_stop_before_scanning_when_cancelled() { + let input = ExploreCorrelationInput { + repository: repository(), + anchors: vec![ResolvedSymbol { + local_id: None, + name: "anchor".to_owned(), + qualified_name: None, + kind: "function".to_owned(), + file_path: "src/lib.rs".to_owned(), + start_line: 1, + score: None, + }], + nodes: Vec::new(), + edges: Vec::new(), + evidence: Vec::new(), + repositories: std::collections::BTreeMap::new(), + policy: ExecutionPolicy::default(), + }; + + assert!(correlate_explore_handoffs(&input, || true).is_err()); + } + + #[test] + fn total_handoff_limit_should_record_a_later_anchor_as_truncated() { + let result = correlate_explore_handoffs(&correlation_input(true), || false) + .expect("correlation should complete"); + + assert_eq!(result.handoffs.len(), 1); + assert_eq!(result.truncations, ["maxExploreFederatedHandoffs"]); + } + + #[test] + fn exact_total_handoff_limit_should_not_claim_truncation() { + let result = correlate_explore_handoffs(&correlation_input(false), || false) + .expect("correlation should complete"); + + assert_eq!(result.handoffs.len(), 1); + assert_eq!(result.truncations, [] as [String; 0]); + } +} diff --git a/crates/code-system-graph-cli/src/explore/model.rs b/crates/code-system-graph-cli/src/explore/model.rs new file mode 100644 index 0000000..0efda33 --- /dev/null +++ b/crates/code-system-graph-cli/src/explore/model.rs @@ -0,0 +1,136 @@ +//! Public Explore request and report contracts. + +use code_system_graph_core::{ + AgentNextAction, ExecutionPolicy, LocalNeighbor, LocalNeighborDirection, ProviderExecution, ResolvedSymbol +}; +use code_system_graph_model::{EpistemicStatus, NodeId, RepoFreshnessState, RepoId}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// Ephemeral intra-repository source and flow exploration input. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct ExploreInput { + /// Workspace selected by the delivery adapter. + pub workspace: String, + /// Registered repository alias; omitted only when the workspace contains one repository. + #[serde(default)] + pub repository: Option, + /// Focused symbol, flow, architecture, or implementation question. + pub query: String, + /// Maximum source files returned by the local provider. + #[serde(default)] + #[schemars(range(min = 1))] + pub max_files: Option, +} + +/// Repository identity and freshness attached to an Explore response. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct ExploreRepositoryContext { + /// Registered alias. + pub alias: String, + /// Stable repository identity. + pub repo_id: RepoId, + /// Canonical checkout display path. + pub root: String, + /// Current Git revision when available. + pub revision: Option, + /// Persisted freshness for this repository. + pub freshness: RepoFreshnessState, +} + +/// One local caller/callee relationship discovered ephemerally. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct ExploreLocalRelationship { + /// Exact anchor selected for traversal. + pub anchor: String, + /// Direction relative to the anchor. + pub direction: LocalNeighborDirection, + /// Bounded neighboring symbol. + pub neighbor: LocalNeighbor, +} + +/// Verifiable persisted evidence location used by a federated handoff. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct ExploreEvidenceLocation { + /// Repository containing the evidence. + pub repo_id: RepoId, + /// Repository-relative path. + pub path: String, + /// Optional inclusive first line. + pub start_line: Option, + /// Optional inclusive last line. + pub end_line: Option, +} + +/// Navigation from a local source anchor to a persisted federated entity. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct ExploreFederatedHandoff { + /// Local source anchor correlated with persisted evidence. + pub anchor: String, + /// Real persisted entity identifier. + pub node_id: NodeId, + /// Human-readable persisted entity label. + pub label: String, + /// Owning remote repository, when known. + pub remote_repository: Option, + /// Confirmed, inferred, or ambiguous evidence state. + pub status: EpistemicStatus, + /// Persisted relationship confidence. + pub confidence: f32, + /// Bounded navigable evidence locations. + pub evidence: Vec, +} + +/// Exact execution accounting and applied bounds for Explore. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct ExploreExecution { + /// Effective immutable workspace policy. + pub effective_policy: ExecutionPolicy, + /// Public provider operations started. + pub provider_operations: usize, + /// Maximum concurrent provider operations observed. + pub maximum_concurrency_observed: usize, + /// Provider output bytes retained across successful operations. + pub retained_bytes: usize, + /// Per-operation provider metadata in completion order. + pub operations: Vec, + /// Non-fatal budget, timeout, or provider degradations. + pub degradations: Vec, +} + +/// Coverage and exact truncation accounting for Explore. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct ExploreCoverage { + /// Whether source context was attempted and retained. + pub source_context: bool, + /// Whether symbol resolution was attempted. + pub symbol_resolution: bool, + /// Number of anchors traversed in both directions. + pub anchors_traversed: usize, + /// Explicit incomplete coverage reasons. + pub gaps: Vec, + /// Stable names of limits that truncated work or output. + pub truncations: Vec, +} + +/// Complete ephemeral, bounded agent-facing Explore result. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct ExploreReport { + /// Selected repository identity and freshness. + pub repository: ExploreRepositoryContext, + /// Untrusted provider Markdown containing source; never persisted by this server. + pub source_markdown: String, + /// Bounded exact local symbols. + pub resolved_symbols: Vec, + /// Bounded local caller/callee relationships. + pub local_relationships: Vec, + /// Bounded correlations into persisted federated entities. + pub federated_handoffs: Vec, + /// Coverage and truncation facts. + pub coverage: ExploreCoverage, + /// Deterministic next navigation using real IDs only. + pub next_actions: Vec, + /// Effective limits and observed consumption. + pub execution: ExploreExecution, +} diff --git a/crates/code-system-graph-cli/src/explore/provider.rs b/crates/code-system-graph-cli/src/explore/provider.rs new file mode 100644 index 0000000..b8f9c82 --- /dev/null +++ b/crates/code-system-graph-cli/src/explore/provider.rs @@ -0,0 +1,378 @@ +//! Bounded `CodeGraph` provider stages for Explore. + +use std::path::Path; + +use code_system_graph_core::{ExecutionPolicy, LocalNeighborResult, ProviderError, ResolvedSymbol}; +use code_system_graph_model::RepositoryRecord; + +use super::runtime::{ExploreBudgetLedger, ExploreExecutionContext, ExploreProviderData}; +use super::{ + ExploreInput, ExploreLocalRelationship, explore_provider_request, fallback_explore_anchors, select_explore_anchors, truncate_utf8_owned +}; +use crate::{ + CodeGraphConfig, CodeGraphProvider, LocalCodeIntelligenceProvider, LocalContextRequest, LocalNeighborDirection, LocalNeighborsRequest +}; + +struct ExploreProviderStage<'a> { + provider: &'a CodeGraphProvider, + repository: &'a RepositoryRecord, + project_path: &'a Path, + query: &'a str, + context: &'a ExploreExecutionContext, +} + +pub(super) struct ExploreProviderInput<'a> { + pub(super) provider: &'a CodeGraphProvider, + pub(super) repository: &'a RepositoryRecord, + pub(super) project_path: &'a Path, + pub(super) query: &'a str, + pub(super) policy: &'a ExecutionPolicy, + pub(super) context: &'a ExploreExecutionContext, + pub(super) max_files: usize, +} + +pub(super) struct ExploreProviderOutcome { + pub(super) data: ExploreProviderData, + pub(super) ledger: ExploreBudgetLedger, + pub(super) anchors: Vec, +} + +pub(super) async fn run_explore_provider_stages( + input: ExploreProviderInput<'_>, +) -> ExploreProviderOutcome { + let mut ledger = ExploreBudgetLedger::default(); + let mut data = ExploreProviderData::default(); + let stage = ExploreProviderStage { + provider: input.provider, + repository: input.repository, + project_path: input.project_path, + query: input.query, + context: input.context, + }; + let operation_limit = usize::try_from(input.policy.max_explore_codegraph_operations) + .expect("validated policy count is usize-representable"); + let source_bytes = usize::try_from(input.policy.max_explore_source_markdown_bytes) + .expect("validated policy bytes are usize-representable"); + let enrichment_bytes = usize::try_from(input.policy.max_explore_enrichment_bytes) + .expect("validated policy bytes are usize-representable"); + let resolved_limit = usize::try_from(input.policy.max_explore_resolved_symbols) + .expect("validated policy count is usize-representable"); + collect_explore_source( + &stage, + input.max_files, + source_bytes, + operation_limit, + &mut ledger, + &mut data, + ) + .await; + resolve_explore_symbols( + &stage, + operation_limit, + enrichment_bytes, + resolved_limit, + &mut ledger, + &mut data, + ) + .await; + let anchor_limit = usize::try_from(input.policy.max_explore_anchors) + .expect("validated policy count is usize-representable"); + let anchors = select_explore_anchors(&data.resolved_symbols, anchor_limit); + collect_explore_neighbors( + &stage, + &anchors, + &ExploreNeighborLimits { + operation_limit, + enrichment_bytes, + neighbor_limit: usize::try_from(input.policy.max_explore_neighbors_per_direction) + .expect("validated policy count is usize-representable"), + relationship_limit: usize::try_from(input.policy.max_explore_local_relationships) + .expect("validated policy count is usize-representable"), + }, + &mut ledger, + &mut data, + ) + .await; + ExploreProviderOutcome { + data, + ledger, + anchors, + } +} + +pub(super) fn create_explore_provider( + binary: Option, + policy: &ExecutionPolicy, +) -> Result { + let mut config = CodeGraphConfig { + max_concurrent_processes: usize::try_from( + policy.max_explore_concurrent_codegraph_processes, + ) + .expect("validated policy count is usize-representable"), + ..CodeGraphConfig::default() + }; + if let Some(binary) = binary { + config.binary = binary; + } + CodeGraphProvider::new(config).map_err(|error| error.to_string()) +} + +pub(super) fn validate_explore_input( + workspace: &str, + input: &ExploreInput, + policy: &ExecutionPolicy, +) -> Result { + if input.workspace != workspace { + return Err(format!( + "workspace `{}` is outside this server's configured workspace `{workspace}`", + input.workspace + )); + } + let source_file_limit = usize::try_from(policy.max_explore_source_files) + .expect("validated policy count is usize-representable"); + let max_files = input.max_files.unwrap_or(source_file_limit.min(12)); + if max_files == 0 || max_files > source_file_limit { + return Err(format!( + "max_files must be between 1 and the effective workspace limit ({source_file_limit})" + )); + } + Ok(max_files) +} + +async fn collect_explore_source( + stage: &ExploreProviderStage<'_>, + max_files: usize, + source_bytes: usize, + operation_limit: usize, + ledger: &mut ExploreBudgetLedger, + data: &mut ExploreProviderData, +) { + if !ledger.try_reserve_operations(1, operation_limit, stage.context) { + ledger.gaps.push( + "source context skipped because the Explore deadline or operation budget was exhausted" + .to_owned(), + ); + return; + } + let result = stage + .provider + .build_local_context(LocalContextRequest { + request: explore_provider_request( + stage.repository, + stage.project_path, + source_bytes, + max_files, + stage.context.deadline, + &stage.context.cancellation, + ), + query: stage.query.to_owned(), + max_files, + }) + .await; + match result { + Ok(result) => { + data.source_markdown = truncate_utf8_owned(result.content, source_bytes); + data.source_context = true; + if result.execution.truncated { + ledger + .truncations + .push("maxExploreSourceMarkdownBytes".to_owned()); + } + ledger.record_execution(result.execution); + } + Err(error) => ledger + .gaps + .push(format!("source context unavailable: {error}")), + } +} + +async fn resolve_explore_symbols( + stage: &ExploreProviderStage<'_>, + operation_limit: usize, + enrichment_bytes: usize, + resolved_limit: usize, + ledger: &mut ExploreBudgetLedger, + data: &mut ExploreProviderData, +) { + if ledger.try_reserve_operations(1, operation_limit, stage.context) { + let result = stage + .provider + .resolve_symbols(code_system_graph_core::ResolveSymbolsRequest { + request: explore_provider_request( + stage.repository, + stage.project_path, + enrichment_bytes, + resolved_limit, + stage.context.deadline, + &stage.context.cancellation, + ), + query: stage.query.to_owned(), + }) + .await; + match result { + Ok(mut result) => { + data.symbol_resolution = true; + if result.symbols.len() > resolved_limit || result.execution.truncated { + ledger + .truncations + .push("maxExploreResolvedSymbols".to_owned()); + } + result.symbols.truncate(resolved_limit); + data.resolved_symbols = result.symbols; + ledger.retain_enrichment_bytes(result.execution.output_bytes, enrichment_bytes); + ledger.record_execution(result.execution); + } + Err(error) => ledger + .gaps + .push(format!("symbol resolution unavailable: {error}")), + } + } else { + ledger.gaps.push( + "symbol resolution skipped because the Explore deadline or operation budget was exhausted" + .to_owned(), + ); + } + if data.resolved_symbols.is_empty() { + data.resolved_symbols = fallback_explore_anchors(&data.source_markdown, resolved_limit); + ledger.gaps.push(if data.resolved_symbols.is_empty() { + "no Explore anchors were available from structured symbol resolution or recognized CodeGraph blast-radius entries".to_owned() + } else { + "structured symbol anchors were unavailable; strict fallback anchors were derived from explicit CodeGraph blast-radius entries".to_owned() + }); + } +} + +pub(super) struct ExploreNeighborLimits { + pub(super) operation_limit: usize, + pub(super) enrichment_bytes: usize, + pub(super) neighbor_limit: usize, + pub(super) relationship_limit: usize, +} + +async fn collect_explore_neighbors( + stage: &ExploreProviderStage<'_>, + anchors: &[ResolvedSymbol], + limits: &ExploreNeighborLimits, + ledger: &mut ExploreBudgetLedger, + data: &mut ExploreProviderData, +) { + for (anchor_index, anchor) in anchors.iter().enumerate() { + let remaining = ledger.remaining_enrichment_bytes(limits.enrichment_bytes); + if remaining < 2 { + ledger + .truncations + .push("maxExploreEnrichmentBytes".to_owned()); + break; + } + if !ledger.try_reserve_operations(2, limits.operation_limit, stage.context) { + ledger.gaps.push(format!( + "neighbors for `{}` skipped because the Explore deadline or operation budget was exhausted", + anchor.name + )); + break; + } + let symbol = anchor + .qualified_name + .clone() + .unwrap_or_else(|| anchor.name.clone()); + let request = |direction, bytes| LocalNeighborsRequest { + request: explore_provider_request( + stage.repository, + stage.project_path, + bytes, + limits.neighbor_limit, + stage.context.deadline, + &stage.context.cancellation, + ), + symbol: symbol.clone(), + direction, + }; + let (incoming, outgoing) = tokio::join!( + stage + .provider + .get_local_neighbors(request(LocalNeighborDirection::Callers, remaining / 2)), + stage.provider.get_local_neighbors(request( + LocalNeighborDirection::Callees, + remaining - (remaining / 2), + )), + ); + let anchor_completed = + record_explore_neighbor_results(&symbol, incoming, outgoing, limits, ledger, data); + data.anchors_traversed += usize::from(anchor_completed); + let observed_relationships = data.local_relationships.len(); + if observed_relationships >= limits.relationship_limit { + data.local_relationships.truncate(limits.relationship_limit); + if local_relationships_truncated( + observed_relationships, + limits.relationship_limit, + anchor_index + 1 < anchors.len(), + ) { + ledger + .truncations + .push("maxExploreLocalRelationships".to_owned()); + } + break; + } + } +} + +pub(super) fn local_relationships_truncated( + observed: usize, + maximum: usize, + anchors_remaining: bool, +) -> bool { + observed > maximum || (observed == maximum && anchors_remaining) +} + +pub(super) fn record_explore_neighbor_results( + symbol: &str, + incoming: Result, + outgoing: Result, + limits: &ExploreNeighborLimits, + ledger: &mut ExploreBudgetLedger, + data: &mut ExploreProviderData, +) -> bool { + let mut completed_directions = 0_usize; + for (direction, result) in [("callers", incoming), ("callees", outgoing)] { + match result { + Ok(mut result) => { + completed_directions += 1; + if result.neighbors.len() > limits.neighbor_limit || result.execution.truncated { + ledger + .truncations + .push("maxExploreNeighborsPerDirection".to_owned()); + } + result.neighbors.truncate(limits.neighbor_limit); + ledger.retain_enrichment_bytes( + result.execution.output_bytes, + limits.enrichment_bytes, + ); + data.local_relationships + .extend(result.neighbors.into_iter().map(|neighbor| { + ExploreLocalRelationship { + anchor: symbol.to_owned(), + direction: result.direction, + neighbor, + } + })); + ledger.record_execution(result.execution); + } + Err(error) => ledger.gaps.push(format!( + "{direction} neighbors for `{symbol}` degraded: {error}" + )), + } + } + completed_directions == 2 +} + +#[cfg(test)] +mod tests { + use super::local_relationships_truncated; + + #[test] + fn exact_relationship_limit_only_truncates_when_work_remains() { + assert!(!local_relationships_truncated(4, 4, false)); + assert!(local_relationships_truncated(4, 4, true)); + assert!(local_relationships_truncated(5, 4, false)); + } +} diff --git a/crates/code-system-graph-cli/src/explore/runtime.rs b/crates/code-system-graph-cli/src/explore/runtime.rs new file mode 100644 index 0000000..208db2a --- /dev/null +++ b/crates/code-system-graph-cli/src/explore/runtime.rs @@ -0,0 +1,429 @@ +//! Explore execution ownership, blocking isolation, and staged snapshot loading. + +use std::future::Future; +use std::path::Path; +use std::sync::{Arc, OnceLock}; + +use code_system_graph_core::{ExecutionPolicy, ProviderExecution, ResolvedSymbol}; +use code_system_graph_model::{Edge, Evidence, Node, RepoFreshness, WorkspaceRecord}; +use code_system_graph_store_sqlite::SqliteStore; +use tokio_util::sync::CancellationToken; + +use super::ExploreLocalRelationship; + +pub(super) struct ExploreExecutionContext { + pub(super) deadline: tokio::time::Instant, + pub(super) cancellation: CancellationToken, +} + +impl ExploreExecutionContext { + pub(super) fn new(policy: &ExecutionPolicy) -> Self { + Self { + deadline: tokio::time::Instant::now() + + std::time::Duration::from_millis(policy.max_explore_wall_time_ms), + cancellation: CancellationToken::new(), + } + } + + pub(super) fn expired(&self) -> bool { + tokio::time::Instant::now() >= self.deadline + } +} + +impl Drop for ExploreExecutionContext { + fn drop(&mut self) { + self.cancellation.cancel(); + } +} + +#[derive(Default)] +pub(super) struct ExploreBudgetLedger { + pub(super) operations: Vec, + pub(super) degradations: Vec, + pub(super) truncations: Vec, + pub(super) gaps: Vec, + pub(super) provider_operations: usize, + enrichment_retained_bytes: usize, +} + +impl ExploreBudgetLedger { + pub(super) fn try_reserve_operations( + &mut self, + count: usize, + limit: usize, + context: &ExploreExecutionContext, + ) -> bool { + if context.expired() || self.provider_operations.saturating_add(count) > limit { + return false; + } + self.provider_operations += count; + true + } + + pub(super) fn remaining_enrichment_bytes(&self, limit: usize) -> usize { + limit.saturating_sub(self.enrichment_retained_bytes) + } + + pub(super) fn retain_enrichment_bytes(&mut self, bytes: usize, limit: usize) { + self.enrichment_retained_bytes = self + .enrichment_retained_bytes + .saturating_add(bytes) + .min(limit); + } + + pub(super) fn record_execution(&mut self, execution: ProviderExecution) { + self.degradations.extend( + execution + .degradations + .iter() + .map(|item| item.message.clone()), + ); + self.operations.push(execution); + } + + pub(super) fn normalize(&mut self) { + self.truncations.sort(); + self.truncations.dedup(); + self.gaps.sort(); + self.gaps.dedup(); + self.degradations.sort(); + self.degradations.dedup(); + } +} + +#[derive(Default)] +pub(super) struct ExploreProviderData { + pub(super) source_markdown: String, + pub(super) source_context: bool, + pub(super) resolved_symbols: Vec, + pub(super) symbol_resolution: bool, + pub(super) local_relationships: Vec, + pub(super) anchors_traversed: usize, +} + +pub(super) struct ExploreSnapshotData { + pub(super) registry: WorkspaceRecord, + pub(super) freshness: Vec, + pub(super) freshness_loaded: bool, + pub(super) nodes: Vec, + pub(super) edges: Vec, + pub(super) evidence: Vec, + pub(super) gaps: Vec, + pub(super) incomplete_stage: Option<&'static str>, +} + +#[derive(Debug)] +pub(super) enum ExploreBlockingError { + Failed(String), + Deadline, +} + +fn explore_blocking_permits() -> Arc { + static PERMITS: OnceLock> = OnceLock::new(); + PERMITS + .get_or_init(|| Arc::new(tokio::sync::Semaphore::new(2))) + .clone() +} + +pub(super) async fn run_bounded_explore_blocking( + context: &ExploreExecutionContext, + operation: F, +) -> Result +where + T: Send + 'static, + F: FnOnce() -> Result + Send + 'static, +{ + if context.expired() { + context.cancellation.cancel(); + return Err(ExploreBlockingError::Deadline); + } + let permit = + match tokio::time::timeout_at(context.deadline, explore_blocking_permits().acquire_owned()) + .await + { + Ok(Ok(permit)) => permit, + Ok(Err(error)) => return Err(ExploreBlockingError::Failed(error.to_string())), + Err(_) => { + context.cancellation.cancel(); + return Err(ExploreBlockingError::Deadline); + } + }; + if context.expired() { + context.cancellation.cancel(); + return Err(ExploreBlockingError::Deadline); + } + let task = tokio::task::spawn_blocking(move || { + let _permit = permit; + operation() + }); + match tokio::time::timeout_at(context.deadline, task).await { + Ok(Ok(Ok(value))) => Ok(value), + Ok(Ok(Err(error))) => Err(ExploreBlockingError::Failed(error)), + Ok(Err(error)) => Err(ExploreBlockingError::Failed(format!( + "Explore blocking stage failed: {error}" + ))), + Err(_) => { + context.cancellation.cancel(); + Err(ExploreBlockingError::Deadline) + } + } +} + +async fn load_snapshot_stage( + database_path: &Path, + workspace: &str, + context: &ExploreExecutionContext, + load: F, +) -> Result +where + T: Send + 'static, + F: FnOnce(&SqliteStore, &str) -> Result + Send + 'static, +{ + let database_path = database_path.to_path_buf(); + let workspace = workspace.to_owned(); + let cancellation = context.cancellation.clone(); + let deadline = context.deadline; + let result = run_bounded_explore_blocking(context, move || { + let store = + SqliteStore::open_read_only(&database_path).map_err(|error| error.to_string())?; + store + .interrupt_queries_when(move || { + cancellation.is_cancelled() || tokio::time::Instant::now() >= deadline + }) + .map_err(|error| error.to_string())?; + load(&store, &workspace) + }) + .await; + match result { + Err(ExploreBlockingError::Failed(_)) + if context.expired() || context.cancellation.is_cancelled() => + { + Err(ExploreBlockingError::Deadline) + } + other => other, + } +} + +pub(super) async fn load_explore_snapshot( + database_path: &Path, + workspace: &str, + context: &ExploreExecutionContext, +) -> Result { + assemble_explore_snapshot( + load_snapshot_stage(database_path, workspace, context, |store, workspace| { + store + .load_workspace_registry(workspace) + .map_err(|error| error.to_string()) + }), + load_snapshot_stage(database_path, workspace, context, |store, workspace| { + store + .load_current_freshness(workspace) + .map_err(|error| error.to_string()) + }), + load_snapshot_stage(database_path, workspace, context, |store, workspace| { + store + .load_current_graph(workspace) + .map_err(|error| error.to_string()) + }), + load_snapshot_stage(database_path, workspace, context, |store, workspace| { + store + .load_current_evidence(workspace) + .map_err(|error| error.to_string()) + }), + ) + .await +} + +async fn assemble_explore_snapshot( + registry: R, + freshness: F, + graph: G, + evidence: E, +) -> Result +where + R: Future>, + F: Future, ExploreBlockingError>>, + G: Future, Vec), ExploreBlockingError>>, + E: Future, ExploreBlockingError>>, +{ + let registry = registry.await?; + let mut snapshot = ExploreSnapshotData { + registry, + freshness: Vec::new(), + freshness_loaded: false, + nodes: Vec::new(), + edges: Vec::new(), + evidence: Vec::new(), + gaps: Vec::new(), + incomplete_stage: None, + }; + + match freshness.await { + Ok(freshness) => { + snapshot.freshness = freshness; + snapshot.freshness_loaded = true; + } + Err(ExploreBlockingError::Failed(error)) => snapshot + .gaps + .push(format!("persisted freshness unavailable: {error}")), + Err(ExploreBlockingError::Deadline) => { + snapshot.incomplete_stage = Some("freshness loading"); + return Ok(snapshot); + } + } + + match graph.await { + Ok((nodes, edges)) => { + snapshot.nodes = nodes; + snapshot.edges = edges; + } + Err(ExploreBlockingError::Failed(error)) => snapshot + .gaps + .push(format!("persisted graph unavailable: {error}")), + Err(ExploreBlockingError::Deadline) => { + snapshot.incomplete_stage = Some("graph loading"); + return Ok(snapshot); + } + } + + match evidence.await { + Ok(evidence) => snapshot.evidence = evidence, + Err(ExploreBlockingError::Failed(error)) => snapshot + .gaps + .push(format!("persisted evidence unavailable: {error}")), + Err(ExploreBlockingError::Deadline) => { + snapshot.incomplete_stage = Some("evidence loading"); + } + } + Ok(snapshot) +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use code_system_graph_core::ExecutionPolicy; + use code_system_graph_model::{ + CheckoutId, NativePath, NativePathEncoding, RepoId, RepositoryRecord, WorkspaceId, WorkspaceRecord + }; + use code_system_graph_store_sqlite::SqliteStore; + + use super::{ + ExploreBlockingError, ExploreExecutionContext, assemble_explore_snapshot, load_explore_snapshot, run_bounded_explore_blocking + }; + + fn workspace() -> WorkspaceRecord { + WorkspaceRecord { + id: WorkspaceId::new("workspace:commerce"), + name: "commerce".to_owned(), + manifest_hash: "manifest".to_owned(), + config_path: None, + repositories: vec![RepositoryRecord { + id: RepoId::new("repo:api"), + checkout_id: CheckoutId::new("checkout:api"), + alias: "api".to_owned(), + canonical_path: NativePath { + encoding: NativePathEncoding::Utf8, + bytes: b"/api".to_vec(), + display: "/api".to_owned(), + }, + git_common_dir: None, + normalized_remote: None, + head_commit: None, + is_linked_worktree: false, + working_tree_dirty: false, + }], + } + } + + #[tokio::test] + async fn completed_context_should_survive_a_later_blocking_deadline() { + let context = ExploreExecutionContext::new(&ExecutionPolicy { + max_explore_wall_time_ms: 25, + ..ExecutionPolicy::default() + }); + let registry_context = run_bounded_explore_blocking(&context, || Ok("registry".to_owned())) + .await + .expect("first stage"); + let result = run_bounded_explore_blocking(&context, || { + std::thread::sleep(Duration::from_millis(100)); + Ok(()) + }) + .await; + + assert!(matches!(result, Err(ExploreBlockingError::Deadline))); + assert_eq!(registry_context, "registry"); + } + + #[tokio::test] + async fn dropping_request_should_cancel_blocking_work_and_release_its_permit() { + let (started_sender, started_receiver) = tokio::sync::oneshot::channel(); + let (stopped_sender, stopped_receiver) = tokio::sync::oneshot::channel(); + let task = tokio::spawn(async move { + let context = ExploreExecutionContext::new(&ExecutionPolicy { + max_explore_wall_time_ms: 5_000, + ..ExecutionPolicy::default() + }); + let cancellation = context.cancellation.clone(); + run_bounded_explore_blocking(&context, move || { + let _ = started_sender.send(()); + while !cancellation.is_cancelled() { + std::thread::yield_now(); + } + let _ = stopped_sender.send(()); + Ok(()) + }) + .await + }); + started_receiver.await.expect("blocking stage started"); + task.abort(); + + tokio::time::timeout(Duration::from_millis(250), stopped_receiver) + .await + .expect("drop cancellation should stop blocking work promptly") + .expect("blocking stage should report its stop"); + let context = ExploreExecutionContext::new(&ExecutionPolicy::default()); + run_bounded_explore_blocking(&context, || Ok(())) + .await + .expect("released permit should admit the next stage"); + } + + #[tokio::test] + async fn snapshot_assembly_should_stop_after_a_later_stage_deadline() { + let evidence_polled = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let evidence_observer = evidence_polled.clone(); + let snapshot = assemble_explore_snapshot( + async { Ok(workspace()) }, + async { Ok(Vec::new()) }, + async { Err(ExploreBlockingError::Deadline) }, + async move { + evidence_observer.store(true, std::sync::atomic::Ordering::Relaxed); + Ok(Vec::new()) + }, + ) + .await + .expect("loaded registry and freshness should be preserved"); + + assert!(snapshot.freshness_loaded); + assert_eq!(snapshot.incomplete_stage, Some("graph loading")); + assert!(!evidence_polled.load(std::sync::atomic::Ordering::Relaxed)); + } + + #[tokio::test] + async fn expired_deadline_should_interrupt_real_snapshot_loading() { + let temporary = tempfile::tempdir().expect("temporary directory"); + let database = temporary.path().join("graph.db"); + let mut store = SqliteStore::open(&database).expect("store"); + store + .save_workspace_registry(&workspace()) + .expect("workspace registry"); + drop(store); + let context = ExploreExecutionContext { + deadline: tokio::time::Instant::now(), + cancellation: tokio_util::sync::CancellationToken::new(), + }; + + let result = load_explore_snapshot(&database, "commerce", &context).await; + assert!(matches!(result, Err(ExploreBlockingError::Deadline))); + } +} diff --git a/crates/code-system-graph-cli/src/explore/tests.rs b/crates/code-system-graph-cli/src/explore/tests.rs new file mode 100644 index 0000000..7353c9f --- /dev/null +++ b/crates/code-system-graph-cli/src/explore/tests.rs @@ -0,0 +1,282 @@ +//! Unit tests for independently bounded Explore stages. + +use code_system_graph_core::{ + LocalNeighbor, LocalNeighborDirection, LocalNeighborResult, ProviderError, ProviderExecution, ProviderTransport, ResolvedSymbol +}; +use code_system_graph_model::{ + CheckoutId, NativePath, NativePathEncoding, OverallFreshness, RepoId, RepositoryRecord, ToolStatus, WorkspaceId, WorkspaceRecord +}; + +use super::correlation::ExploreCorrelationInput; +use super::provider::{ + ExploreNeighborLimits, local_relationships_truncated, record_explore_neighbor_results +}; +use super::runtime::{ + ExploreBlockingError, ExploreBudgetLedger, ExploreExecutionContext, ExploreProviderData, ExploreSnapshotData, run_bounded_explore_blocking +}; +use super::{correlate_explore_with_deadline, explore_next_actions, partial_snapshot_envelope}; + +fn repository() -> RepositoryRecord { + RepositoryRecord { + id: RepoId::new("repo:api"), + checkout_id: CheckoutId::new("checkout:api"), + alias: "api".to_owned(), + canonical_path: NativePath { + encoding: NativePathEncoding::Utf8, + bytes: b"/api".to_vec(), + display: "/api".to_owned(), + }, + git_common_dir: None, + normalized_remote: None, + head_commit: None, + is_linked_worktree: false, + working_tree_dirty: false, + } +} + +fn workspace(repository: RepositoryRecord) -> WorkspaceRecord { + WorkspaceRecord { + id: WorkspaceId::new("workspace:commerce"), + name: "commerce".to_owned(), + manifest_hash: "manifest".to_owned(), + config_path: None, + repositories: vec![repository], + } +} + +fn neighbor_result(direction: LocalNeighborDirection) -> LocalNeighborResult { + LocalNeighborResult { + symbol: "anchor".to_owned(), + direction, + neighbors: vec![LocalNeighbor { + name: "neighbor".to_owned(), + kind: "function".to_owned(), + file_path: "src/lib.rs".to_owned(), + start_line: 1, + }], + execution: ProviderExecution { + transport: ProviderTransport::Cli, + output_bytes: 32, + truncated: false, + degradations: Vec::new(), + }, + } +} + +#[test] +fn isolated_neighbor_direction_failure_should_not_complete_anchor() { + for fail_callers in [true, false] { + let incoming = if fail_callers { + Err(ProviderError::Cancelled) + } else { + Ok(neighbor_result(LocalNeighborDirection::Callers)) + }; + let outgoing = if fail_callers { + Ok(neighbor_result(LocalNeighborDirection::Callees)) + } else { + Err(ProviderError::Cancelled) + }; + let mut ledger = ExploreBudgetLedger::default(); + let mut data = ExploreProviderData::default(); + let completed = record_explore_neighbor_results( + "anchor", + incoming, + outgoing, + &ExploreNeighborLimits { + operation_limit: 4, + enrichment_bytes: 1_024, + neighbor_limit: 4, + relationship_limit: 8, + }, + &mut ledger, + &mut data, + ); + + assert!(!completed); + assert_eq!(data.local_relationships.len(), 1); + let failed_direction = if fail_callers { "callers" } else { "callees" }; + assert!(ledger.gaps.iter().any(|gap| gap.contains(failed_direction))); + } +} + +#[test] +fn both_neighbor_directions_should_complete_one_anchor() { + let mut ledger = ExploreBudgetLedger::default(); + let mut data = ExploreProviderData::default(); + let completed = record_explore_neighbor_results( + "anchor", + Ok(neighbor_result(LocalNeighborDirection::Callers)), + Ok(neighbor_result(LocalNeighborDirection::Callees)), + &ExploreNeighborLimits { + operation_limit: 4, + enrichment_bytes: 1_024, + neighbor_limit: 4, + relationship_limit: 8, + }, + &mut ledger, + &mut data, + ); + + assert!(completed); + assert_eq!(data.local_relationships.len(), 2); + assert_eq!(ledger.gaps, [] as [String; 0]); +} + +#[test] +fn recognized_blast_radius_entries_should_form_strict_fallback_anchors() { + let source = "**Blast radius — what depends on these**\n\n\ + - `ExecutionPolicy` (crates/core/src/policy.rs:198) — 34 callers\n\ + - `unsafe` (../outside.rs:2) — rejected\n\ + - `second` (src/second.rs:7) — retained\n\n\ + **Source Code**\n\n\ + - `after` (src/after.rs:3) — ignored\n"; + let anchors = super::fallback_explore_anchors(source, 2); + + assert_eq!(anchors.len(), 2); + assert_eq!(anchors[0].name, "ExecutionPolicy"); + assert_eq!(anchors[0].file_path, "crates/core/src/policy.rs"); + assert_eq!(anchors[0].start_line, 198); + assert_eq!(anchors[1].name, "second"); +} + +#[test] +fn arbitrary_markdown_should_not_form_fallback_anchors() { + let anchors = super::fallback_explore_anchors( + "# Symbols\n\n- `invented` (src/invented.rs:9) — untrusted\n", + 5, + ); + assert_eq!(anchors, [] as [code_system_graph_core::ResolvedSymbol; 0]); +} + +#[test] +fn explore_follow_up_should_retain_the_required_query() { + let actions = explore_next_actions( + "commerce", + &repository(), + "create_order callers", + &[], + &code_system_graph_core::ExecutionPolicy::default(), + ); + let action = actions + .iter() + .find(|action| action.tool == "explore") + .expect("Explore should advertise one scoped follow-up"); + + assert_eq!( + action.arguments.get("query").map(String::as_str), + Some("create_order callers") + ); + assert_eq!( + action.arguments.get("repository").map(String::as_str), + Some("api") + ); +} + +#[test] +fn local_relationship_limit_should_only_report_omitted_work() { + assert!(!local_relationships_truncated(48, 48, false)); + assert!(local_relationships_truncated(48, 48, true)); + assert!(local_relationships_truncated(49, 48, false)); +} + +#[test] +fn later_snapshot_deadline_should_preserve_loaded_context_in_degraded_data() { + let repository = repository(); + let snapshot = ExploreSnapshotData { + registry: workspace(repository.clone()), + freshness: Vec::new(), + freshness_loaded: true, + nodes: Vec::new(), + edges: Vec::new(), + evidence: Vec::new(), + gaps: vec!["persisted graph unavailable".to_owned()], + incomplete_stage: Some("evidence loading"), + }; + let envelope = partial_snapshot_envelope( + "commerce", + &super::ExploreInput { + workspace: "commerce".to_owned(), + repository: Some("api".to_owned()), + query: "create_order callers".to_owned(), + max_files: None, + }, + repository, + snapshot, + &code_system_graph_core::ExecutionPolicy::default(), + ); + + assert_eq!(envelope.status, ToolStatus::Degraded); + assert_eq!(envelope.freshness.overall, OverallFreshness::Fresh); + let report = envelope.data.expect("loaded registry should remain usable"); + assert_eq!(report.repository.alias, "api"); + assert!( + report + .coverage + .gaps + .iter() + .any(|gap| gap.contains("persisted graph")) + ); + assert!( + report + .coverage + .gaps + .iter() + .any(|gap| gap.contains("evidence loading")) + ); + assert_eq!(report.execution.provider_operations, 0); +} + +#[tokio::test] +async fn blocking_stage_should_signal_cancellation_at_the_global_deadline() { + let context = ExploreExecutionContext { + deadline: tokio::time::Instant::now() + std::time::Duration::from_millis(5), + cancellation: tokio_util::sync::CancellationToken::new(), + }; + let result = run_bounded_explore_blocking(&context, || { + std::thread::sleep(std::time::Duration::from_millis(50)); + Ok::<_, String>(()) + }) + .await; + + assert!(matches!(result, Err(ExploreBlockingError::Deadline))); + assert!(context.cancellation.is_cancelled()); +} + +#[tokio::test] +async fn correlation_stage_should_record_its_own_deadline_gap() { + let context = ExploreExecutionContext { + deadline: tokio::time::Instant::now(), + cancellation: tokio_util::sync::CancellationToken::new(), + }; + let mut ledger = ExploreBudgetLedger::default(); + let handoffs = correlate_explore_with_deadline( + ExploreCorrelationInput { + repository: repository(), + anchors: vec![ResolvedSymbol { + local_id: None, + name: "anchor".to_owned(), + qualified_name: None, + kind: "function".to_owned(), + file_path: "src/lib.rs".to_owned(), + start_line: 1, + score: None, + }], + nodes: Vec::new(), + edges: Vec::new(), + evidence: Vec::new(), + repositories: std::collections::BTreeMap::new(), + policy: code_system_graph_core::ExecutionPolicy::default(), + }, + &context, + &mut ledger, + ) + .await; + + assert_eq!(handoffs, [] as [super::ExploreFederatedHandoff; 0]); + assert!( + ledger + .gaps + .iter() + .any(|gap| { gap == "federated handoff correlation exceeded maxExploreWallTimeMs" }) + ); +} diff --git a/crates/code-system-graph-cli/src/http_server.rs b/crates/code-system-graph-cli/src/http_server.rs index 70d0375..0d1d030 100644 --- a/crates/code-system-graph-cli/src/http_server.rs +++ b/crates/code-system-graph-cli/src/http_server.rs @@ -19,7 +19,7 @@ use axum::middleware::{self, Next}; use axum::response::{IntoResponse, Response}; use axum::routing::{get, post}; use code_system_graph_core::{ - ChangeAnalysisOptions, ImpactReport, ImpactRequest, LocalContextResult, SearchReport + ChangeAnalysisOptions, ExecutionPolicy, ImpactReport, ImpactRequest, SearchReport }; use code_system_graph_model::{ FreshnessSummary, NodeKind, OverallFreshness, RepoId, ToolEnvelope, ToolStatus @@ -34,7 +34,7 @@ use tokio_util::sync::CancellationToken; use tower_http::sensitive_headers::SetSensitiveRequestHeadersLayer; use crate::{ - ApplicationError, CODEGRAPH_DISABLED_CODE, CODEGRAPH_DISABLED_MESSAGE, ChangesInput, CommunityInput, CommunityReport, ExploreInput, SearchInput, TraceInput, analyze_workspace_changes, communities_workspace, explore_repository, impact_workspace, impact_workspace_with_codegraph, search_workspace, status_workspace, trace_workspace + ApplicationError, CODEGRAPH_DISABLED_CODE, CODEGRAPH_DISABLED_MESSAGE, ChangesInput, CommunityInput, CommunityReport, ExploreInput, ExploreReport, QueryActionCapabilities, SearchInput, TraceInput, analyze_workspace_changes, communities_workspace, explore_repository, impact_workspace, impact_workspace_with_codegraph, search_workspace_for_delivery, status_workspace, trace_workspace }; /// Default loopback address used by optional HTTP delivery. @@ -48,7 +48,7 @@ const RATE_LIMIT_REQUESTS: u32 = 60; const RATE_LIMIT_WINDOW: Duration = Duration::from_mins(1); const MAX_RATE_LIMIT_CLIENTS: usize = 4096; const MAX_TOKEN_BYTES: usize = 4096; -const SCHEMA_VERSION: u32 = 1; +const SCHEMA_VERSION: u32 = 2; const VERSION_HEADER: HeaderName = HeaderName::from_static("x-code-system-graph-version"); const SCHEMA_HEADER: HeaderName = HeaderName::from_static("x-code-system-graph-schema-version"); @@ -143,6 +143,8 @@ pub struct HttpServerConfig { pub codegraph_enabled: bool, /// Optional explicit `CodeGraph` executable used by local exploration and impact enrichment. pub codegraph_binary: Option, + /// Immutable effective workspace policy shared by all handlers. + pub execution_policy: ExecutionPolicy, } impl HttpServerConfig { @@ -161,6 +163,7 @@ impl HttpServerConfig { bearer_token: None, codegraph_enabled: false, codegraph_binary: None, + execution_policy: ExecutionPolicy::default(), } } @@ -186,6 +189,13 @@ impl HttpServerConfig { self } + /// Applies the validated immutable workspace execution policy. + #[must_use] + pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self { + self.execution_policy = policy; + self + } + fn validate_for(&self, bind: SocketAddr) -> Result<(), HttpServerError> { if self.workspace.trim().is_empty() { return Err(HttpServerError::InvalidConfiguration( @@ -469,7 +479,7 @@ fn secure_response(mut response: Response) -> Response { VERSION_HEADER, HeaderValue::from_static(env!("CARGO_PKG_VERSION")), ); - headers.insert(SCHEMA_HEADER, HeaderValue::from_static("1")); + headers.insert(SCHEMA_HEADER, HeaderValue::from_static("2")); headers.insert(X_CONTENT_TYPE_OPTIONS, HeaderValue::from_static("nosniff")); headers.insert(X_FRAME_OPTIONS, HeaderValue::from_static("DENY")); headers.insert(REFERRER_POLICY, HeaderValue::from_static("no-referrer")); @@ -547,8 +557,19 @@ async fn query( }; let config = Arc::clone(&state.config); tool_service_response( - run_blocking(move || search_workspace(&config.database_path, &config.workspace, &input)) - .await, + run_blocking(move || { + search_workspace_for_delivery( + &config.database_path, + &config.workspace, + &input, + &config.execution_policy, + QueryActionCapabilities { + source_context: false, + explore: config.codegraph_enabled, + }, + ) + }) + .await, ) } @@ -582,11 +603,12 @@ async fn explore( Ok(input) => input, Err(rejection) => return json_rejection_response(&rejection), }; - let envelope: ToolEnvelope = explore_repository( + let envelope: ToolEnvelope = explore_repository( &state.config.database_path, &state.config.workspace, &input, state.config.codegraph_binary.clone(), + &state.config.execution_policy, ) .await; success_response(envelope) @@ -662,9 +684,19 @@ async fn contracts( limit: input.limit, }; let config = Arc::clone(&state.config); - let result: Result, ToolFailure> = - run_blocking(move || search_workspace(&config.database_path, &config.workspace, &search)) - .await; + let result: Result, ToolFailure> = run_blocking(move || { + search_workspace_for_delivery( + &config.database_path, + &config.workspace, + &search, + &config.execution_policy, + QueryActionCapabilities { + source_context: false, + explore: config.codegraph_enabled, + }, + ) + }) + .await; tool_service_response(result) } diff --git a/crates/code-system-graph-cli/src/lib.rs b/crates/code-system-graph-cli/src/lib.rs index ed4849c..8f1a83c 100644 --- a/crates/code-system-graph-cli/src/lib.rs +++ b/crates/code-system-graph-cli/src/lib.rs @@ -1,6 +1,8 @@ //! Delivery-layer orchestration shared by the `Code System Graph` CLI and MCP server. +mod agent_markdown; mod agent_plugin; +mod explore; pub mod http_server; pub mod mcp; mod sync; @@ -20,7 +22,7 @@ pub use agent_plugin::{ }; use atomic_write_file::AtomicWriteFile; use code_system_graph_core::{ - AffectedTestsRequest, AnalyzerVersions, ArtifactKey, BatchAction, BatchPlanError, BitbucketProvider, ChangeAnalysisError, ChangeAnalysisOptions, ChangeError, ChangeImpactReport, ChangeProvider, ChangeRequest, ChangeScope, ChangeSet, CodeGraphConfig, CodeGraphCorroborationAnchorLimit, CodeGraphProvider, CommunityError, ConfigDoctorInput, ConfigError, ConfigExtractionError, ContractReport, ContractRequest, CorroborationReport, DEFAULT_MAX_CODEGRAPH_CORROBORATION_ANCHORS_PER_REPO, DataDocument, DataExtractionError, DeclaredImplementation, DeclaredTestCase, DoctorReport, DoctorRequest, DocumentationDocument, DocumentationExtractionError, EXTRACTION_CONTRACT_VERSION, EffectiveRepositoryConfig, EventDocument, EventExtractionError, EventGraphFacts, ExecutionPolicy, ExitCode, ExportReport, ExportRequest, ExtractionBudgets, ExtractionGraphFacts, ExtractionLimitExceeded, ExtractionTracker, ExtractorBatch, ExtractorBatchPlan, FederatedGraph, FreshnessDoctorInput, GeneratedClientError, GeneratedClientMetadata, GitCliChangeProvider, GitHubProvider, GraphqlDocument, GraphqlExtractionError, GraphqlGraphFacts, HttpBoundary, HttpExtractionError, ImpactContext, ImpactError, ImpactReport, ImpactRequest, ImpactTarget, IncrementalPlan, InfrastructureDocument, InfrastructureExtractionError, IntegrityDoctorInput, InterfaceError, LinkError, LocalCodeIntelligenceProvider, LocalContextRequest, LocalContextResult, LocalEnrichmentInput, LocalEnrichmentStatus, LocalImpactItem, LocalImpactRequest, ManifestEdit, ManifestEditError, ManifestError, ManualLinkConfig, ManualLinkError, PackageGraphFacts, PackageManifest, PackageManifestError, PrAuthToken, ProtobufDocument, ProtobufExtractionError, ProtobufGraphFacts, ProviderBudget, ProviderCapability, ProviderDoctorInput, ProviderDoctorStatus, ProviderError, ProviderRequest, ProviderStatus, PullRequestCoordinates, PullRequestError, PullRequestInspectRequest, PullRequestInspection, PullRequestListPage, PullRequestListRequest, PullRequestListState, PullRequestProvider, PullRequestProviderConfig, PullRequestProviderKind, QueryError, RecommendedCommand, RegisteredWorkspace, RegistryError, ReqwestPrHttpTransport, SafeConfigDocument, SchemaDoctorInput, SearchFilters, SearchReport, SearchRequest, SourceEpistemicStatus, SourceGraphFacts, SourceLanguage, SourceObservation, SourceRole, SourceSyntaxError, SourceSyntaxLanguage, SourceWarning, SymbolAnchor, SymbolCorroboration, TraceError, TraversalReport, TraversalRequest, WorkspaceManifest, affected_link_keys, analyze_changes, analyze_communities_with_progress, analyze_impact, apply_openapi_override, classify_interface_error, commit_manifest_edit, compare_community_snapshots, corroborate_repository, declared_implementation, declared_test_case, doctor, documents_to_graph, encode_native_path, event_documents_to_graph, export_graph, extract_asyncapi, extract_codeowners, extract_data_artifact, extract_docker_compose, extract_generated_client_metadata, extract_graphql_document_with_tracker, extract_graphql_persisted_operations_with_tracker, extract_helm, extract_kubernetes, extract_markdown, extract_openapi_with_tracker, extract_package_manifest_with_tracker, extract_protobuf_with_tracker, extract_safe_config, extract_service_catalog, extract_terraform, graphql_documents_to_graph, inspect_contracts, inspect_source_syntax, link_declared_implementations_with_ambiguities, link_declared_tests_with_ambiguities, link_http_boundaries_with_ambiguities, link_registered_package_owners, load_extractor_batch_with_budgets, merge_affected_link_neighborhoods, package_manifest_to_graph, parse_event_source, parse_go_source_with_tracker, parse_graphql_source_with_tracker, parse_java_source_with_tracker, parse_javascript_source_at_path_with_tracker, parse_literal_sql_source_at_root, parse_manifest, parse_manifest_with_extensions, parse_protobuf_generated_source, parse_python_source_with_tracker, parse_rust_source_with_tracker, parse_typescript_source_at_path_with_tracker, plan_extractor_batches, plan_incremental_scan, precheck_focused_source_values, preview_add_manual_link, preview_add_repository, preview_remove_repository, protobuf_documents_to_graph, register_workspace, resolve_manual_links, resolve_repository_config, resolve_repository_config_with_use_gitignore, search, source_observations_to_graph, store_extractor_batch, traverse + AffectedTestsRequest, AgentNextAction, AnalyzerVersions, ArtifactKey, BatchAction, BatchPlanError, BitbucketProvider, ChangeAnalysisError, ChangeAnalysisOptions, ChangeError, ChangeImpactReport, ChangeProvider, ChangeRequest, ChangeScope, ChangeSet, CodeGraphConfig, CodeGraphProvider, CommunityError, ConfigDoctorInput, ConfigError, ConfigExtractionError, ContractReport, ContractRequest, CorroborationReport, DataDocument, DataExtractionError, DeclaredImplementation, DeclaredTestCase, DoctorReport, DoctorRequest, DocumentationDocument, DocumentationExtractionError, EXTRACTION_CONTRACT_VERSION, EffectiveRepositoryConfig, EventDocument, EventExtractionError, EventGraphFacts, ExecutionPolicy, ExitCode, ExportReport, ExportRequest, ExtractionBudgets, ExtractionGraphFacts, ExtractionLimitExceeded, ExtractionTracker, ExtractorBatch, ExtractorBatchPlan, FederatedGraph, FreshnessDoctorInput, GeneratedClientError, GeneratedClientMetadata, GitCliChangeProvider, GitHubProvider, GraphqlDocument, GraphqlExtractionError, GraphqlGraphFacts, HttpBoundary, HttpExtractionError, ImpactContext, ImpactError, ImpactReport, ImpactRequest, ImpactTarget, IncrementalPlan, InfrastructureDocument, InfrastructureExtractionError, IntegrityDoctorInput, InterfaceError, LinkError, LocalCodeIntelligenceProvider, LocalContextRequest, LocalEnrichmentInput, LocalEnrichmentStatus, LocalImpactItem, LocalImpactRequest, LocalNeighborDirection, LocalNeighborsRequest, ManifestEdit, ManifestEditError, ManifestError, ManualLinkConfig, ManualLinkError, PackageGraphFacts, PackageManifest, PackageManifestError, PrAuthToken, ProtobufDocument, ProtobufExtractionError, ProtobufGraphFacts, ProviderBudget, ProviderCapability, ProviderDoctorInput, ProviderDoctorStatus, ProviderRequest, ProviderStatus, PullRequestCoordinates, PullRequestError, PullRequestInspectRequest, PullRequestInspection, PullRequestListPage, PullRequestListRequest, PullRequestListState, PullRequestProvider, PullRequestProviderConfig, PullRequestProviderKind, QueryError, RecommendedCommand, RegisteredWorkspace, RegistryError, ReqwestPrHttpTransport, SafeConfigDocument, SchemaDoctorInput, SearchFilters, SearchReport, SearchRequest, SourceEpistemicStatus, SourceGraphFacts, SourceLanguage, SourceObservation, SourceRole, SourceSyntaxError, SourceSyntaxLanguage, SourceWarning, SymbolAnchor, SymbolCorroboration, TraceError, TraversalReport, TraversalRequest, WorkspaceManifest, affected_link_keys, analyze_changes, analyze_communities_with_progress, analyze_impact, apply_openapi_override, classify_interface_error, commit_manifest_edit, compare_community_snapshots, corroborate_repository, declared_implementation, declared_test_case, doctor, documents_to_graph, encode_native_path, event_documents_to_graph, export_graph, extract_asyncapi, extract_codeowners, extract_data_artifact, extract_docker_compose, extract_generated_client_metadata, extract_graphql_document_with_tracker, extract_graphql_persisted_operations_with_tracker, extract_helm, extract_kubernetes, extract_markdown, extract_openapi_with_tracker, extract_package_manifest_with_tracker, extract_protobuf_with_tracker, extract_safe_config, extract_service_catalog, extract_terraform, graphql_documents_to_graph, inspect_contracts, inspect_source_syntax, link_declared_implementations_with_ambiguities, link_declared_tests_with_ambiguities, link_http_boundaries_with_ambiguities, link_registered_package_owners, load_extractor_batch_with_budgets, merge_affected_link_neighborhoods, package_manifest_to_graph, parse_event_source, parse_go_source_with_tracker, parse_graphql_source_with_tracker, parse_java_source_with_tracker, parse_javascript_source_at_path_with_tracker, parse_literal_sql_source_at_root, parse_manifest, parse_manifest_with_extensions, parse_protobuf_generated_source, parse_python_source_with_tracker, parse_rust_source_with_tracker, parse_typescript_source_at_path_with_tracker, plan_extractor_batches, plan_incremental_scan, precheck_focused_source_values, preview_add_manual_link, preview_add_repository, preview_remove_repository, protobuf_documents_to_graph, register_workspace, resolve_manual_links, resolve_repository_config, resolve_repository_config_with_use_gitignore, search, source_observations_to_graph, store_extractor_batch, traverse }; pub use code_system_graph_core::{ ConfigSource, DEFAULT_EXCLUDES, IgnorePolicy, PROTECTED_EXCLUDES, discover_repository_files @@ -31,6 +33,9 @@ use code_system_graph_model::{ use code_system_graph_store_sqlite::{ ManualLinkDisposition, ManualLinkRecord, ProviderCapabilityRecord, QueryCacheRecord, SnapshotBatch, SqliteStore, StoreError, StoreLock, latest_schema_version }; +pub use explore::{ + ExploreCoverage, ExploreEvidenceLocation, ExploreExecution, ExploreFederatedHandoff, ExploreInput, ExploreLocalRelationship, ExploreReport, ExploreRepositoryContext, explore_repository +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; pub use sync::{ @@ -309,6 +314,30 @@ pub(crate) fn load_execution_policy( .map_err(Into::into) } +/// Loads the immutable global execution policy for a delivery server and verifies its workspace. +/// +/// # Errors +/// +/// Returns [`ApplicationError`] when the manifest is invalid, untrusted, or names another +/// workspace. +pub fn load_server_execution_policy( + config_path: &Path, + expected_workspace: &str, +) -> Result { + let manifest_source = read_file(config_path)?; + let manifest = parse_manifest(&manifest_source)?; + validate_global_policy_source(config_path, &manifest)?; + if manifest.name != expected_workspace { + return Err(ApplicationError::Initialization(format!( + "workspace name `{expected_workspace}` does not match manifest name `{}`", + manifest.name + ))); + } + ExecutionPolicy::resolve(manifest.execution_policy.as_ref()) + .map_err(ManifestError::from) + .map_err(Into::into) +} + /// Observable result of a successful scan. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct ScanSummary { @@ -453,40 +482,18 @@ pub struct ConfigReport { pub extraction_budgets: ExtractionBudgets, /// Effective global supervised-execution policy, including applied defaults. pub execution_policy: ExecutionPolicy, - /// Canonical fingerprint of the effective supervised-execution policy. - pub execution_policy_fingerprint: String, + /// Canonical fingerprint of scan-affecting operational limits. + pub scan_fingerprint: String, + /// Canonical fingerprint of ephemeral agent-delivery limits. + pub agent_delivery_fingerprint: String, + /// Trusted source of the effective policy. + pub execution_policy_origin: String, /// Effective per-repository configuration in alias order. pub repositories: Vec, } -/// Additive execution-policy view used by the CLI without expanding [`ExecutionPolicy`]. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct ExecutionPolicyReport { - /// Patch-compatible base execution policy. - #[serde(flatten)] - pub base: ExecutionPolicy, - /// Effective `CodeGraph` corroboration bound. - #[serde(rename = "maxCodeGraphCorroborationAnchorsPerRepo")] - #[schemars(with = "i64")] - pub max_codegraph_corroboration_anchors_per_repo: CodeGraphCorroborationAnchorLimit, -} - -impl std::ops::Deref for ExecutionPolicyReport { - type Target = ExecutionPolicy; - - fn deref(&self) -> &Self::Target { - &self.base - } -} - -impl ExecutionPolicyReport { - /// Returns the canonical fingerprint of every effective policy value in this report. - #[must_use] - pub fn fingerprint(&self) -> String { - self.base - .fingerprint_with_codegraph_limit(self.max_codegraph_corroboration_anchors_per_repo) - } -} +/// Effective execution-policy view emitted by configuration reporting. +pub type ExecutionPolicyReport = ExecutionPolicy; /// Extended configuration report emitted by `csgraph config show`. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] @@ -499,8 +506,12 @@ pub struct ExtendedConfigReport { pub extraction_budgets: ExtractionBudgets, /// Effective global supervised-execution policy, including additive patch settings. pub execution_policy: ExecutionPolicyReport, - /// Canonical fingerprint of the complete effective supervised-execution policy. - pub execution_policy_fingerprint: String, + /// Canonical fingerprint of scan-affecting operational limits. + pub scan_fingerprint: String, + /// Canonical fingerprint of ephemeral agent-delivery limits. + pub agent_delivery_fingerprint: String, + /// Trusted source of the effective policy. + pub execution_policy_origin: String, /// Effective per-repository configuration in alias order. pub repositories: Vec, } @@ -699,27 +710,6 @@ pub struct ChangesInput { pub scope: ChangeScope, } -/// Ephemeral intra-repository source and flow exploration input. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct ExploreInput { - /// Workspace selected by the delivery adapter. - pub workspace: String, - /// Registered repository alias; omitted only when the workspace contains one repository. - #[serde(default)] - pub repository: Option, - /// Focused symbol, flow, architecture, or implementation question. - pub query: String, - /// Maximum source files returned by the local provider. - #[serde(default = "default_explore_max_files")] - #[schemars(range(min = 1, max = 25))] - pub max_files: usize, -} - -const fn default_explore_max_files() -> usize { - 12 -} - /// Explicit remote pull-request inspection input shared by CLI and MCP. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct PullRequestInput { @@ -794,7 +784,6 @@ struct WorkspaceContext { repository_configs: BTreeMap, extraction_budgets: ExtractionBudgets, execution_policy: ExecutionPolicy, - codegraph_corroboration_anchor_limit: CodeGraphCorroborationAnchorLimit, } /// Resolves and reports native discovery exclusions without opening a graph database. @@ -810,10 +799,12 @@ pub fn show_config( let context = load_workspace_context(config_path, &ScanOverrides::default())?; let repositories = config_report_repositories(&context, selected_repository)?; Ok(ConfigReport { - schema_version: 1, + schema_version: 2, workspace: context.manifest.name, extraction_budgets: context.extraction_budgets, - execution_policy_fingerprint: context.execution_policy.fingerprint(), + scan_fingerprint: context.execution_policy.scan_fingerprint(), + agent_delivery_fingerprint: context.execution_policy.agent_delivery_fingerprint(), + execution_policy_origin: "global_manifest".to_owned(), execution_policy: context.execution_policy, repositories, }) @@ -830,19 +821,14 @@ pub fn show_extended_config( ) -> Result { let context = load_workspace_context(config_path, &ScanOverrides::default())?; let repositories = config_report_repositories(&context, selected_repository)?; - let fingerprint = context - .execution_policy - .fingerprint_with_codegraph_limit(context.codegraph_corroboration_anchor_limit); Ok(ExtendedConfigReport { - schema_version: 1, + schema_version: 2, workspace: context.manifest.name, extraction_budgets: context.extraction_budgets, - execution_policy: ExecutionPolicyReport { - base: context.execution_policy, - max_codegraph_corroboration_anchors_per_repo: context - .codegraph_corroboration_anchor_limit, - }, - execution_policy_fingerprint: fingerprint, + scan_fingerprint: context.execution_policy.scan_fingerprint(), + agent_delivery_fingerprint: context.execution_policy.agent_delivery_fingerprint(), + execution_policy_origin: "global_manifest".to_owned(), + execution_policy: context.execution_policy, repositories, }) } @@ -1786,7 +1772,7 @@ pub fn trace_workspace( ToolStatus::Degraded }; Ok(ToolEnvelope { - schema_version: 1, + schema_version: 2, status, data: Some(report), freshness, @@ -1804,12 +1790,53 @@ pub fn search_workspace( database_path: &Path, workspace: &str, input: &SearchInput, +) -> Result, ApplicationError> { + search_workspace_with_policy(database_path, workspace, input, &ExecutionPolicy::default()) +} + +/// Searches with the immutable workspace policy used by a long-lived delivery server. +/// +/// # Errors +/// +/// Returns the same failures as [`search_workspace`]. +pub fn search_workspace_with_policy( + database_path: &Path, + workspace: &str, + input: &SearchInput, + policy: &ExecutionPolicy, +) -> Result, ApplicationError> { + search_workspace_for_delivery( + database_path, + workspace, + input, + policy, + QueryActionCapabilities::NONE, + ) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub(crate) struct QueryActionCapabilities { + pub(crate) source_context: bool, + pub(crate) explore: bool, +} + +impl QueryActionCapabilities { + const NONE: Self = Self { + source_context: false, + explore: false, + }; +} + +pub(crate) fn search_workspace_for_delivery( + database_path: &Path, + workspace: &str, + input: &SearchInput, + policy: &ExecutionPolicy, + action_capabilities: QueryActionCapabilities, ) -> Result, ApplicationError> { let store = SqliteStore::open_read_only(database_path)?; let snapshot = store.current_snapshot_summary(workspace)?; - let input_json = serde_json::to_vec(input) - .map_err(|error| ApplicationError::Initialization(error.to_string()))?; - let input_fingerprint = stable_id_bytes("query-cache", &input_json); + let input_fingerprint = query_cache_fingerprint(input, policy, action_capabilities)?; let now_unix_ms = current_unix_millis(); if let Some(cached) = store.load_query_cache( workspace, @@ -1852,7 +1879,25 @@ pub fn search_workspace( offset: input.offset, limit: input.limit, }; - let report = search(&nodes, &request)?; + let mut report = search(&nodes, &request)?; + let registry = store.load_workspace_registry(workspace)?; + report.next_actions = query_next_actions( + workspace, + input, + &report, + ®istry, + policy, + action_capabilities, + ); + if report.hits.is_empty() { + report.coverage.gaps.push(if action_capabilities.explore { + "Query searches persisted architecture entities and contracts, not source-code bodies; use Explore for implementation text." + .to_owned() + } else { + "Query searches persisted architecture entities and contracts, not source-code bodies; source exploration is unavailable in this delivery profile." + .to_owned() + }); + } let status = if report.coverage.gaps.is_empty() && freshness.overall == OverallFreshness::Fresh { ToolStatus::Ok @@ -1860,7 +1905,7 @@ pub fn search_workspace( ToolStatus::Degraded }; let envelope = ToolEnvelope { - schema_version: 1, + schema_version: 2, status, data: Some(report), freshness, @@ -1886,6 +1931,76 @@ pub fn search_workspace( Ok(envelope) } +fn query_cache_fingerprint( + input: &SearchInput, + policy: &ExecutionPolicy, + capabilities: QueryActionCapabilities, +) -> Result { + let input_json = + serde_json::to_vec(&(input, policy.agent_delivery_fingerprint(), capabilities)) + .map_err(|error| ApplicationError::Initialization(error.to_string()))?; + Ok(stable_id_bytes("query-cache", &input_json)) +} + +fn query_next_actions( + workspace: &str, + input: &SearchInput, + report: &SearchReport, + registry: &WorkspaceRecord, + policy: &ExecutionPolicy, + capabilities: QueryActionCapabilities, +) -> Vec { + let maximum = usize::try_from(policy.max_agent_next_actions_per_response) + .expect("validated policy count is usize-representable"); + let repository_maximum = usize::try_from(policy.max_query_repository_suggestions) + .expect("validated policy count is usize-representable"); + let mut actions = if capabilities.source_context { + report + .hits + .iter() + .take(maximum) + .map(|hit| AgentNextAction { + tool: "source_context".to_owned(), + arguments: BTreeMap::from([ + ("workspace".to_owned(), workspace.to_owned()), + ("node_id".to_owned(), hit.node.id.as_str().to_owned()), + ]), + rationale: format!("Inspect persisted evidence for `{}`.", hit.node.label), + }) + .collect::>() + } else { + Vec::new() + }; + if report.hits.is_empty() && capabilities.explore { + let normalized = input.query.to_lowercase(); + actions.extend( + registry + .repositories + .iter() + .filter(|repository| { + normalized + .split_whitespace() + .any(|token| token == repository.alias.to_lowercase()) + }) + .take(repository_maximum) + .map(|repository| AgentNextAction { + tool: "explore".to_owned(), + arguments: BTreeMap::from([ + ("workspace".to_owned(), workspace.to_owned()), + ("repository".to_owned(), repository.alias.clone()), + ("query".to_owned(), input.query.clone()), + ]), + rationale: format!( + "Explore source in the recognized repository alias `{}`.", + repository.alias + ), + }), + ); + } + actions.truncate(maximum); + actions +} + /// Executes an advanced bounded traversal against the current immutable graph. /// /// # Errors @@ -1919,7 +2034,7 @@ pub fn traverse_workspace( ToolStatus::Degraded }; Ok(ToolEnvelope { - schema_version: 1, + schema_version: 2, status, data: Some(report), freshness, @@ -1993,7 +2108,7 @@ pub fn communities_workspace( ToolStatus::Degraded }; Ok(ToolEnvelope { - schema_version: 1, + schema_version: 2, status, data: Some(report), freshness, @@ -2004,184 +2119,6 @@ pub fn communities_workspace( }) } -/// Explores one registered checkout through bounded, ephemeral local code intelligence. -/// -/// The returned [`LocalContextResult::content`] may contain source code and must never be -/// persisted, logged, cached, or included in audit records. -pub async fn explore_repository( - database_path: &Path, - workspace: &str, - input: &ExploreInput, - binary: Option, -) -> ToolEnvelope { - if input.workspace != workspace { - return explore_error_envelope(format!( - "workspace `{}` is outside this server's configured workspace `{workspace}`", - input.workspace - )); - } - if !(1..=25).contains(&input.max_files) { - return explore_error_envelope("max_files must be between 1 and 25".to_owned()); - } - - let store = match SqliteStore::open_read_only(database_path) { - Ok(store) => store, - Err(error) => return explore_error_envelope(error.to_string()), - }; - let registry = match store.load_workspace_registry(workspace) { - Ok(registry) => registry, - Err(error) => return explore_error_envelope(error.to_string()), - }; - let persisted_freshness = match store.load_current_freshness(workspace) { - Ok(freshness) => freshness, - Err(error) => return explore_error_envelope(error.to_string()), - }; - let freshness = freshness_summary(&persisted_freshness); - let requested_alias = input - .repository - .as_deref() - .map(str::trim) - .filter(|alias| !alias.is_empty()); - let repository = match select_explore_repository(®istry, requested_alias, workspace) { - Ok(repository) => repository, - Err(message) => { - return explore_scoped_error_envelope(freshness, ToolStatus::Error, message); - } - }; - - let mut config = CodeGraphConfig::default(); - if let Some(binary) = binary { - config.binary = binary; - } - let provider = match CodeGraphProvider::new(config) { - Ok(provider) => provider, - Err(error) => { - return explore_scoped_error_envelope( - freshness, - ToolStatus::Degraded, - error.to_string(), - ); - } - }; - let result = provider - .build_local_context(LocalContextRequest { - request: ProviderRequest { - repo_id: repository.id.clone(), - project_path: native_relative_path(&repository.canonical_path), - budget: ProviderBudget { - timeout: Duration::from_secs(5), - max_output_bytes: 256 * 1024, - max_items: 25, - }, - cancellation: CancellationToken::new(), - }, - query: input.query.clone(), - max_files: input.max_files, - }) - .await; - let shutdown = provider.shutdown().await; - explore_result_envelope(result, shutdown, freshness) -} - -fn select_explore_repository<'a>( - registry: &'a WorkspaceRecord, - requested_alias: Option<&str>, - workspace: &str, -) -> Result<&'a RepositoryRecord, String> { - if let Some(alias) = requested_alias { - return registry - .repositories - .iter() - .find(|repository| repository.alias == alias) - .ok_or_else(|| { - format!("repository alias `{alias}` is not registered in workspace `{workspace}`") - }); - } - if registry.repositories.len() == 1 { - return registry.repositories.first().ok_or_else(|| { - format!("workspace `{workspace}` unexpectedly has no registered repository") - }); - } - Err(format!( - "repository is required because workspace `{workspace}` contains {} repositories", - registry.repositories.len() - )) -} - -fn explore_result_envelope( - result: Result, - shutdown: Result<(), ProviderError>, - freshness: FreshnessSummary, -) -> ToolEnvelope { - match result { - Ok(context) => { - let mut warnings = context - .execution - .degradations - .iter() - .map(|degradation| degradation.message.clone()) - .collect::>(); - if context.execution.truncated { - warnings.push("Local context was truncated by the configured budget.".to_owned()); - } - if let Err(error) = shutdown { - warnings.push(format!("CodeGraph shutdown degraded: {error}")); - } - let status = if warnings.is_empty() && freshness.overall == OverallFreshness::Fresh { - ToolStatus::Ok - } else { - ToolStatus::Degraded - }; - ToolEnvelope { - schema_version: 1, - status, - data: Some(context), - freshness, - warnings, - } - } - Err(error) => ToolEnvelope { - schema_version: 1, - status: if matches!(&error, ProviderError::InvalidRequest(_)) { - ToolStatus::Error - } else { - ToolStatus::Degraded - }, - data: None, - freshness, - warnings: vec![error.to_string()], - }, - } -} - -fn explore_scoped_error_envelope( - freshness: FreshnessSummary, - status: ToolStatus, - message: String, -) -> ToolEnvelope { - ToolEnvelope { - schema_version: 1, - status, - data: None, - freshness, - warnings: vec![message], - } -} - -fn explore_error_envelope(message: String) -> ToolEnvelope { - ToolEnvelope { - schema_version: 1, - status: ToolStatus::Error, - data: None, - freshness: FreshnessSummary { - overall: OverallFreshness::Unknown, - stale_repositories: Vec::new(), - reasons: vec!["Local repository exploration could not start.".to_owned()], - }, - warnings: vec![message], - } -} - /// Computes conservative upstream or downstream impact from the current immutable graph. /// /// The default delivery path uses persisted graph, freshness, community, test, and ownership data. @@ -2325,7 +2262,7 @@ pub async fn collect_workspace_changes_with_cancellation( item.checkout_id == repository.checkout_id && item.state == RepoFreshnessState::Fresh }); Ok(ToolEnvelope { - schema_version: 1, + schema_version: 2, status: if repository_is_fresh { ToolStatus::Ok } else { @@ -2408,7 +2345,7 @@ pub async fn analyze_workspace_changes_with_cancellation( ToolStatus::Degraded }; Ok(ToolEnvelope { - schema_version: 1, + schema_version: 2, status, data: Some(report), freshness: collected.freshness, @@ -2513,7 +2450,7 @@ pub async fn inspect_pull_request_with_cancellation( ToolStatus::Degraded }; Ok(ToolEnvelope { - schema_version: 1, + schema_version: 2, status, data: Some(inspection), freshness: FreshnessSummary { @@ -2563,7 +2500,7 @@ pub async fn list_pull_requests( ToolStatus::Degraded }; Ok(ToolEnvelope { - schema_version: 1, + schema_version: 2, status, data: Some(page), freshness: FreshnessSummary { @@ -2634,7 +2571,7 @@ fn impact_envelope( ToolStatus::Ok }; Ok(ToolEnvelope { - schema_version: 1, + schema_version: 2, status, data: Some(report), freshness, @@ -3442,7 +3379,7 @@ pub fn create_diagnostic_bundle( .map_err(|error| ApplicationError::Initialization(error.to_string()))? .as_millis(); let bundle = DiagnosticBundle { - schema_version: 1, + schema_version: 2, binary_version: env!("CARGO_PKG_VERSION").to_owned(), operating_system: std::env::consts::OS.to_owned(), architecture: std::env::consts::ARCH.to_owned(), @@ -3779,12 +3716,6 @@ fn load_workspace_context( .map_err(ManifestError::from)?; let execution_policy = ExecutionPolicy::resolve(manifest.execution_policy.as_ref()) .map_err(ManifestError::from)?; - let codegraph_corroboration_anchor_limit = CodeGraphCorroborationAnchorLimit::try_from( - manifest_extensions - .max_codegraph_corroboration_anchors_per_repo() - .unwrap_or(DEFAULT_MAX_CODEGRAPH_CORROBORATION_ANCHORS_PER_REPO), - ) - .map_err(ManifestError::from)?; for alias in overrides.repo_openapi.keys() { if !manifest.repos.contains_key(alias) { return Err(ApplicationError::UnknownOverrideRepository(alias.clone())); @@ -3796,6 +3727,8 @@ fn load_workspace_context( semantic_manifest.execution_policy = None; let mut fingerprint_material = serde_json::to_string(&semantic_manifest) .map_err(|error| ApplicationError::Initialization(error.to_string()))?; + fingerprint_material.push_str("\nscanFingerprint:"); + fingerprint_material.push_str(&execution_policy.scan_fingerprint()); fingerprint_material.push('\n'); fingerprint_material.push_str( &serde_json::to_string(&manifest_extensions) @@ -3829,7 +3762,6 @@ fn load_workspace_context( repository_configs, extraction_budgets, execution_policy, - codegraph_corroboration_anchor_limit, }) } @@ -5282,7 +5214,9 @@ fn prepare_codegraph_jobs( anchors.dedup(); limit_codegraph_anchors( &mut anchors, - context.codegraph_corroboration_anchor_limit, + context + .execution_policy + .max_codegraph_corroboration_anchors_per_repo, &repository.alias, &mut setup_degradations, ); diff --git a/crates/code-system-graph-cli/src/main.rs b/crates/code-system-graph-cli/src/main.rs index 087b7c7..627b7ab 100644 --- a/crates/code-system-graph-cli/src/main.rs +++ b/crates/code-system-graph-cli/src/main.rs @@ -13,7 +13,7 @@ use clap::{ArgAction, CommandFactory, Parser, Subcommand, ValueEnum}; use code_system_graph::http_server::{BearerToken, HttpServerConfig, serve_http}; use code_system_graph::mcp::CodeSystemGraphServer; use code_system_graph::{ - AgentPluginCreateRequest, AgentPluginCreateTarget, AgentPluginError, AgentPluginUninstallRequest, ApplicationError, ChangesInput, CommunityInput, PullRequestInput, PullRequestListInput, ScanOverrides, SearchInput, TraceInput, add_repository_to_manifest, add_workspace_to_registry, agent_plugin_exit_code, analyze_workspace_changes_with_cancellation, application_exit_code, backup_database, communities_workspace, contracts_workspace, create_agent_plugin, create_diagnostic_bundle, doctor_workspace, export_workspace, impact_workspace, impact_workspace_with_codegraph, initialize_workspace, inspect_pull_request_with_cancellation, list_pull_requests, list_repository_registry, list_workspace_registry, load_agent_plugin_mcp_binding, remove_repository_from_manifest, remove_workspace_from_registry, restore_database, run_worker_from_stdio, scan_workspace_with_overrides, search_workspace, show_extended_config, status_workspace, sync_workspace_with_overrides, trace_workspace, traverse_workspace, uninstall_composed_integration + AgentPluginCreateRequest, AgentPluginCreateTarget, AgentPluginError, AgentPluginUninstallRequest, ApplicationError, ChangesInput, CommunityInput, PullRequestInput, PullRequestListInput, ScanOverrides, SearchInput, TraceInput, add_repository_to_manifest, add_workspace_to_registry, agent_plugin_exit_code, analyze_workspace_changes_with_cancellation, application_exit_code, backup_database, communities_workspace, contracts_workspace, create_agent_plugin, create_diagnostic_bundle, doctor_workspace, export_workspace, impact_workspace, impact_workspace_with_codegraph, initialize_workspace, inspect_pull_request_with_cancellation, list_pull_requests, list_repository_registry, list_workspace_registry, load_agent_plugin_mcp_binding, load_server_execution_policy, remove_repository_from_manifest, remove_workspace_from_registry, restore_database, run_worker_from_stdio, scan_workspace_with_overrides, search_workspace_with_policy, show_extended_config, status_workspace, sync_workspace_with_overrides, trace_workspace, traverse_workspace, uninstall_composed_integration }; use code_system_graph_core::{ ChangeAnalysisOptions, ChangeScope, ContractAction, ContractRequest, ExitCode, ExportFormat, ExportRequest, ImpactDirection, ImpactOptions, ImpactRequest, ImpactTarget, PullRequestListState, PullRequestOrderSuggestion, PullRequestOverlap, PullRequestProviderKind, PullRequestSemanticInput, TraversalAlgorithm, TraversalDirection, TraversalFilters, TraversalOptions, TraversalRequest, semantic_pull_request_overlap, suggest_pull_request_order @@ -30,7 +30,7 @@ use serde::Serialize; name = "csgraph", version, about = "Federated multi-repository code intelligence", - after_help = "Examples:\n csgraph init . --name commerce\n csgraph scan --database .code-system-graph/code-system-graph.db\n csgraph query \"orders contract\" --workspace commerce --database .code-system-graph/code-system-graph.db --json\n csgraph mcp --workspace commerce --database .code-system-graph/code-system-graph.db" + after_help = "Examples:\n csgraph init . --name commerce\n csgraph scan --database .code-system-graph/code-system-graph.db\n csgraph query \"orders contract\" --config code-system-graph.yaml --workspace commerce --database .code-system-graph/code-system-graph.db --json\n csgraph mcp --config code-system-graph.yaml --workspace commerce --database .code-system-graph/code-system-graph.db" )] struct Cli { /// Suppress non-result diagnostics. @@ -227,6 +227,9 @@ enum Command { }, /// Search ranked entities in the current federated snapshot. Query { + /// Trusted global workspace manifest. + #[arg(long)] + config: PathBuf, /// `SQLite` database path. #[arg(long)] database: PathBuf, @@ -507,6 +510,9 @@ enum Command { }, /// Run the read-only MCP server over stdio. Mcp { + /// Trusted global workspace manifest; required in direct mode. + #[arg(long, required_unless_present = "binding", conflicts_with = "binding")] + config: Option, /// `SQLite` database path. #[arg(long, required_unless_present = "binding", conflicts_with = "binding")] database: Option, @@ -614,6 +620,24 @@ fn parse_impact_direction(value: &str) -> Result { } } +#[cfg(test)] +mod help_tests { + use clap::Parser as _; + + use super::Cli; + + #[test] + fn root_examples_should_include_the_required_manifest_for_direct_commands() { + let help = Cli::try_parse_from(["csgraph", "--help"]) + .expect_err("help should stop argument parsing") + .to_string(); + assert!(help.contains( + "csgraph query \"orders contract\" --config code-system-graph.yaml --workspace" + )); + assert!(help.contains("csgraph mcp --config code-system-graph.yaml --workspace")); + } +} + async fn shutdown_signal() -> std::io::Result<()> { #[cfg(unix)] { @@ -816,7 +840,7 @@ fn handle_pr_overlap(left: &std::path::Path, right: &std::path::Path) -> anyhow: println!( "{}", serde_json::to_string(&PrOverlapReport { - schema_version: 1, + schema_version: 2, overlap, order })? @@ -1274,13 +1298,15 @@ fn handle_repo_command(action: RepoCommand) -> anyhow::Result<()> { } fn handle_query( + config: &std::path::Path, database: &std::path::Path, workspace: &str, question: String, offset: usize, limit: usize, ) -> anyhow::Result<()> { - let envelope = search_workspace( + let policy = load_server_execution_policy(config, workspace)?; + let envelope = search_workspace_with_policy( database, workspace, &SearchInput { @@ -1292,6 +1318,7 @@ fn handle_query( offset, limit, }, + &policy, )?; println!("{}", serde_json::to_string(&envelope)?); Ok(()) @@ -1725,12 +1752,13 @@ async fn dispatch(cli: Cli) -> anyhow::Result<()> { max_depth, } => handle_trace(&database, &workspace, from, to, max_depth)?, Command::Query { + config, database, workspace, question, offset, limit, - } => handle_query(&database, &workspace, question, offset, limit)?, + } => handle_query(&config, &database, &workspace, question, offset, limit)?, Command::Traverse { database, workspace, @@ -2004,7 +2032,9 @@ async fn dispatch(cli: Cli) -> anyhow::Result<()> { } => { let (codegraph, codegraph_binary) = codegraph_server_policy(codegraph, codegraph_binary); + let execution_policy = load_server_execution_policy(&config, &workspace)?; let mut server_config = HttpServerConfig::new(config, database, workspace) + .with_execution_policy(execution_policy) .with_bind(SocketAddr::new(host, port)) .with_codegraph(codegraph, codegraph_binary); if let Some(name) = bearer_token_env { @@ -2026,6 +2056,7 @@ async fn dispatch(cli: Cli) -> anyhow::Result<()> { } } Command::Mcp { + config, database, workspace, binding, @@ -2038,26 +2069,30 @@ async fn dispatch(cli: Cli) -> anyhow::Result<()> { let admin = admin || std::env::var("CODE_SYSTEM_GRAPH_MCP_ADMIN") .is_ok_and(|value| value.trim() == "1"); - let (database, workspace, codegraph, codegraph_binary) = if let Some(binding) = binding - { - let binding = load_agent_plugin_mcp_binding(&binding)?; - ( - PathBuf::from(binding.database), - binding.workspace, - binding.codegraph_enabled, - binding.codegraph_binary.map(PathBuf::from), - ) - } else { - ( - database.expect("clap requires database without binding"), - workspace.expect("clap requires workspace without binding"), - codegraph, - codegraph_binary, - ) - }; + let (config, database, workspace, codegraph, codegraph_binary) = + if let Some(binding) = binding { + let binding = load_agent_plugin_mcp_binding(&binding)?; + ( + PathBuf::from(binding.config), + PathBuf::from(binding.database), + binding.workspace, + binding.codegraph_enabled, + binding.codegraph_binary.map(PathBuf::from), + ) + } else { + ( + config.expect("clap requires config without binding"), + database.expect("clap requires database without binding"), + workspace.expect("clap requires workspace without binding"), + codegraph, + codegraph_binary, + ) + }; let (codegraph, codegraph_binary) = codegraph_server_policy(codegraph, codegraph_binary); + let execution_policy = load_server_execution_policy(&config, &workspace)?; let service = CodeSystemGraphServer::new(database, workspace) + .with_execution_policy(execution_policy) .with_pull_request_providers( enable_github_pull_requests, enable_bitbucket_pull_requests, diff --git a/crates/code-system-graph-cli/src/mcp.rs b/crates/code-system-graph-cli/src/mcp.rs index 141dc4f..3a51918 100644 --- a/crates/code-system-graph-cli/src/mcp.rs +++ b/crates/code-system-graph-cli/src/mcp.rs @@ -5,29 +5,27 @@ use std::path::PathBuf; use std::time::Duration; use code_system_graph_core::{ - ChangeAnalysisOptions, ChangeImpactReport, ContractReport, ImpactReport, ImpactRequest, LocalContextResult, PullRequestInspection, PullRequestProviderKind, SearchReport -}; -use code_system_graph_model::{ - FreshnessSummary, OverallFreshness, ToolEnvelope, ToolStatus, TraceReport + ChangeAnalysisOptions, ExecutionPolicy, ImpactRequest, PullRequestProviderKind }; +use code_system_graph_model::{FreshnessSummary, OverallFreshness, ToolEnvelope, ToolStatus}; use code_system_graph_store_sqlite::{SqliteStore, StoreLock}; use rmcp::handler::server::router::tool::ToolRouter; use rmcp::handler::server::wrapper::Parameters; use rmcp::model::{ - Implementation, ListResourcesResult, PaginatedRequestParams, ReadResourceRequestParams, ReadResourceResponse, ReadResourceResult, Resource, ResourceContents, ServerCapabilities, ServerInfo + CallToolResult, ContentBlock, Implementation, ListResourceTemplatesResult, ListResourcesResult, PaginatedRequestParams, ReadResourceRequestParams, ReadResourceResponse, ReadResourceResult, Resource, ResourceContents, ResourceTemplate, ServerCapabilities, ServerInfo }; use rmcp::service::{RequestContext, RoleServer}; -use rmcp::{ErrorData as McpError, Json, ServerHandler, tool, tool_handler, tool_router}; +use rmcp::{ErrorData as McpError, ServerHandler, tool, tool_handler, tool_router}; use crate::{ - CODEGRAPH_DISABLED_CODE, CODEGRAPH_DISABLED_MESSAGE, ChangesInput, CommunityReport, ExploreInput, PullRequestInput, ScanOverrides, SearchInput, TraceInput, add_manual_link_to_manifest, add_repository_to_manifest, analyze_workspace_changes, communities_workspace, explore_repository, impact_workspace, impact_workspace_with_codegraph, inspect_pull_request, remove_repository_from_manifest, scan_workspace_with_overrides, search_workspace, trace_workspace + CODEGRAPH_DISABLED_CODE, CODEGRAPH_DISABLED_MESSAGE, ChangesInput, CommunityReport, ExploreInput, PullRequestInput, QueryActionCapabilities, ScanOverrides, SearchInput, TraceInput, add_manual_link_to_manifest, add_repository_to_manifest, analyze_workspace_changes, communities_workspace, explore_repository, impact_workspace, impact_workspace_with_codegraph, inspect_pull_request, remove_repository_from_manifest, scan_workspace_with_overrides, search_workspace_for_delivery, trace_workspace }; #[path = "mcp_support/mod.rs"] mod mcp_support; use mcp_support::{ - ADMIN_TOOL_NAMES, AdminAudit, CacheCleanInput, CacheCleanReport, CommunitiesInput, ContractsInput, GraphStatusReport, JSON_MIME_TYPE, ManifestAdminReport, ManualLinkWriteInput, ResourceErrorKind, SourceContextInput, SourceContextReport, WorkspaceInput, WorkspaceUpdateInput, admin_audit_envelope, admin_mutation_envelope, configured_manifest_path, contracts_envelope, read_resource, resource_uris, source_context_envelope, status_envelope + ADMIN_TOOL_NAMES, AdminAudit, AgentToolResult, CacheCleanInput, CacheCleanReport, CommunitiesInput, ContractsInput, MARKDOWN_MIME_TYPE, ManifestAdminReport, ManualLinkWriteInput, ResourceErrorKind, SourceContextInput, WorkspaceInput, WorkspaceUpdateInput, admin_audit_envelope, admin_mutation_envelope, configured_manifest_path, contracts_envelope, read_resource, resource_templates, resource_uris, source_context_envelope, status_envelope }; const EXPLORE_TOOL_NAME: &str = "explore"; @@ -48,6 +46,7 @@ pub struct CodeSystemGraphServer { codegraph: CodeGraphPolicy, github_pull_requests_enabled: bool, bitbucket_pull_requests_enabled: bool, + execution_policy: ExecutionPolicy, tool_router: ToolRouter, } @@ -68,6 +67,7 @@ impl CodeSystemGraphServer { codegraph: CodeGraphPolicy::default(), github_pull_requests_enabled: false, bitbucket_pull_requests_enabled: false, + execution_policy: ExecutionPolicy::default(), tool_router, } } @@ -102,6 +102,13 @@ impl CodeSystemGraphServer { self } + /// Applies the validated immutable workspace execution policy. + #[must_use] + pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self { + self.execution_policy = policy; + self + } + /// Enables selected public pull-request providers while retaining per-request consent. #[must_use] pub fn with_pull_request_providers(mut self, github: bool, bitbucket: bool) -> Self { @@ -110,10 +117,22 @@ impl CodeSystemGraphServer { self } + fn markdown_result(&self, result: AgentToolResult<'_>) -> CallToolResult { + let maximum = usize::try_from(self.execution_policy.max_mcp_tool_response_bytes) + .expect("validated policy bytes are usize-representable"); + let (markdown, is_error) = result.render(maximum); + let content = vec![ContentBlock::text(markdown)]; + if is_error { + CallToolResult::error(content) + } else { + CallToolResult::success(content) + } + } + /// Traces a bounded path through the current federated snapshot. #[tool( name = "trace", - description = "Finds a bounded, explainable path between two persisted entities across repository boundaries. Use when both endpoint identifiers are known; use query first to discover identifiers. Returns a versioned ToolEnvelope containing trace segments and freshness metadata.", + description = "Finds a bounded, explainable path between two persisted entities across repository boundaries. Use when both endpoint identifiers are known; use query first to discover identifiers. Returns bounded Markdown with trace segments and freshness metadata.", annotations( title = "Cross-repository path trace", read_only_hint = true, @@ -122,14 +141,11 @@ impl CodeSystemGraphServer { open_world_hint = false ) )] - pub async fn trace( - &self, - Parameters(input): Parameters, - ) -> Json> { - match trace_workspace(&self.database_path, &self.workspace, &input) { - Ok(envelope) => Json(envelope), - Err(error) => Json(ToolEnvelope { - schema_version: 1, + pub async fn trace(&self, Parameters(input): Parameters) -> CallToolResult { + let envelope = match trace_workspace(&self.database_path, &self.workspace, &input) { + Ok(envelope) => envelope, + Err(error) => ToolEnvelope { + schema_version: 2, status: ToolStatus::Error, data: None, freshness: FreshnessSummary { @@ -138,14 +154,15 @@ impl CodeSystemGraphServer { reasons: vec!["Trace inputs could not be validated.".to_owned()], }, warnings: vec![error.to_string()], - }), - } + }, + }; + self.markdown_result(AgentToolResult::Trace(&envelope)) } /// Searches ranked federated entities without returning source bodies. #[tool( name = "query", - description = "Searches persisted architecture entities, contracts, and communities across the workspace without returning source bodies. Use to discover entity identifiers before trace, source_context, or impact. Returns a versioned ToolEnvelope containing ranked matches and freshness metadata.", + description = "Searches persisted architecture entities, contracts, and communities across the workspace without returning source bodies. Use to discover entity identifiers before trace, source_context, or impact. Returns bounded Markdown with ranked matches and freshness metadata.", annotations( title = "Federated entity search", read_only_hint = true, @@ -154,23 +171,27 @@ impl CodeSystemGraphServer { open_world_hint = false ) )] - pub async fn query( - &self, - Parameters(input): Parameters, - ) -> Json> { - match search_workspace(&self.database_path, &self.workspace, &input) { - Ok(envelope) => Json(envelope), - Err(error) => Json(error_envelope( - "Query inputs could not be validated.", - error, - )), - } + pub async fn query(&self, Parameters(input): Parameters) -> CallToolResult { + let envelope = match search_workspace_for_delivery( + &self.database_path, + &self.workspace, + &input, + &self.execution_policy, + QueryActionCapabilities { + source_context: true, + explore: self.codegraph.enabled, + }, + ) { + Ok(envelope) => envelope, + Err(error) => error_envelope("Query inputs could not be validated.", error), + }; + self.markdown_result(AgentToolResult::Query(&envelope)) } /// Explores bounded repository-local source and flow context without persisting source. #[tool( name = "explore", - description = "Retrieves bounded, ephemeral repository-local source and call-flow context through CodeGraph. Use for symbols, callers, callees, tests, and implementation details; use query for persisted cross-repository entities. Returns a versioned ToolEnvelope containing source-bearing local context that is never persisted.", + description = "Retrieves bounded, ephemeral repository-local source and call-flow context through CodeGraph. Use for symbols, callers, callees, tests, and implementation details; use query for persisted cross-repository entities. Returns bounded Markdown with source-bearing local context that is never persisted.", annotations( title = "Repository source exploration", read_only_hint = true, @@ -182,25 +203,25 @@ impl CodeSystemGraphServer { pub async fn explore( &self, Parameters(input): Parameters, - ) -> Result>, McpError> { + ) -> Result { if !self.codegraph.enabled { return Err(codegraph_disabled_error()); } - Ok(Json( - explore_repository( - &self.database_path, - &self.workspace, - &input, - self.codegraph.binary.clone(), - ) - .await, - )) + let envelope = explore_repository( + &self.database_path, + &self.workspace, + &input, + self.codegraph.binary.clone(), + &self.execution_policy, + ) + .await; + Ok(self.markdown_result(AgentToolResult::Explore(&envelope))) } /// Lists, inspects, or compares persisted deterministic communities. #[tool( name = "communities", - description = "Lists, shows, or compares deterministic persisted communities using one explicit action. Use for inferred service boundaries, memberships, metrics, and snapshot comparisons; use query for general entity search. Returns a versioned ToolEnvelope containing a CommunityReport and freshness metadata.", + description = "Lists, shows, or compares deterministic persisted communities using one explicit action. Use for inferred service boundaries, memberships, metrics, and snapshot comparisons; use query for general entity search. Returns bounded Markdown with community results and freshness metadata.", annotations( title = "Community inspection", read_only_hint = true, @@ -212,34 +233,33 @@ impl CodeSystemGraphServer { pub async fn communities( &self, Parameters(input): Parameters, - ) -> Json> { + ) -> CallToolResult { if input.workspace() != self.workspace { - return Json(error_envelope( + let envelope = error_envelope::( "Workspace policy rejected the request.", format!( "workspace `{}` is outside this server's configured workspace `{}`", input.workspace(), self.workspace ), - )); + ); + return self.markdown_result(AgentToolResult::Communities(&envelope)); } - match communities_workspace( + let envelope = match communities_workspace( &self.database_path, &self.workspace, &input.application_input(), ) { - Ok(envelope) => Json(envelope), - Err(error) => Json(error_envelope( - "Community inputs could not be validated.", - error, - )), - } + Ok(envelope) => envelope, + Err(error) => error_envelope("Community inputs could not be validated.", error), + }; + self.markdown_result(AgentToolResult::Communities(&envelope)) } /// Computes conservative impact without executing tests or repository commands. #[tool( name = "impact", - description = "Analyzes bounded upstream or downstream effects and conservative risk for one persisted graph target across repositories. Use for a known entity; use analyze_changes for staged, worktree, or committed Git changes. Returns a versioned ToolEnvelope containing an ImpactReport, freshness metadata, and optional ephemeral CodeGraph enrichment.", + description = "Analyzes bounded upstream or downstream effects and conservative risk for one persisted graph target across repositories. Use for a known entity; use analyze_changes for staged, worktree, or committed Git changes. Returns bounded Markdown with impact, freshness, and optional ephemeral CodeGraph enrichment.", annotations( title = "Cross-repository impact analysis", read_only_hint = true, @@ -248,10 +268,7 @@ impl CodeSystemGraphServer { open_world_hint = false ) )] - pub async fn impact( - &self, - Parameters(input): Parameters, - ) -> Json> { + pub async fn impact(&self, Parameters(input): Parameters) -> CallToolResult { let result = if self.codegraph.enabled { impact_workspace_with_codegraph( &self.database_path, @@ -263,19 +280,17 @@ impl CodeSystemGraphServer { } else { impact_workspace(&self.database_path, &self.workspace, &input) }; - match result { - Ok(envelope) => Json(envelope), - Err(error) => Json(error_envelope( - "Impact inputs could not be validated.", - error, - )), - } + let envelope = match result { + Ok(envelope) => envelope, + Err(error) => error_envelope("Impact inputs could not be validated.", error), + }; + self.markdown_result(AgentToolResult::Impact(&envelope)) } /// Inspects bounded local Git changes without modifying repository state. #[tool( name = "analyze_changes", - description = "Analyzes fingerprinted staged, worktree, or committed local Git changes and maps them to conservative graph impact. Use for repository diffs; use impact for one known persisted target. Returns a versioned ToolEnvelope containing a ChangeImpactReport and freshness metadata without modifying Git state.", + description = "Analyzes fingerprinted staged, worktree, or committed local Git changes and maps them to conservative graph impact. Use for repository diffs; use impact for one known persisted target. Returns bounded Markdown with change impact and freshness metadata without modifying Git state.", annotations( title = "Local change impact analysis", read_only_hint = true, @@ -287,8 +302,8 @@ impl CodeSystemGraphServer { pub async fn analyze_changes( &self, Parameters(input): Parameters, - ) -> Json> { - match analyze_workspace_changes( + ) -> CallToolResult { + let envelope = match analyze_workspace_changes( &self.database_path, &self.workspace, &input, @@ -297,18 +312,16 @@ impl CodeSystemGraphServer { ) .await { - Ok(envelope) => Json(envelope), - Err(error) => Json(error_envelope( - "Change inputs could not be validated.", - error, - )), - } + Ok(envelope) => envelope, + Err(error) => error_envelope("Change inputs could not be validated.", error), + }; + self.markdown_result(AgentToolResult::AnalyzeChanges(&envelope)) } /// Inspects one explicitly enabled public pull-request provider. #[tool( name = "analyze_pull_request", - description = "Fetches and analyzes one consented GitHub or Bitbucket Cloud pull request from a provider enabled at server startup. Use for remote pull-request metadata and changed-file context; use analyze_changes for local Git state. Returns a versioned ToolEnvelope containing a PullRequestInspection and provider freshness metadata.", + description = "Fetches and analyzes one consented GitHub or Bitbucket Cloud pull request from a provider enabled at server startup. Use for remote pull-request metadata and changed-file context; use analyze_changes for local Git state. Returns bounded Markdown with pull-request inspection and provider freshness metadata.", annotations( title = "Pull request analysis", read_only_hint = true, @@ -320,7 +333,7 @@ impl CodeSystemGraphServer { pub async fn analyze_pull_request( &self, Parameters(input): Parameters, - ) -> Json> { + ) -> CallToolResult { let (enabled, token, basic_auth_username) = match input.provider { PullRequestProviderKind::GitHub => ( self.github_pull_requests_enabled, @@ -334,19 +347,21 @@ impl CodeSystemGraphServer { ), PullRequestProviderKind::BitbucketDataCenter => (false, None, None), }; - match inspect_pull_request(&input, enabled, token, basic_auth_username).await { - Ok(envelope) => Json(envelope), - Err(error) => Json(error_envelope( + let envelope = match inspect_pull_request(&input, enabled, token, basic_auth_username).await + { + Ok(envelope) => envelope, + Err(error) => error_envelope( "Pull-request inputs or provider response could not be validated.", error, - )), - } + ), + }; + self.markdown_result(AgentToolResult::AnalyzePullRequest(&envelope)) } /// Reports persisted snapshot health without reading repository source files. #[tool( name = "status", - description = "Reports persisted graph health, freshness, and current snapshot metadata for the configured workspace. Use to verify workspace readiness before other analysis; not for entity search. Returns a versioned ToolEnvelope containing a source-free GraphStatusReport.", + description = "Reports persisted graph health, freshness, and current snapshot metadata for the configured workspace. Use to verify workspace readiness before other analysis; not for entity search. Returns bounded source-free Markdown.", annotations( title = "Workspace graph status", read_only_hint = true, @@ -355,21 +370,15 @@ impl CodeSystemGraphServer { open_world_hint = false ) )] - pub async fn status( - &self, - Parameters(input): Parameters, - ) -> Json> { - Json(status_envelope( - &self.database_path, - &self.workspace, - &input.workspace, - )) + pub async fn status(&self, Parameters(input): Parameters) -> CallToolResult { + let envelope = status_envelope(&self.database_path, &self.workspace, &input.workspace); + self.markdown_result(AgentToolResult::Status(&envelope)) } /// Lists a bounded page of contract entities from the immutable graph snapshot. #[tool( name = "contracts", - description = "Lists, shows, validates, diffs, or explains persisted contracts using one explicit action. Use for API, event, database, package, and infrastructure contract analysis; use query for non-contract entities. Returns a versioned ToolEnvelope containing a ContractReport and freshness metadata.", + description = "Lists, shows, validates, diffs, or explains persisted contracts using one explicit action. Use for API, event, database, package, and infrastructure contract analysis; use query for non-contract entities. Returns bounded Markdown with contract results and freshness metadata.", annotations( title = "Contract inspection and validation", read_only_hint = true, @@ -378,21 +387,15 @@ impl CodeSystemGraphServer { open_world_hint = false ) )] - pub async fn contracts( - &self, - Parameters(input): Parameters, - ) -> Json> { - Json(contracts_envelope( - &self.database_path, - &self.workspace, - &input, - )) + pub async fn contracts(&self, Parameters(input): Parameters) -> CallToolResult { + let envelope = contracts_envelope(&self.database_path, &self.workspace, &input); + self.markdown_result(AgentToolResult::Contracts(&envelope)) } /// Returns bounded persisted graph and evidence metadata for one entity. #[tool( name = "source_context", - description = "Returns bounded, source-free persisted graph context and evidence metadata for one exact entity. Use after query to explain relationships and provenance without source bodies. It does not return implementation source. Returns a versioned ToolEnvelope containing a SourceContextReport and freshness metadata.", + description = "Returns bounded, source-free persisted graph context and evidence metadata for one exact entity. Use after query to explain relationships and provenance without source bodies. It does not return implementation source. Returns bounded Markdown with evidence and freshness metadata.", annotations( title = "Persisted entity context", read_only_hint = true, @@ -404,18 +407,15 @@ impl CodeSystemGraphServer { pub async fn source_context( &self, Parameters(input): Parameters, - ) -> Json> { - Json(source_context_envelope( - &self.database_path, - &self.workspace, - &input, - )) + ) -> CallToolResult { + let envelope = source_context_envelope(&self.database_path, &self.workspace, &input); + self.markdown_result(AgentToolResult::SourceContext(&envelope)) } /// Scans and atomically publishes the configured workspace. #[tool( name = "scan", - description = "Scans the configured workspace and atomically publishes a new persisted graph snapshot. Use only in the enabled admin profile after repository or configuration changes; use status for a read-only health check. Returns an audited ToolEnvelope containing a ScanSummary, snapshot identity, freshness, and visible CodeGraph degradations.", + description = "Scans the configured workspace and atomically publishes a new persisted graph snapshot. Use only in the enabled admin profile after repository or configuration changes; use status for a read-only health check. Returns bounded audited Markdown with scan, snapshot, freshness, and visible CodeGraph degradation details.", annotations( title = "Publish workspace snapshot", read_only_hint = false, @@ -424,17 +424,15 @@ impl CodeSystemGraphServer { open_world_hint = false ) )] - pub async fn scan( - &self, - Parameters(input): Parameters, - ) -> Json>> { - Json(self.run_admin_scan(&input.workspace, "scan")) + pub async fn scan(&self, Parameters(input): Parameters) -> CallToolResult { + let envelope = self.run_admin_scan(&input.workspace, "scan"); + self.markdown_result(AgentToolResult::Scan(&envelope)) } /// Adds or removes one repository entry through the constrained manifest editor. #[tool( name = "update_workspace", - description = "Adds or removes one repository registration in the configured workspace manifest, then validates and rescans it. Use only in the enabled admin profile for constrained workspace membership changes; use scan when membership is unchanged. Returns an audited ToolEnvelope containing the manifest mutation, backup metadata, and ScanSummary.", + description = "Adds or removes one repository registration in the configured workspace manifest, then validates and rescans it. Use only in the enabled admin profile for constrained workspace membership changes; use scan when membership is unchanged. Returns bounded audited Markdown with mutation, backup, and scan details.", annotations( title = "Update workspace repositories", read_only_hint = false, @@ -446,14 +444,15 @@ impl CodeSystemGraphServer { pub async fn update_workspace( &self, Parameters(input): Parameters, - ) -> Json>> { - Json(self.run_workspace_update(&input)) + ) -> CallToolResult { + let envelope = self.run_workspace_update(&input); + self.markdown_result(AgentToolResult::UpdateWorkspace(&envelope)) } /// Appends one exact manual relationship declaration and republishes the workspace. #[tool( name = "write_manual_link", - description = "Adds or suppresses one exact manual graph relationship in the configured manifest, then validates and rescans it. Use only in the enabled admin profile when an operator must record a reasoned relationship decision. Returns an audited ToolEnvelope containing the manifest mutation, backup metadata, and ScanSummary.", + description = "Adds or suppresses one exact manual graph relationship in the configured manifest, then validates and rescans it. Use only in the enabled admin profile when an operator must record a reasoned relationship decision. Returns bounded audited Markdown with mutation, backup, and scan details.", annotations( title = "Write manual relationship", read_only_hint = false, @@ -465,14 +464,15 @@ impl CodeSystemGraphServer { pub async fn write_manual_link( &self, Parameters(input): Parameters, - ) -> Json>> { - Json(self.run_manual_link_write(&input)) + ) -> CallToolResult { + let envelope = self.run_manual_link_write(&input); + self.markdown_result(AgentToolResult::WriteManualLink(&envelope)) } /// Removes bounded reusable query results for the configured workspace. #[tool( name = "clean_cache", - description = "Removes reusable query-summary cache entries for the configured workspace without changing graph snapshots. Use only in the enabled admin profile to invalidate cached query results; not to rescan source. Returns an audited ToolEnvelope containing the number of removed entries and current snapshot identity.", + description = "Removes reusable query-summary cache entries for the configured workspace without changing graph snapshots. Use only in the enabled admin profile to invalidate cached query results; not to rescan source. Returns bounded audited Markdown with removed-entry and snapshot details.", annotations( title = "Clear query cache", read_only_hint = false, @@ -484,14 +484,15 @@ impl CodeSystemGraphServer { pub async fn clean_cache( &self, Parameters(input): Parameters, - ) -> Json>> { - Json(self.run_cache_clean(&input)) + ) -> CallToolResult { + let envelope = self.run_cache_clean(&input); + self.markdown_result(AgentToolResult::CleanCache(&envelope)) } /// Recomputes communities by publishing a freshly analyzed workspace snapshot. #[tool( name = "recompute_communities", - description = "Rescans the configured workspace and deterministically recomputes communities in a newly published snapshot. Use only in the enabled admin profile when community results must be refreshed; use communities for read-only inspection. Returns an audited ToolEnvelope containing the ScanSummary, snapshot identity, and freshness metadata.", + description = "Rescans the configured workspace and deterministically recomputes communities in a newly published snapshot. Use only in the enabled admin profile when community results must be refreshed; use communities for read-only inspection. Returns bounded audited Markdown with scan, snapshot, and freshness details.", annotations( title = "Recompute workspace communities", read_only_hint = false, @@ -503,8 +504,9 @@ impl CodeSystemGraphServer { pub async fn recompute_communities( &self, Parameters(input): Parameters, - ) -> Json>> { - Json(self.run_admin_scan(&input.workspace, "community_recompute")) + ) -> CallToolResult { + let envelope = self.run_admin_scan(&input.workspace, "community_recompute"); + self.markdown_result(AgentToolResult::RecomputeCommunities(&envelope)) } fn run_admin_scan( @@ -703,7 +705,7 @@ impl CodeSystemGraphServer { fn error_envelope(reason: &str, error: impl std::fmt::Display) -> ToolEnvelope { ToolEnvelope { - schema_version: 1, + schema_version: 2, status: ToolStatus::Error, data: None, freshness: FreshnessSummary { @@ -732,12 +734,14 @@ impl ServerHandler for CodeSystemGraphServer { fn get_info(&self) -> ServerInfo { let instructions = if self.codegraph.enabled { "Code System Graph exposes bounded cross-repository intelligence. Tool and resource results \ - are versioned JSON; use query for persisted entity discovery, explore for ephemeral \ + are delivered as one bounded Markdown text block without structuredContent; schema version 2 \ + remains the logical contract, and only the schema catalog embeds fenced JSON. Use query for persisted entity discovery, explore for ephemeral \ repository source, source_context for source-free evidence, impact for known targets, \ and analyze_changes for Git diffs. Administrative tools mutate state only when enabled." } else { "Code System Graph exposes bounded cross-repository intelligence. Tool and resource results \ - are versioned JSON; use query for persisted entity discovery, source_context for source-free \ + are delivered as one bounded Markdown text block without structuredContent; schema version 2 \ + remains the logical contract, and only the schema catalog embeds fenced JSON. Use query for persisted entity discovery, source_context for source-free \ evidence, impact for known targets, and analyze_changes for Git diffs. Repository-local \ source access is unavailable because CodeGraph is disabled. Administrative tools mutate \ state only when enabled." @@ -765,21 +769,42 @@ impl ServerHandler for CodeSystemGraphServer { .map(|(uri, name, description)| { Resource::new(uri, name) .with_description(description) - .with_mime_type(JSON_MIME_TYPE) + .with_mime_type(MARKDOWN_MIME_TYPE) }) .collect(); Ok(ListResourcesResult::with_all_items(resources).with_ttl_ms(1_000)) } + async fn list_resource_templates( + &self, + _request: Option, + _context: RequestContext, + ) -> Result { + let templates = resource_templates() + .into_iter() + .map(|(uri, name, description)| { + ResourceTemplate::new(uri, name) + .with_description(description) + .with_mime_type(MARKDOWN_MIME_TYPE) + }) + .collect(); + Ok(ListResourceTemplatesResult::with_all_items(templates).with_ttl_ms(1_000)) + } + async fn read_resource( &self, request: ReadResourceRequestParams, _context: RequestContext, ) -> Result { let uri = request.uri; - match read_resource(&self.database_path, &self.workspace, &uri) { + match read_resource( + &self.database_path, + &self.workspace, + &uri, + &self.execution_policy, + ) { Ok(text) => Ok(ReadResourceResult::new(vec![ - ResourceContents::text(text, uri).with_mime_type(JSON_MIME_TYPE), + ResourceContents::text(text, uri).with_mime_type(MARKDOWN_MIME_TYPE), ]) .into()), Err(error) => match error.kind { @@ -794,335 +819,4 @@ impl ServerHandler for CodeSystemGraphServer { } #[cfg(test)] -mod tests { - #[cfg(unix)] - use std::os::unix::fs::PermissionsExt; - use std::path::PathBuf; - - use code_system_graph_store_sqlite::SqliteStore; - use rmcp::ServerHandler; - #[cfg(unix)] - use rmcp::handler::server::wrapper::Parameters; - - use super::{CodeSystemGraphServer, mcp_support}; - #[cfg(unix)] - use crate::{CODEGRAPH_DISABLED_CODE, ExploreInput, scan_workspace}; - - #[test] - fn server_should_publish_read_only_tools() { - let server = CodeSystemGraphServer::new(PathBuf::from("graph.db"), "commerce".to_owned()) - .with_codegraph(true, None); - let tools = server.tool_router.list_all(); - let names = tools - .iter() - .map(|tool| tool.name.as_ref()) - .collect::>(); - - assert_eq!( - names, - vec![ - "analyze_changes", - "analyze_pull_request", - "communities", - "contracts", - "explore", - "impact", - "query", - "source_context", - "status", - "trace" - ] - ); - assert!(tools.iter().all(|tool| { - tool.annotations.as_ref().is_some_and(|annotations| { - annotations.read_only_hint == Some(true) - && annotations.destructive_hint == Some(false) - }) - })); - } - - #[test] - fn explore_should_be_hidden_until_codegraph_is_enabled() { - let disabled = CodeSystemGraphServer::new(PathBuf::from("graph.db"), "commerce".to_owned()); - let enabled = disabled.clone().with_codegraph(true, None); - - assert!( - disabled - .tool_router - .list_all() - .iter() - .all(|tool| tool.name != "explore") - ); - assert!( - enabled - .tool_router - .list_all() - .iter() - .any(|tool| tool.name == "explore") - ); - } - - #[cfg(unix)] - #[tokio::test] - async fn explore_handler_should_reject_disabled_codegraph_before_execution() - -> Result<(), Box> { - let temporary = tempfile::tempdir()?; - let database = temporary.path().join("graph.db"); - let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../../fixtures/platform-demo/code-system-graph.yaml"); - scan_workspace(&manifest, &database)?; - let binary = temporary.path().join("codegraph-marker"); - let marker = temporary.path().join("codegraph-invoked"); - std::fs::write( - &binary, - "#!/bin/sh\n: > \"$(dirname \"$0\")/codegraph-invoked\"\nexit 1\n", - )?; - let mut permissions = std::fs::metadata(&binary)?.permissions(); - permissions.set_mode(0o755); - std::fs::set_permissions(&binary, permissions)?; - let server = CodeSystemGraphServer::new(database, "commerce-platform".to_owned()) - .with_codegraph(false, Some(binary.into_os_string())); - - let result = server - .explore(Parameters(ExploreInput { - workspace: "commerce-platform".to_owned(), - repository: Some("orders".to_owned()), - query: "create_order callers".to_owned(), - max_files: 4, - })) - .await; - let Err(error) = result else { - panic!("disabled CodeGraph policy must reject direct handler calls"); - }; - - assert_eq!( - ( - error.data.as_ref().and_then(|data| data["code"].as_str()), - marker.exists() - ), - (Some(CODEGRAPH_DISABLED_CODE), false) - ); - Ok(()) - } - - #[test] - fn initialize_contract_should_align_instructions_with_advertised_tools() { - let disabled = CodeSystemGraphServer::new(PathBuf::from("graph.db"), "commerce".to_owned()); - let enabled = disabled.clone().with_codegraph(true, None); - let disabled_info = disabled.get_info(); - - assert!(disabled_info.capabilities.tools.is_some()); - assert!(disabled_info.capabilities.resources.is_some()); - assert_eq!(disabled_info.server_info.name, "code_system_graph"); - - for (server, codegraph_enabled) in [(&disabled, false), (&enabled, true)] { - let explore_advertised = server - .tool_router - .list_all() - .iter() - .any(|tool| tool.name == "explore"); - let instructions = server - .get_info() - .instructions - .expect("server instructions should be present"); - - assert_eq!(explore_advertised, codegraph_enabled); - assert_eq!(instructions.contains("explore"), codegraph_enabled); - } - - let source_context = disabled - .tool_router - .list_all() - .into_iter() - .find(|tool| tool.name == "source_context") - .expect("source_context should be advertised"); - let description = source_context - .description - .as_deref() - .expect("source_context description should be present"); - assert!(!description.contains("explore")); - } - - #[test] - fn admin_tools_should_be_hidden_until_constructor_profile_is_enabled() { - let hidden = CodeSystemGraphServer::new(PathBuf::from("graph.db"), "commerce".to_owned()); - assert!( - hidden - .tool_router - .list_all() - .iter() - .all(|tool| !mcp_support::ADMIN_TOOL_NAMES.contains(&tool.name.as_ref())) - ); - - let enabled = hidden.with_admin_profile(true); - for name in mcp_support::ADMIN_TOOL_NAMES { - let tool = enabled - .tool_router - .list_all() - .into_iter() - .find(|tool| tool.name == name); - assert!(tool.is_some()); - assert!(tool.is_some_and(|tool| { - tool.annotations.as_ref().is_some_and(|annotations| { - annotations.read_only_hint == Some(false) - && annotations.destructive_hint == Some(true) - }) - })); - } - } - - #[test] - fn resource_list_should_publish_stable_workspace_policy_uris() { - let uris = mcp_support::resource_uris("commerce") - .into_iter() - .map(|(uri, _, _)| uri) - .collect::>(); - - assert_eq!(uris.len(), 10); - assert!(uris.contains(&"code-system-graph://workspaces".to_owned())); - assert!(uris.contains(&"code-system-graph://workspace/commerce/schema".to_owned())); - assert!(uris.contains(&"code-system-graph://evidence/{id}".to_owned())); - } - - #[test] - fn resource_read_should_reject_wrong_workspace_before_store_access() { - let error = mcp_support::read_resource( - &PathBuf::from("missing.db"), - "commerce", - "code-system-graph://workspace/payments/status", - ); - - assert!(error.is_err()); - assert!(error.is_err_and(|error| { - error.kind == mcp_support::ResourceErrorKind::Invalid - && error.message.contains("outside this server's configured") - })); - } - - #[test] - fn resource_read_should_return_versioned_source_free_json() - -> Result<(), Box> { - let text = mcp_support::read_resource( - &PathBuf::from("missing.db"), - "commerce", - "code-system-graph://workspaces", - ) - .map_err(|error| std::io::Error::other(error.message))?; - let value: serde_json::Value = serde_json::from_str(&text)?; - - assert_eq!(value["schema_version"], 1); - assert_eq!(value["workspaces"][0]["name"], "commerce"); - assert!(!text.contains("source_body")); - Ok(()) - } - - #[test] - fn evidence_template_read_should_require_concrete_identifier() { - let error = mcp_support::read_resource( - &PathBuf::from("missing.db"), - "commerce", - "code-system-graph://evidence/{id}", - ); - - assert!(error.is_err_and(|error| { - error.kind == mcp_support::ResourceErrorKind::Invalid - && error.message.contains("concrete evidence identifier") - })); - } - - #[test] - fn evidence_read_should_report_missing_metadata() -> Result<(), Box> { - let temporary = tempfile::tempdir()?; - let database = temporary.path().join("graph.db"); - drop(SqliteStore::open(&database)?); - - let error = mcp_support::read_resource( - &database, - "commerce", - "code-system-graph://evidence/evidence:missing", - ); - - assert!(error.is_err_and(|error| { - error.kind == mcp_support::ResourceErrorKind::Missing - && error.message.contains("no evidence snapshot") - })); - Ok(()) - } - - #[test] - fn schema_resource_should_catalog_every_tool_input_and_result() - -> Result<(), Box> { - let catalog = mcp_support::schema_catalog(); - let schemas = catalog - .get("schemas") - .and_then(serde_json::Value::as_object); - - assert!(schemas.is_some()); - let schemas = schemas.map_or(0, serde_json::Map::len); - assert_eq!(schemas, 30); - for name in [ - "explore.input", - "explore.result", - "update_workspace.input", - "update_workspace.result", - "write_manual_link.input", - "write_manual_link.result", - "clean_cache.input", - "clean_cache.result", - ] { - assert!(catalog["schemas"].get(name).is_some(), "missing {name}"); - } - assert_eq!(catalog["schema_version"], 1); - assert!(catalog["application_interfaces"]["schemas"].is_array()); - assert!(serde_json::to_vec(&catalog)?.len() <= 2 * 1024 * 1024); - Ok(()) - } - - #[test] - fn tool_schemas_should_keep_codegraph_policy_out_of_llm_inputs() { - let catalog = mcp_support::schema_catalog(); - let impact = catalog["schemas"]["impact.input"].to_string(); - let scan = catalog["schemas"]["scan.input"].to_string(); - - assert!(!impact.contains("local_enrichment")); - assert!(!scan.contains("codegraph")); - assert!(!scan.contains("codegraph_binary")); - } - - #[test] - fn conditional_tool_inputs_should_require_action_specific_fields() { - let contract: Result = - serde_json::from_value(serde_json::json!({ - "action": "diff", - "workspace": "commerce", - "contract": "contract:a" - })); - let workspace_update: Result = - serde_json::from_value(serde_json::json!({ - "action": "remove_repository", - "workspace": "commerce", - "alias": "api", - "repository_path": "api" - })); - let manual_link: Result = - serde_json::from_value(serde_json::json!({ - "workspace": "commerce", - "from": "a", - "to": "b", - "relation": "documents", - "reason": "Operator decision" - })); - let communities: Result = - serde_json::from_value(serde_json::json!({ - "action": "show", - "workspace": "commerce", - "community_id": "community:a", - "snapshot_id": "snapshot:old" - })); - - assert!(contract.is_err()); - assert!(workspace_update.is_err()); - assert!(manual_link.is_err()); - assert!(communities.is_err()); - } -} +mod tests; diff --git a/crates/code-system-graph-cli/src/mcp/tests.rs b/crates/code-system-graph-cli/src/mcp/tests.rs new file mode 100644 index 0000000..3d9863c --- /dev/null +++ b/crates/code-system-graph-cli/src/mcp/tests.rs @@ -0,0 +1,387 @@ +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; +use std::path::PathBuf; + +use code_system_graph_core::ExecutionPolicy; +use code_system_graph_store_sqlite::SqliteStore; +use rmcp::ServerHandler; +use rmcp::handler::server::wrapper::Parameters; +use rmcp::model::ContentBlock; + +use super::{CodeSystemGraphServer, mcp_support}; +#[cfg(unix)] +use crate::{CODEGRAPH_DISABLED_CODE, ExploreInput}; +use crate::{SearchInput, scan_workspace}; + +fn tool_text(result: &rmcp::model::CallToolResult) -> &str { + match result.content.first() { + Some(ContentBlock::Text(content)) => &content.text, + _ => panic!("tool result did not contain one text block"), + } +} + +#[test] +fn server_should_publish_read_only_tools() { + let server = CodeSystemGraphServer::new(PathBuf::from("graph.db"), "commerce".to_owned()) + .with_codegraph(true, None); + let tools = server.tool_router.list_all(); + let names = tools + .iter() + .map(|tool| tool.name.as_ref()) + .collect::>(); + + assert_eq!( + names, + vec![ + "analyze_changes", + "analyze_pull_request", + "communities", + "contracts", + "explore", + "impact", + "query", + "source_context", + "status", + "trace" + ] + ); + assert!(tools.iter().all(|tool| { + tool.annotations.as_ref().is_some_and(|annotations| { + annotations.read_only_hint == Some(true) && annotations.destructive_hint == Some(false) + }) + })); + assert!(tools.iter().all(|tool| tool.output_schema.is_none())); +} + +#[test] +fn explore_should_be_hidden_until_codegraph_is_enabled() { + let disabled = CodeSystemGraphServer::new(PathBuf::from("graph.db"), "commerce".to_owned()); + let enabled = disabled.clone().with_codegraph(true, None); + + assert!( + disabled + .tool_router + .list_all() + .iter() + .all(|tool| tool.name != "explore") + ); + assert!( + enabled + .tool_router + .list_all() + .iter() + .any(|tool| tool.name == "explore") + ); +} + +#[tokio::test] +async fn query_actions_should_match_mcp_codegraph_capability() +-> Result<(), Box> { + let temporary = tempfile::tempdir()?; + let database = temporary.path().join("graph.db"); + let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../fixtures/platform-demo/code-system-graph.yaml"); + scan_workspace(&manifest, &database)?; + let disabled = CodeSystemGraphServer::new(database.clone(), "commerce-platform".to_owned()); + let enabled = disabled.clone().with_codegraph(true, None); + let input = |query: &str| SearchInput { + query: query.to_owned(), + node_kinds: Vec::new(), + repo_ids: Vec::new(), + service_ids: Vec::new(), + community_ids: Vec::new(), + offset: 0, + limit: 5, + }; + + let hit = disabled.query(Parameters(input("orders"))).await; + assert!(tool_text(&hit).contains("source\\_context")); + + let missing = disabled + .query(Parameters(input("api source_literal_that_does_not_exist"))) + .await; + assert!(!tool_text(&missing).contains("\nexplore\n")); + assert!(!tool_text(&missing).contains("use Explore")); + + let enabled_missing = enabled + .query(Parameters(input("api source_literal_that_does_not_exist"))) + .await; + assert!(tool_text(&enabled_missing).contains("\nexplore\n")); + assert!(tool_text(&enabled_missing).contains("use Explore")); + Ok(()) +} + +#[cfg(unix)] +#[tokio::test] +async fn explore_handler_should_reject_disabled_codegraph_before_execution() +-> Result<(), Box> { + let temporary = tempfile::tempdir()?; + let database = temporary.path().join("graph.db"); + let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../fixtures/platform-demo/code-system-graph.yaml"); + scan_workspace(&manifest, &database)?; + let binary = temporary.path().join("codegraph-marker"); + let marker = temporary.path().join("codegraph-invoked"); + std::fs::write( + &binary, + "#!/bin/sh\n: > \"$(dirname \"$0\")/codegraph-invoked\"\nexit 1\n", + )?; + let mut permissions = std::fs::metadata(&binary)?.permissions(); + permissions.set_mode(0o755); + std::fs::set_permissions(&binary, permissions)?; + let server = CodeSystemGraphServer::new(database, "commerce-platform".to_owned()) + .with_codegraph(false, Some(binary.into_os_string())); + + let result = server + .explore(Parameters(ExploreInput { + workspace: "commerce-platform".to_owned(), + repository: Some("orders".to_owned()), + query: "create_order callers".to_owned(), + max_files: Some(4), + })) + .await; + let Err(error) = result else { + panic!("disabled CodeGraph policy must reject direct handler calls"); + }; + + assert_eq!( + ( + error.data.as_ref().and_then(|data| data["code"].as_str()), + marker.exists() + ), + (Some(CODEGRAPH_DISABLED_CODE), false) + ); + Ok(()) +} + +#[test] +fn initialize_contract_should_align_instructions_with_advertised_tools() { + let disabled = CodeSystemGraphServer::new(PathBuf::from("graph.db"), "commerce".to_owned()); + let enabled = disabled.clone().with_codegraph(true, None); + let disabled_info = disabled.get_info(); + + assert!(disabled_info.capabilities.tools.is_some()); + assert!(disabled_info.capabilities.resources.is_some()); + assert_eq!(disabled_info.server_info.name, "code_system_graph"); + + for (server, codegraph_enabled) in [(&disabled, false), (&enabled, true)] { + let explore_advertised = server + .tool_router + .list_all() + .iter() + .any(|tool| tool.name == "explore"); + let instructions = server + .get_info() + .instructions + .expect("server instructions should be present"); + + assert_eq!(explore_advertised, codegraph_enabled); + assert_eq!(instructions.contains("explore"), codegraph_enabled); + assert!(instructions.contains("bounded Markdown")); + assert!(instructions.contains("without structuredContent")); + assert!(instructions.contains("schema version 2")); + assert!(!instructions.contains("versioned JSON")); + } + + let source_context = disabled + .tool_router + .list_all() + .into_iter() + .find(|tool| tool.name == "source_context") + .expect("source_context should be advertised"); + let description = source_context + .description + .as_deref() + .expect("source_context description should be present"); + assert!(!description.contains("explore")); +} + +#[test] +fn admin_tools_should_be_hidden_until_constructor_profile_is_enabled() { + let hidden = CodeSystemGraphServer::new(PathBuf::from("graph.db"), "commerce".to_owned()); + assert!( + hidden + .tool_router + .list_all() + .iter() + .all(|tool| !mcp_support::ADMIN_TOOL_NAMES.contains(&tool.name.as_ref())) + ); + + let enabled = hidden.with_admin_profile(true); + for name in mcp_support::ADMIN_TOOL_NAMES { + let tool = enabled + .tool_router + .list_all() + .into_iter() + .find(|tool| tool.name == name); + assert!(tool.is_some()); + assert!(tool.is_some_and(|tool| { + tool.annotations.as_ref().is_some_and(|annotations| { + annotations.read_only_hint == Some(false) + && annotations.destructive_hint == Some(true) + }) + })); + } +} + +#[test] +fn resource_list_should_publish_stable_workspace_policy_uris() { + let uris = mcp_support::resource_uris("commerce") + .into_iter() + .map(|(uri, _, _)| uri) + .collect::>(); + + assert_eq!(uris.len(), 9); + assert!(uris.contains(&"code-system-graph://workspaces".to_owned())); + assert!(uris.contains(&"code-system-graph://workspace/commerce/schema".to_owned())); + assert!(!uris.iter().any(|uri| uri.contains("{id}"))); + + let templates = mcp_support::resource_templates(); + assert_eq!(templates.len(), 1); + assert_eq!(templates[0].0, "code-system-graph://evidence/{id}"); +} + +#[test] +fn resource_read_should_reject_wrong_workspace_before_store_access() { + let error = mcp_support::read_resource( + &PathBuf::from("missing.db"), + "commerce", + "code-system-graph://workspace/payments/status", + &ExecutionPolicy::default(), + ); + + assert!(error.is_err()); + assert!(error.is_err_and(|error| { + error.kind == mcp_support::ResourceErrorKind::Invalid + && error.message.contains("outside this server's configured") + })); +} + +#[test] +fn resource_read_should_return_versioned_source_free_markdown() +-> Result<(), Box> { + let text = mcp_support::read_resource( + &PathBuf::from("missing.db"), + "commerce", + "code-system-graph://workspaces", + &ExecutionPolicy::default(), + ) + .map_err(|error| std::io::Error::other(error.message))?; + assert!(text.starts_with("# Configured workspaces")); + assert!(text.contains("commerce")); + assert!(text.contains("Delivery schema")); + assert!(!text.contains("source_body")); + Ok(()) +} + +#[test] +fn evidence_template_read_should_require_concrete_identifier() { + let error = mcp_support::read_resource( + &PathBuf::from("missing.db"), + "commerce", + "code-system-graph://evidence/{id}", + &ExecutionPolicy::default(), + ); + + assert!(error.is_err_and(|error| { + error.kind == mcp_support::ResourceErrorKind::Invalid + && error.message.contains("concrete evidence identifier") + })); +} + +#[test] +fn evidence_read_should_report_missing_metadata() -> Result<(), Box> { + let temporary = tempfile::tempdir()?; + let database = temporary.path().join("graph.db"); + drop(SqliteStore::open(&database)?); + + let error = mcp_support::read_resource( + &database, + "commerce", + "code-system-graph://evidence/evidence:missing", + &ExecutionPolicy::default(), + ); + + assert!(error.is_err_and(|error| { + error.kind == mcp_support::ResourceErrorKind::Missing + && error.message.contains("no evidence snapshot") + })); + Ok(()) +} + +#[test] +fn schema_resource_should_catalog_every_tool_input_and_result() +-> Result<(), Box> { + let catalog = mcp_support::schema_catalog(); + let schemas = catalog + .get("schemas") + .and_then(serde_json::Value::as_object); + + assert!(schemas.is_some()); + let schemas = schemas.map_or(0, serde_json::Map::len); + assert_eq!(schemas, 30); + for name in [ + "explore.input", + "explore.result", + "update_workspace.input", + "update_workspace.result", + "write_manual_link.input", + "write_manual_link.result", + "clean_cache.input", + "clean_cache.result", + ] { + assert!(catalog["schemas"].get(name).is_some(), "missing {name}"); + } + assert_eq!(catalog["schema_version"], 2); + assert!(catalog["application_interfaces"]["schemas"].is_array()); + assert!(serde_json::to_vec(&catalog)?.len() <= 2 * 1024 * 1024); + Ok(()) +} + +#[test] +fn tool_schemas_should_keep_codegraph_policy_out_of_llm_inputs() { + let catalog = mcp_support::schema_catalog(); + let impact = catalog["schemas"]["impact.input"].to_string(); + let scan = catalog["schemas"]["scan.input"].to_string(); + + assert!(!impact.contains("local_enrichment")); + assert!(!scan.contains("codegraph")); + assert!(!scan.contains("codegraph_binary")); +} + +#[test] +fn conditional_tool_inputs_should_require_action_specific_fields() { + let contract: Result = + serde_json::from_value(serde_json::json!({ + "action": "diff", + "workspace": "commerce", + "contract": "contract:a" + })); + let workspace_update: Result = + serde_json::from_value(serde_json::json!({ + "action": "remove_repository", + "workspace": "commerce", + "alias": "api", + "repository_path": "api" + })); + let manual_link: Result = + serde_json::from_value(serde_json::json!({ + "workspace": "commerce", + "from": "a", + "to": "b", + "relation": "documents", + "reason": "Operator decision" + })); + let communities: Result = + serde_json::from_value(serde_json::json!({ + "action": "show", + "workspace": "commerce", + "community_id": "community:a", + "snapshot_id": "snapshot:old" + })); + + assert!(contract.is_err()); + assert!(workspace_update.is_err()); + assert!(manual_link.is_err()); + assert!(communities.is_err()); +} diff --git a/crates/code-system-graph-cli/src/mcp_support/mod.rs b/crates/code-system-graph-cli/src/mcp_support/mod.rs index 3ede4cd..907ac5e 100644 --- a/crates/code-system-graph-cli/src/mcp_support/mod.rs +++ b/crates/code-system-graph-cli/src/mcp_support/mod.rs @@ -5,20 +5,28 @@ use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; use code_system_graph_core::{ - ChangeImpactReport, ContractAction, ContractReport, ContractRequest, ImpactReport, ImpactRequest, LocalContextResult, ManualLinkConfig, PullRequestInspection, SearchReport, inspect_contracts, public_schema_catalog + ChangeImpactReport, ContractAction, ContractReport, ContractRequest, ImpactReport, ImpactRequest, ManualLinkConfig, PullRequestInspection, SearchReport, inspect_contracts, public_schema_catalog }; use code_system_graph_model::{ - Community, CommunityId, Evidence, FreshnessSummary, Node, NodeId, NodeKind, OverallFreshness, RepoFreshness, RepoFreshnessState, RepositoryRecord, ToolEnvelope, ToolStatus, TraceReport + CommunityId, Evidence, FreshnessSummary, Node, NodeId, OverallFreshness, RepoFreshness, RepoFreshnessState, ToolEnvelope, ToolStatus, TraceReport }; -use code_system_graph_store_sqlite::{SqliteStore, StoreError}; +use code_system_graph_store_sqlite::SqliteStore; use schemars::{JsonSchema, schema_for}; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use crate::{ - ChangesInput, CommunityInput, CommunityReport, ExploreInput, ManifestMutationSummary, PullRequestInput, ScanSummary, SearchInput, TraceInput + ChangesInput, CommunityInput, CommunityReport, ExploreInput, ExploreReport, ManifestMutationSummary, PullRequestInput, ScanSummary, SearchInput, TraceInput }; +mod presentation; +#[cfg(test)] +mod presentation_goldens; +mod resources; + +pub(super) use presentation::AgentToolResult; +pub(super) use resources::{ResourceErrorKind, read_resource, resource_templates, resource_uris}; + pub(super) const ADMIN_TOOL_NAMES: [&str; 5] = [ "scan", "update_workspace", @@ -26,11 +34,9 @@ pub(super) const ADMIN_TOOL_NAMES: [&str; 5] = [ "clean_cache", "recompute_communities", ]; -pub(super) const JSON_MIME_TYPE: &str = "application/json"; +pub(super) const MARKDOWN_MIME_TYPE: &str = "text/markdown"; const MAX_PAGE_SIZE: usize = 100; -const MAX_RESOURCE_BYTES: usize = 256 * 1024; -const MAX_SCHEMA_RESOURCE_BYTES: usize = 2 * 1024 * 1024; #[derive(Debug, Clone, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] @@ -359,7 +365,7 @@ pub(super) struct SourceContextInput { pub evidence_limit: usize, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub(super) struct SnapshotMetrics { pub snapshot_id: String, pub node_count: usize, @@ -367,7 +373,7 @@ pub(super) struct SnapshotMetrics { pub evidence_count: usize, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub(super) struct GraphStatusReport { pub workspace: String, pub schema_version: i64, @@ -376,7 +382,7 @@ pub(super) struct GraphStatusReport { pub repositories: Vec, } -#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub(super) struct SourceContextReport { pub workspace: String, pub entity: Node, @@ -386,7 +392,7 @@ pub(super) struct SourceContextReport { pub truncated: bool, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub(super) struct AdminAudit { pub schema_version: u32, pub operation: String, @@ -396,41 +402,17 @@ pub(super) struct AdminAudit { pub result: T, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub(super) struct ManifestAdminReport { pub mutation: ManifestMutationSummary, pub scan: ScanSummary, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub(super) struct CacheCleanReport { pub removed_entries: usize, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -struct RepositoryView { - id: String, - checkout_id: String, - alias: String, - normalized_remote: Option, - head_commit: Option, - linked_worktree: bool, - working_tree_dirty: bool, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(super) enum ResourceErrorKind { - Invalid, - Missing, - Internal, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(super) struct ResourceError { - pub kind: ResourceErrorKind, - pub message: String, -} - pub(super) fn status_envelope( database_path: &Path, configured_workspace: &str, @@ -447,7 +429,7 @@ pub(super) fn status_envelope( ToolStatus::Degraded }; ToolEnvelope { - schema_version: 1, + schema_version: 2, status, data: Some(report), freshness, @@ -492,7 +474,7 @@ pub(super) fn contracts_envelope( ToolStatus::Ok }; ToolEnvelope { - schema_version: 1, + schema_version: 2, status, data: Some(report), freshness, @@ -586,7 +568,7 @@ pub(super) fn source_context_envelope( ToolStatus::Ok }; ToolEnvelope { - schema_version: 1, + schema_version: 2, status, data: Some(report), freshness, @@ -628,14 +610,14 @@ pub(super) fn admin_mutation_envelope( |items| freshness_summary(&items), ); ToolEnvelope { - schema_version: 1, + schema_version: 2, status: if freshness.overall == OverallFreshness::Fresh && audit_persisted { ToolStatus::Ok } else { ToolStatus::Degraded }, data: Some(AdminAudit { - schema_version: 1, + schema_version: 2, operation: operation.to_owned(), workspace: workspace.to_owned(), mutated: true, @@ -674,7 +656,7 @@ fn persist_admin_audit( .map_err(|error| format!("failed to timestamp administrative audit entry: {error}"))? .as_secs(); let entry = json!({ - "schema_version": 1, + "schema_version": 2, "timestamp_unix": timestamp, "workspace": workspace, "operation": operation, @@ -701,103 +683,6 @@ pub(super) fn configured_manifest_path( Ok(native_path(&path)) } -pub(super) fn resource_uris(workspace: &str) -> Vec<(String, String, String)> { - let prefix = format!("code-system-graph://workspace/{workspace}"); - vec![ - ( - "code-system-graph://workspaces".to_owned(), - "workspaces".to_owned(), - "Configured workspace catalog.".to_owned(), - ), - ( - format!("{prefix}/overview"), - "workspace-overview".to_owned(), - "Current immutable snapshot counts.".to_owned(), - ), - ( - format!("{prefix}/status"), - "workspace-status".to_owned(), - "Persisted workspace integrity and freshness.".to_owned(), - ), - ( - format!("{prefix}/repositories"), - "workspace-repositories".to_owned(), - "Credential-free repository registration metadata.".to_owned(), - ), - ( - format!("{prefix}/services"), - "workspace-services".to_owned(), - "Bounded service entity catalog.".to_owned(), - ), - ( - format!("{prefix}/contracts"), - "workspace-contracts".to_owned(), - "Bounded public contract entity catalog.".to_owned(), - ), - ( - format!("{prefix}/communities"), - "workspace-communities".to_owned(), - "Bounded deterministic community catalog.".to_owned(), - ), - ( - format!("{prefix}/schema"), - "schema-catalog".to_owned(), - "Versioned JSON Schema catalog for MCP tool inputs and results.".to_owned(), - ), - ( - format!("{prefix}/coverage"), - "workspace-coverage".to_owned(), - "Bounded extractor and freshness coverage metadata.".to_owned(), - ), - ( - "code-system-graph://evidence/{id}".to_owned(), - "evidence-metadata".to_owned(), - "Template URI for one evidence metadata record; replace {id} with its stable ID." - .to_owned(), - ), - ] -} - -pub(super) fn read_resource( - database_path: &Path, - workspace: &str, - uri: &str, -) -> Result { - let value = if uri == "code-system-graph://workspaces" { - json!({ - "schema_version": 1, - "workspaces": [{"name": workspace, "configured": true}] - }) - } else if let Some(path) = uri.strip_prefix("code-system-graph://workspace/") { - let (requested, resource) = path.split_once('/').ok_or_else(|| invalid_resource(uri))?; - validate_workspace(workspace, requested).map_err(|message| ResourceError { - kind: ResourceErrorKind::Invalid, - message, - })?; - read_workspace_resource(database_path, workspace, resource)? - } else if let Some(evidence_id) = uri.strip_prefix("code-system-graph://evidence/") { - read_evidence_resource(database_path, workspace, evidence_id)? - } else { - return Err(invalid_resource(uri)); - }; - let text = serde_json::to_string_pretty(&value).map_err(|error| ResourceError { - kind: ResourceErrorKind::Internal, - message: format!("resource serialization failed: {error}"), - })?; - let maximum = if uri.ends_with("/schema") { - MAX_SCHEMA_RESOURCE_BYTES - } else { - MAX_RESOURCE_BYTES - }; - if text.len() > maximum { - return Err(ResourceError { - kind: ResourceErrorKind::Internal, - message: format!("resource exceeds the {maximum}-byte response bound"), - }); - } - Ok(text) -} - pub(super) fn schema_catalog() -> Value { let mut schemas = BTreeMap::new(); insert_schema::(&mut schemas, "trace.input"); @@ -807,7 +692,7 @@ pub(super) fn schema_catalog() -> Value { insert_schema::(&mut schemas, "communities.input"); insert_schema::>(&mut schemas, "communities.result"); insert_schema::(&mut schemas, "explore.input"); - insert_schema::>(&mut schemas, "explore.result"); + insert_schema::>(&mut schemas, "explore.result"); insert_schema::(&mut schemas, "impact.input"); insert_schema::>(&mut schemas, "impact.result"); insert_schema::(&mut schemas, "analyze_changes.input"); @@ -845,182 +730,27 @@ pub(super) fn schema_catalog() -> Value { let application_interfaces = public_schema_catalog().map_or_else( |error| { json!({ - "schema_version": 1, + "schema_version": 2, "error": error.to_string() }) }, |catalog| { serde_json::to_value(catalog).unwrap_or_else(|error| { json!({ - "schema_version": 1, + "schema_version": 2, "error": error.to_string() }) }) }, ); json!({ - "schema_version": 1, + "schema_version": 2, "media_type": "application/schema+json", "schemas": schemas, "application_interfaces": application_interfaces }) } -fn read_workspace_resource( - database_path: &Path, - workspace: &str, - resource: &str, -) -> Result { - if resource == "schema" { - return Ok(schema_catalog()); - } - let store = SqliteStore::open_read_only(database_path).map_err(internal_store)?; - match resource { - "overview" => { - let summary = store - .current_snapshot_summary(workspace) - .map_err(internal_store)?; - Ok(json!({ - "schema_version": 1, - "workspace": workspace, - "snapshot": { - "id": summary.snapshot_id, - "nodes": summary.node_count, - "edges": summary.edge_count, - "evidence": summary.evidence_count - } - })) - } - "status" => { - let (status, freshness) = - load_status(database_path, workspace).map_err(internal_message)?; - Ok(json!({ - "schema_version": 1, - "status": status, - "freshness": freshness - })) - } - "repositories" => { - let registry = store - .load_workspace_registry(workspace) - .map_err(internal_store)?; - let repositories = registry - .repositories - .iter() - .take(MAX_PAGE_SIZE) - .map(repository_view) - .collect::>(); - Ok(json!({ - "schema_version": 1, - "workspace": workspace, - "total": registry.repositories.len(), - "truncated": registry.repositories.len() > MAX_PAGE_SIZE, - "repositories": repositories - })) - } - "services" => node_resource(&store, workspace, |kind| kind == NodeKind::Service), - "contracts" => node_resource(&store, workspace, is_contract), - "communities" => { - let snapshot = store - .load_current_community_snapshot(workspace) - .map_err(internal_store)?; - let total = snapshot.communities.len(); - let communities = snapshot - .communities - .into_iter() - .take(MAX_PAGE_SIZE) - .collect::>(); - Ok(json!({ - "schema_version": 1, - "workspace": workspace, - "snapshot_id": snapshot.snapshot_id, - "engine_version": snapshot.engine_version, - "config": snapshot.config, - "total": total, - "truncated": total > MAX_PAGE_SIZE, - "communities": communities - })) - } - "coverage" => { - let runs = store - .load_current_extractor_runs(workspace) - .map_err(internal_store)?; - let freshness = store - .load_current_freshness(workspace) - .map_err(internal_store)?; - let run_total = runs.len(); - Ok(json!({ - "schema_version": 1, - "workspace": workspace, - "run_total": run_total, - "truncated": run_total > MAX_PAGE_SIZE, - "runs": runs.into_iter().take(MAX_PAGE_SIZE).collect::>(), - "freshness": freshness_summary(&freshness) - })) - } - _ => Err(ResourceError { - kind: ResourceErrorKind::Missing, - message: format!("workspace resource `{resource}` was not found"), - }), - } -} - -fn read_evidence_resource( - database_path: &Path, - workspace: &str, - evidence_id: &str, -) -> Result { - if evidence_id.is_empty() || evidence_id == "{id}" { - return Err(ResourceError { - kind: ResourceErrorKind::Invalid, - message: "a concrete evidence identifier is required".to_owned(), - }); - } - let store = SqliteStore::open_read_only(database_path).map_err(internal_store)?; - let evidence = store - .load_current_evidence(workspace) - .map_err(|error| match error { - StoreError::CurrentSnapshotMissing(_) => ResourceError { - kind: ResourceErrorKind::Missing, - message: format!("workspace `{workspace}` has no evidence snapshot"), - }, - other => internal_store(other), - })? - .into_iter() - .find(|item| item.id.as_str() == evidence_id) - .ok_or_else(|| ResourceError { - kind: ResourceErrorKind::Missing, - message: format!("evidence `{evidence_id}` was not found in workspace `{workspace}`"), - })?; - Ok(json!({ - "schema_version": 1, - "workspace": workspace, - "evidence": evidence - })) -} - -fn node_resource( - store: &SqliteStore, - workspace: &str, - predicate: impl Fn(NodeKind) -> bool, -) -> Result { - let (nodes, _) = store - .load_current_graph(workspace) - .map_err(internal_store)?; - let selected = nodes - .into_iter() - .filter(|node| predicate(node.kind)) - .collect::>(); - let total = selected.len(); - Ok(json!({ - "schema_version": 1, - "workspace": workspace, - "total": total, - "truncated": total > MAX_PAGE_SIZE, - "entities": selected.into_iter().take(MAX_PAGE_SIZE).collect::>() - })) -} - fn load_status( database_path: &Path, workspace: &str, @@ -1102,39 +832,13 @@ fn validate_workspace(configured: &str, requested: &str) -> Result<(), String> { } } -fn is_contract(kind: NodeKind) -> bool { - matches!( - kind, - NodeKind::HttpOperation - | NodeKind::GraphqlOperation - | NodeKind::RpcMethod - | NodeKind::EventChannel - | NodeKind::EventSchema - | NodeKind::DatabaseTable - | NodeKind::DatabaseColumn - | NodeKind::ConfigKey - ) -} - -fn repository_view(repository: &RepositoryRecord) -> RepositoryView { - RepositoryView { - id: repository.id.as_str().to_owned(), - checkout_id: repository.checkout_id.as_str().to_owned(), - alias: repository.alias.clone(), - normalized_remote: repository.normalized_remote.clone(), - head_commit: repository.head_commit.clone(), - linked_worktree: repository.is_linked_worktree, - working_tree_dirty: repository.working_tree_dirty, - } -} - fn insert_schema(catalog: &mut BTreeMap, name: &str) { catalog.insert(name.to_owned(), json!(schema_for!(T))); } fn error_envelope(reason: &str, error: impl std::fmt::Display) -> ToolEnvelope { ToolEnvelope { - schema_version: 1, + schema_version: 2, status: ToolStatus::Error, data: None, freshness: FreshnessSummary { @@ -1146,24 +850,6 @@ fn error_envelope(reason: &str, error: impl std::fmt::Display) -> ToolEnvelop } } -fn invalid_resource(uri: &str) -> ResourceError { - ResourceError { - kind: ResourceErrorKind::Missing, - message: format!("resource `{uri}` was not found"), - } -} - -fn internal_store(error: impl std::fmt::Display) -> ResourceError { - internal_message(error.to_string()) -} - -fn internal_message(message: String) -> ResourceError { - ResourceError { - kind: ResourceErrorKind::Internal, - message, - } -} - fn default_page_size() -> usize { 50 } diff --git a/crates/code-system-graph-cli/src/mcp_support/presentation.rs b/crates/code-system-graph-cli/src/mcp_support/presentation.rs new file mode 100644 index 0000000..a5c97ad --- /dev/null +++ b/crates/code-system-graph-cli/src/mcp_support/presentation.rs @@ -0,0 +1,395 @@ +//! Exhaustive typed MCP tool presentation. + +use code_system_graph_core::{ + AgentNextAction, ChangeImpactReport, ContractReport, ImpactReport, PullRequestInspection, SearchReport +}; +use code_system_graph_model::{ToolEnvelope, ToolStatus, TraceReport}; + +use super::{ + AdminAudit, CacheCleanReport, GraphStatusReport, ManifestAdminReport, SourceContextReport +}; +use crate::agent_markdown::MarkdownDocument; +use crate::{CommunityReport, ExploreReport, ScanSummary}; + +#[derive(Clone, Copy)] +pub(crate) enum AgentToolResult<'a> { + Trace(&'a ToolEnvelope), + Query(&'a ToolEnvelope), + Explore(&'a ToolEnvelope), + Communities(&'a ToolEnvelope), + Impact(&'a ToolEnvelope), + AnalyzeChanges(&'a ToolEnvelope), + AnalyzePullRequest(&'a ToolEnvelope), + Status(&'a ToolEnvelope), + Contracts(&'a ToolEnvelope), + SourceContext(&'a ToolEnvelope), + Scan(&'a ToolEnvelope>), + UpdateWorkspace(&'a ToolEnvelope>), + WriteManualLink(&'a ToolEnvelope>), + CleanCache(&'a ToolEnvelope>), + RecomputeCommunities(&'a ToolEnvelope>), +} + +impl AgentToolResult<'_> { + pub(crate) fn render(self, maximum: usize) -> (String, bool) { + match self { + Self::Trace(envelope) => render_trace(envelope, maximum), + Self::Query(envelope) => render_query(envelope, maximum), + Self::Explore(envelope) => render_explore(envelope, maximum), + Self::Communities(envelope) => render_communities(envelope, maximum), + Self::Impact(envelope) => render_impact(envelope, maximum), + Self::AnalyzeChanges(envelope) => render_changes(envelope, maximum), + Self::AnalyzePullRequest(envelope) => render_pull_request(envelope, maximum), + Self::Status(envelope) => render_status(envelope, maximum), + Self::Contracts(envelope) => render_contracts(envelope, maximum), + Self::SourceContext(envelope) => render_source_context(envelope, maximum), + Self::Scan(envelope) => render_scan("scan", envelope, maximum), + Self::UpdateWorkspace(envelope) => { + render_manifest_admin("update_workspace", envelope, maximum) + } + Self::WriteManualLink(envelope) => { + render_manifest_admin("write_manual_link", envelope, maximum) + } + Self::CleanCache(envelope) => render_cache_clean(envelope, maximum), + Self::RecomputeCommunities(envelope) => { + render_scan("recompute_communities", envelope, maximum) + } + } + } +} + +fn tool_document( + name: &str, + envelope: &ToolEnvelope, + coverage: bool, + path: Option<&str>, +) -> MarkdownDocument { + MarkdownDocument::tool( + name, + envelope.schema_version, + envelope.status, + &envelope.freshness, + &envelope.warnings, + coverage && envelope.data.is_some(), + path, + ) +} + +fn finish(document: MarkdownDocument, status: ToolStatus, maximum: usize) -> (String, bool) { + (document.render(maximum), status == ToolStatus::Error) +} + +fn render_trace(envelope: &ToolEnvelope, maximum: usize) -> (String, bool) { + let path = envelope.data.as_ref().and_then(|report| { + report + .segments + .first() + .map(|segment| segment.source.stable_key.as_str()) + }); + let mut document = tool_document("trace", envelope, true, path); + if let Some(report) = &envelope.data { + document.debug_collection("Segments", &report.segments); + document.scalar("Truncated", report.truncated); + document.string_collection("Coverage", &report.coverage_gaps); + } + finish(document, envelope.status, maximum) +} + +fn render_query(envelope: &ToolEnvelope, maximum: usize) -> (String, bool) { + let mut document = tool_document("query", envelope, true, None); + if let Some(report) = &envelope.data { + document.debug_collection("Hits", &report.hits); + document.scalar("Total matches", report.total_matches); + document.scalar("Offset", report.offset); + document.scalar("Limit", report.limit); + document.scalar("Truncated", report.truncated); + document.debug("Coverage", &report.coverage); + render_next_actions(&mut document, &report.next_actions); + } + finish(document, envelope.status, maximum) +} + +fn render_explore(envelope: &ToolEnvelope, maximum: usize) -> (String, bool) { + let path = envelope.data.as_ref().and_then(|report| { + report + .federated_handoffs + .iter() + .flat_map(|handoff| &handoff.evidence) + .next() + .map(|location| location.path.as_str()) + .or(Some(report.repository.root.as_str())) + }); + let mut document = tool_document("explore", envelope, true, path); + if let Some(report) = &envelope.data { + document.debug("Repository", &report.repository); + document.debug("Coverage", &report.coverage); + document.debug_collection("Resolved symbols", &report.resolved_symbols); + document.debug_collection("Local relationships", &report.local_relationships); + document.debug_collection("Federated handoffs", &report.federated_handoffs); + render_next_actions(&mut document, &report.next_actions); + document.debug("Execution", &report.execution); + document.source("Source", &report.source_markdown); + } + finish(document, envelope.status, maximum) +} + +fn render_communities(envelope: &ToolEnvelope, maximum: usize) -> (String, bool) { + let mut document = tool_document("communities", envelope, false, None); + if let Some(report) = &envelope.data { + document.text("Snapshot", &report.snapshot_id); + document.text("Engine version", &report.engine_version); + document.debug("Configuration", &report.config); + document.debug_collection("Communities", &report.communities); + document.scalar("Total communities", report.total_communities); + document.scalar("Offset", report.offset); + document.scalar("Limit", report.limit); + document.scalar("Truncated", report.truncated); + if let Some(delta) = &report.delta { + document.debug("Delta", delta); + } + } + finish(document, envelope.status, maximum) +} + +fn render_impact(envelope: &ToolEnvelope, maximum: usize) -> (String, bool) { + let mut document = tool_document("impact", envelope, true, None); + if let Some(report) = &envelope.data { + document.text("Risk model version", &report.risk_model_version); + document.debug("Target", &report.target); + document.debug("Direction", &report.direction); + document.debug("Risk", &report.risk); + document.debug("Risk score", &report.risk_score); + document.debug_collection("Reasons", &report.reasons); + document.debug_collection("Direct consumers", &report.direct_consumers); + document.debug_collection("Transitive consumers", &report.transitive_consumers); + document.debug_collection("Possibly affected", &report.possibly_affected); + document.debug_collection("Unknown due to coverage", &report.unknown_due_to_coverage); + document.debug_collection("Affected repositories", &report.affected_repositories); + document.debug_collection("Affected services", &report.affected_services); + document.debug_collection("Affected contracts", &report.affected_contracts); + document.debug_collection("Affected communities", &report.affected_communities); + document.debug_collection("Local impact summaries", &report.local_impact_summaries); + document.debug_collection("Test recommendations", &report.test_recommendations); + document.debug_collection("Depth buckets", &report.depth_buckets); + document.debug("Coverage", &report.coverage); + document.debug("Truncation", &report.truncation); + } + finish(document, envelope.status, maximum) +} + +fn render_changes(envelope: &ToolEnvelope, maximum: usize) -> (String, bool) { + let mut document = tool_document("analyze_changes", envelope, true, None); + if let Some(report) = &envelope.data { + document.text("Analyzer version", &report.analyzer_version); + document.text("Analyzer fingerprint", &report.analyzer_fingerprint); + document.debug("Change set", &report.change_set); + document.debug_collection("Mappings", &report.mappings); + document.debug_collection("Changed entities", &report.changed_entities); + document.debug_collection("Contract deltas", &report.contract_deltas); + document.debug_collection("Impacts", &report.impacts); + document.debug_collection("Touched repositories", &report.touched_repositories); + document.debug_collection("Touched services", &report.touched_services); + document.debug_collection("Touched communities", &report.touched_communities); + document.debug_collection("Touched contracts", &report.touched_contracts); + document.debug("Coverage", &report.coverage); + document.debug("Summary", &report.summary); + } + finish(document, envelope.status, maximum) +} + +fn render_pull_request( + envelope: &ToolEnvelope, + maximum: usize, +) -> (String, bool) { + let path = envelope + .data + .as_ref() + .and_then(|report| report.changed_files.first()) + .and_then(|file| file.new_path.as_deref().or(file.old_path.as_deref())); + let mut document = tool_document("analyze_pull_request", envelope, false, path); + if let Some(report) = &envelope.data { + document.debug("Coordinates", &report.coordinates); + document.debug("Metadata", &report.metadata); + document.debug_collection("Changed files", &report.changed_files); + document.debug("CI", &report.ci); + document.debug("Review", &report.review); + document.debug("Rate limit", &report.rate_limit); + document.debug_collection("Provider warnings", &report.warnings); + document.text("Fingerprint", &report.fingerprint); + document.scalar("From cache", report.from_cache); + } + finish(document, envelope.status, maximum) +} + +fn render_status(envelope: &ToolEnvelope, maximum: usize) -> (String, bool) { + let mut document = tool_document("status", envelope, false, None); + if let Some(report) = &envelope.data { + document.text("Workspace", &report.workspace); + document.scalar("Database schema", report.schema_version); + document.scalar("Integrity ok", report.integrity_ok); + document.debug("Snapshot", &report.snapshot); + document.debug_collection("Repositories", &report.repositories); + } + finish(document, envelope.status, maximum) +} + +fn render_contracts(envelope: &ToolEnvelope, maximum: usize) -> (String, bool) { + let mut document = tool_document("contracts", envelope, false, None); + if let Some(report) = &envelope.data { + document.scalar("Result version", report.result_version); + document.debug("Action", &report.action); + document.debug_collection("Contracts", &report.contracts); + document.debug_collection("Links", &report.links); + document.debug_collection("Compatibility", &report.compatibility); + document.debug_collection("Differences", &report.differences); + document.debug_collection("Issues", &report.issues); + document.debug("Valid", &report.valid); + document.scalar("Complete", report.complete); + document.scalar("Truncated", report.truncated); + } + finish(document, envelope.status, maximum) +} + +fn render_source_context( + envelope: &ToolEnvelope, + maximum: usize, +) -> (String, bool) { + let path = envelope.data.as_ref().and_then(|report| { + report + .evidence + .iter() + .find_map(|item| item.file_path.as_deref()) + }); + let mut document = tool_document("source_context", envelope, false, path); + if let Some(report) = &envelope.data { + document.text("Workspace", &report.workspace); + document.debug("Entity", &report.entity); + document.debug_collection("Related entities", &report.related_entities); + document.debug_collection("Evidence", &report.evidence); + document.scalar("Total evidence", report.total_evidence); + document.scalar("Truncated", report.truncated); + } + finish(document, envelope.status, maximum) +} + +fn render_scan( + name: &str, + envelope: &ToolEnvelope>, + maximum: usize, +) -> (String, bool) { + let mut document = tool_document(name, envelope, false, None); + if let Some(audit) = &envelope.data { + render_audit_control(&mut document, audit); + render_scan_summary(&mut document, &audit.result); + } + finish(document, envelope.status, maximum) +} + +fn render_manifest_admin( + name: &str, + envelope: &ToolEnvelope>, + maximum: usize, +) -> (String, bool) { + let mut document = tool_document(name, envelope, false, None); + if let Some(audit) = &envelope.data { + render_audit_control(&mut document, audit); + document.debug("Mutation", &audit.result.mutation); + render_scan_summary(&mut document, &audit.result.scan); + } + finish(document, envelope.status, maximum) +} + +fn render_cache_clean( + envelope: &ToolEnvelope>, + maximum: usize, +) -> (String, bool) { + let mut document = tool_document("clean_cache", envelope, false, None); + if let Some(audit) = &envelope.data { + render_audit_control(&mut document, audit); + document.scalar("Removed entries", audit.result.removed_entries); + } + finish(document, envelope.status, maximum) +} + +fn render_audit_control(document: &mut MarkdownDocument, audit: &AdminAudit) { + document.text("Operation", &audit.operation); + document.text("Workspace", &audit.workspace); + document.scalar("Mutated", audit.mutated); + document.scalar("Audit persisted", audit.audit_persisted); +} + +fn render_next_actions(document: &mut MarkdownDocument, actions: &[AgentNextAction]) { + let fragments = actions + .iter() + .enumerate() + .map(|(index, action)| { + let mut fragment = MarkdownDocument::fragment(); + let prefix = format!("Next action {}", index + 1); + fragment.text(&format!("{prefix} tool"), &action.tool); + fragment.debug(&format!("{prefix} arguments"), &action.arguments); + fragment.text(&format!("{prefix} rationale"), &action.rationale); + fragment.into_complete() + }) + .collect(); + document.bounded_fragments("Next actions", actions.len(), false, fragments); +} + +fn render_scan_summary(document: &mut MarkdownDocument, report: &ScanSummary) { + document.debug("Execution", &report.execution); + document.text("Scan workspace", &report.workspace); + document.text("Snapshot", &report.snapshot_id); + document.scalar("Node count", report.node_count); + document.scalar("Edge count", report.edge_count); + document.scalar("Evidence count", report.evidence_count); + document.scalar("Community count", report.community_count); + document.scalar("Community delta count", report.community_delta_count); + document.scalar("Discovered input count", report.discovered_input_count); + document.scalar("Changed input count", report.changed_input_count); + document.scalar("Reused snapshot", report.reused_snapshot); + document.scalar( + "Corroborated symbol count", + report.corroborated_symbol_count, + ); + document.scalar("Affected test count", report.affected_test_count); + document.scalar("Degradation count", report.degradation_count); + document.string_collection("Degradations", &report.degradations); +} + +#[cfg(test)] +mod tests { + use code_system_graph_model::{ + FreshnessSummary, Node, NodeId, NodeKind, OverallFreshness, ToolEnvelope, ToolStatus, TraceReport, TraceSegment + }; + + use super::AgentToolResult; + + #[test] + fn trace_variant_should_render_real_typed_fields() { + let node = Node { + id: NodeId::new("node:a"), + kind: NodeKind::Service, + repo_id: None, + stable_key: "service:a".to_owned(), + label: "A".to_owned(), + }; + let envelope = ToolEnvelope { + schema_version: 2, + status: ToolStatus::Ok, + data: Some(TraceReport { + segments: Vec::::new(), + truncated: false, + coverage_gaps: vec![format!("{} has no path", node.id.as_str())], + }), + freshness: FreshnessSummary { + overall: OverallFreshness::Fresh, + stale_repositories: Vec::new(), + reasons: Vec::new(), + }, + warnings: Vec::new(), + }; + let (rendered, is_error) = AgentToolResult::Trace(&envelope).render(4_096); + assert!(!is_error); + assert!(rendered.contains("## Segments")); + assert!(rendered.contains("node:a has no path")); + } +} diff --git a/crates/code-system-graph-cli/src/mcp_support/presentation_goldens.rs b/crates/code-system-graph-cli/src/mcp_support/presentation_goldens.rs new file mode 100644 index 0000000..87e5923 --- /dev/null +++ b/crates/code-system-graph-cli/src/mcp_support/presentation_goldens.rs @@ -0,0 +1,300 @@ +fn schema_value( + schema: &serde_json::Value, + root: &serde_json::Value, + depth: usize, +) -> serde_json::Value { + assert!( + depth < 64, + "fixture schema should not require unbounded recursion" + ); + if let Some(reference) = schema.get("$ref").and_then(serde_json::Value::as_str) { + let mut target = root; + for component in reference.trim_start_matches("#/").split('/') { + let component = component.replace("~1", "/").replace("~0", "~"); + target = &target[&component]; + } + return schema_value(target, root, depth + 1); + } + if let Some(value) = schema.get("const") { + return value.clone(); + } + if let Some(value) = schema + .get("enum") + .and_then(serde_json::Value::as_array) + .and_then(|values| values.first()) + { + return value.clone(); + } + for alternatives in ["oneOf", "anyOf"] { + if let Some(values) = schema + .get(alternatives) + .and_then(serde_json::Value::as_array) + { + let selected = values + .iter() + .find(|value| { + value.get("type") != Some(&serde_json::Value::String("null".to_owned())) + }) + .unwrap_or(&values[0]); + return schema_value(selected, root, depth + 1); + } + } + if let Some(value) = schema.get("default") { + return value.clone(); + } + if let Some(types) = schema.get("type").and_then(serde_json::Value::as_array) { + let selected = types + .iter() + .find(|value| value.as_str() != Some("null")) + .and_then(serde_json::Value::as_str) + .unwrap_or("null"); + let mut selected_schema = schema.clone(); + selected_schema["type"] = serde_json::Value::String(selected.to_owned()); + return schema_value(&selected_schema, root, depth + 1); + } + match schema.get("type").and_then(serde_json::Value::as_str) { + Some("object") => { + let properties = schema + .get("properties") + .and_then(serde_json::Value::as_object); + properties + .into_iter() + .flatten() + .map(|name| (name.0.to_owned(), schema_value(name.1, root, depth + 1))) + .collect::>() + .into() + } + Some("array") => schema + .get("items") + .map(|items| vec![schema_value(items, root, depth + 1)]) + .unwrap_or_default() + .into(), + Some("string") => serde_json::Value::String("fixture".to_owned()), + Some("integer") => serde_json::json!(1), + Some("number") => serde_json::json!(1.0), + Some("boolean") => serde_json::Value::Bool(false), + Some("null") => serde_json::Value::Null, + _ => serde_json::Value::Object(serde_json::Map::new()), + } +} + +fn envelope( + name: &str, +) -> code_system_graph_model::ToolEnvelope { + let catalog = mcp_support::schema_catalog(); + let schema = &catalog["schemas"][name]; + let mut value = schema_value(schema, schema, 0); + let data_schema = &schema["properties"]["data"]; + value["data"] = schema_value(data_schema, schema, 0); + value["status"] = serde_json::json!("ok"); + let fixture = value.clone(); + let envelope: code_system_graph_model::ToolEnvelope = serde_json::from_value(value) + .unwrap_or_else(|error| { + panic!("generated real typed fixture {name} failed: {error}; {fixture}") + }); + assert!( + envelope.data.is_some(), + "{name} fixture must contain report data: {fixture}" + ); + envelope +} + +fn golden_fingerprint(name: &str) -> &'static str { + match name { + "trace.result" => { + "mcp-markdown-golden:ff3e0e1d64e278a8df3ee2ca2562662ea7f72cea21ff8081c787989bfe023b77" + } + "query.result" => { + "mcp-markdown-golden:77ff3d78b5ad7075229a96ecd54e493c73599e9d7e4a9ac1df00dd825f1e0cdb" + } + "explore.result" => { + "mcp-markdown-golden:a9413e442659f3ea76c434c7bb58a08918856e630e6ad30f5727e8dde8ad32d8" + } + "communities.result" => { + "mcp-markdown-golden:5a1ef5125a028637658d28ca62b9166af11bf31dc73e2dd857a322ed5cea5b56" + } + "impact.result" => { + "mcp-markdown-golden:94149d2320df0d0a26b7b396b3ec663ac3763deb8b58a678101efc093214d0da" + } + "analyze_changes.result" => { + "mcp-markdown-golden:2b4db983ec52fd967c491915d8f344b25134dccaa2eb8885206c598a9550900b" + } + "analyze_pull_request.result" => { + "mcp-markdown-golden:bf108cc2c6d7faa94be192110cad226f8c37bc75a0cdd1c55a822fa0e70633bc" + } + "status.result" => { + "mcp-markdown-golden:3af15ee97798f51ac2a4e2604d02d91a48f25963758d95347240d0df5baf4a61" + } + "contracts.result" => { + "mcp-markdown-golden:ab73b99fabe2c87f7c61c1a588311c690a9b15fe48d2817b5182cce245981b19" + } + "source_context.result" => { + "mcp-markdown-golden:f845b69a6bad8ff15e647167657992e43d7c00a7516ce2b8f8eec99fac3a060c" + } + "scan.result" => { + "mcp-markdown-golden:f765efe1f85cb3f5bff0e82ea8cf57932f15e234f31f53aa5a6e708360620659" + } + "update_workspace.result" => { + "mcp-markdown-golden:db792a79b966c5de817b846801f36020df1cbf9913bdc545a82e64ec156d70fd" + } + "write_manual_link.result" => { + "mcp-markdown-golden:bcc6350ac77602746acf02b06dfeaa6e892b81de3583cc7327bbe905744e3c90" + } + "clean_cache.result" => { + "mcp-markdown-golden:6b9d3ffa7ddc57985ff3218911602e16ff586ec8a68437a47a605b05b5565623" + } + "recompute_communities.result" => { + "mcp-markdown-golden:0e9913c024e667c3aca870f23bb8c4dbc23577f0ac2e01d103bf0832b2438f3a" + } + other => panic!("missing Markdown golden for {other}"), + } +} + +macro_rules! assert_golden { + ($variant:ident, $report:ty, $schema:literal, $heading:literal, $field:literal) => {{ + let envelope = envelope::<$report>($schema); + let (markdown, is_error) = AgentToolResult::$variant(&envelope).render(1_048_576); + let expected_prefix = concat!("# ", $heading, "\n\n## Status\n\n- State: `ok`"); + assert!(markdown.starts_with(expected_prefix), "{markdown}"); + assert!(markdown.contains($field), "{}: {markdown}", $schema); + assert_eq!( + code_system_graph_model::stable_id("mcp-markdown-golden", &markdown), + golden_fingerprint($schema), + "{} full Markdown changed", + $schema + ); + assert!(!is_error); + }}; +} + +#[test] +fn read_only_tools_should_match_full_markdown_goldens() { + assert_golden!( + Trace, + code_system_graph_model::TraceReport, + "trace.result", + "trace", + "## Segments" + ); + assert_golden!( + Query, + code_system_graph_core::SearchReport, + "query.result", + "query", + "## Hits" + ); + assert_golden!( + Explore, + crate::ExploreReport, + "explore.result", + "explore", + "## Repository" + ); + assert_golden!( + Communities, + crate::CommunityReport, + "communities.result", + "communities", + "## Snapshot" + ); + assert_golden!( + Impact, + code_system_graph_core::ImpactReport, + "impact.result", + "impact", + "## Risk model version" + ); + assert_golden!( + AnalyzeChanges, + code_system_graph_core::ChangeImpactReport, + "analyze_changes.result", + "analyze changes", + "## Analyzer version" + ); + assert_golden!( + AnalyzePullRequest, + code_system_graph_core::PullRequestInspection, + "analyze_pull_request.result", + "analyze pull request", + "## Coordinates" + ); + assert_golden!( + Status, + mcp_support::GraphStatusReport, + "status.result", + "status", + "## Workspace" + ); + assert_golden!( + Contracts, + code_system_graph_core::ContractReport, + "contracts.result", + "contracts", + "## Result version" + ); + assert_golden!( + SourceContext, + mcp_support::SourceContextReport, + "source_context.result", + "source context", + "## Workspace" + ); +} + +#[test] +fn administrative_tools_should_match_full_markdown_goldens() { + assert_golden!( + Scan, + mcp_support::AdminAudit, + "scan.result", + "scan", + "## Operation" + ); + assert_golden!( + UpdateWorkspace, + mcp_support::AdminAudit, + "update_workspace.result", + "update workspace", + "## Mutation" + ); + assert_golden!( + WriteManualLink, + mcp_support::AdminAudit, + "write_manual_link.result", + "write manual link", + "## Mutation" + ); + assert_golden!( + CleanCache, + mcp_support::AdminAudit, + "clean_cache.result", + "clean cache", + "## Removed entries" + ); + assert_golden!( + RecomputeCommunities, + mcp_support::AdminAudit, + "recompute_communities.result", + "recompute communities", + "## Operation" + ); +} + +#[test] +fn compact_error_should_report_absent_coverage() { + let error = code_system_graph_model::ToolEnvelope:: { + schema_version: 2, + status: code_system_graph_model::ToolStatus::Error, + data: None, + freshness: code_system_graph_model::FreshnessSummary { + overall: code_system_graph_model::OverallFreshness::Unknown, + stale_repositories: Vec::new(), + reasons: vec!["fixture".to_owned()], + }, + warnings: vec!["typed fixture".to_owned()], + }; + let (compact, is_error) = AgentToolResult::Query(&error).render(256); + assert!(compact.contains("coverage=absent"), "{compact}"); + assert!(is_error); +} +use super::{self as mcp_support, AgentToolResult}; diff --git a/crates/code-system-graph-cli/src/mcp_support/resources.rs b/crates/code-system-graph-cli/src/mcp_support/resources.rs new file mode 100644 index 0000000..02acabc --- /dev/null +++ b/crates/code-system-graph-cli/src/mcp_support/resources.rs @@ -0,0 +1,797 @@ +//! Typed, bounded MCP resource contracts and Markdown delivery. + +use std::path::Path; + +#[cfg(test)] +use code_system_graph_model::RepoFreshnessState; +use code_system_graph_model::{ + Community, CommunityAlgorithm, CommunityConfig, CommunityEdgeWeight, CommunityId, CommunityLabelEvidence, CommunityMetrics, CommunityScope, Evidence, ExtractorRun, FreshnessSummary, Node, NodeId, NodeKind, OverallFreshness, RepoFreshness, RepoId, RepositoryRecord +}; +use code_system_graph_store_sqlite::{SqliteStore, StoreError}; +use serde_json::{Value, json}; + +#[cfg(test)] +use super::SnapshotMetrics; +use super::{ + GraphStatusReport, freshness_summary, load_status, schema_catalog, validate_workspace +}; +use crate::agent_markdown::MarkdownDocument; + +#[derive(Debug)] +struct BoundedCollection { + total: usize, + items: Vec, +} + +impl BoundedCollection { + fn retained(&self) -> usize { + self.items.len() + } + + fn truncated(&self) -> bool { + self.total > self.retained() + } +} + +fn bounded_collection( + items: impl IntoIterator, + item_limit: usize, +) -> BoundedCollection { + let items = items.into_iter().collect::>(); + let total = items.len(); + BoundedCollection { + total, + items: items.into_iter().take(item_limit).collect(), + } +} + +#[derive(Debug)] +struct RepositoryView { + id: String, + checkout_id: String, + alias: String, + normalized_remote: Option, + head_commit: Option, + linked_worktree: bool, + working_tree_dirty: bool, +} + +#[derive(Debug)] +struct WorkspaceResourceItem { + name: String, + configured: bool, +} + +#[derive(Debug)] +struct WorkspacesResource { + schema_version: u32, + workspaces: BoundedCollection, +} + +#[derive(Debug)] +struct OverviewSnapshot { + id: String, + nodes: usize, + edges: usize, + evidence: usize, +} + +#[derive(Debug)] +struct OverviewResource { + schema_version: u32, + workspace: String, + snapshot: OverviewSnapshot, +} + +#[derive(Debug)] +struct FreshnessResource { + overall: OverallFreshness, + stale_repositories: BoundedCollection, + reasons: BoundedCollection, +} + +#[derive(Debug)] +struct StatusResource { + schema_version: u32, + status: GraphStatusReport, + repositories: BoundedCollection, + freshness: FreshnessResource, +} + +#[derive(Debug)] +struct RepositoriesResource { + schema_version: u32, + workspace: String, + repositories: BoundedCollection, +} + +#[derive(Debug)] +struct EntitiesResource { + schema_version: u32, + workspace: String, + entities: BoundedCollection, +} + +#[derive(Debug)] +struct CommunityConfigView { + algorithm: CommunityAlgorithm, + scope: CommunityScope, + seed: u64, + resolution: f64, + minimum_confidence: f32, + edge_weights: BoundedCollection, + max_iterations: u32, +} + +#[derive(Debug)] +struct CommunityView { + id: CommunityId, + label: String, + members: BoundedCollection, + central_nodes: BoundedCollection, + repositories: BoundedCollection, + services: BoundedCollection, + inbound_contracts: BoundedCollection, + outbound_contracts: BoundedCollection, + metrics: CommunityMetrics, + label_evidence: BoundedCollection, + limitations: BoundedCollection, +} + +#[derive(Debug)] +struct CommunitiesResource { + schema_version: u32, + workspace: String, + snapshot_id: String, + engine_version: String, + config: CommunityConfigView, + communities: BoundedCollection, +} + +#[derive(Debug)] +struct CoverageResource { + schema_version: u32, + workspace: String, + runs: BoundedCollection, + freshness: FreshnessResource, +} + +#[derive(Debug)] +struct EvidenceResource { + schema_version: u32, + workspace: String, + evidence: Evidence, +} + +enum ResourceDocument { + Workspaces(WorkspacesResource), + Overview(OverviewResource), + Status(StatusResource), + Repositories(RepositoriesResource), + Services(EntitiesResource), + Contracts(EntitiesResource), + Communities(CommunitiesResource), + Coverage(CoverageResource), + Evidence(EvidenceResource), + SchemaCatalog(Value), +} + +impl ResourceDocument { + fn render(&self, maximum: usize) -> String { + match self { + Self::Workspaces(resource) => render_workspaces(resource, maximum), + Self::Overview(resource) => render_overview(resource, maximum), + Self::Status(resource) => render_status(resource, maximum), + Self::Repositories(resource) => render_repositories(resource, maximum), + Self::Services(resource) => render_entities("Workspace services", resource, maximum), + Self::Contracts(resource) => render_entities("Workspace contracts", resource, maximum), + Self::Communities(resource) => render_communities(resource, maximum), + Self::Coverage(resource) => render_coverage(resource, maximum), + Self::Evidence(resource) => render_evidence(resource, maximum), + Self::SchemaCatalog(resource) => { + crate::agent_markdown::render_schema_catalog(resource, maximum) + } + } + } +} + +fn render_workspaces(resource: &WorkspacesResource, maximum: usize) -> String { + let mut document = MarkdownDocument::resource("Configured workspaces", resource.schema_version); + render_collection_items( + &mut document, + "Workspaces", + &resource.workspaces, + |document, index, workspace| { + document.text(&format!("Workspace {} name", index + 1), &workspace.name); + document.scalar( + &format!("Workspace {} configured", index + 1), + workspace.configured, + ); + }, + ); + document.render(maximum) +} + +fn render_overview(resource: &OverviewResource, maximum: usize) -> String { + let mut document = MarkdownDocument::resource("Workspace overview", resource.schema_version); + document.text("Workspace", &resource.workspace); + document.text("Snapshot", &resource.snapshot.id); + document.scalar("Nodes", resource.snapshot.nodes); + document.scalar("Edges", resource.snapshot.edges); + document.scalar("Evidence", resource.snapshot.evidence); + document.render(maximum) +} + +fn render_status(resource: &StatusResource, maximum: usize) -> String { + let mut document = MarkdownDocument::resource("Workspace status", resource.schema_version); + document.text("Workspace", &resource.status.workspace); + document.scalar("Database schema", resource.status.schema_version); + document.scalar("Integrity ok", resource.status.integrity_ok); + document.debug("Snapshot", &resource.status.snapshot); + render_collection(&mut document, "Repositories", &resource.repositories); + render_freshness(&mut document, &resource.freshness); + document.render(maximum) +} + +fn render_repositories(resource: &RepositoriesResource, maximum: usize) -> String { + let mut document = + MarkdownDocument::resource("Workspace repositories", resource.schema_version); + document.text("Workspace", &resource.workspace); + render_collection_items( + &mut document, + "Repositories", + &resource.repositories, + |document, index, repository| { + let prefix = format!("Repository {}", index + 1); + document.text(&format!("{prefix} id"), &repository.id); + document.text(&format!("{prefix} checkout"), &repository.checkout_id); + document.text(&format!("{prefix} alias"), &repository.alias); + document.debug(&format!("{prefix} remote"), &repository.normalized_remote); + document.debug(&format!("{prefix} head"), &repository.head_commit); + document.scalar( + &format!("{prefix} linked worktree"), + repository.linked_worktree, + ); + document.scalar(&format!("{prefix} dirty"), repository.working_tree_dirty); + }, + ); + document.render(maximum) +} + +fn render_entities(name: &str, resource: &EntitiesResource, maximum: usize) -> String { + let mut document = MarkdownDocument::resource(name, resource.schema_version); + document.text("Workspace", &resource.workspace); + render_collection(&mut document, "Entities", &resource.entities); + document.render(maximum) +} + +fn render_communities(resource: &CommunitiesResource, maximum: usize) -> String { + let mut document = MarkdownDocument::resource("Workspace communities", resource.schema_version); + document.text("Workspace", &resource.workspace); + document.text("Snapshot", &resource.snapshot_id); + document.text("Engine version", &resource.engine_version); + document.debug("Algorithm", &resource.config.algorithm); + document.debug("Scope", &resource.config.scope); + document.scalar("Seed", resource.config.seed); + document.scalar("Resolution", resource.config.resolution); + document.scalar("Minimum confidence", resource.config.minimum_confidence); + document.scalar("Maximum iterations", resource.config.max_iterations); + render_collection( + &mut document, + "Configuration edge weights", + &resource.config.edge_weights, + ); + render_collection_items( + &mut document, + "Communities", + &resource.communities, + |document, index, community| { + let prefix = format!("Community {}", index + 1); + document.debug(&format!("{prefix} identity"), &community.id); + document.text(&format!("{prefix} label"), &community.label); + document.debug(&format!("{prefix} metrics"), &community.metrics); + render_collection(document, &format!("{prefix} members"), &community.members); + render_collection( + document, + &format!("{prefix} central nodes"), + &community.central_nodes, + ); + render_collection( + document, + &format!("{prefix} repositories"), + &community.repositories, + ); + render_collection(document, &format!("{prefix} services"), &community.services); + render_collection( + document, + &format!("{prefix} inbound contracts"), + &community.inbound_contracts, + ); + render_collection( + document, + &format!("{prefix} outbound contracts"), + &community.outbound_contracts, + ); + render_collection( + document, + &format!("{prefix} label evidence"), + &community.label_evidence, + ); + render_collection( + document, + &format!("{prefix} limitations"), + &community.limitations, + ); + }, + ); + document.render(maximum) +} + +fn render_coverage(resource: &CoverageResource, maximum: usize) -> String { + let mut document = MarkdownDocument::resource("Workspace coverage", resource.schema_version); + document.text("Workspace", &resource.workspace); + render_collection(&mut document, "Extractor runs", &resource.runs); + render_freshness(&mut document, &resource.freshness); + document.render(maximum) +} + +fn render_evidence(resource: &EvidenceResource, maximum: usize) -> String { + let mut document = MarkdownDocument::resource("Evidence metadata", resource.schema_version); + document.text("Workspace", &resource.workspace); + document.debug("Evidence", &resource.evidence); + document.render(maximum) +} + +fn render_freshness(document: &mut MarkdownDocument, freshness: &FreshnessResource) { + document.debug("Freshness overall", &freshness.overall); + render_collection( + document, + "Freshness stale repositories", + &freshness.stale_repositories, + ); + render_collection(document, "Freshness reasons", &freshness.reasons); +} + +fn render_collection( + document: &mut MarkdownDocument, + name: &str, + collection: &BoundedCollection, +) { + document.bounded_collection( + name, + collection.total, + collection.retained(), + collection.truncated(), + &collection.items, + ); +} + +fn render_collection_items( + document: &mut MarkdownDocument, + name: &str, + collection: &BoundedCollection, + mut render_item: impl FnMut(&mut MarkdownDocument, usize, &T), +) { + let items = collection + .items + .iter() + .enumerate() + .map(|(index, item)| { + let mut fragment = MarkdownDocument::fragment(); + render_item(&mut fragment, index, item); + fragment.into_complete() + }) + .collect(); + document.bounded_fragments(name, collection.total, collection.truncated(), items); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ResourceErrorKind { + Invalid, + Missing, + Internal, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct ResourceError { + pub kind: ResourceErrorKind, + pub message: String, +} +pub(crate) fn resource_uris(workspace: &str) -> Vec<(String, String, String)> { + let prefix = format!("code-system-graph://workspace/{workspace}"); + vec![ + ( + "code-system-graph://workspaces".to_owned(), + "workspaces".to_owned(), + "Configured workspace catalog.".to_owned(), + ), + ( + format!("{prefix}/overview"), + "workspace-overview".to_owned(), + "Current immutable snapshot counts.".to_owned(), + ), + ( + format!("{prefix}/status"), + "workspace-status".to_owned(), + "Persisted workspace integrity and freshness.".to_owned(), + ), + ( + format!("{prefix}/repositories"), + "workspace-repositories".to_owned(), + "Credential-free repository registration metadata.".to_owned(), + ), + ( + format!("{prefix}/services"), + "workspace-services".to_owned(), + "Bounded service entity catalog.".to_owned(), + ), + ( + format!("{prefix}/contracts"), + "workspace-contracts".to_owned(), + "Bounded public contract entity catalog.".to_owned(), + ), + ( + format!("{prefix}/communities"), + "workspace-communities".to_owned(), + "Bounded deterministic community catalog.".to_owned(), + ), + ( + format!("{prefix}/schema"), + "schema-catalog".to_owned(), + "Versioned JSON Schema catalog for MCP tool inputs and results.".to_owned(), + ), + ( + format!("{prefix}/coverage"), + "workspace-coverage".to_owned(), + "Bounded extractor and freshness coverage metadata.".to_owned(), + ), + ] +} + +pub(crate) fn resource_templates() -> Vec<(String, String, String)> { + vec![( + "code-system-graph://evidence/{id}".to_owned(), + "evidence-metadata".to_owned(), + "One bounded evidence metadata record selected by stable ID.".to_owned(), + )] +} + +pub(crate) fn read_resource( + database_path: &Path, + workspace: &str, + uri: &str, + policy: &code_system_graph_core::ExecutionPolicy, +) -> Result { + let item_limit = usize::try_from(policy.max_mcp_resource_items) + .expect("validated policy count is usize-representable"); + let resource = if uri == "code-system-graph://workspaces" { + let workspaces = bounded_collection( + [WorkspaceResourceItem { + name: workspace.to_owned(), + configured: true, + }], + item_limit, + ); + ResourceDocument::Workspaces(WorkspacesResource { + schema_version: 2, + workspaces, + }) + } else if let Some(path) = uri.strip_prefix("code-system-graph://workspace/") { + let (requested, resource) = path.split_once('/').ok_or_else(|| invalid_resource(uri))?; + validate_workspace(workspace, requested).map_err(|message| ResourceError { + kind: ResourceErrorKind::Invalid, + message, + })?; + read_workspace_resource(database_path, workspace, resource, item_limit)? + } else if let Some(evidence_id) = uri.strip_prefix("code-system-graph://evidence/") { + read_evidence_resource(database_path, workspace, evidence_id)? + } else { + return Err(invalid_resource(uri)); + }; + let maximum = if uri.ends_with("/schema") { + usize::try_from(policy.max_mcp_schema_catalog_bytes) + .expect("validated policy bytes are usize-representable") + } else { + usize::try_from(policy.max_mcp_resource_bytes) + .expect("validated policy bytes are usize-representable") + }; + Ok(resource.render(maximum)) +} + +fn read_workspace_resource( + database_path: &Path, + workspace: &str, + resource: &str, + item_limit: usize, +) -> Result { + if resource == "schema" { + return Ok(ResourceDocument::SchemaCatalog(bounded_schema_catalog( + item_limit, + ))); + } + let store = SqliteStore::open_read_only(database_path).map_err(internal_store)?; + match resource { + "overview" => { + let summary = store + .current_snapshot_summary(workspace) + .map_err(internal_store)?; + Ok(ResourceDocument::Overview(OverviewResource { + schema_version: 2, + workspace: workspace.to_owned(), + snapshot: OverviewSnapshot { + id: summary.snapshot_id, + nodes: summary.node_count, + edges: summary.edge_count, + evidence: summary.evidence_count, + }, + })) + } + "status" => workspace_status_resource(database_path, workspace, item_limit), + "repositories" => { + let registry = store + .load_workspace_registry(workspace) + .map_err(internal_store)?; + let repositories = bounded_collection( + registry.repositories.iter().map(repository_view), + item_limit, + ); + Ok(ResourceDocument::Repositories(RepositoriesResource { + schema_version: 2, + workspace: workspace.to_owned(), + repositories, + })) + } + "services" => node_resource(&store, workspace, item_limit, |kind| { + kind == NodeKind::Service + }) + .map(ResourceDocument::Services), + "contracts" => node_resource(&store, workspace, item_limit, is_contract) + .map(ResourceDocument::Contracts), + "communities" => { + let snapshot = store + .load_current_community_snapshot(workspace) + .map_err(internal_store)?; + let communities = bounded_collection( + snapshot + .communities + .into_iter() + .map(|community| community_view(community, item_limit)), + item_limit, + ); + Ok(ResourceDocument::Communities(CommunitiesResource { + schema_version: 2, + workspace: workspace.to_owned(), + snapshot_id: snapshot.snapshot_id, + engine_version: snapshot.engine_version, + config: community_config_view(snapshot.config, item_limit), + communities, + })) + } + "coverage" => workspace_coverage_resource(&store, workspace, item_limit), + _ => Err(ResourceError { + kind: ResourceErrorKind::Missing, + message: format!("workspace resource `{resource}` was not found"), + }), + } +} + +fn bounded_schema_catalog(item_limit: usize) -> Value { + let mut catalog = schema_catalog(); + let Some(root) = catalog.as_object_mut() else { + return catalog; + }; + let (total, retained_count) = { + let Some(schemas) = root.get_mut("schemas").and_then(Value::as_object_mut) else { + return catalog; + }; + let total = schemas.len(); + let retained = schemas + .iter() + .take(item_limit) + .map(|(name, schema)| (name.clone(), schema.clone())) + .collect::>(); + *schemas = retained; + (total, schemas.len()) + }; + root.insert("schema_total".to_owned(), json!(total)); + root.insert("schema_retained".to_owned(), json!(retained_count)); + root.insert( + "schemas_truncated".to_owned(), + json!(total > retained_count), + ); + catalog +} + +fn workspace_status_resource( + database_path: &Path, + workspace: &str, + item_limit: usize, +) -> Result { + let (status, freshness) = load_status(database_path, workspace).map_err(internal_message)?; + Ok(ResourceDocument::Status(status_resource_value( + status, freshness, item_limit, + ))) +} + +fn status_resource_value( + mut status: GraphStatusReport, + freshness: FreshnessSummary, + item_limit: usize, +) -> StatusResource { + let repositories = bounded_collection(std::mem::take(&mut status.repositories), item_limit); + StatusResource { + schema_version: 2, + status, + repositories, + freshness: bounded_freshness(freshness, item_limit), + } +} + +fn workspace_coverage_resource( + store: &SqliteStore, + workspace: &str, + item_limit: usize, +) -> Result { + let runs = store + .load_current_extractor_runs(workspace) + .map_err(internal_store)?; + let freshness = store + .load_current_freshness(workspace) + .map_err(internal_store)?; + Ok(ResourceDocument::Coverage(coverage_resource_value( + workspace, + runs, + freshness_summary(&freshness), + item_limit, + ))) +} + +fn coverage_resource_value( + workspace: &str, + runs: Vec, + freshness: FreshnessSummary, + item_limit: usize, +) -> CoverageResource { + let runs = bounded_collection(runs, item_limit); + CoverageResource { + schema_version: 2, + workspace: workspace.to_owned(), + runs, + freshness: bounded_freshness(freshness, item_limit), + } +} + +fn read_evidence_resource( + database_path: &Path, + workspace: &str, + evidence_id: &str, +) -> Result { + if evidence_id.is_empty() || evidence_id == "{id}" { + return Err(ResourceError { + kind: ResourceErrorKind::Invalid, + message: "a concrete evidence identifier is required".to_owned(), + }); + } + let store = SqliteStore::open_read_only(database_path).map_err(internal_store)?; + let evidence = store + .load_current_evidence(workspace) + .map_err(|error| match error { + StoreError::CurrentSnapshotMissing(_) => ResourceError { + kind: ResourceErrorKind::Missing, + message: format!("workspace `{workspace}` has no evidence snapshot"), + }, + other => internal_store(other), + })? + .into_iter() + .find(|item| item.id.as_str() == evidence_id) + .ok_or_else(|| ResourceError { + kind: ResourceErrorKind::Missing, + message: format!("evidence `{evidence_id}` was not found in workspace `{workspace}`"), + })?; + Ok(ResourceDocument::Evidence(EvidenceResource { + schema_version: 2, + workspace: workspace.to_owned(), + evidence, + })) +} + +fn node_resource( + store: &SqliteStore, + workspace: &str, + item_limit: usize, + predicate: impl Fn(NodeKind) -> bool, +) -> Result { + let (nodes, _) = store + .load_current_graph(workspace) + .map_err(internal_store)?; + let entities = bounded_collection( + nodes.into_iter().filter(|node| predicate(node.kind)), + item_limit, + ); + Ok(EntitiesResource { + schema_version: 2, + workspace: workspace.to_owned(), + entities, + }) +} +fn bounded_freshness(freshness: FreshnessSummary, item_limit: usize) -> FreshnessResource { + FreshnessResource { + overall: freshness.overall, + stale_repositories: bounded_collection(freshness.stale_repositories, item_limit), + reasons: bounded_collection(freshness.reasons, item_limit), + } +} + +fn community_config_view(config: CommunityConfig, item_limit: usize) -> CommunityConfigView { + CommunityConfigView { + algorithm: config.algorithm, + scope: config.scope, + seed: config.seed, + resolution: config.resolution, + minimum_confidence: config.minimum_confidence, + edge_weights: bounded_collection(config.edge_weights, item_limit), + max_iterations: config.max_iterations, + } +} + +fn community_view(community: Community, item_limit: usize) -> CommunityView { + CommunityView { + id: community.id, + label: community.label, + members: bounded_collection(community.members, item_limit), + central_nodes: bounded_collection(community.central_nodes, item_limit), + repositories: bounded_collection(community.repositories, item_limit), + services: bounded_collection(community.services, item_limit), + inbound_contracts: bounded_collection(community.inbound_contracts, item_limit), + outbound_contracts: bounded_collection(community.outbound_contracts, item_limit), + metrics: community.metrics, + label_evidence: bounded_collection(community.label_evidence, item_limit), + limitations: bounded_collection(community.limitations, item_limit), + } +} + +fn is_contract(kind: NodeKind) -> bool { + matches!( + kind, + NodeKind::HttpOperation + | NodeKind::GraphqlOperation + | NodeKind::RpcMethod + | NodeKind::EventChannel + | NodeKind::EventSchema + | NodeKind::DatabaseTable + | NodeKind::DatabaseColumn + | NodeKind::ConfigKey + ) +} + +fn repository_view(repository: &RepositoryRecord) -> RepositoryView { + RepositoryView { + id: repository.id.as_str().to_owned(), + checkout_id: repository.checkout_id.as_str().to_owned(), + alias: repository.alias.clone(), + normalized_remote: repository.normalized_remote.clone(), + head_commit: repository.head_commit.clone(), + linked_worktree: repository.is_linked_worktree, + working_tree_dirty: repository.working_tree_dirty, + } +} +fn invalid_resource(uri: &str) -> ResourceError { + ResourceError { + kind: ResourceErrorKind::Missing, + message: format!("resource `{uri}` was not found"), + } +} + +fn internal_store(error: impl std::fmt::Display) -> ResourceError { + internal_message(error.to_string()) +} + +fn internal_message(message: String) -> ResourceError { + ResourceError { + kind: ResourceErrorKind::Internal, + message, + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/code-system-graph-cli/src/mcp_support/resources/tests.rs b/crates/code-system-graph-cli/src/mcp_support/resources/tests.rs new file mode 100644 index 0000000..a1effc3 --- /dev/null +++ b/crates/code-system-graph-cli/src/mcp_support/resources/tests.rs @@ -0,0 +1,333 @@ +use super::*; +fn repository_freshness(name: &str) -> RepoFreshness { + RepoFreshness { + repo_id: code_system_graph_model::RepoId::new(format!("repo:{name}")), + checkout_id: code_system_graph_model::CheckoutId::new(format!("checkout:{name}")), + head_commit: Some(format!("commit-{name}")), + manifest_hash: "manifest".to_owned(), + state: RepoFreshnessState::WorkingTreeChanged, + reason: Some(format!("{name} is stale")), + } +} + +fn extractor_run(name: &str) -> ExtractorRun { + ExtractorRun { + id: format!("run:{name}"), + snapshot_id: "snapshot:one".to_owned(), + repo_id: code_system_graph_model::RepoId::new(format!("repo:{name}")), + checkout_id: code_system_graph_model::CheckoutId::new(format!("checkout:{name}")), + extractor: name.to_owned(), + extractor_version: "1".to_owned(), + status: code_system_graph_model::ExtractorRunStatus::Success, + discovered_files: 2, + parsed_files: 2, + skipped_files: 0, + elapsed_ms: 1, + } +} + +fn empty_freshness_resource() -> FreshnessResource { + bounded_freshness( + FreshnessSummary { + overall: OverallFreshness::Fresh, + stale_repositories: Vec::new(), + reasons: Vec::new(), + }, + 1, + ) +} + +fn entities_resource() -> EntitiesResource { + EntitiesResource { + schema_version: 2, + workspace: "workspace".to_owned(), + entities: bounded_collection(Vec::new(), 1), + } +} + +fn workspace_resource_fixtures() -> Vec<(&'static str, ResourceDocument)> { + vec![ + ( + "workspaces", + ResourceDocument::Workspaces(WorkspacesResource { + schema_version: 2, + workspaces: bounded_collection( + [WorkspaceResourceItem { + name: "workspace".to_owned(), + configured: true, + }], + 1, + ), + }), + ), + ( + "snapshot", + ResourceDocument::Overview(OverviewResource { + schema_version: 2, + workspace: "workspace".to_owned(), + snapshot: OverviewSnapshot { + id: "snapshot:one".to_owned(), + nodes: 1, + edges: 2, + evidence: 3, + }, + }), + ), + ( + "Integrity ok", + ResourceDocument::Status(StatusResource { + schema_version: 2, + status: GraphStatusReport { + workspace: "workspace".to_owned(), + schema_version: 2, + integrity_ok: true, + snapshot: SnapshotMetrics { + snapshot_id: "snapshot:one".to_owned(), + node_count: 1, + edge_count: 2, + evidence_count: 3, + }, + repositories: Vec::new(), + }, + repositories: bounded_collection(Vec::new(), 1), + freshness: empty_freshness_resource(), + }), + ), + ( + "repositories", + ResourceDocument::Repositories(RepositoriesResource { + schema_version: 2, + workspace: "workspace".to_owned(), + repositories: bounded_collection(Vec::new(), 1), + }), + ), + ] +} + +fn graph_resource_fixtures() -> Vec<(&'static str, ResourceDocument)> { + vec![ + ("Entities", ResourceDocument::Services(entities_resource())), + ("Entities", ResourceDocument::Contracts(entities_resource())), + ( + "Communities", + ResourceDocument::Communities(CommunitiesResource { + schema_version: 2, + workspace: "workspace".to_owned(), + snapshot_id: "snapshot:one".to_owned(), + engine_version: "1".to_owned(), + config: community_config_view( + CommunityConfig { + algorithm: code_system_graph_model::CommunityAlgorithm::Louvain, + scope: code_system_graph_model::CommunityScope::Federated, + seed: 0, + resolution: 1.0, + minimum_confidence: 0.5, + edge_weights: Vec::new(), + max_iterations: 100, + }, + 1, + ), + communities: bounded_collection(Vec::new(), 1), + }), + ), + ( + "Extractor runs", + ResourceDocument::Coverage(CoverageResource { + schema_version: 2, + workspace: "workspace".to_owned(), + runs: bounded_collection(Vec::new(), 1), + freshness: empty_freshness_resource(), + }), + ), + ( + "ev:one", + ResourceDocument::Evidence(EvidenceResource { + schema_version: 2, + workspace: "workspace".to_owned(), + evidence: Evidence { + id: code_system_graph_model::EvidenceId::new("ev:one"), + repo_id: None, + file_path: Some("src/lib.rs".to_owned()), + start_line: Some(1), + end_line: Some(1), + extractor: "fixture".to_owned(), + extractor_version: "1".to_owned(), + provenance: code_system_graph_model::Provenance::Extracted, + confidence: 1.0, + observed_at_commit: None, + content_hash: None, + note: None, + }, + }), + ), + ( + "```json", + ResourceDocument::SchemaCatalog(serde_json::json!({ + "query": {"type": "object"} + })), + ), + ] +} + +#[test] +fn every_typed_resource_variant_should_render_its_own_markdown_fixture() { + let goldens = [ + "mcp-resource-golden:4a0739c1a776d2951cc3f3da46f8818ffd62562b4c023a49103abc31f09ee5a5", + "mcp-resource-golden:5bf552e3d0bdda5e42c1cf1ee9ae5de13e263870165cc5b1ed9f5850b801ada2", + "mcp-resource-golden:0bcdb56b1f90dc69cdb60ebd3816e410baa7e44e855fd97b668f2404fe6aa3bb", + "mcp-resource-golden:c21d183ace7081c78f80ecb6f3ae117c7fdde9219436dbc25cabe99fe794941a", + "mcp-resource-golden:1fc4418c3ef31389ac48f43c3785de986be8621a1e06bac9361f7cec220b2c66", + "mcp-resource-golden:cd051baf008bc31e8c87d86bf6a95d71ffb931eaddc9a4b7a90cc3a5d71caf0d", + "mcp-resource-golden:d5f3b700d3f70bb59c0a03407f752f754a5233ce8751b56cefffafb9f023145b", + "mcp-resource-golden:0489a80e514c31d3f392dd464843579563baa7f8e3971e92d7a1fc466e457c68", + "mcp-resource-golden:e4a27ab1170b53c17cb04f9ba84dee61806da6b493fa258444221da65897fd22", + "mcp-resource-golden:4869cf77e9b9e91b47a11f153130722dd46277844b4302f6265dad76f9e30cce", + ]; + let resources = workspace_resource_fixtures() + .into_iter() + .chain(graph_resource_fixtures()); + + for (index, (expected, resource)) in resources.enumerate() { + let rendered = resource.render(4_096); + assert_eq!( + code_system_graph_model::stable_id("mcp-resource-golden", &rendered), + goldens[index], + "resource fixture {index} changed its full Markdown" + ); + assert!(rendered.contains(expected), "{expected}: {rendered}"); + } +} +#[test] +fn resource_freshness_collections_should_apply_item_limits() { + let value = bounded_freshness( + FreshnessSummary { + overall: OverallFreshness::Partial, + stale_repositories: vec![ + code_system_graph_model::RepoId::new("repo:one"), + code_system_graph_model::RepoId::new("repo:two"), + ], + reasons: vec!["one".to_owned(), "two".to_owned()], + }, + 1, + ); + + assert_eq!(value.stale_repositories.total, 2); + assert_eq!(value.stale_repositories.retained(), 1); + assert!(value.stale_repositories.truncated()); + assert_eq!(value.reasons.total, 2); + assert_eq!(value.reasons.retained(), 1); + assert!(value.reasons.truncated()); +} + +#[test] +fn status_resource_should_limit_repositories_with_exact_metadata() { + let repositories = vec![repository_freshness("one"), repository_freshness("two")]; + let value = status_resource_value( + GraphStatusReport { + workspace: "workspace".to_owned(), + schema_version: 2, + integrity_ok: true, + snapshot: SnapshotMetrics { + snapshot_id: "snapshot:one".to_owned(), + node_count: 0, + edge_count: 0, + evidence_count: 0, + }, + repositories: repositories.clone(), + }, + freshness_summary(&repositories), + 1, + ); + + assert_eq!(value.repositories.total, 2); + assert_eq!(value.repositories.retained(), 1); + assert!(value.repositories.truncated()); + assert_eq!( + value.status.repositories, + [] as [code_system_graph_model::RepoFreshness; 0] + ); +} + +#[test] +fn coverage_resource_should_limit_runs_with_exact_metadata() { + let value = coverage_resource_value( + "workspace", + vec![extractor_run("one"), extractor_run("two")], + FreshnessSummary { + overall: OverallFreshness::Fresh, + stale_repositories: Vec::new(), + reasons: Vec::new(), + }, + 1, + ); + + assert_eq!(value.runs.total, 2); + assert_eq!(value.runs.retained(), 1); + assert!(value.runs.truncated()); + assert_eq!(value.runs.items.len(), 1); +} + +#[test] +fn community_view_should_limit_every_nested_collection() { + let community = Community { + id: CommunityId::new("community:one"), + label: "one".to_owned(), + members: vec![NodeId::new("node:one"), NodeId::new("node:two")], + central_nodes: vec![NodeId::new("node:one"), NodeId::new("node:two")], + repositories: vec![RepoId::new("repo:one"), RepoId::new("repo:two")], + services: vec![NodeId::new("service:one"), NodeId::new("service:two")], + inbound_contracts: vec![NodeId::new("in:one"), NodeId::new("in:two")], + outbound_contracts: vec![NodeId::new("out:one"), NodeId::new("out:two")], + metrics: CommunityMetrics { + size: 2, + density: 1.0, + cohesion: 1.0, + coupling: 0.0, + cross_community_edges: 0, + }, + label_evidence: vec![ + CommunityLabelEvidence { + node_id: NodeId::new("node:one"), + term: "one".to_owned(), + }, + CommunityLabelEvidence { + node_id: NodeId::new("node:two"), + term: "two".to_owned(), + }, + ], + limitations: vec!["one".to_owned(), "two".to_owned()], + }; + let view = community_view(community, 1); + + for collection in [ + &view.members, + &view.central_nodes, + &view.services, + &view.inbound_contracts, + &view.outbound_contracts, + ] { + assert_eq!(collection.total, 2); + assert_eq!(collection.retained(), 1); + assert!(collection.truncated()); + } + assert_eq!(view.repositories.retained(), 1); + assert_eq!(view.label_evidence.retained(), 1); + assert_eq!(view.limitations.retained(), 1); +} + +#[test] +fn schema_catalog_should_apply_resource_item_limit() { + let catalog = bounded_schema_catalog(1); + + assert_eq!(catalog["schema_retained"], 1); + assert_eq!( + catalog["schemas"].as_object().map(serde_json::Map::len), + Some(1) + ); + assert_eq!(catalog["schemas_truncated"], true); + assert!( + catalog["schema_total"] + .as_u64() + .is_some_and(|total| total > 1) + ); +} diff --git a/crates/code-system-graph-cli/src/sync.rs b/crates/code-system-graph-cli/src/sync.rs index 31c1403..5666e53 100644 --- a/crates/code-system-graph-cli/src/sync.rs +++ b/crates/code-system-graph-cli/src/sync.rs @@ -326,7 +326,7 @@ pub(crate) fn sync_workspace_direct( codegraph.changed_count == 0, )?; Ok(SyncSummary { - schema_version: 1, + schema_version: 2, execution: code_system_graph_core::ExecutionSummary::default(), scan, codegraph, diff --git a/crates/code-system-graph-cli/src/sync_watch.rs b/crates/code-system-graph-cli/src/sync_watch.rs index 581686c..b4e7f2c 100644 --- a/crates/code-system-graph-cli/src/sync_watch.rs +++ b/crates/code-system-graph-cli/src/sync_watch.rs @@ -261,12 +261,12 @@ fn spawn_watch_event_reader( return; }; match message { - WatchEventMessage::Ready { schema_version: 1 } => { + WatchEventMessage::Ready { schema_version: 2 } => { current.initial_ready = true; current.refresh_started = None; } WatchEventMessage::Dirty { - schema_version: 1, + schema_version: 2, refresh_scope, } => { current.refresh_started.get_or_insert_with(Instant::now); @@ -634,10 +634,10 @@ pub(crate) async fn run_watch_event_worker( requested_poll, &policy, )?; - emit_watch_event(&WatchEventMessage::Ready { schema_version: 1 })?; + emit_watch_event(&WatchEventMessage::Ready { schema_version: 2 })?; while let Some(signal) = receiver.recv().await { emit_watch_event(&WatchEventMessage::Dirty { - schema_version: 1, + schema_version: 2, refresh_scope: scope_refresh_required.swap(false, Ordering::AcqRel), })?; let refresh = refresh_required.swap(false, Ordering::AcqRel) @@ -658,7 +658,7 @@ pub(crate) async fn run_watch_event_worker( } } } - emit_watch_event(&WatchEventMessage::Ready { schema_version: 1 })?; + emit_watch_event(&WatchEventMessage::Ready { schema_version: 2 })?; } anyhow::bail!("filesystem event channel stopped") } @@ -947,7 +947,7 @@ fn emit_sync( println!( "{}", serde_json::to_string(&WatchOutput::SyncResult { - schema_version: 1, + schema_version: 2, summary: &summary, })? ); @@ -1130,7 +1130,7 @@ fn finish_and_emit( println!( "{}", serde_json::to_string(&WatchOutput::Termination { - schema_version: 1, + schema_version: 2, state, detail: &bounded_detail, })? @@ -1482,8 +1482,8 @@ mod tests { failed: false, })); let input = concat!( - "{\"type\":\"dirty\",\"schema_version\":1}\n", - "{\"type\":\"ready\",\"schema_version\":1}\n" + "{\"type\":\"dirty\",\"schema_version\":2}\n", + "{\"type\":\"ready\",\"schema_version\":2}\n" ); spawn_watch_event_reader( std::io::Cursor::new(input.as_bytes().to_vec()), @@ -1519,8 +1519,8 @@ mod tests { failed: false, })); let input = concat!( - "{\"type\":\"dirty\",\"schema_version\":1}\n", - "{\"type\":\"dirty\",\"schema_version\":1}\n" + "{\"type\":\"dirty\",\"schema_version\":2}\n", + "{\"type\":\"dirty\",\"schema_version\":2}\n" ); spawn_watch_event_reader( std::io::Cursor::new(input.as_bytes().to_vec()), diff --git a/crates/code-system-graph-cli/src/work_state.rs b/crates/code-system-graph-cli/src/work_state.rs index dcdf948..8af553d 100644 --- a/crates/code-system-graph-cli/src/work_state.rs +++ b/crates/code-system-graph-cli/src/work_state.rs @@ -14,7 +14,7 @@ use rusqlite::{Connection, ErrorCode, OpenFlags, OptionalExtension, TransactionB use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; -const WORK_SCHEMA_VERSION: &str = "1.0.0"; +const WORK_SCHEMA_VERSION: &str = "1.1.0"; const SQLITE_ARTIFACT_SUFFIXES: [&str; 4] = ["", "-wal", "-shm", "-journal"]; enum WorkOpenError { diff --git a/crates/code-system-graph-cli/src/worker.rs b/crates/code-system-graph-cli/src/worker.rs index bfc7c6b..e0f8a14 100644 --- a/crates/code-system-graph-cli/src/worker.rs +++ b/crates/code-system-graph-cli/src/worker.rs @@ -20,7 +20,7 @@ use super::{ }; use crate::sync::{SyncSummary, sync_workspace_direct}; -const PROTOCOL_VERSION: u8 = 1; +const PROTOCOL_VERSION: u8 = 2; const MAX_REQUEST_BYTES: u64 = 1_048_576; const MAX_PROTOCOL_LINE_BYTES: usize = 1_048_576; const PROTOCOL_QUEUE_CAPACITY: usize = 256; @@ -1558,7 +1558,7 @@ mod tests { }) .expect("policy protocol"); let script = format!( - "printf '%s\\n' '{policy_message}'; while true; do printf '%s\\n' '{{\"type\":\"progress\",\"schema_version\":1,\"phase\":\"discovery\",\"completed_units\":1}}'; sleep 0.02; done" + "printf '%s\\n' '{policy_message}'; while true; do printf '%s\\n' '{{\"type\":\"progress\",\"schema_version\":2,\"phase\":\"discovery\",\"completed_units\":1}}'; sleep 0.02; done" ); let error = supervise_script(&script, &policy); assert!( diff --git a/crates/code-system-graph-cli/tests/agent_plugin_e2e.rs b/crates/code-system-graph-cli/tests/agent_plugin_e2e.rs index bf0b8d3..f005e5d 100644 --- a/crates/code-system-graph-cli/tests/agent_plugin_e2e.rs +++ b/crates/code-system-graph-cli/tests/agent_plugin_e2e.rs @@ -279,7 +279,7 @@ fn plugin_create_should_render_official_structure_and_be_idempotent() -> anyhow: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json" ); assert_eq!(plugin["name"], first.plugin_name); - assert_eq!(plugin["version"], "1.0.3"); + assert_eq!(plugin["version"], "1.1.0"); let mcp: serde_json::Value = serde_json::from_slice(&std::fs::read(output.join("mcp.json"))?)?; let mcp_schema: serde_json::Value = serde_json::from_str(MCP_SCHEMA)?; assert!(jsonschema::validator_for(&mcp_schema)?.is_valid(&mcp)); @@ -837,7 +837,7 @@ fn plugin_uninstall_should_ignore_a_codex_manifest_added_after_installation() -> } #[test] -fn plugin_uninstall_should_accept_a_legacy_integration_receipt() -> anyhow::Result<()> { +fn plugin_uninstall_should_reject_an_incomplete_integration_receipt() -> anyhow::Result<()> { let temporary = tempfile::tempdir()?; let workspace = temporary.path().join("workspace"); std::fs::create_dir(&workspace)?; @@ -859,11 +859,7 @@ fn plugin_uninstall_should_accept_a_legacy_integration_receipt() -> anyhow::Resu std::fs::write(&receipt_path, serde_json::to_vec_pretty(&receipt)?)?; let removed = uninstall_existing_with_cli(&base)?; - assert!( - removed.status.success(), - "{}", - String::from_utf8_lossy(&removed.stderr) - ); + assert!(!removed.status.success()); let portable: serde_json::Value = serde_json::from_slice(&std::fs::read(base.join("mcp.json"))?)?; let codex: serde_json::Value = @@ -871,22 +867,22 @@ fn plugin_uninstall_should_accept_a_legacy_integration_receipt() -> anyhow::Resu assert!( portable["mcpServers"] .get("hugint-code-system-graph") - .is_none() + .is_some() ); assert!( codex["mcpServers"] .get("hugint-code-system-graph") - .is_none() + .is_some() ); assert!(portable["mcpServers"].get("codegraph").is_some()); assert!(codex["mcpServers"].get("codegraph").is_some()); - assert!(!base.join("skills/hugint-system-graph").exists()); - assert!(!base.join(".local/code-system-graph").exists()); + assert!(base.join("skills/hugint-system-graph").exists()); + assert!(base.join(".local/code-system-graph").exists()); Ok(()) } #[test] -fn plugin_uninstall_should_reject_a_modified_legacy_binding() -> anyhow::Result<()> { +fn plugin_uninstall_should_reject_a_v1_integration_receipt() -> anyhow::Result<()> { let temporary = tempfile::tempdir()?; let workspace = temporary.path().join("workspace"); std::fs::create_dir(&workspace)?; @@ -903,8 +899,7 @@ fn plugin_uninstall_should_reject_a_modified_legacy_binding() -> anyhow::Result< let receipt = receipt .as_object_mut() .ok_or_else(|| anyhow::anyhow!("integration receipt must be an object"))?; - receipt.remove("managedDocuments"); - receipt.remove("managedLocalFiles"); + receipt.insert("schemaVersion".to_owned(), serde_json::json!(1)); std::fs::write(&receipt_path, serde_json::to_vec_pretty(&receipt)?)?; let binding_path = base.join(".local/code-system-graph/mcp-binding.json"); diff --git a/crates/code-system-graph-cli/tests/config_show_e2e.rs b/crates/code-system-graph-cli/tests/config_show_e2e.rs index 150ef66..c300c7d 100644 --- a/crates/code-system-graph-cli/tests/config_show_e2e.rs +++ b/crates/code-system-graph-cli/tests/config_show_e2e.rs @@ -33,13 +33,13 @@ fn config_show_should_report_defaults_and_selected_repository_rules() -> anyhow: "stderr: {}", String::from_utf8_lossy(&output.stderr) ); - assert_eq!(report.schema_version, 1); + assert_eq!(report.schema_version, 2); assert_eq!(report.workspace, "configuration"); assert_eq!(report.extraction_budgets, ExtractionBudgets::default()); - assert_eq!(report.execution_policy.base, ExecutionPolicy::default()); + assert_eq!(report.execution_policy, ExecutionPolicy::default()); assert_eq!( - report.execution_policy_fingerprint, - report.execution_policy.fingerprint() + report.scan_fingerprint, + report.execution_policy.scan_fingerprint() ); assert_eq!(report.repositories.len(), 1); assert_eq!(report.repositories[0].alias, "api"); @@ -111,8 +111,8 @@ fn config_show_should_report_effective_execution_policy() -> anyhow::Result<()> ExecutionPolicy::default().max_worker_memory_bytes ); assert_eq!( - report.execution_policy_fingerprint, - report.execution_policy.fingerprint() + report.scan_fingerprint, + report.execution_policy.scan_fingerprint() ); Ok(()) } diff --git a/crates/code-system-graph-cli/tests/explore_e2e.rs b/crates/code-system-graph-cli/tests/explore_e2e.rs index 748a240..227317f 100644 --- a/crates/code-system-graph-cli/tests/explore_e2e.rs +++ b/crates/code-system-graph-cli/tests/explore_e2e.rs @@ -6,10 +6,18 @@ use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use code_system_graph::{ExploreInput, explore_repository, scan_workspace}; +use code_system_graph_core::ExecutionPolicy; use code_system_graph_model::ToolStatus; fn fake_codegraph(directory: &Path) -> Result> { - let binary = directory.join("codegraph-ok"); + fake_codegraph_mode(directory, "ok") +} + +fn fake_codegraph_mode( + directory: &Path, + mode: &str, +) -> Result> { + let binary = directory.join(format!("codegraph-{mode}")); std::fs::copy( PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../fixtures/codegraph/fake/codegraph.py"), @@ -50,16 +58,18 @@ async fn explore_should_default_to_the_only_registered_repository() workspace: "explore-test".to_owned(), repository: None, query: "create_order callers".to_owned(), - max_files: 4, + max_files: Some(4), }, Some(binary.into_os_string()), + &ExecutionPolicy::default(), ) .await; - assert_eq!( - envelope.data.map(|result| result.content), - Some("ephemeral local context".to_owned()) - ); + let report = envelope.data.ok_or("explore report")?; + assert_eq!(report.source_markdown, "ephemeral local context"); + assert_eq!(report.resolved_symbols.len(), 1); + assert_eq!(report.local_relationships.len(), 2); + assert_eq!(report.execution.provider_operations, 4); Ok(()) } @@ -78,9 +88,10 @@ async fn explore_should_require_an_alias_for_multi_repository_workspaces() workspace: "commerce-platform".to_owned(), repository: None, query: "create order".to_owned(), - max_files: 4, + max_files: Some(4), }, None, + &ExecutionPolicy::default(), ) .await; @@ -110,12 +121,55 @@ async fn explore_should_select_an_explicit_alias_in_multi_repository_workspaces( workspace: "commerce-platform".to_owned(), repository: Some("api".to_owned()), query: "create order".to_owned(), - max_files: 4, + max_files: Some(4), }, Some(binary.into_os_string()), + &ExecutionPolicy::default(), ) .await; assert!(envelope.data.is_some()); Ok(()) } + +#[tokio::test] +async fn explore_provider_timeout_should_preserve_partial_context_and_return_bounded() +-> Result<(), Box> { + let temporary = tempfile::tempdir()?; + let (_manifest, database) = mono_repository_fixture(temporary.path())?; + let binary = fake_codegraph_mode(temporary.path(), "slow-cli")?; + let policy = ExecutionPolicy { + // Leave enough time for the source-context stage on slower CI hosts while the fake + // symbol query still deterministically exceeds the request-wide deadline. + max_explore_wall_time_ms: 500, + ..ExecutionPolicy::default() + }; + let started = tokio::time::Instant::now(); + let envelope = explore_repository( + &database, + "explore-test", + &ExploreInput { + workspace: "explore-test".to_owned(), + repository: None, + query: "create_order callers".to_owned(), + max_files: Some(4), + }, + Some(binary.into_os_string()), + &policy, + ) + .await; + + assert!(started.elapsed() < std::time::Duration::from_secs(2)); + assert_eq!(envelope.status, ToolStatus::Degraded); + let report = envelope.data.ok_or("partial explore report")?; + assert_eq!(report.source_markdown, "ephemeral local context"); + assert!(!report.coverage.symbol_resolution); + assert!( + report + .coverage + .gaps + .iter() + .any(|gap| gap.contains("symbol resolution")) + ); + Ok(()) +} diff --git a/crates/code-system-graph-cli/tests/http_server_e2e.rs b/crates/code-system-graph-cli/tests/http_server_e2e.rs index 8c70aaf..4eb1bc5 100644 --- a/crates/code-system-graph-cli/tests/http_server_e2e.rs +++ b/crates/code-system-graph-cli/tests/http_server_e2e.rs @@ -117,6 +117,27 @@ impl RunningServer { }) } + async fn start_for_query(codegraph_enabled: bool) -> anyhow::Result { + let fixture = Fixture::create()?; + let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?; + let address = listener.local_addr()?; + let config = fixture + .server_config() + .with_bind(address) + .with_codegraph(codegraph_enabled, None); + let cancellation = CancellationToken::new(); + let task_cancellation = cancellation.clone(); + let task = tokio::spawn(async move { + serve_http_on_listener(listener, config, task_cancellation).await + }); + Ok(Self { + address, + cancellation, + task, + fixture, + }) + } + #[cfg(unix)] async fn start_with_codegraph() -> anyhow::Result { let fixture = Fixture::create()?; @@ -233,6 +254,11 @@ async fn loopback_should_serve_anonymous_health_and_status() -> anyhow::Result<( .get("x-code-system-graph-version") .and_then(|value| value.to_str().ok()) .map(str::to_owned); + let schema_header = health + .headers() + .get("x-code-system-graph-schema-version") + .and_then(|value| value.to_str().ok()) + .map(str::to_owned); let content_type_policy = health .headers() .get("x-content-type-options") @@ -249,6 +275,7 @@ async fn loopback_should_serve_anonymous_health_and_status() -> anyhow::Result<( health_body["status"].as_str(), version_header.as_deref(), content_type_policy.as_deref(), + schema_header.as_deref(), cors_header_present, status_status, status_body["data"]["workspace"].as_str(), @@ -258,6 +285,7 @@ async fn loopback_should_serve_anonymous_health_and_status() -> anyhow::Result<( Some("ok"), Some(env!("CARGO_PKG_VERSION")), Some("nosniff"), + Some("2"), false, StatusCode::OK, Some(server.fixture.workspace_name.as_str()), @@ -340,6 +368,94 @@ async fn configured_auth_should_accept_valid_bearer_token() -> anyhow::Result<() server.stop().await } +#[tokio::test] +async fn query_should_only_advertise_http_available_actions() -> anyhow::Result<()> { + let disabled = RunningServer::start_for_query(false).await?; + let client = Client::new(); + let hit = client + .post(disabled.url("/v1/tools/query")) + .json(&json!({ + "query": "orders", + "node_kinds": [], + "repo_ids": [], + "service_ids": [], + "community_ids": [], + "offset": 0, + "limit": 5 + })) + .send() + .await?; + let (hit_status, hit_body) = response_json(hit).await?; + let hit_actions = hit_body["data"]["next_actions"] + .as_array() + .context("query hit actions")?; + assert_eq!(hit_status, StatusCode::OK); + assert!( + !hit_body["data"]["hits"] + .as_array() + .is_none_or(Vec::is_empty) + ); + assert!( + hit_actions + .iter() + .all(|action| action["tool"] != "source_context") + ); + + let missing = client + .post(disabled.url("/v1/tools/query")) + .json(&json!({ + "query": "api source_literal_that_does_not_exist", + "node_kinds": [], + "repo_ids": [], + "service_ids": [], + "community_ids": [], + "offset": 0, + "limit": 5 + })) + .send() + .await?; + let (_, missing_body) = response_json(missing).await?; + assert!( + missing_body["data"]["next_actions"] + .as_array() + .context("disabled query actions")? + .iter() + .all(|action| action["tool"] != "explore") + ); + assert!( + missing_body["data"]["coverage"]["gaps"] + .as_array() + .context("disabled query gaps")? + .iter() + .all(|gap| !gap.as_str().is_some_and(|gap| gap.contains("use Explore"))) + ); + disabled.stop().await?; + + let enabled = RunningServer::start_for_query(true).await?; + let response = client + .post(enabled.url("/v1/tools/query")) + .json(&json!({ + "query": "api source_literal_that_does_not_exist", + "node_kinds": [], + "repo_ids": [], + "service_ids": [], + "community_ids": [], + "offset": 0, + "limit": 5 + })) + .send() + .await?; + let (_, body) = response_json(response).await?; + assert!( + body["data"]["next_actions"] + .as_array() + .context("enabled query actions")? + .iter() + .any(|action| action["tool"] == "explore") + ); + enabled.stop().await +} + #[cfg(unix)] #[tokio::test] async fn explore_route_should_return_ephemeral_local_context() -> anyhow::Result<()> { @@ -359,10 +475,10 @@ async fn explore_route_should_return_ephemeral_local_context() -> anyhow::Result assert_eq!( ( status, - body["data"]["content"].as_str(), + body["data"]["source_markdown"].as_str(), body["schema_version"].as_u64() ), - (StatusCode::OK, Some("ephemeral local context"), Some(1)) + (StatusCode::OK, Some("ephemeral local context"), Some(2)) ); let target = SqliteStore::open_read_only(&server.fixture.database)? .load_current_graph(&server.fixture.workspace_name)? diff --git a/crates/code-system-graph-cli/tests/mcp_protocol_e2e.rs b/crates/code-system-graph-cli/tests/mcp_protocol_e2e.rs index 75dd73a..4809842 100644 --- a/crates/code-system-graph-cli/tests/mcp_protocol_e2e.rs +++ b/crates/code-system-graph-cli/tests/mcp_protocol_e2e.rs @@ -8,9 +8,16 @@ use code_system_graph::scan_workspace; use code_system_graph_store_sqlite::SqliteStore; use rmcp::ServiceExt; use rmcp::model::{ - CallToolRequestParams, ClientCapabilities, ClientInfo, Implementation, ReadResourceRequestParams, ResourceContents + CallToolRequestParams, CallToolResult, ClientCapabilities, ClientInfo, ContentBlock, Implementation, ReadResourceRequestParams, ResourceContents }; +fn tool_text(result: &CallToolResult) -> &str { + match result.content.first() { + Some(ContentBlock::Text(content)) => &content.text, + _ => panic!("tool result did not contain one text block"), + } +} + #[cfg(unix)] fn fake_codegraph(directory: &std::path::Path) -> anyhow::Result { let binary = directory.join("codegraph-ok"); @@ -35,6 +42,8 @@ async fn stdio_should_initialize_without_noise_and_hide_admin_tools() -> anyhow: let mut child = tokio::process::Command::new(env!("CARGO_BIN_EXE_csgraph")) .args([ "mcp", + "--config", + fixture.to_string_lossy().as_ref(), "--database", database.to_string_lossy().as_ref(), "--workspace", @@ -73,15 +82,32 @@ async fn stdio_should_initialize_without_noise_and_hide_admin_tools() -> anyhow: let resources = service.list_all_resources().await?; let schema_uri = "code-system-graph://workspace/commerce-platform/schema"; assert!(resources.iter().any(|resource| resource.uri == schema_uri)); + assert!( + !resources + .iter() + .any(|resource| resource.uri.contains("{id}")) + ); + let templates = service.list_all_resource_templates().await?; + let evidence_template = templates + .iter() + .find(|template| template.name == "evidence-metadata") + .ok_or_else(|| anyhow::anyhow!("evidence resource template"))?; + assert_eq!( + evidence_template.uri_template, + "code-system-graph://evidence/{id}" + ); + assert_eq!( + evidence_template.mime_type.as_deref(), + Some("text/markdown") + ); let schema = service .read_resource(ReadResourceRequestParams::new(schema_uri)) .await?; let ResourceContents::TextResourceContents { text, .. } = &schema.contents[0] else { panic!("schema resource was not textual"); }; - let schema: serde_json::Value = serde_json::from_str(text)?; - assert!(schema["schemas"].is_object()); - assert!(schema["application_interfaces"]["schemas"].is_array()); + assert!(text.starts_with("# Schema Catalog")); + assert!(text.contains("```json")); let contracts = service @@ -94,7 +120,9 @@ async fn stdio_should_initialize_without_noise_and_hide_admin_tools() -> anyhow: )) .await?; assert_ne!(contracts.is_error, Some(true)); - assert!(contracts.structured_content.is_some()); + assert!(contracts.structured_content.is_none()); + assert_eq!(contracts.content.len(), 1); + assert!(tool_text(&contracts).starts_with("# contracts")); let _ = service.close().await; let status = tokio::time::timeout(std::time::Duration::from_secs(5), child.wait()).await??; @@ -132,6 +160,8 @@ async fn stdio_explore_should_proxy_bounded_ephemeral_codegraph_context() -> any let mut child = tokio::process::Command::new(env!("CARGO_BIN_EXE_csgraph")) .args([ "mcp", + "--config", + manifest.to_string_lossy().as_ref(), "--database", database.to_string_lossy().as_ref(), "--workspace", @@ -164,13 +194,8 @@ async fn stdio_explore_should_proxy_bounded_ephemeral_codegraph_context() -> any )) .await?; - assert_eq!( - explored - .structured_content - .as_ref() - .and_then(|value| value["data"]["content"].as_str()), - Some("ephemeral local context") - ); + assert!(explored.structured_content.is_none()); + assert!(tool_text(&explored).contains("ephemeral local context")); let impact = service .call_tool( CallToolRequestParams::new("impact").with_arguments(serde_json::from_value( @@ -182,12 +207,8 @@ async fn stdio_explore_should_proxy_bounded_ephemeral_codegraph_context() -> any ) .await?; assert_ne!(impact.is_error, Some(true)); - assert!( - impact - .structured_content - .as_ref() - .is_some_and(|value| value["data"]["local_impact_summaries"].is_array()) - ); + assert!(impact.structured_content.is_none()); + assert!(tool_text(&impact).contains("Local impact summaries")); let _ = service.close().await; let status = tokio::time::timeout(std::time::Duration::from_secs(5), child.wait()).await??; assert!(status.success()); @@ -235,6 +256,8 @@ async fn explicit_admin_stdio_should_apply_bounded_audited_mutations() -> anyhow let mut command = tokio::process::Command::new(env!("CARGO_BIN_EXE_csgraph")); command.args([ "mcp", + "--config", + manifest.to_string_lossy().as_ref(), "--database", database.to_string_lossy().as_ref(), "--workspace", @@ -287,13 +310,8 @@ async fn explicit_admin_stdio_should_apply_bounded_audited_mutations() -> anyhow ) .await?; assert_ne!(manual.is_error, Some(true)); - assert_ne!( - manual - .structured_content - .as_ref() - .map(|value| &value["status"]), - Some(&serde_json::Value::String("error".to_owned())) - ); + assert!(manual.structured_content.is_none()); + assert!(!tool_text(&manual).contains("State: `error`")); let update = service .call_tool( @@ -326,12 +344,8 @@ async fn explicit_admin_stdio_should_apply_bounded_audited_mutations() -> anyhow ) .await?; assert_ne!(scan.is_error, Some(true)); - assert_ne!( - scan.structured_content - .as_ref() - .map(|value| &value["status"]), - Some(&serde_json::Value::String("error".to_owned())) - ); + assert!(scan.structured_content.is_none()); + assert!(!tool_text(&scan).contains("State: `error`")); let _ = service.close().await; let status = tokio::time::timeout(std::time::Duration::from_secs(5), child.wait()).await??; diff --git a/crates/code-system-graph-cli/tests/query_communities_e2e.rs b/crates/code-system-graph-cli/tests/query_communities_e2e.rs index b1c9e2f..368ab80 100644 --- a/crates/code-system-graph-cli/tests/query_communities_e2e.rs +++ b/crates/code-system-graph-cli/tests/query_communities_e2e.rs @@ -152,6 +152,8 @@ fn cli_should_expose_query_and_community_json() -> anyhow::Result<()> { let query = std::process::Command::new(env!("CARGO_BIN_EXE_csgraph")) .args(["query", "orders", "--database"]) .arg(&database) + .arg("--config") + .arg(&fixture) .args(["--workspace", "commerce-platform", "--limit", "2"]) .output()?; let communities = std::process::Command::new(env!("CARGO_BIN_EXE_csgraph")) diff --git a/crates/code-system-graph-cli/tests/scan_controls_e2e.rs b/crates/code-system-graph-cli/tests/scan_controls_e2e.rs index fc9e6af..0a2620d 100644 --- a/crates/code-system-graph-cli/tests/scan_controls_e2e.rs +++ b/crates/code-system-graph-cli/tests/scan_controls_e2e.rs @@ -250,8 +250,7 @@ fn duplicate_http_providers_should_degrade_without_aborting_unrelated_links() -> } #[test] -fn execution_policy_change_should_not_invalidate_batches_or_require_full_scan() -> anyhow::Result<()> -{ +fn scan_execution_policy_change_should_publish_a_new_snapshot() -> anyhow::Result<()> { let temporary = tempfile::tempdir()?; std::fs::create_dir(temporary.path().join("api"))?; std::fs::write( @@ -277,8 +276,16 @@ fn execution_policy_change_should_not_invalidate_batches_or_require_full_scan() }, )?; - assert!(changed_policy.reused_snapshot); - assert_eq!(changed_policy.snapshot_id, initial.snapshot_id); + assert!(!changed_policy.reused_snapshot); + assert_ne!(changed_policy.snapshot_id, initial.snapshot_id); + + std::fs::write( + &manifest, + "version: 1\nname: operational-policy\nexecutionPolicy:\n maxScanWallTimeMs: 28800000\n maxNoProgressTimeMs: 600000\n maxMcpToolResponseBytes: 600000\nrepos:\n api:\n path: api\n openapi: openapi.yaml\n", + )?; + let changed_delivery = scan_workspace(&manifest, &database)?; + assert!(changed_delivery.reused_snapshot); + assert_eq!(changed_delivery.snapshot_id, changed_policy.snapshot_id); Ok(()) } diff --git a/crates/code-system-graph-cli/tests/sync_e2e.rs b/crates/code-system-graph-cli/tests/sync_e2e.rs index 710815a..3e3ce04 100644 --- a/crates/code-system-graph-cli/tests/sync_e2e.rs +++ b/crates/code-system-graph-cli/tests/sync_e2e.rs @@ -192,7 +192,7 @@ fn one_shot_sync_should_report_optional_codegraph_and_reuse_snapshot() -> anyhow let (manifest, database) = write_workspace(temporary.path())?; let first = run_sync(&manifest, &database)?; - assert_eq!(first.schema_version, 1); + assert_eq!(first.schema_version, 2); assert!(first.codegraph.enabled); assert_eq!(first.codegraph.repository_count, 1); assert_eq!(first.codegraph.skipped_count, 1); diff --git a/crates/code-system-graph-core/Cargo.toml b/crates/code-system-graph-core/Cargo.toml index e7d86d1..ab38034 100644 --- a/crates/code-system-graph-core/Cargo.toml +++ b/crates/code-system-graph-core/Cargo.toml @@ -23,7 +23,7 @@ libc = "0.2" nix = { version = "0.31.3", features = ["fs"] } proto-parser = "1.14.3" pulldown-cmark = "0.13.4" -code-system-graph-model = { version = "1.0.3", path = "../code-system-graph-model" } +code-system-graph-model = { version = "1.1.0", path = "../code-system-graph-model" } reqwest = { version = "0.13.4", default-features = false, features = ["json", "rustls"] } rmcp = { version = "3.1.0", default-features = false, features = [ "client", diff --git a/crates/code-system-graph-core/src/codegraph/mod.rs b/crates/code-system-graph-core/src/codegraph/mod.rs index b728f15..5556f13 100644 --- a/crates/code-system-graph-core/src/codegraph/mod.rs +++ b/crates/code-system-graph-core/src/codegraph/mod.rs @@ -6,7 +6,7 @@ use std::collections::BTreeMap; use std::ffi::OsString; use std::path::{Component, Path}; use std::sync::Arc; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; use std::time::Duration; use async_trait::async_trait; @@ -55,6 +55,19 @@ pub struct CodeGraphProvider { mcp_circuits: Mutex>, cli_version: Mutex>, closed: AtomicBool, + active_processes: Arc, + maximum_concurrency_observed: Arc, +} + +struct ProviderPermit { + _permit: OwnedSemaphorePermit, + active_processes: Arc, +} + +impl Drop for ProviderPermit { + fn drop(&mut self) { + self.active_processes.fetch_sub(1, Ordering::AcqRel); + } } struct CliProbe { @@ -88,9 +101,17 @@ impl CodeGraphProvider { mcp_circuits: Mutex::new(BTreeMap::new()), cli_version: Mutex::new(None), closed: AtomicBool::new(false), + active_processes: Arc::new(AtomicUsize::new(0)), + maximum_concurrency_observed: Arc::new(AtomicUsize::new(0)), }) } + /// Returns the maximum number of provider operations that simultaneously held process permits. + #[must_use] + pub fn maximum_concurrency_observed(&self) -> usize { + self.maximum_concurrency_observed.load(Ordering::Acquire) + } + /// Reads the structured local-index status without starting MCP or modifying the index. /// /// # Errors @@ -115,7 +136,7 @@ impl CodeGraphProvider { &self, request: &ProviderRequest, deadline: tokio::time::Instant, - ) -> Result { + ) -> Result { validate_request(request)?; if self.closed.load(Ordering::Acquire) { return Err(ProviderError::InvalidRequest( @@ -126,9 +147,17 @@ impl CodeGraphProvider { biased; () = request.cancellation.cancelled() => Err(ProviderError::Cancelled), () = tokio::time::sleep_until(deadline) => Err(timeout_error()), - permit = self.permits.clone().acquire_owned() => permit.map_err(|_| { - ProviderError::InvalidRequest("provider has already been shut down".to_owned()) - }), + permit = self.permits.clone().acquire_owned() => { + let permit = permit.map_err(|_| { + ProviderError::InvalidRequest("provider has already been shut down".to_owned()) + })?; + let active = self.active_processes.fetch_add(1, Ordering::AcqRel) + 1; + self.maximum_concurrency_observed.fetch_max(active, Ordering::AcqRel); + Ok(ProviderPermit { + _permit: permit, + active_processes: self.active_processes.clone(), + }) + }, } } @@ -624,6 +653,7 @@ fn degradation_from_error(error: &ProviderError) -> ProviderDegradation { #[cfg(test)] mod tests { use std::path::PathBuf; + use std::sync::atomic::Ordering; use code_system_graph_model::RepoId; use tokio_util::sync::CancellationToken; @@ -631,6 +661,10 @@ mod tests { use super::{CodeGraphConfig, CodeGraphProvider, validate_changed_files}; use crate::{ProviderBudget, ProviderError, ProviderRequest}; + fn absolute_project_path() -> PathBuf { + std::env::current_dir().expect("test process should expose an absolute current directory") + } + #[test] fn provider_should_reject_zero_process_budget() { let result = CodeGraphProvider::new(CodeGraphConfig { @@ -658,7 +692,7 @@ mod tests { &provider, ProviderRequest { repo_id: RepoId::new("repo:test"), - project_path: PathBuf::from("/tmp"), + project_path: absolute_project_path(), budget: ProviderBudget::default(), cancellation, }, @@ -667,4 +701,40 @@ mod tests { assert_eq!(result, Err(ProviderError::Cancelled)); } + + #[tokio::test] + async fn provider_should_measure_held_process_permits() { + let provider = CodeGraphProvider::new(CodeGraphConfig { + max_concurrent_processes: 2, + ..CodeGraphConfig::default() + }) + .expect("valid provider config"); + let request = ProviderRequest { + repo_id: RepoId::new("repo:test"), + project_path: absolute_project_path(), + budget: ProviderBudget::default(), + cancellation: CancellationToken::new(), + }; + assert_eq!(provider.maximum_concurrency_observed(), 0); + + let first = provider + .enter( + &request, + tokio::time::Instant::now() + std::time::Duration::from_secs(1), + ) + .await + .expect("first permit"); + assert_eq!(provider.maximum_concurrency_observed(), 1); + let second = provider + .enter( + &request, + tokio::time::Instant::now() + std::time::Duration::from_secs(1), + ) + .await + .expect("second permit"); + assert_eq!(provider.maximum_concurrency_observed(), 2); + + drop((first, second)); + assert_eq!(provider.active_processes.load(Ordering::Acquire), 0); + } } diff --git a/crates/code-system-graph-core/src/execution_policy.rs b/crates/code-system-graph-core/src/execution_policy.rs index f23cfec..5155dd8 100644 --- a/crates/code-system-graph-core/src/execution_policy.rs +++ b/crates/code-system-graph-core/src/execution_policy.rs @@ -1,6 +1,5 @@ use std::fmt::Write as _; use std::num::NonZeroUsize; -use std::sync::Arc; use std::time::{Duration, Instant}; use code_system_graph_model::stable_id; @@ -8,6 +7,10 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use thiserror::Error; +mod tracker; + +pub use tracker::{MonotonicClock, ScanJobTracker}; + /// Default maximum wall time for one supervised scan or sync pass. pub const DEFAULT_MAX_SCAN_WALL_TIME_MS: u64 = 21_600_000; /// Default maximum time without verified forward progress. @@ -28,6 +31,57 @@ pub const DEFAULT_MAX_WATCH_SESSION_WALL_TIME_MS: u64 = 86_400_000; pub const DEFAULT_MIN_WATCH_RESCAN_INTERVAL_MS: u64 = 10_000; /// Default maximum retained historical checkpoint-cache bytes. pub const DEFAULT_MAX_CHECKPOINT_CACHE_BYTES: u64 = 10_737_418_240; +pub const DEFAULT_MAX_EXPLORE_WALL_TIME_MS: u64 = 8_000; +pub const DEFAULT_MAX_EXPLORE_CODEGRAPH_OPERATIONS: u64 = 8; +pub const DEFAULT_MAX_EXPLORE_CONCURRENT_CODEGRAPH_PROCESSES: u64 = 2; +pub const DEFAULT_MAX_EXPLORE_SOURCE_FILES: u64 = 25; +pub const DEFAULT_MAX_EXPLORE_RESOLVED_SYMBOLS: u64 = 5; +pub const DEFAULT_MAX_EXPLORE_ANCHORS: u64 = 3; +pub const DEFAULT_MAX_EXPLORE_NEIGHBORS_PER_DIRECTION: u64 = 8; +pub const DEFAULT_MAX_EXPLORE_LOCAL_RELATIONSHIPS: u64 = 48; +pub const DEFAULT_MAX_EXPLORE_FEDERATED_HANDOFFS_PER_ANCHOR: u64 = 10; +pub const DEFAULT_MAX_EXPLORE_FEDERATED_HANDOFFS: u64 = 30; +pub const DEFAULT_MAX_EXPLORE_EVIDENCE_LOCATIONS_PER_HANDOFF: u64 = 4; +pub const DEFAULT_MAX_EXPLORE_SOURCE_MARKDOWN_BYTES: u64 = 262_144; +pub const DEFAULT_MAX_EXPLORE_ENRICHMENT_BYTES: u64 = 65_536; +pub const DEFAULT_MAX_AGENT_NEXT_ACTIONS_PER_RESPONSE: u64 = 12; +pub const DEFAULT_MAX_QUERY_REPOSITORY_SUGGESTIONS: u64 = 20; +pub const DEFAULT_MAX_MCP_TOOL_RESPONSE_BYTES: u64 = 524_288; +pub const DEFAULT_MAX_MCP_RESOURCE_ITEMS: u64 = 100; +pub const DEFAULT_MAX_MCP_RESOURCE_BYTES: u64 = 262_144; +pub const DEFAULT_MAX_MCP_SCHEMA_CATALOG_BYTES: u64 = 2_097_152; +/// Smallest Markdown response budget that can retain the mandatory MCP control block. +pub const MIN_MCP_MARKDOWN_BYTES: u64 = 256; + +// Keep the agent-facing policy inventory in one declarative list. The two public serde structs +// intentionally remain flat for schema-v2 compatibility; defaults, override resolution, and the +// delivery fingerprint are generated from this list so a new field cannot silently omit one of +// those behaviors. +macro_rules! with_agent_policy_fields { + ($consumer:ident) => { + $consumer! { + max_explore_wall_time_ms: DEFAULT_MAX_EXPLORE_WALL_TIME_MS => "maxExploreWallTimeMs"; + max_explore_codegraph_operations: DEFAULT_MAX_EXPLORE_CODEGRAPH_OPERATIONS => "maxExploreCodeGraphOperations"; + max_explore_concurrent_codegraph_processes: DEFAULT_MAX_EXPLORE_CONCURRENT_CODEGRAPH_PROCESSES => "maxExploreConcurrentCodeGraphProcesses"; + max_explore_source_files: DEFAULT_MAX_EXPLORE_SOURCE_FILES => "maxExploreSourceFiles"; + max_explore_resolved_symbols: DEFAULT_MAX_EXPLORE_RESOLVED_SYMBOLS => "maxExploreResolvedSymbols"; + max_explore_anchors: DEFAULT_MAX_EXPLORE_ANCHORS => "maxExploreAnchors"; + max_explore_neighbors_per_direction: DEFAULT_MAX_EXPLORE_NEIGHBORS_PER_DIRECTION => "maxExploreNeighborsPerDirection"; + max_explore_local_relationships: DEFAULT_MAX_EXPLORE_LOCAL_RELATIONSHIPS => "maxExploreLocalRelationships"; + max_explore_federated_handoffs_per_anchor: DEFAULT_MAX_EXPLORE_FEDERATED_HANDOFFS_PER_ANCHOR => "maxExploreFederatedHandoffsPerAnchor"; + max_explore_federated_handoffs: DEFAULT_MAX_EXPLORE_FEDERATED_HANDOFFS => "maxExploreFederatedHandoffs"; + max_explore_evidence_locations_per_handoff: DEFAULT_MAX_EXPLORE_EVIDENCE_LOCATIONS_PER_HANDOFF => "maxExploreEvidenceLocationsPerHandoff"; + max_explore_source_markdown_bytes: DEFAULT_MAX_EXPLORE_SOURCE_MARKDOWN_BYTES => "maxExploreSourceMarkdownBytes"; + max_explore_enrichment_bytes: DEFAULT_MAX_EXPLORE_ENRICHMENT_BYTES => "maxExploreEnrichmentBytes"; + max_agent_next_actions_per_response: DEFAULT_MAX_AGENT_NEXT_ACTIONS_PER_RESPONSE => "maxAgentNextActionsPerResponse"; + max_query_repository_suggestions: DEFAULT_MAX_QUERY_REPOSITORY_SUGGESTIONS => "maxQueryRepositorySuggestions"; + max_mcp_tool_response_bytes: DEFAULT_MAX_MCP_TOOL_RESPONSE_BYTES => "maxMcpToolResponseBytes"; + max_mcp_resource_items: DEFAULT_MAX_MCP_RESOURCE_ITEMS => "maxMcpResourceItems"; + max_mcp_resource_bytes: DEFAULT_MAX_MCP_RESOURCE_BYTES => "maxMcpResourceBytes"; + max_mcp_schema_catalog_bytes: DEFAULT_MAX_MCP_SCHEMA_CATALOG_BYTES => "maxMcpSchemaCatalogBytes"; + } + }; +} /// Effective per-repository limit for source-symbol corroboration through `CodeGraph`. /// @@ -128,6 +182,49 @@ pub struct ExecutionPolicyOverrides { pub min_watch_rescan_interval_ms: Option, /// Optional maximum retained checkpoint-cache bytes. pub max_checkpoint_cache_bytes: Option, + /// Optional corroboration-anchor count, or `-1` for unlimited. + #[serde(rename = "maxCodeGraphCorroborationAnchorsPerRepo")] + pub max_codegraph_corroboration_anchors_per_repo: Option, + /// Optional Explore wall-time budget. + pub max_explore_wall_time_ms: Option, + /// Optional maximum public provider operations per Explore request. + #[serde(rename = "maxExploreCodeGraphOperations")] + pub max_explore_codegraph_operations: Option, + /// Optional maximum concurrent provider child processes. + #[serde(rename = "maxExploreConcurrentCodeGraphProcesses")] + pub max_explore_concurrent_codegraph_processes: Option, + /// Optional maximum source files returned by Explore. + pub max_explore_source_files: Option, + /// Optional maximum resolved local symbols. + pub max_explore_resolved_symbols: Option, + /// Optional maximum symbols selected as traversal anchors. + pub max_explore_anchors: Option, + /// Optional maximum neighbors per anchor and direction. + pub max_explore_neighbors_per_direction: Option, + /// Optional maximum retained local relationships. + pub max_explore_local_relationships: Option, + /// Optional maximum federated handoffs per anchor. + pub max_explore_federated_handoffs_per_anchor: Option, + /// Optional maximum federated handoffs per response. + pub max_explore_federated_handoffs: Option, + /// Optional maximum evidence locations per federated handoff. + pub max_explore_evidence_locations_per_handoff: Option, + /// Optional maximum retained source Markdown bytes. + pub max_explore_source_markdown_bytes: Option, + /// Optional maximum retained enrichment bytes. + pub max_explore_enrichment_bytes: Option, + /// Optional maximum agent next actions per response. + pub max_agent_next_actions_per_response: Option, + /// Optional maximum repository suggestions from Query. + pub max_query_repository_suggestions: Option, + /// Optional final MCP tool response byte limit. + pub max_mcp_tool_response_bytes: Option, + /// Optional MCP resource item limit. + pub max_mcp_resource_items: Option, + /// Optional final MCP resource byte limit. + pub max_mcp_resource_bytes: Option, + /// Optional final MCP schema-catalog byte limit. + pub max_mcp_schema_catalog_bytes: Option, } /// Effective global execution policy for one workspace operation. @@ -153,11 +250,57 @@ pub struct ExecutionPolicy { pub min_watch_rescan_interval_ms: u64, /// Maximum retained historical checkpoint-cache bytes. pub max_checkpoint_cache_bytes: u64, + /// Effective corroboration-anchor count, or unlimited. + #[serde(rename = "maxCodeGraphCorroborationAnchorsPerRepo")] + #[schemars(with = "i64")] + pub max_codegraph_corroboration_anchors_per_repo: CodeGraphCorroborationAnchorLimit, + /// Effective Explore wall-time budget. + pub max_explore_wall_time_ms: u64, + /// Effective maximum public provider operations per Explore request. + #[serde(rename = "maxExploreCodeGraphOperations")] + pub max_explore_codegraph_operations: u64, + /// Effective maximum concurrent provider child processes. + #[serde(rename = "maxExploreConcurrentCodeGraphProcesses")] + pub max_explore_concurrent_codegraph_processes: u64, + /// Effective maximum source files returned by Explore. + pub max_explore_source_files: u64, + /// Effective maximum resolved local symbols. + pub max_explore_resolved_symbols: u64, + /// Effective maximum symbols selected as traversal anchors. + pub max_explore_anchors: u64, + /// Effective maximum neighbors per anchor and direction. + pub max_explore_neighbors_per_direction: u64, + /// Effective maximum retained local relationships. + pub max_explore_local_relationships: u64, + /// Effective maximum federated handoffs per anchor. + pub max_explore_federated_handoffs_per_anchor: u64, + /// Effective maximum federated handoffs per response. + pub max_explore_federated_handoffs: u64, + /// Effective maximum evidence locations per federated handoff. + pub max_explore_evidence_locations_per_handoff: u64, + /// Effective maximum retained source Markdown bytes. + pub max_explore_source_markdown_bytes: u64, + /// Effective maximum retained enrichment bytes. + pub max_explore_enrichment_bytes: u64, + /// Effective maximum agent next actions per response. + pub max_agent_next_actions_per_response: u64, + /// Effective maximum repository suggestions from Query. + pub max_query_repository_suggestions: u64, + /// Effective final MCP tool response byte limit. + pub max_mcp_tool_response_bytes: u64, + /// Effective MCP resource item limit. + pub max_mcp_resource_items: u64, + /// Effective final MCP resource byte limit. + pub max_mcp_resource_bytes: u64, + /// Effective final MCP schema-catalog byte limit. + pub max_mcp_schema_catalog_bytes: u64, } impl Default for ExecutionPolicy { fn default() -> Self { - Self { + macro_rules! policy_defaults { + ($($field:ident: $default:ident => $external:literal;)*) => { + Self { max_scan_wall_time_ms: DEFAULT_MAX_SCAN_WALL_TIME_MS, max_no_progress_time_ms: DEFAULT_MAX_NO_PROGRESS_TIME_MS, max_codegraph_sync_wall_time_ms_per_repo: @@ -168,7 +311,16 @@ impl Default for ExecutionPolicy { max_watch_session_wall_time_ms: DEFAULT_MAX_WATCH_SESSION_WALL_TIME_MS, min_watch_rescan_interval_ms: DEFAULT_MIN_WATCH_RESCAN_INTERVAL_MS, max_checkpoint_cache_bytes: DEFAULT_MAX_CHECKPOINT_CACHE_BYTES, + max_codegraph_corroboration_anchors_per_repo: + CodeGraphCorroborationAnchorLimit::try_from( + DEFAULT_MAX_CODEGRAPH_CORROBORATION_ANCHORS_PER_REPO, + ) + .expect("the built-in corroboration limit is valid"), + $($field: $default,)* + } + }; } + with_agent_policy_fields!(policy_defaults) } } @@ -199,24 +351,74 @@ impl ExecutionPolicy { apply!(max_watch_session_wall_time_ms); apply!(min_watch_rescan_interval_ms); apply!(max_checkpoint_cache_bytes); + if let Some(value) = values.max_codegraph_corroboration_anchors_per_repo { + policy.max_codegraph_corroboration_anchors_per_repo = value.try_into()?; + } + macro_rules! apply_agent_overrides { + ($($field:ident: $default:ident => $external:literal;)*) => { + $(apply!($field);)* + }; + } + with_agent_policy_fields!(apply_agent_overrides); } policy.validate()?; Ok(policy) } fn validate(&self) -> Result<(), InvalidExecutionPolicy> { - for (field, value) in self.canonical_values() { + self.validate_scalar_values()?; + self.validate_mcp_minimums()?; + self.validate_scan_relationships()?; + self.validate_explore_relationships() + } + + fn validate_mcp_minimums(&self) -> Result<(), InvalidExecutionPolicy> { + for (field, value) in [ + ("maxMcpToolResponseBytes", self.max_mcp_tool_response_bytes), + ("maxMcpResourceBytes", self.max_mcp_resource_bytes), + ( + "maxMcpSchemaCatalogBytes", + self.max_mcp_schema_catalog_bytes, + ), + ] { + if value < MIN_MCP_MARKDOWN_BYTES { + return Err(InvalidExecutionPolicy::BelowMinimum { + field, + value, + minimum: MIN_MCP_MARKDOWN_BYTES, + }); + } + } + Ok(()) + } + + fn validate_scalar_values(&self) -> Result<(), InvalidExecutionPolicy> { + for (field, value) in self + .scan_canonical_values() + .into_iter() + .chain(self.agent_canonical_values()) + { let invalid_bytes = field.ends_with("Bytes") && usize::try_from(value).is_err(); + let invalid_count = !field.ends_with("Ms") && usize::try_from(value).is_err(); let invalid_sqlite_quota = field == "maxCheckpointCacheBytes" && i64::try_from(value).is_err(); let invalid_deadline = field.ends_with("Ms") && Instant::now() .checked_add(Duration::from_millis(value)) .is_none(); - if value == 0 || invalid_bytes || invalid_sqlite_quota || invalid_deadline { + if value == 0 + || invalid_bytes + || invalid_count + || invalid_sqlite_quota + || invalid_deadline + { return Err(InvalidExecutionPolicy::InvalidValue { field, value }); } } + Ok(()) + } + + fn validate_scan_relationships(&self) -> Result<(), InvalidExecutionPolicy> { Self::require_not_greater( "maxNoProgressTimeMs", self.max_no_progress_time_ms, @@ -249,6 +451,58 @@ impl ExecutionPolicy { ) } + fn validate_explore_relationships(&self) -> Result<(), InvalidExecutionPolicy> { + Self::require_not_greater( + "maxExploreAnchors", + self.max_explore_anchors, + "maxExploreResolvedSymbols", + self.max_explore_resolved_symbols, + )?; + Self::require_not_greater( + "maxExploreConcurrentCodeGraphProcesses", + self.max_explore_concurrent_codegraph_processes, + "maxExploreCodeGraphOperations", + self.max_explore_codegraph_operations, + )?; + let local_ceiling = self + .max_explore_anchors + .checked_mul(2) + .and_then(|value| value.checked_mul(self.max_explore_neighbors_per_direction)) + .ok_or(InvalidExecutionPolicy::ArithmeticOverflow { + expression: "maxExploreAnchors × 2 × maxExploreNeighborsPerDirection", + })?; + Self::require_not_greater( + "maxExploreLocalRelationships", + self.max_explore_local_relationships, + "anchor neighbor capacity", + local_ceiling, + )?; + let handoff_ceiling = self + .max_explore_anchors + .checked_mul(self.max_explore_federated_handoffs_per_anchor) + .ok_or(InvalidExecutionPolicy::ArithmeticOverflow { + expression: "maxExploreAnchors × maxExploreFederatedHandoffsPerAnchor", + })?; + Self::require_not_greater( + "maxExploreFederatedHandoffs", + self.max_explore_federated_handoffs, + "anchor handoff capacity", + handoff_ceiling, + )?; + Self::require_not_greater( + "maxExploreSourceMarkdownBytes", + self.max_explore_source_markdown_bytes, + "maxMcpToolResponseBytes", + self.max_mcp_tool_response_bytes, + )?; + Self::require_not_greater( + "maxExploreEnrichmentBytes", + self.max_explore_enrichment_bytes, + "maxMcpToolResponseBytes", + self.max_mcp_tool_response_bytes, + ) + } + fn require_not_greater( field: &'static str, value: u64, @@ -266,7 +520,7 @@ impl ExecutionPolicy { Ok(()) } - fn canonical_values(&self) -> [(&'static str, u64); 9] { + fn scan_canonical_values(&self) -> [(&'static str, u64); 9] { [ ("maxScanWallTimeMs", self.max_scan_wall_time_ms), ("maxNoProgressTimeMs", self.max_no_progress_time_ms), @@ -289,18 +543,39 @@ impl ExecutionPolicy { ] } + fn agent_canonical_values(&self) -> Vec<(&'static str, u64)> { + macro_rules! canonical_agent_values { + ($($field:ident: $default:ident => $external:literal;)*) => { + vec![$(($external, self.$field),)*] + }; + } + with_agent_policy_fields!(canonical_agent_values) + } + /// Returns the stable canonical fingerprint of the effective operational policy. #[must_use] - pub fn fingerprint(&self) -> String { - let canonical = self - .canonical_values() + pub fn scan_fingerprint(&self) -> String { + let mut canonical = self + .scan_canonical_values() .into_iter() .map(|(name, value)| format!("{name}={value}")) .collect::>() .join(";"); + write!( + canonical, + ";maxCodeGraphCorroborationAnchorsPerRepo={}", + self.max_codegraph_corroboration_anchors_per_repo + ) + .expect("writing to a String cannot fail"); stable_id("execution-policy", &canonical) } + #[must_use] + /// Returns the historical scan-only fingerprint alias. + pub fn fingerprint(&self) -> String { + self.scan_fingerprint() + } + /// Returns a fingerprint that also includes the additive `CodeGraph` corroboration bound. #[must_use] pub fn fingerprint_with_codegraph_limit( @@ -308,7 +583,7 @@ impl ExecutionPolicy { limit: CodeGraphCorroborationAnchorLimit, ) -> String { let mut canonical = self - .canonical_values() + .scan_canonical_values() .into_iter() .map(|(name, value)| format!("{name}={value}")) .collect::>() @@ -320,6 +595,19 @@ impl ExecutionPolicy { .expect("writing to a String cannot fail"); stable_id("execution-policy", &canonical) } + + /// Returns the fingerprint for limits that only affect ephemeral agent delivery. + #[must_use] + pub fn agent_delivery_fingerprint(&self) -> String { + let mut canonical = String::new(); + for (index, (name, value)) in self.agent_canonical_values().into_iter().enumerate() { + if index > 0 { + canonical.push(';'); + } + write!(canonical, "{name}={value}").expect("writing to a String cannot fail"); + } + stable_id("agent-delivery-policy", &canonical) + } } /// Invalid operator-owned execution policy. @@ -333,6 +621,16 @@ pub enum InvalidExecutionPolicy { /// Rejected numeric value. value: u64, }, + /// An MCP Markdown budget cannot retain its mandatory control block. + #[error("execution policy `{field}` must be at least {minimum}; received {value}")] + BelowMinimum { + /// Manifest field containing the undersized budget. + field: &'static str, + /// Rejected byte budget. + value: u64, + /// Smallest supported byte budget. + minimum: u64, + }, /// One subordinate deadline exceeds its containing deadline. #[error("execution policy `{field}` ({value}) must not exceed `{maximum_field}` ({maximum})")] InvalidRelationship { @@ -345,6 +643,12 @@ pub enum InvalidExecutionPolicy { /// Supplied containing value. maximum: u64, }, + /// A checked capacity relationship overflowed before it could be compared. + #[error("execution policy arithmetic overflow in `{expression}`")] + ArithmeticOverflow { + /// Checked expression that overflowed. + expression: &'static str, + }, } /// Observable phase of one supervised scan or synchronization pass. @@ -435,168 +739,10 @@ pub struct ExecutionLimitExceeded { pub completed_units: u64, } -/// Injectable monotonic time source used by execution watchdogs. -pub trait MonotonicClock: std::fmt::Debug + Send + Sync { - /// Duration since an arbitrary stable origin. - fn now(&self) -> Duration; -} - -#[derive(Debug)] -struct SystemMonotonicClock { - origin: Instant, -} - -impl SystemMonotonicClock { - fn new() -> Self { - Self { - origin: Instant::now(), - } - } -} - -impl MonotonicClock for SystemMonotonicClock { - fn now(&self) -> Duration { - self.origin.elapsed() - } -} - -/// Monotonic progress tracker used inside one worker process. -#[derive(Debug)] -pub struct ScanJobTracker { - run_id: String, - policy: ExecutionPolicy, - clock: Arc, - started: Duration, - last_progress: Duration, - phase: JobPhase, - completed_units: u64, -} - -impl ScanJobTracker { - /// Starts a tracker for one run at configuration validation. - #[must_use] - pub fn new(run_id: impl Into, policy: ExecutionPolicy) -> Self { - Self::with_clock(run_id, policy, Arc::new(SystemMonotonicClock::new())) - } - - /// Starts a tracker with an injected monotonic clock for deterministic execution tests. - #[must_use] - pub fn with_clock( - run_id: impl Into, - policy: ExecutionPolicy, - clock: Arc, - ) -> Self { - let now = clock.now(); - Self { - run_id: run_id.into(), - policy, - clock, - started: now, - last_progress: now, - phase: JobPhase::Configuration, - completed_units: 0, - } - } - - /// Changes phase after checking the active deadlines. - /// - /// # Errors - /// - /// Returns [`ExecutionLimitExceeded`] when wall time or no-progress time is exhausted. - pub fn enter_phase(&mut self, phase: JobPhase) -> Result<(), ExecutionLimitExceeded> { - self.check_time()?; - self.phase = phase; - Ok(()) - } - - /// Charges verified completed work using checked arithmetic. - /// - /// # Errors - /// - /// Returns [`ExecutionLimitExceeded`] on arithmetic overflow or an exhausted deadline. - pub fn progress(&mut self, amount: u64) -> Result<(), ExecutionLimitExceeded> { - let completed_units = self - .completed_units - .checked_add(amount) - .ok_or_else(|| self.exceeded(ExecutionResource::WorkUnits, u64::MAX, u64::MAX - 1))?; - self.check_time()?; - self.completed_units = completed_units; - self.last_progress = self.clock.now(); - Ok(()) - } - - /// Checks monotonic wall time and time since the last verified progress. - /// - /// # Errors - /// - /// Returns [`ExecutionLimitExceeded`] when either effective duration is exhausted. - pub fn check_time(&self) -> Result<(), ExecutionLimitExceeded> { - let now = self.clock.now(); - self.check_duration( - ExecutionResource::WallTimeMs, - now.saturating_sub(self.started), - self.policy.max_scan_wall_time_ms, - )?; - self.check_duration( - ExecutionResource::NoProgressTimeMs, - now.saturating_sub(self.last_progress), - self.policy.max_no_progress_time_ms, - ) - } - - fn check_duration( - &self, - resource: ExecutionResource, - observed: Duration, - maximum: u64, - ) -> Result<(), ExecutionLimitExceeded> { - let observed = u64::try_from(observed.as_millis()).unwrap_or(u64::MAX); - if observed > maximum { - return Err(self.exceeded(resource, observed, maximum)); - } - Ok(()) - } - - fn exceeded( - &self, - resource: ExecutionResource, - observed: u64, - maximum: u64, - ) -> ExecutionLimitExceeded { - ExecutionLimitExceeded { - run_id: self.run_id.clone(), - phase: self.phase, - resource, - observed, - maximum, - completed_units: self.completed_units, - } - } -} - #[cfg(test)] mod tests { - use std::sync::atomic::{AtomicU64, Ordering}; - use super::*; - #[derive(Debug, Default)] - struct FakeClock { - milliseconds: AtomicU64, - } - - impl FakeClock { - fn advance(&self, milliseconds: u64) { - self.milliseconds.fetch_add(milliseconds, Ordering::Relaxed); - } - } - - impl MonotonicClock for FakeClock { - fn now(&self) -> Duration { - Duration::from_millis(self.milliseconds.load(Ordering::Relaxed)) - } - } - #[test] fn defaults_should_be_generous_and_finite() { let policy = ExecutionPolicy::default(); @@ -694,6 +840,42 @@ mod tests { assert!(matches!(quota, InvalidExecutionPolicy::InvalidValue { .. })); } + #[test] + fn mcp_markdown_budgets_should_enforce_the_control_block_minimum() { + for overrides in [ + ExecutionPolicyOverrides { + max_mcp_tool_response_bytes: Some(MIN_MCP_MARKDOWN_BYTES - 1), + ..ExecutionPolicyOverrides::default() + }, + ExecutionPolicyOverrides { + max_mcp_resource_bytes: Some(MIN_MCP_MARKDOWN_BYTES - 1), + ..ExecutionPolicyOverrides::default() + }, + ExecutionPolicyOverrides { + max_mcp_schema_catalog_bytes: Some(MIN_MCP_MARKDOWN_BYTES - 1), + ..ExecutionPolicyOverrides::default() + }, + ] { + assert!(matches!( + ExecutionPolicy::resolve(Some(&overrides)), + Err(InvalidExecutionPolicy::BelowMinimum { + minimum: MIN_MCP_MARKDOWN_BYTES, + .. + }) + )); + } + + let minimum = ExecutionPolicy::resolve(Some(&ExecutionPolicyOverrides { + max_mcp_tool_response_bytes: Some(MIN_MCP_MARKDOWN_BYTES), + max_mcp_resource_bytes: Some(MIN_MCP_MARKDOWN_BYTES), + max_mcp_schema_catalog_bytes: Some(MIN_MCP_MARKDOWN_BYTES), + max_explore_source_markdown_bytes: Some(MIN_MCP_MARKDOWN_BYTES), + max_explore_enrichment_bytes: Some(MIN_MCP_MARKDOWN_BYTES), + ..ExecutionPolicyOverrides::default() + })); + assert!(minimum.is_ok()); + } + #[test] fn subordinate_deadline_should_not_exceed_scan_deadline() { let error = ExecutionPolicy::resolve(Some(&ExecutionPolicyOverrides { @@ -724,44 +906,50 @@ mod tests { } #[test] - fn injected_clock_should_accept_exact_deadline_and_reject_one_unit_over() { - let clock = Arc::new(FakeClock::default()); - let policy = ExecutionPolicy { - max_scan_wall_time_ms: 10, - max_no_progress_time_ms: 10, - ..ExecutionPolicy::default() - }; - let tracker = ScanJobTracker::with_clock("run", policy, clock.clone()); - - clock.advance(10); - tracker.check_time().expect("exact deadline is inclusive"); - clock.advance(1); - let error = tracker.check_time().expect_err("one over must fail"); + fn agent_delivery_limits_should_not_change_scan_fingerprint() { + let baseline = ExecutionPolicy::default(); + let changed = ExecutionPolicy::resolve(Some(&ExecutionPolicyOverrides { + max_mcp_tool_response_bytes: Some(600_000), + max_explore_source_markdown_bytes: Some(300_000), + ..ExecutionPolicyOverrides::default() + })) + .expect("agent-only override is valid"); - assert_eq!(error.resource, ExecutionResource::WallTimeMs); - assert_eq!(error.observed, 11); - assert_eq!(error.maximum, 10); + assert_eq!(baseline.scan_fingerprint(), changed.scan_fingerprint()); + assert_ne!( + baseline.agent_delivery_fingerprint(), + changed.agent_delivery_fingerprint() + ); } #[test] - fn phase_changes_should_not_fake_progress() { - let clock = Arc::new(FakeClock::default()); - let policy = ExecutionPolicy { - max_scan_wall_time_ms: 100, - max_no_progress_time_ms: 5, - ..ExecutionPolicy::default() - }; - let mut tracker = ScanJobTracker::with_clock("run", policy, clock.clone()); - - clock.advance(5); - tracker - .enter_phase(JobPhase::Discovery) - .expect("exact idle deadline is inclusive"); - clock.advance(1); - let error = tracker - .enter_phase(JobPhase::Fingerprinting) - .expect_err("phase churn must not renew watchdog"); - - assert_eq!(error.resource, ExecutionResource::NoProgressTimeMs); + fn explore_capacity_relationships_should_be_checked() { + for overrides in [ + ExecutionPolicyOverrides { + max_explore_anchors: Some(6), + ..ExecutionPolicyOverrides::default() + }, + ExecutionPolicyOverrides { + max_explore_concurrent_codegraph_processes: Some(9), + ..ExecutionPolicyOverrides::default() + }, + ExecutionPolicyOverrides { + max_explore_local_relationships: Some(49), + ..ExecutionPolicyOverrides::default() + }, + ExecutionPolicyOverrides { + max_explore_federated_handoffs: Some(31), + ..ExecutionPolicyOverrides::default() + }, + ExecutionPolicyOverrides { + max_explore_source_markdown_bytes: Some(524_289), + ..ExecutionPolicyOverrides::default() + }, + ] { + assert!(matches!( + ExecutionPolicy::resolve(Some(&overrides)), + Err(InvalidExecutionPolicy::InvalidRelationship { .. }) + )); + } } } diff --git a/crates/code-system-graph-core/src/execution_policy/tracker.rs b/crates/code-system-graph-core/src/execution_policy/tracker.rs new file mode 100644 index 0000000..b736bd9 --- /dev/null +++ b/crates/code-system-graph-core/src/execution_policy/tracker.rs @@ -0,0 +1,210 @@ +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use super::{ExecutionLimitExceeded, ExecutionPolicy, ExecutionResource, JobPhase}; + +/// Injectable monotonic time source used by execution watchdogs. +pub trait MonotonicClock: std::fmt::Debug + Send + Sync { + /// Duration since an arbitrary stable origin. + fn now(&self) -> Duration; +} + +#[derive(Debug)] +struct SystemMonotonicClock { + origin: Instant, +} + +impl SystemMonotonicClock { + fn new() -> Self { + Self { + origin: Instant::now(), + } + } +} + +impl MonotonicClock for SystemMonotonicClock { + fn now(&self) -> Duration { + self.origin.elapsed() + } +} + +/// Monotonic progress tracker used inside one worker process. +#[derive(Debug)] +pub struct ScanJobTracker { + run_id: String, + policy: ExecutionPolicy, + clock: Arc, + started: Duration, + last_progress: Duration, + phase: JobPhase, + completed_units: u64, +} + +impl ScanJobTracker { + /// Starts a tracker for one run at configuration validation. + #[must_use] + pub fn new(run_id: impl Into, policy: ExecutionPolicy) -> Self { + Self::with_clock(run_id, policy, Arc::new(SystemMonotonicClock::new())) + } + + /// Starts a tracker with an injected monotonic clock for deterministic execution tests. + #[must_use] + pub fn with_clock( + run_id: impl Into, + policy: ExecutionPolicy, + clock: Arc, + ) -> Self { + let now = clock.now(); + Self { + run_id: run_id.into(), + policy, + clock, + started: now, + last_progress: now, + phase: JobPhase::Configuration, + completed_units: 0, + } + } + + /// Changes phase after checking the active deadlines. + /// + /// # Errors + /// + /// Returns [`ExecutionLimitExceeded`] when wall time or no-progress time is exhausted. + pub fn enter_phase(&mut self, phase: JobPhase) -> Result<(), ExecutionLimitExceeded> { + self.check_time()?; + self.phase = phase; + Ok(()) + } + + /// Charges verified completed work using checked arithmetic. + /// + /// # Errors + /// + /// Returns [`ExecutionLimitExceeded`] on arithmetic overflow or an exhausted deadline. + pub fn progress(&mut self, amount: u64) -> Result<(), ExecutionLimitExceeded> { + let completed_units = self + .completed_units + .checked_add(amount) + .ok_or_else(|| self.exceeded(ExecutionResource::WorkUnits, u64::MAX, u64::MAX - 1))?; + self.check_time()?; + self.completed_units = completed_units; + self.last_progress = self.clock.now(); + Ok(()) + } + + /// Checks monotonic wall time and time since the last verified progress. + /// + /// # Errors + /// + /// Returns [`ExecutionLimitExceeded`] when either effective duration is exhausted. + pub fn check_time(&self) -> Result<(), ExecutionLimitExceeded> { + let now = self.clock.now(); + self.check_duration( + ExecutionResource::WallTimeMs, + now.saturating_sub(self.started), + self.policy.max_scan_wall_time_ms, + )?; + self.check_duration( + ExecutionResource::NoProgressTimeMs, + now.saturating_sub(self.last_progress), + self.policy.max_no_progress_time_ms, + ) + } + + fn check_duration( + &self, + resource: ExecutionResource, + observed: Duration, + maximum: u64, + ) -> Result<(), ExecutionLimitExceeded> { + let observed = u64::try_from(observed.as_millis()).unwrap_or(u64::MAX); + if observed > maximum { + return Err(self.exceeded(resource, observed, maximum)); + } + Ok(()) + } + + fn exceeded( + &self, + resource: ExecutionResource, + observed: u64, + maximum: u64, + ) -> ExecutionLimitExceeded { + ExecutionLimitExceeded { + run_id: self.run_id.clone(), + phase: self.phase, + resource, + observed, + maximum, + completed_units: self.completed_units, + } + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + use std::sync::atomic::{AtomicU64, Ordering}; + + use super::*; + + #[derive(Debug, Default)] + struct FakeClock { + milliseconds: AtomicU64, + } + + impl FakeClock { + fn advance(&self, milliseconds: u64) { + self.milliseconds.fetch_add(milliseconds, Ordering::Relaxed); + } + } + + impl MonotonicClock for FakeClock { + fn now(&self) -> Duration { + Duration::from_millis(self.milliseconds.load(Ordering::Relaxed)) + } + } + + #[test] + fn injected_clock_should_accept_exact_deadline_and_reject_one_unit_over() { + let clock = Arc::new(FakeClock::default()); + let policy = ExecutionPolicy { + max_scan_wall_time_ms: 10, + max_no_progress_time_ms: 10, + ..ExecutionPolicy::default() + }; + let tracker = ScanJobTracker::with_clock("run", policy, clock.clone()); + + clock.advance(10); + tracker.check_time().expect("exact deadline is inclusive"); + clock.advance(1); + let error = tracker.check_time().expect_err("one over must fail"); + + assert_eq!(error.resource, ExecutionResource::WallTimeMs); + assert_eq!(error.observed, 11); + assert_eq!(error.maximum, 10); + } + + #[test] + fn phase_changes_should_not_fake_progress() { + let clock = Arc::new(FakeClock::default()); + let policy = ExecutionPolicy { + max_scan_wall_time_ms: 100, + max_no_progress_time_ms: 5, + ..ExecutionPolicy::default() + }; + let mut tracker = ScanJobTracker::with_clock("run", policy, clock.clone()); + + clock.advance(5); + tracker + .enter_phase(JobPhase::Discovery) + .expect("exact idle deadline is inclusive"); + clock.advance(1); + let error = tracker + .enter_phase(JobPhase::Fingerprinting) + .expect_err("phase churn must not renew watchdog"); + + assert_eq!(error.resource, ExecutionResource::NoProgressTimeMs); + } +} diff --git a/crates/code-system-graph-core/src/interfaces.rs b/crates/code-system-graph-core/src/interfaces.rs index 009c1ef..89b34b1 100644 --- a/crates/code-system-graph-core/src/interfaces.rs +++ b/crates/code-system-graph-core/src/interfaces.rs @@ -15,11 +15,11 @@ use crate::{ }; /// Schema version shared by the application-layer contracts in this module. -pub const INTERFACE_SCHEMA_VERSION: u32 = 1; +pub const INTERFACE_SCHEMA_VERSION: u32 = 2; /// Result version shared by the application-layer reports in this module. -pub const INTERFACE_RESULT_VERSION: u32 = 1; +pub const INTERFACE_RESULT_VERSION: u32 = 2; /// Version of reusable delivery metadata. -pub const DELIVERY_METADATA_VERSION: u32 = 1; +pub const DELIVERY_METADATA_VERSION: u32 = 2; /// Hard maximum number of nodes accepted by one graph export. pub const MAX_EXPORT_NODES: usize = 100_000; /// Hard maximum number of edges accepted by one graph export. diff --git a/crates/code-system-graph-core/src/lib.rs b/crates/code-system-graph-core/src/lib.rs index 18adec0..b096e56 100644 --- a/crates/code-system-graph-core/src/lib.rs +++ b/crates/code-system-graph-core/src/lib.rs @@ -86,7 +86,7 @@ pub use events::{ DeliverySemantics, EventBroker, EventDocument, EventEvidenceLine, EventExtractionError, EventObservation, EventRole, EventSchemaDefinition, EventSchemaField, extract_asyncapi, parse_event_source }; pub use execution_policy::{ - CodeGraphCorroborationAnchorLimit, DEFAULT_MAX_CODEGRAPH_CORROBORATION_ANCHORS_PER_REPO, ExecutionLimitExceeded, ExecutionPolicy, ExecutionPolicyOverrides, ExecutionResource, ExecutionSummary, InvalidExecutionPolicy, JobPhase, MonotonicClock, ScanJobTracker + CodeGraphCorroborationAnchorLimit, DEFAULT_MAX_CODEGRAPH_CORROBORATION_ANCHORS_PER_REPO, ExecutionLimitExceeded, ExecutionPolicy, ExecutionPolicyOverrides, ExecutionResource, ExecutionSummary, InvalidExecutionPolicy, JobPhase, MIN_MCP_MARKDOWN_BYTES, MonotonicClock, ScanJobTracker }; pub use extraction_budget::{ BoundedJsonWriter, EXTRACTION_CONTRACT_VERSION, ExtractionBudgetOverrides, ExtractionBudgets, ExtractionClock, ExtractionLimitExceeded, ExtractionResource, ExtractionTracker, InvalidExtractionBudget @@ -144,7 +144,7 @@ pub use pull_requests::{ BitbucketProvider, ChangedFileStatus as PullRequestChangedFileStatus, CheckState, ContractRole, GitHubProvider, PrAuthToken, PrHttpAuthentication, PrHttpMethod, PrHttpRequest, PrHttpResponse, PrHttpTransport, PrHttpTransportError, PullRequestChangedFile, PullRequestCheck, PullRequestCi, PullRequestContractChange, PullRequestCoordinates, PullRequestError, PullRequestInspectRequest, PullRequestInspection, PullRequestListPage, PullRequestListRequest, PullRequestListState, PullRequestMetadata, PullRequestOrder, PullRequestOrderSuggestion, PullRequestOverlap, PullRequestOverlapKind, PullRequestProvider, PullRequestProviderConfig, PullRequestProviderKind, PullRequestRateLimit, PullRequestReadiness, PullRequestRef, PullRequestRepository, PullRequestReview, PullRequestReviewSummary, PullRequestSemanticInput, PullRequestState, PullRequestSummary, PullRequestWarning, ReqwestPrHttpTransport, ReviewState, semantic_pull_request_overlap, suggest_pull_request_order }; pub use query::{ - EdgeKindCost, PathSegment, PathSegmentScope, QueryError, SearchCoverage, SearchExplanation, SearchFilters, SearchHit, SearchReport, SearchRequest, TraversalAlgorithm, TraversalDirection, TraversalFilters, TraversalLimits, TraversalOptions, TraversalPath, TraversalReport, TraversalRequest, search, traverse + AgentNextAction, EdgeKindCost, PathSegment, PathSegmentScope, QueryError, SearchCoverage, SearchExplanation, SearchFilters, SearchHit, SearchReport, SearchRequest, TraversalAlgorithm, TraversalDirection, TraversalFilters, TraversalLimits, TraversalOptions, TraversalPath, TraversalReport, TraversalRequest, search, traverse }; pub use registry::{RegisteredWorkspace, RegistryError, encode_native_path, register_workspace}; pub use secret_safety::{ diff --git a/crates/code-system-graph-core/src/manifest.rs b/crates/code-system-graph-core/src/manifest.rs index d175701..0a2b90e 100644 --- a/crates/code-system-graph-core/src/manifest.rs +++ b/crates/code-system-graph-core/src/manifest.rs @@ -5,9 +5,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::execution_policy::{ - CodeGraphCorroborationAnchorLimit, ExecutionPolicy, ExecutionPolicyOverrides, InvalidExecutionPolicy -}; +use crate::execution_policy::{ExecutionPolicy, ExecutionPolicyOverrides, InvalidExecutionPolicy}; use crate::extraction_budget::{ ExtractionBudgetOverrides, ExtractionBudgets, InvalidExtractionBudget }; @@ -66,7 +64,6 @@ pub struct RepositoryConfig { #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] pub struct ManifestExtensions { repository_use_gitignore: BTreeMap, - max_codegraph_corroboration_anchors_per_repo: Option, } impl ManifestExtensions { @@ -75,12 +72,6 @@ impl ManifestExtensions { pub fn repository_use_gitignore(&self, alias: &str) -> Option { self.repository_use_gitignore.get(alias).copied() } - - /// Returns the configured `CodeGraph` corroboration bound, including `-1` for unlimited. - #[must_use] - pub const fn max_codegraph_corroboration_anchors_per_repo(&self) -> Option { - self.max_codegraph_corroboration_anchors_per_repo - } } #[derive(Deserialize)] @@ -96,7 +87,7 @@ struct WorkspaceManifestWire { #[serde(rename = "extractionBudgets", default)] extraction_budgets: Option, #[serde(rename = "executionPolicy", default)] - execution_policy: Option, + execution_policy: Option, } #[derive(Deserialize)] @@ -112,23 +103,6 @@ struct RepositoryConfigWire { use_gitignore: Option, } -#[derive(Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct ExecutionPolicyOverridesWire { - max_scan_wall_time_ms: Option, - max_no_progress_time_ms: Option, - #[serde(rename = "maxCodeGraphSyncWallTimeMsPerRepo")] - max_codegraph_sync_wall_time_ms_per_repo: Option, - #[serde(rename = "maxCodeGraphCorroborationAnchorsPerRepo")] - max_codegraph_corroboration_anchors_per_repo: Option, - max_worker_memory_bytes: Option, - graceful_termination_ms: Option, - watch_idle_timeout_ms: Option, - max_watch_session_wall_time_ms: Option, - min_watch_rescan_interval_ms: Option, - max_checkpoint_cache_bytes: Option, -} - /// Exact manual relationship or automatic-link suppression. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] @@ -323,13 +297,6 @@ pub fn parse_manifest_with_extensions( ) }) .collect(); - let (execution_policy, max_codegraph_corroboration_anchors_per_repo) = wire - .execution_policy - .map(ExecutionPolicyOverridesWire::into_parts) - .map_or((None, None), |(policy, limit)| (Some(policy), limit)); - if let Some(value) = max_codegraph_corroboration_anchors_per_repo { - CodeGraphCorroborationAnchorLimit::try_from(value)?; - } let manifest = WorkspaceManifest { version: wire.version, name: wire.name, @@ -337,39 +304,18 @@ pub fn parse_manifest_with_extensions( repos, manual_links: wire.manual_links, extraction_budgets: wire.extraction_budgets, - execution_policy, + execution_policy: wire.execution_policy, }; validate_manifest(manifest).map(|manifest| { ( manifest, ManifestExtensions { repository_use_gitignore, - max_codegraph_corroboration_anchors_per_repo, }, ) }) } -impl ExecutionPolicyOverridesWire { - fn into_parts(self) -> (ExecutionPolicyOverrides, Option) { - ( - ExecutionPolicyOverrides { - max_scan_wall_time_ms: self.max_scan_wall_time_ms, - max_no_progress_time_ms: self.max_no_progress_time_ms, - max_codegraph_sync_wall_time_ms_per_repo: self - .max_codegraph_sync_wall_time_ms_per_repo, - max_worker_memory_bytes: self.max_worker_memory_bytes, - graceful_termination_ms: self.graceful_termination_ms, - watch_idle_timeout_ms: self.watch_idle_timeout_ms, - max_watch_session_wall_time_ms: self.max_watch_session_wall_time_ms, - min_watch_rescan_interval_ms: self.min_watch_rescan_interval_ms, - max_checkpoint_cache_bytes: self.max_checkpoint_cache_bytes, - }, - self.max_codegraph_corroboration_anchors_per_repo, - ) - } -} - fn validate_manifest(manifest: WorkspaceManifest) -> Result { if manifest.version != 1 { return Err(ManifestError::UnsupportedVersion { @@ -740,7 +686,7 @@ repos: } #[test] - fn parse_manifest_should_keep_additive_settings_out_of_public_structs() { + fn parse_manifest_should_normalize_execution_policy_settings() { let input = VALID.replace( "name: commerce", "name: commerce\nexecutionPolicy:\n maxCodeGraphCorroborationAnchorsPerRepo: 12", @@ -755,8 +701,13 @@ repos: assert!(manifest.execution_policy.is_some()); assert_eq!(extensions.repository_use_gitignore("web"), Some(true)); + let policy = + ExecutionPolicy::resolve(manifest.execution_policy.as_ref()).expect("effective policy"); assert_eq!( - extensions.max_codegraph_corroboration_anchors_per_repo(), + policy + .max_codegraph_corroboration_anchors_per_repo + .bounded() + .map(std::num::NonZeroUsize::get), Some(12) ); } diff --git a/crates/code-system-graph-core/src/provider.rs b/crates/code-system-graph-core/src/provider.rs index efc88af..ae96a93 100644 --- a/crates/code-system-graph-core/src/provider.rs +++ b/crates/code-system-graph-core/src/provider.rs @@ -9,7 +9,7 @@ use thiserror::Error; use tokio_util::sync::CancellationToken; /// Availability state reported by a local intelligence provider. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum ProviderStatus { /// Provider and repository index are ready. @@ -143,7 +143,7 @@ pub struct ResolveSymbolsRequest { } /// Direction used for local neighbor traversal. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum LocalNeighborDirection { /// Functions or methods that call the anchor. @@ -210,7 +210,7 @@ pub struct ProviderExecution { } /// Exact local symbol returned by a compatible provider contract. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct ResolvedSymbol { /// Local provider symbol identifier; never treated as a global identity. pub local_id: Option, @@ -238,7 +238,7 @@ pub struct ResolveSymbolsResult { } /// One bounded caller or callee. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct LocalNeighbor { /// Local symbol name. pub name: String, diff --git a/crates/code-system-graph-core/src/query.rs b/crates/code-system-graph-core/src/query.rs index 304b67e..830022f 100644 --- a/crates/code-system-graph-core/src/query.rs +++ b/crates/code-system-graph-core/src/query.rs @@ -127,6 +127,17 @@ pub struct SearchCoverage { pub gaps: Vec, } +/// Deterministic non-executing navigation suggested to an agent. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct AgentNextAction { + /// Public tool to call next. + pub tool: String, + /// Exact real identifiers or bounded query arguments for the tool. + pub arguments: BTreeMap, + /// Short explanation of why the action is useful. + pub rationale: String, +} + /// Paginated ranked search result. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct SearchReport { @@ -142,6 +153,9 @@ pub struct SearchReport { pub truncated: bool, /// Search coverage and degradation details. pub coverage: SearchCoverage, + /// Navigation based only on observed repositories and entity identifiers. + #[serde(default)] + pub next_actions: Vec, } /// Traversal strategy used to find confirmed paths. @@ -437,6 +451,7 @@ pub fn search(nodes: &[Node], request: &SearchRequest) -> Result(&self, should_interrupt: F) -> Result<(), StoreError> + where + F: FnMut() -> bool + Send + 'static, + { + self.connection + .progress_handler(1_000, Some(should_interrupt))?; + Ok(()) + } + + /// Opens an exact 1.1.0 store or initializes a new empty database. /// /// # Errors /// @@ -489,7 +506,7 @@ impl SqliteStore { }) } - /// Creates a validated online backup of the exact 1.0.0 schema. + /// Creates a validated online backup of the exact 1.1.0 schema. /// /// A source on read-only media is treated as immutable only when no `SQLite` sidecars exist. /// @@ -501,7 +518,7 @@ impl SqliteStore { backup_restore::backup_file(access_lock.database_path(), destination) } - /// Restores a validated backup with the exact 1.0.0 schema. + /// Restores a validated backup with the exact 1.1.0 schema. /// /// The existing destination is first preserved as a non-overwriting safety backup. /// Restore is refused while another [`SqliteStore`] has the destination open. @@ -541,7 +558,7 @@ impl SqliteStore { /// /// # Errors /// - /// Returns [`StoreError`] if the database is absent, corrupt, or not the exact 1.0.0 schema. + /// Returns [`StoreError`] if the database is absent, corrupt, or not the exact 1.1.0 schema. pub fn open_read_only(path: impl AsRef) -> Result { let path = path.as_ref(); let access_lock = StoreAccessLock::shared(path)?; @@ -3448,6 +3465,25 @@ mod tests { const LOCK_DATABASE_ENV: &str = "CODE_SYSTEM_GRAPH_LOCK_DATABASE"; const LOCK_READY_ENV: &str = "CODE_SYSTEM_GRAPH_LOCK_READY"; + #[test] + fn query_progress_handler_should_interrupt_expensive_reads() + -> Result<(), Box> { + let temporary = tempfile::tempdir()?; + let store = SqliteStore::open(temporary.path().join("graph.db"))?; + store.interrupt_queries_when(|| true)?; + + let result = store.connection.query_row( + "WITH RECURSIVE values_(value) AS (\ + SELECT 1 UNION ALL SELECT value + 1 FROM values_ WHERE value < 1000000) \ + SELECT sum(value) FROM values_", + [], + |row| row.get::<_, i64>(0), + ); + + assert!(result.is_err()); + Ok(()) + } + fn fixture() -> (Vec, Vec, Vec) { let evidence = Evidence { id: EvidenceId::new("evidence:1"), @@ -4105,7 +4141,7 @@ mod tests { fn fresh_database_should_apply_initial_schema() { let result = SqliteStore::in_memory().and_then(|store| store.schema_version()); - assert!(matches!(result, Ok(1))); + assert!(matches!(result, Ok(2))); } #[test] @@ -4133,7 +4169,7 @@ mod tests { |row| row.get::<_, i64>(0), )?; - assert_eq!((store.schema_version()?, table_count), (1, 12)); + assert_eq!((store.schema_version()?, table_count), (2, 12)); Ok(()) } @@ -4145,7 +4181,7 @@ mod tests { super::validate_exact_schema(&connection)?; super::validate_exact_schema(&connection)?; - assert_eq!(super::schema_version(&connection)?, 1); + assert_eq!(super::schema_version(&connection)?, 2); Ok(()) } @@ -4226,6 +4262,22 @@ mod tests { assert!(matches!(result, Err(StoreError::InvalidSchema))); } + #[test] + fn exact_schema_should_reject_structurally_current_v1_database() + -> Result<(), Box> { + let mut connection = rusqlite::Connection::open_in_memory()?; + super::initialize_empty_schema(&mut connection)?; + connection.execute( + "UPDATE schema_metadata SET version = 1 WHERE version = ?1", + [super::LATEST_SCHEMA_VERSION], + )?; + + let result = SqliteStore::from_connection(connection); + + assert!(matches!(result, Err(StoreError::InvalidSchema))); + Ok(()) + } + #[test] fn exact_schema_should_reject_missing_schema_objects() -> Result<(), Box> { @@ -4400,10 +4452,10 @@ mod tests { let database = temporary.path().join("store.db"); let initial = SqliteStore::open(&database)?; - assert_eq!(initial.schema_version()?, 1); + assert_eq!(initial.schema_version()?, 2); drop(initial); let repeated = SqliteStore::open(&database)?; - assert_eq!(repeated.schema_version()?, 1); + assert_eq!(repeated.schema_version()?, 2); Ok(()) } @@ -4803,7 +4855,7 @@ mod tests { workspace_name: "commerce".to_owned(), snapshot_id: "snapshot:query-cache".to_owned(), input_fingerprint: "query:one".to_owned(), - result_summary_json: br#"{"schema_version":1}"#.to_vec(), + result_summary_json: br#"{"schema_version":2}"#.to_vec(), stored_at_unix_ms: 100, expires_at_unix_ms: Some(200), }; diff --git a/docs/AGENT_SETUP.md b/docs/AGENT_SETUP.md index 346ecc6..af2b0b3 100644 --- a/docs/AGENT_SETUP.md +++ b/docs/AGENT_SETUP.md @@ -44,7 +44,7 @@ csgraph plugin create \ Omit `--codegraph` to keep `explore` unavailable, or add `--codegraph-binary /absolute/path/to/codegraph` with the opt-in flag. Generation requires a valid manifest and an existing matching snapshot. Stale snapshots are allowed and reported. `csgraph -1.0.3` must be in the client's `PATH`, and no platform binary is bundled. Repeating the command is a +1.1.0` must be in the client's `PATH`, and no platform binary is bundled. Repeating the command is a no-op only for an exactly identical directory; conflicts, additional files, and symlinks fail without partial writes. @@ -208,6 +208,7 @@ Register a project-local stdio server: ```bash claude mcp add code-system-graph --scope local -- \ csgraph mcp \ + --config /absolute/path/to/workspace/code-system-graph.yaml \ --codegraph \ --workspace commerce \ --database /absolute/path/to/workspace/.code-system-graph/code-system-graph.db @@ -228,6 +229,7 @@ Register the server: ```bash codex mcp add code-system-graph -- \ csgraph mcp \ + --config /absolute/path/to/workspace/code-system-graph.yaml \ --codegraph \ --workspace commerce \ --database /absolute/path/to/workspace/.code-system-graph/code-system-graph.db @@ -249,6 +251,7 @@ For a project-scoped setup, Codex also accepts `.codex/config.toml` in a trusted command = "csgraph" args = [ "mcp", + "--config", "/absolute/path/to/workspace/code-system-graph.yaml", "--codegraph", "--workspace", "commerce", "--database", "/absolute/path/to/workspace/.code-system-graph/code-system-graph.db", @@ -262,6 +265,7 @@ Register a project-scoped server: ```bash gemini mcp add --scope project code-system-graph \ csgraph mcp \ + --config /absolute/path/to/workspace/code-system-graph.yaml \ --codegraph \ --workspace commerce \ --database /absolute/path/to/workspace/.code-system-graph/code-system-graph.db @@ -286,6 +290,8 @@ Create or merge `.agents/mcp_config.json` in the workspace: "command": "csgraph", "args": [ "mcp", + "--config", + "/absolute/path/to/workspace/code-system-graph.yaml", "--codegraph", "--workspace", "commerce", @@ -314,6 +320,8 @@ workspace. An absolute database path identifies the snapshot but does not make t "command": "/absolute/path/to/bin/csgraph", "args": [ "mcp", + "--config", + "/absolute/path/to/workspace/code-system-graph.yaml", "--codegraph", "--workspace", "commerce", @@ -421,7 +429,7 @@ See [MCP reference](MCP.md) for every tool, resource, limit, and administrative If the server is absent or disconnected: -1. run `csgraph mcp --workspace ... --database ...` directly and check stderr; +1. run `csgraph mcp --config ... --workspace ... --database ...` directly and check stderr; 2. verify that the agent can resolve `csgraph` on its `PATH`; 3. replace relative database paths with absolute paths; 4. confirm the workspace name matches the scanned snapshot; diff --git a/docs/CLI.md b/docs/CLI.md index 67e134b..74a3be2 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -103,8 +103,8 @@ contain the exact `/.local/` rule. The ignored `.local/code-system-graph/` directory contains the runtime binding and an ownership receipt with hashes for managed skill files. Both record the generating package version, exact -executable fingerprint, and build-time source commit/dirty state when available. Older owned -bindings without this additive provenance remain readable and replaceable. Existing-plugin mode +executable fingerprint, and build-time source commit/dirty state when available. Receipts and +bindings generated by 1.0.x are rejected. Existing-plugin mode reports `changed: false` for an identical integration. `--replace-generated` atomically replaces only recognized local state and never adopts an unmanaged directory. Updating a changed versioned skill requires `plugin uninstall` followed by `plugin create`. Uninstall requires that receipt and @@ -116,7 +116,7 @@ manifest, database, and optional CodeGraph executable before starting the read-o ## Intelligence ```text -csgraph query --workspace --database +csgraph query --config --workspace --database csgraph trace --from --to --workspace --database csgraph traverse ... csgraph communities ... @@ -146,7 +146,7 @@ email variable selected by `--user-env`. Overlap is local and source-free. ## Delivery and integration ```text -csgraph mcp --workspace --database +csgraph mcp --config --workspace --database csgraph serve --workspace [--host 127.0.0.1] [--port 4767] csgraph hooks install|status|uninstall ... [--codegraph] csgraph completions diff --git a/docs/CODEGRAPH_INTEGRATION.md b/docs/CODEGRAPH_INTEGRATION.md index e192338..3c94772 100644 --- a/docs/CODEGRAPH_INTEGRATION.md +++ b/docs/CODEGRAPH_INTEGRATION.md @@ -92,6 +92,7 @@ because an agent may start outside the workspace directory: ```bash codex mcp add code-system-graph -- \ csgraph mcp \ + --config /absolute/path/to/commerce/code-system-graph.yaml \ --codegraph \ --workspace commerce \ --database /absolute/path/to/commerce/.code-system-graph/code-system-graph.db diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index f37fc7e..ffdb0d4 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -307,6 +307,27 @@ executionPolicy: maxWatchSessionWallTimeMs: 86400000 minWatchRescanIntervalMs: 10000 maxCheckpointCacheBytes: 10737418240 + + maxExploreWallTimeMs: 8000 + maxExploreCodeGraphOperations: 8 + maxExploreConcurrentCodeGraphProcesses: 2 + maxExploreSourceFiles: 25 + maxExploreResolvedSymbols: 5 + maxExploreAnchors: 3 + maxExploreNeighborsPerDirection: 8 + maxExploreLocalRelationships: 48 + maxExploreFederatedHandoffsPerAnchor: 10 + maxExploreFederatedHandoffs: 30 + maxExploreEvidenceLocationsPerHandoff: 4 + maxExploreSourceMarkdownBytes: 262144 + maxExploreEnrichmentBytes: 65536 + maxAgentNextActionsPerResponse: 12 + maxQueryRepositorySuggestions: 20 + + maxMcpToolResponseBytes: 524288 + maxMcpResourceItems: 100 + maxMcpResourceBytes: 262144 + maxMcpSchemaCatalogBytes: 2097152 ``` The defaults allow six hours and 16 GiB per worker, five minutes without verified completed work, @@ -321,12 +342,32 @@ All values must be positive and representable except repository. Unlimited corroboration remains subject to scan/provider time, memory, and request budgets. The independent changed-file ceiling remains 1,024. The no-progress and per-repository CodeGraph deadlines cannot exceed the pass deadline; the termination grace cannot exceed the no-progress -deadline; and watcher sub-deadlines cannot exceed the session deadline. Raising values explicitly +deadline; and watcher sub-deadlines cannot exceed the session deadline. Explore anchors cannot +exceed resolved symbols, child-process concurrency cannot exceed provider operations, local +relationships cannot exceed `anchors × 2 × neighbors`, total handoffs cannot exceed +`anchors × handoffsPerAnchor`, and source/enrichment byte budgets cannot exceed the final MCP tool +response budget. Every multiplication and numeric conversion is checked. Raising values explicitly authorizes greater maximum CPU, memory, or cloud-agent cost. Like extraction budgets, this block is accepted only from a global manifest outside every analyzed checkout; it is rejected in repository local configuration and has no environment or CLI equivalent. Changing it does not invalidate deterministic extractor batches. +Explore budgets use milliseconds, item counts, or decoded UTF-8 Markdown bytes as indicated by +their names. One provider operation is one public context, symbol-resolution, caller, or callee +request; internal transport negotiation consumes the shared deadline and bytes but not the +operation count. `maxMcpToolResponseBytes` applies after Markdown rendering. Resource rendering +uses its own item and byte limits, with a separate schema-catalog ceiling. Reducing a value may +produce explicit gaps or truncations but cannot be overridden upward by a request, environment +variable, repository-local file, or CLI argument. The three final MCP Markdown byte limits must be +at least 256 bytes so every response can retain its mandatory control and truncation block. + +`csgraph config show` emits schema v2, every default/effective value, origin +`global_manifest`, `scan_fingerprint`, and `agent_delivery_fingerprint`. Scan-affecting limits, +including the corroboration bound, change the scan fingerprint and publish a new snapshot identity +on the next scan. Explore, Query, MCP-tool, and MCP +resource delivery limits change only the agent-delivery fingerprint, so they do not invalidate +snapshots, extractor batches, checkpoints, or deterministic scan results. + ## Optional features ### CodeGraph @@ -347,6 +388,7 @@ For MCP: ```bash csgraph mcp \ + --config code-system-graph.yaml \ --codegraph \ --workspace commerce \ --database .code-system-graph/code-system-graph.db @@ -380,6 +422,7 @@ workspace mutations: ```bash csgraph mcp \ + --config code-system-graph.yaml \ --admin \ --workspace commerce \ --database .code-system-graph/code-system-graph.db diff --git a/docs/DATA_MODEL.md b/docs/DATA_MODEL.md index 558669a..5b592e7 100644 --- a/docs/DATA_MODEL.md +++ b/docs/DATA_MODEL.md @@ -186,9 +186,9 @@ Unknown, unavailable, corrupt, or partial inputs never produce a fresh or safe c ## Schema, locking, and recovery -The definitive 1.0.0 schema version and an opaque database instance identity are recorded in -`schema_metadata`. The complete SQLite schema must match the initial 1.0.0 definition exactly. -Any other development schema is rejected with an instruction to remove the disposable database +The definitive 1.1.0 schema version (`2`) and an opaque database instance identity are recorded in +`schema_metadata`. The complete SQLite schema must match the 1.1.0 definition exactly. Version 1 +databases from the 1.0.x line and any other development schema are rejected with an instruction to remove the disposable database and run a full scan. Operational checkpoints are bound to the instance identity, so a sidecar from another or rebuilt database is discarded rather than reused. diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index 1557d43..c58a043 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -151,6 +151,7 @@ Search is the usual entry point: ```bash csgraph query "orders" \ + --config code-system-graph.yaml \ --workspace my-project \ --database .code-system-graph/code-system-graph.db ``` @@ -179,6 +180,7 @@ The MCP server runs over stdio and is started by the agent when needed: ```bash csgraph mcp \ + --config code-system-graph.yaml \ --codegraph \ --workspace my-project \ --database .code-system-graph/code-system-graph.db @@ -190,6 +192,7 @@ example, Codex can write its configuration with: ```bash codex mcp add code-system-graph -- \ csgraph mcp \ + --config /absolute/path/to/my-project/code-system-graph.yaml \ --codegraph \ --workspace my-project \ --database /absolute/path/to/my-project/.code-system-graph/code-system-graph.db diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 1ddc419..259cebb 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -5,7 +5,7 @@ for maintainers are in [Release engineering](RELEASE.md). ## Current availability -Code System Graph `1.0.3` is published on [crates.io](https://crates.io/crates/code-system-graph) +Code System Graph `1.1.0` is published on [crates.io](https://crates.io/crates/code-system-graph) and [GitHub Releases](https://github.com/dertin/code-system-graph/releases). Native release CI validates Linux x86_64/ARM64, macOS x86_64/ARM64, and Windows x86_64 before their archives are published. @@ -84,8 +84,8 @@ For a Linux x86_64 archive: ```bash sha256sum --ignore-missing --check SHA256SUMS -tar -xzf code-system-graph-x86_64-unknown-linux-gnu-v1.0.3.tgz -PREFIX="$HOME/.local" ./code-system-graph-x86_64-unknown-linux-gnu-v1.0.3/install.sh +tar -xzf code-system-graph-x86_64-unknown-linux-gnu-v1.1.0.tgz +PREFIX="$HOME/.local" ./code-system-graph-x86_64-unknown-linux-gnu-v1.1.0/install.sh ``` Replace the target in the archive name with `x86_64-apple-darwin`, @@ -93,7 +93,7 @@ Replace the target in the archive name with `x86_64-apple-darwin`, same installer. The Windows archive is a ZIP file. Verify `SHA256SUMS`, extract -`code-system-graph-x86_64-pc-windows-msvc-v1.0.3.zip`, and add its `bin` directory containing +`code-system-graph-x86_64-pc-windows-msvc-v1.1.0.zip`, and add its `bin` directory containing `csgraph.exe` and `code-system-graph-hooks.exe` to `PATH`. `PREFIX` defaults to `$HOME/.local`. The installer places binaries under `$PREFIX/bin`, installed @@ -153,9 +153,12 @@ The package installer preserves replaced binaries under: $PREFIX/share/code-system-graph/backups/ ``` -The 1.0.x release line supports one exact initial database schema. An incompatible local database is -disposable: remove it and run a full scan. Backup and restore accept only that exact schema and -never migrate it. +Version 1.1.0 deliberately rejects 1.0.x databases, query caches, checkpoints, generated plugin +bindings, and delivery contracts. Archive or remove the old `.code-system-graph` database and +operational sidecars, regenerate any Agent Plugin/binding, then run a complete scan. Backup and +restore accept only the exact 1.1.0 schema and never migrate older state. Direct MCP registrations +must add `--config`; MCP consumers must switch from JSON/`structuredContent` to the sole Markdown +text block, while HTTP/CLI consumers must accept delivery schema v2. ## Uninstall @@ -171,7 +174,7 @@ cargo uninstall code-system-graph-hooks Run `uninstall.sh` from the verified extracted package with the same prefix: ```bash -PREFIX="$HOME/.local" ./code-system-graph-x86_64-unknown-linux-gnu-v1.0.3/uninstall.sh +PREFIX="$HOME/.local" ./code-system-graph-x86_64-unknown-linux-gnu-v1.1.0/uninstall.sh ``` Before uninstalling either installation type, remove any optional agent hooks: diff --git a/docs/INTERFACES.md b/docs/INTERFACES.md index 0a6fbc4..a001a38 100644 --- a/docs/INTERFACES.md +++ b/docs/INTERFACES.md @@ -20,7 +20,8 @@ pull-request listing and overlap, contracts, bounded graph export, doctor diagno workspace cleanup, hooks, HTTP serving, and shell completions. Scan supports normal changed-input reuse, one-repository selection that preserves other persisted batches, and explicit forced recomputation. -Structured commands emit one JSON document on stdout; diagnostics use stderr. Graph export emits +Structured commands emit one schema-v2 JSON document on stdout; diagnostics use stderr. `explore` +returns `ToolEnvelope` like the other typed HTTP/CLI tools. Graph export emits the selected complete JSON, GraphML, or Markdown document. Interface failures use stable exit-code classes for invalid input, not found, conflicts, partial results, unavailable capabilities, timeouts, internal errors, and cancellation. @@ -29,7 +30,8 @@ timeouts, internal errors, and cancellation. The read-only catalog covers status, query, trace, impact, changes, contracts, communities, source-context handoff, and explicitly consented pull-request inspection. Inputs have generated -JSON schemas and non-overridable server bounds. +JSON input schemas and non-overridable server bounds. Each call result contains one Markdown text +block, no duplicated JSON envelope, no `structuredContent`, and no result `outputSchema`. Stable resources: @@ -42,10 +44,14 @@ Stable resources: - `code-system-graph://workspace/{name}/communities` - `code-system-graph://workspace/{name}/schema` - `code-system-graph://workspace/{name}/coverage` + +Resource template, advertised through `resources/templates/list`: + - `code-system-graph://evidence/{id}` The server rejects resource access outside its configured workspace. Lists and resource payloads -are bounded and source-free. Evidence resources contain identity, provenance, confidence, +are bounded `text/markdown` and source-free. The schema catalog uses fenced JSON inside Markdown. +Evidence resources contain identity, provenance, confidence, locators, extractor version, and observed commit only. Administrative tools are omitted from discovery unless the server starts in the explicit admin @@ -81,8 +87,9 @@ alias and exact staged state; advisory mode fails open. ## Deliberate limits -- No delivery adapter returns source bodies, secrets, provider patches, or raw provider responses. +- No delivery adapter returns source bodies except the explicitly ephemeral `explore` contract; + secrets, provider patches, and raw provider responses are never returned. - GraphML and Markdown exports are deterministic views, not import or round-trip formats. - Doctor reports unknown when an observation is absent; absence never becomes healthy. -- HTTP does not expose administrative, pull-request, or source-context routes in 1.0.0. +- HTTP does not expose administrative, pull-request, or source-context routes in 1.1.0. - Hook guidance cannot guarantee that a host follows the suggested provider routing. diff --git a/docs/MCP.md b/docs/MCP.md index 18767b6..aa063f1 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -1,11 +1,16 @@ # MCP Surface Code System Graph exposes a small read-only-by-default stdio MCP surface and advertises the server -name `code_system_graph` during initialization. Every tool returns a versioned JSON envelope and -uses server-enforced bounds. Persisted graph tools exclude source bodies; `explore` is the explicit +name `code_system_graph` during initialization. Every tool returns exactly one `text` content block +containing bounded Markdown; MCP responses do not include `structuredContent` or result +`outputSchema`. Persisted graph tools exclude source bodies; `explore` is the explicit source-bearing, ephemeral exception. Mutating tools are absent from discovery unless the server starts with the explicit administrative profile. +Initialization instructions describe this Markdown-only delivery explicitly. Schema version 2 +remains the logical tool/resource contract, while fenced JSON appears only inside the schema +catalog. Final tool, resource, and schema-catalog Markdown budgets must each be at least 256 bytes. + ## `status` Returns schema, integrity, snapshot freshness, and per-checkout status for the configured workspace. @@ -30,7 +35,9 @@ paths include coverage gaps rather than asserting independence. ## `query` Searches current graph entities using exact, normalized, FTS5, scope, centrality, community, -evidence-quality, and freshness signals. Every hit includes a score decomposition. Inputs support +evidence-quality, and freshness signals. Every hit includes a score decomposition. Zero-hit +responses explain that Query searches persisted entities rather than code bodies and recommend +`explore` when the question names a registered alias. Inputs support bounded pagination and graph-entity filters. Each hit includes a stable `NodeId`; clients select the intended hit and pass that identifier to `trace`. The server does not silently choose between close or ambiguous candidates. @@ -39,9 +46,13 @@ between close or ambiguous candidates. Delegates one focused repository-local symbol, flow, architecture, or implementation question to the public CodeGraph adapter. `repository` may be omitted only when the workspace has exactly one -registered repository. `max_files` defaults to 12 and is capped at 25; timeout is five seconds and -retained output is capped at 256 KiB. The returned `data.content` may contain source and exists only -in the response. Code System Graph never persists, caches, logs, or audits it. Set +registered repository. Omitted `max_files` resolves to `min(12, maxExploreSourceFiles)`; an +explicit value may reduce that limit but cannot raise it. Explore returns repository identity and +freshness, source Markdown, resolved symbols, callers/callees, persisted federated handoffs, +coverage gaps, deterministic next actions, effective limits, provider operation counts, +concurrency, retained bytes, and degradations. `source_markdown` may contain source and exists only +in the response. Code System Graph never persists, caches, logs, or audits it, although the MCP +host may retain requests and responses in its own history. Set `CODE_SYSTEM_GRAPH_CODEGRAPH_BINARY` on the trusted server process to select a non-default executable. ## `communities` @@ -84,21 +95,26 @@ responses are discarded. - Search limits are at most 100 results. - Community limits are at most 100 communities. - Trace depth is capped by the server. -- Local exploration is capped at 25 files, 256 KiB, and five seconds. +- Local exploration uses the effective global `executionPolicy` budgets documented in + [Configuration](CONFIGURATION.md); tool inputs can only request less work. - FTS input is bound as a quoted phrase, not arbitrary FTS syntax. - No tool initializes, synchronizes, installs, upgrades, or reads internal CodeGraph storage. - Remote providers are disabled by default, HTTPS-only, allowlisted, bounded, and redirect-free. - Bitbucket Data Center is not implemented. -- Tool errors use structured envelopes and do not write protocol noise to stdout. +- Tool errors retain status, freshness, warnings, coverage, verifiable locations, truncations, and + next actions in Markdown and do not write protocol noise to stdout. - Resources are constrained to the configured workspace and expose source-free metadata only. ## Resources and schemas -The server publishes bounded `code-system-graph://workspaces`, workspace overview, status, repository, -service, contract, community, coverage, schema-catalog, and exact evidence-metadata resources. -Resource templates reject access to another workspace. The schema catalog is generated from the -public Rust request and report types. Normal resources are capped at 256 KiB; the complete schema -catalog has a separate 2 MiB hard cap. +The server publishes bounded `code-system-graph://workspaces`, workspace overview, status, +repository, service, contract, community, coverage, and schema-catalog resources. Exact evidence +metadata is discoverable through the `code-system-graph://evidence/{id}` resource template and is +read only after substituting a concrete stable ID. Every resource uses `text/markdown`; the schema +catalog embeds each generated JSON Schema in a closed fenced `json` block. Resource item and byte +limits come from the immutable workspace policy. Every resource collection reports its `total`, +retained count, and truncation state in Markdown; status, coverage, and freshness use +collection-specific names for the same metadata. ## Administrative profile @@ -131,6 +147,9 @@ secret values, or credentials. ## Server CodeGraph policy +Direct MCP mode requires `--config`; binding mode loads the canonical manifest path stored in the +binding. Both MCP and HTTP validate the global manifest and workspace name before serving. + Start `csgraph mcp` or `csgraph serve` with `--codegraph` to enable automatic impact enrichment and scan corroboration. `--codegraph-binary ` selects a trusted executable. Trusted deployments may instead set `CODE_SYSTEM_GRAPH_CODEGRAPH=1` or diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 58152ea..6686798 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -1,8 +1,9 @@ -# Code System Graph 1.0.3 Release +# Code System Graph 1.1.0 Release -Code System Graph 1.0.3 adds configurable CodeGraph corroboration bounds, opt-in Git-native ignore -semantics, portable workspace-bound Agent Plugins, and ignored local workspace bindings for -existing portable plugins. The source tree and package version are `1.0.3`. Continuous integration +Code System Graph 1.1.0 is an intentionally breaking agent-delivery release. MCP tools and +resources are bounded Markdown, typed HTTP/CLI envelopes use delivery schema v2, Explore performs +multi-step ephemeral source/flow enrichment under one global policy, and Query supplies grounded +next actions. The source tree and package version are `1.1.0`. Continuous integration runs on GitHub at `https://github.com/dertin/code-system-graph`. Platform claims below require native build, test, packaging, and archive-smoke evidence from the @@ -24,7 +25,7 @@ target. ## Included capabilities -Code System Graph 1.0.x includes: +Code System Graph 1.1.0 includes: - multi-repository workspace registration with lossless native-path identity; - incremental, source-free extraction for package, HTTP, event, GraphQL, RPC, data, @@ -35,10 +36,31 @@ Code System Graph 1.0.x includes: - configurable per-repository corroboration bounds and opt-in Git-native ignore discovery; - portable Agent Plugins 1.0.0 generation and ownership-checked local bindings for versioned base plugins with a read-only MCP and existing routing guidance; +- schema-v2 JSON over CLI/HTTP and single-block Markdown over MCP, without `structuredContent` or + result `outputSchema`; +- bounded Explore source, symbols, callers/callees, federated handoffs, coverage, actions, and + execution accounting; +- one immutable global `executionPolicy` covering scan, Explore, Query, tools, and resources, with + separate scan and agent-delivery fingerprints; - CLI, read-only MCP stdio, optional authenticated HTTP, exports, diagnostics, and host hooks; - exact-schema SQLite backup, restore, and integrity validation; - deterministic Linux package archives, CycloneDX SBOMs, and SHA-256 checksums. +## Breaking migration from 1.0.x + +- Databases created by 1.0.x are rejected. Remove or archive the old database and operational + sidecars, then perform a complete 1.1.0 scan; there is no in-place migration or legacy mode. +- Regenerate Agent Plugins and local bindings. Binding and ownership contracts are version 2 and + older generated state is not accepted as a runtime binding. +- Direct `csgraph mcp` invocations must add `--config `; `--binding` mode resolves + the manifest recorded in the binding. +- MCP consumers must read the sole Markdown text block. They must not expect `structuredContent`, + JSON tool envelopes, or result `outputSchema` metadata. +- HTTP and CLI consumers must accept `schema_version: 2`; Explore data is now `ExploreReport`, with + source in `source_markdown` rather than `LocalContextResult.content`. +- Workspace manifests may omit all new policy fields to use the documented defaults. Inputs such + as Explore `max_files` can request less work but cannot exceed the effective global policy. + ## Local validation Run the main Rust validation from a clean checkout. This requires stable, nightly with the rustfmt @@ -85,8 +107,8 @@ GNU tar, and SHA-256 tooling. The workspace MSRV remains 1.97.1 and is validated ```text SOURCE_DATE_EPOCH=0 scripts/package-release.sh x86_64-unknown-linux-gnu -scripts/smoke-install.sh dist/code-system-graph-x86_64-unknown-linux-gnu-v1.0.3 -sha256sum --check dist/code-system-graph-x86_64-unknown-linux-gnu-v1.0.3.sha256 +scripts/smoke-install.sh dist/code-system-graph-x86_64-unknown-linux-gnu-v1.1.0 +sha256sum --check dist/code-system-graph-x86_64-unknown-linux-gnu-v1.1.0.sha256 ``` The package contains `csgraph`, `code-system-graph-hooks`, the visible Agent integration template @@ -120,7 +142,7 @@ clean `main` branch aligned with `origin/main`, Cargo credentials for crates.io, selects `prepare`: ```text -.github/workflows/release.sh 1.0.3 prepare +.github/workflows/release.sh 1.1.0 prepare ``` Preparation runs the complete publish-readiness suite and dry-runs all five packages without @@ -128,7 +150,7 @@ creating a tag, publishing a crate, or dispatching a workflow. To perform the ir pass `publish` explicitly: ```text -.github/workflows/release.sh 1.0.3 publish +.github/workflows/release.sh 1.1.0 publish ``` Publish mode verifies that the workspace repository matches `origin`, creates and pushes the diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 3fc688f..69dee15 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -12,8 +12,8 @@ cargo-fuzz = true [dependencies] libfuzzer-sys = "0.4.13" -code-system-graph-core = { version = "1.0.3", path = "../crates/code-system-graph-core" } -code-system-graph-model = { version = "1.0.3", path = "../crates/code-system-graph-model" } +code-system-graph-core = { version = "1.1.0", path = "../crates/code-system-graph-core" } +code-system-graph-model = { version = "1.1.0", path = "../crates/code-system-graph-model" } serde_json = "1.0.151" [[bin]] diff --git a/scripts/validate-publish-ready.sh b/scripts/validate-publish-ready.sh index c83b2b8..7cd976b 100755 --- a/scripts/validate-publish-ready.sh +++ b/scripts/validate-publish-ready.sh @@ -16,7 +16,7 @@ RUSTDOCFLAGS="-D warnings" cargo +stable doc --workspace --exclude code-system-g cargo deny check cargo audit --deny warnings if command -v gitleaks >/dev/null 2>&1; then - gitleaks dir --no-banner --redact "$ROOT" + gitleaks dir --no-banner --redact . elif command -v docker >/dev/null 2>&1; then container="$(docker create zricethezav/gitleaks:latest dir /tmp --no-banner --redact)" trap 'docker rm -f "$container" >/dev/null 2>&1 || true' EXIT @@ -39,7 +39,7 @@ fi "$ROOT/scripts/smoke-install.sh" "$PACKAGE_DIR" if rg -n -i \ - 'phase[ _-]?[0-9]+|fase[ _-]?[0-9]+|PLAN\.md|STATUS\.md|PROMPT-(GPT|RESUME)|schema v[2-9][0-9]*|3700fe3|93c0078|/opt/[[:alnum:]_.-]+' \ + 'phase[ _-]?[0-9]+|fase[ _-]?[0-9]+|PLAN\.md|STATUS\.md|PROMPT-(GPT|RESUME)|3700fe3|93c0078|/opt/[[:alnum:]_.-]+' \ "$ROOT" \ --glob '!target/**' \ --glob '!dist/**' \