diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..a07850d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,81 @@ +name: Bug report +description: Report a reproducible defect in the MCP server or its contracts. +title: "bug: " +body: + - type: markdown + attributes: + value: >- + Do not include secrets or undisclosed vulnerabilities. Use SECURITY.md + for private security reports. + - type: input + id: version + attributes: + label: Version or commit + description: Include the exact crate version or Git commit. + placeholder: 0.2.0 or abc1234 + validations: + required: true + - type: dropdown + id: tool_pack + attributes: + label: Tool pack + options: + - core (default, 13 tools) + - standard (33 tools) + - full (47 tools) + - unknown + validations: + required: true + - type: input + id: client_protocol + attributes: + label: Client and negotiated protocol + placeholder: Codex CLI; 2025-11-25 + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Minimal reproduction + description: Provide ordered steps and a minimal, redacted request. + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected result + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual result + validations: + required: true + - type: dropdown + id: contract_impact + attributes: + label: Contract impact + options: + - Incorrect success or fail-open behavior + - Tool discovery or schema mismatch + - Persistence or audit mismatch + - Startup or transport failure + - Other + validations: + required: true + - type: textarea + id: logs + attributes: + label: Redacted diagnostics + description: Include stderr only; remove secrets and personal paths. + render: text + - type: checkboxes + id: checks + attributes: + label: Preflight + options: + - label: I reproduced this on the stated version or commit. + required: true + - label: I removed secrets and unrelated private data. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3ba13e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..ec1eedc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,55 @@ +name: Feature request +description: Propose a user outcome or auditable contract improvement. +title: "feat: " +body: + - type: textarea + id: problem + attributes: + label: Problem + description: What user problem cannot be solved with the current contract? + validations: + required: true + - type: textarea + id: outcome + attributes: + label: Desired user flow + description: Show the smallest useful request-to-audit path. + validations: + required: true + - type: textarea + id: evidence + attributes: + label: Evidence and failure boundary + description: What must be supplied, and when must the server stop or fail closed? + validations: + required: true + - type: dropdown + id: tool_pack + attributes: + label: Narrowest justified tool pack + options: + - core + - standard + - full compatibility only + - no new tool + - unsure + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Existing tools and alternatives + description: Explain why composition of current tools is insufficient. + validations: + required: true + - type: checkboxes + id: checks + attributes: + label: Contract checks + options: + - label: The proposal does not claim access to hidden chain of thought. + required: true + - label: Model-backed or retrieval-backed behavior has an explicit provider boundary. + required: true + - label: I checked features/manifest.json for an overlapping planning record. + required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..26dcf02 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,21 @@ +## Outcome + +Describe the user-visible result and why the change is needed. + +## Contract impact + +- [ ] No public MCP behavior changes. +- [ ] Tool-pack inventory and `evals/tool-packs.json` are synchronized. +- [ ] Evidence, provider, and fail-closed boundaries remain explicit. +- [ ] Persistence, audit, and compatibility effects are documented. + +## Verification + +List exact commands and results. For behavior changes, include the failing test +observed before the implementation and its passing result afterward. + +- [ ] `just check` +- [ ] `just eval` when protocol, schema, routing, or governance changed +- [ ] `just audit` when dependencies changed +- [ ] Documentation and examples match the implemented behavior +- [ ] No secrets, local absolute paths, generated caches, or private artifacts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61d922b..1dd3e18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [main] +permissions: + contents: read + jobs: reasonkit-think-mcp: runs-on: ubuntu-latest @@ -15,11 +18,34 @@ jobs: run: | rustup toolchain install 1.95.0 --profile minimal --component rustfmt,clippy rustup default 1.95.0 + - name: Install uv 0.12.1 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + version: "0.12.1" + enable-cache: false - name: Format run: cargo fmt --check - name: Clippy - run: cargo clippy -- -D warnings + run: cargo clippy --all-targets --all-features -- -D warnings + - name: Rust unit and integration tests + run: cargo test --all-targets --all-features + - name: Contract unit tests + run: python3 -m unittest discover -s tests -p "test_*.py" -v + - name: Package release candidate + run: CARGO_TARGET_DIR=target/package-check cargo package --locked + - name: Validate MCP Registry schema + run: uvx --from check-jsonschema==0.38.0 check-jsonschema --schemafile https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json server.json + - name: Install cargo-dist 0.32.0 + run: cargo install cargo-dist --version 0.32.0 --locked + - name: Validate configuration-only cargo-dist manifest + run: dist manifest --artifacts=local --output-format=json --no-local-paths - name: Build release run: cargo build --release - - name: Smoke test (stdio MCP) + - name: Full-pack smoke test (stdio MCP) run: python3 scripts/smoke_test.py + - name: MCP contract evaluations + run: python3 evals/run_contract_evals.py --binary target/release/reasonkit-think-mcp + - name: Install cargo-audit + run: cargo install cargo-audit --locked + - name: Dependency security audit + run: cargo audit --deny warnings diff --git a/.gitignore b/.gitignore index d1b1e97..1663837 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ .env.* /target_check/ /local-target/ -scripts/__pycache__/ +**/__pycache__/ +*.py[cod] diff --git a/CHANGELOG.md b/CHANGELOG.md index 4de842c..66d2bb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,21 @@ All notable changes to ReasonKit Think are documented here. -## Unreleased +## Unreleased — 0.2.0 release candidate +- Added explicit `--version`, `--help`, and secret-free `doctor --json` + diagnostics, including fail-closed backend validation, without changing the + no-argument MCP stdio contract. +- Upgraded the official Rust MCP SDK from `rmcp` 1.6.0 to 3.1.4 while keeping + the server stdio-only and extending negotiation checks through MCP 2026-07-28. +- Added startup-fixed `core` (13, default), `standard` (33), and `full` (47) + tool packs with a full-pack compatibility rollback and pack-aware guidance. +- Added `reasoning://config/tool-packs` and dual text plus + `structuredContent` JSON for successful results and handler-generated + application errors. +- Removed the unbound `reasonkit-mem` cache dependency. The full-pack + `think_query` compatibility route now fails closed with + `experimental_memory_unavailable` instead of simulating empty retrieval. - Added `run_thinking_mode` as the recommended user-facing entry point for Auto, Quick, Explore, Map, Sketch, and Test workflows. - Added native MCP resource discovery/read support for `reasoning://thinking-modes` @@ -19,3 +32,20 @@ All notable changes to ReasonKit Think are documented here. `get_thinking_history` with `deliberation_id`, and boolean-like audit flags. - Removed default pseudo-evidence from Auto/autopilot verification; unsupported claims remain `DATA_DEFICIT` until caller evidence is supplied. +- Preserved verification blockers across repeated verification calls, prevented + assumptions from self-verifying without evidence, enforced ReAct action and + observation transitions, and rejected unknown pipeline stages. +- Made verification criticality monotonic across normalized-claim upserts, + made assumption `critical: true` authoritative over conflicting lower + criticality, and kept pipeline and consensus policy overrides call-scoped. +- Required an agent-authored frontier for consensus, excluded heuristic + scaffold/root nodes from allowed answers, and blocked failed pipeline gates + and unresolved checkpoints. Authorship is provenance- and node-type-backed; + reserved-tag spoofing is rejected, and answer provenance lists only selected + candidates. Blocked MCP responses use the exact `consensus_blocked` code and + omit the candidate final answer. +- Added deterministic canonical JSON and PR-ready Markdown reasoning audits with + consistent artifact IDs and neutralized caller-controlled Markdown content. +- Added deterministic protocol, pack, eight-check adversarial governance, + packaging, Registry, cargo-dist, and dependency-audit gates for CI and local + release checks. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..026b266 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,44 @@ +# Contributing to ReasonKit Think + +ReasonKit Think is an auditable stdio MCP server. Contributions should preserve +its fail-closed evidence boundaries, deterministic contracts, and small default +tool surface. + +## Set up + +Install Rust 1.95, Python 3, `uv`, and `just`, then work from a source checkout: + +```sh +cargo build --locked +just check +``` + +`just check` runs formatting, Clippy, Rust tests, and Python contract tests. Run +`just eval` for protocol, schema, tool, routing, or governance changes. Run +`just audit` for dependency changes; the zero-warning baseline and prior cleanup +are documented in [`docs/dependency-audit.md`](docs/dependency-audit.md). +For release-metadata changes, run `just package-check` and `just registry-check`. +If editing `dist-workspace.toml`, install cargo-dist 0.32.0 with +`cargo install cargo-dist --version 0.32.0 --locked`, then run `just dist-check`. + +## Make a focused change + +1. Open an issue for behavior that changes a public tool, schema, or policy. +2. Add a failing test before changing behavior. +3. Keep stdout reserved for MCP frames; diagnostics belong on stderr. +4. Update documentation and contract fixtures with the implementation. +5. Use Conventional Commits, for example `fix(protocol): reject unknown stages`. + +Tool discovery is pack-aware. Any tool-surface change must update +[`evals/tool-packs.json`](evals/tool-packs.json) and preserve exact lexical +counts for every affected pack. New tools should land in the narrowest justified +pack; `full` is the compatibility escape hatch, not the default. + +## Pull-request evidence + +Describe the user-visible outcome, contract or security impact, tests run, and +any known limitation. Include the exact failing-then-passing test for behavior +changes. Do not commit secrets, local absolute paths, generated caches, private +planning material, or release automation that publishes on a tag. + +By contributing, you agree that your work is licensed under Apache-2.0. diff --git a/Cargo.lock b/Cargo.lock index 3d87150..0efe88f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - [[package]] name = "aho-corasick" version = "1.1.4" @@ -17,12 +11,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -32,112 +20,11 @@ dependencies = [ "libc", ] -[[package]] -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", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -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", -] - -[[package]] -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", -] - [[package]] name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "arc-swap" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" -dependencies = [ - "rustversion", -] - -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "atomic-polyfill" -version = "1.0.3" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" -dependencies = [ - "critical-section", -] +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "atomic-waker" @@ -151,126 +38,24 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "axum" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" -dependencies = [ - "async-trait", - "axum-core", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "sync_wrapper", - "tower 0.5.3", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", - "tower-layer", - "tower-service", -] - [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" -[[package]] -name = "bitpacking" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a7139abd3d9cebf8cd6f920a389cf3dc9576172e32f4563f188cae3c3eb019" -dependencies = [ - "crunchy", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bon" -version = "3.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe" -dependencies = [ - "bon-macros", - "rustversion", -] - -[[package]] -name = "bon-macros" -version = "3.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" -dependencies = [ - "darling 0.23.0", - "ident_case", - "prettyplease", - "proc-macro2", - "quote", - "rustversion", - "syn", -] - [[package]] name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "bytes" version = "1.11.1" @@ -284,17 +69,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] -[[package]] -name = "census" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" - [[package]] name = "cfg-if" version = "1.0.4" @@ -307,6 +84,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + [[package]] name = "chrono" version = "0.4.44" @@ -321,81 +109,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "clap" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "cobs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" -dependencies = [ - "thiserror 2.0.18", -] - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -404,209 +117,45 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.17" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" dependencies = [ "libc", ] -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", -] - -[[package]] -name = "darling_core" -version = "0.20.11" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", + "darling_core", + "darling_macro", ] [[package]] name = "darling_core" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" dependencies = [ "ident_case", "proc-macro2", "quote", "strsim", - "syn", + "syn 3.0.3", ] [[package]] name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core 0.20.11", - "quote", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core 0.23.0", - "quote", - "syn", -] - -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core 0.9.12", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", - "serde_core", -] - -[[package]] -name = "derive_builder" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.2" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ - "darling 0.20.11", - "proc-macro2", + "darling_core", "quote", - "syn", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" -dependencies = [ - "derive_builder_core", - "syn", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", + "syn 3.0.3", ] [[package]] @@ -615,28 +164,7 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" dependencies = [ - "dirs-sys 0.5.0", -] - -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys 0.4.1", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.4.6", - "windows-sys 0.48.0", + "dirs-sys", ] [[package]] @@ -647,8 +175,8 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users 0.5.2", - "windows-sys 0.61.2", + "redox_users", + "windows-sys 0.59.0", ] [[package]] @@ -659,119 +187,33 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] -[[package]] -name = "downcast-rs" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" - [[package]] name = "dyn-clone" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "embedded-io" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" - -[[package]] -name = "embedded-io" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -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", -] - -[[package]] -name = "fastdivide" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afc2bd4d5a73106dd53d10d73d3401c2f32730ba2c0b93ddb888a8983680471" - -[[package]] -name = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" - [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -781,26 +223,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "fs4" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" -dependencies = [ - "rustix", - "windows-sys 0.59.0", -] - [[package]] name = "futures" version = "0.3.32" @@ -857,7 +279,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -889,25 +311,6 @@ dependencies = [ "slab", ] -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -921,20 +324,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -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", -] - [[package]] name = "getrandom" version = "0.4.2" @@ -942,60 +331,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", + "js-sys", "libc", - "r-efi 6.0.0", + "r-efi", + "rand_core", "wasip2", "wasip3", + "wasm-bindgen", ] -[[package]] -name = "h2" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap 2.14.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hash32" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" -dependencies = [ - "byteorder", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", "foldhash", ] @@ -1005,44 +355,12 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" -[[package]] -name = "heapless" -version = "0.7.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" -dependencies = [ - "atomic-polyfill", - "hash32", - "rustc_version", - "serde", - "spin", - "stable_deref_trait", -] - [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "htmlescape" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" - [[package]] name = "http" version = "1.4.0" @@ -1082,12 +400,6 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - [[package]] name = "hyper" version = "1.9.0" @@ -1098,11 +410,9 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2", "http", "http-body", "httparse", - "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1126,35 +436,6 @@ dependencies = [ "webpki-roots", ] -[[package]] -name = "hyper-timeout" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" -dependencies = [ - "hyper", - "hyper-util", - "pin-project-lite", - "tokio", - "tower-service", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.20" @@ -1172,21 +453,10 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.3", - "system-configuration", + "socket2 0.5.10", "tokio", "tower-service", "tracing", - "windows-registry", -] - -[[package]] -name = "hyperloglogplus" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "621debdf94dcac33e50475fdd76d34d5ea9c0362a834b9db08c3024696c1fbe3" -dependencies = [ - "serde", ] [[package]] @@ -1328,16 +598,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - [[package]] name = "indexmap" version = "2.14.0" @@ -1350,52 +610,18 @@ dependencies = [ "serde_core", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "ipnet" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - [[package]] name = "js-sys" version = "0.3.97" @@ -1420,24 +646,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" -[[package]] -name = "levenshtein_automata" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" - [[package]] name = "libc" version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - [[package]] name = "libredox" version = "0.1.16" @@ -1447,57 +661,24 @@ dependencies = [ "libc", ] -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - [[package]] name = "litemap" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.5", -] - [[package]] name = "lru-slab" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "lz4_flex" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a" -dependencies = [ - "twox-hash", -] - [[package]] name = "matchers" version = "0.2.0" @@ -1507,58 +688,12 @@ dependencies = [ "regex-automata", ] -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - -[[package]] -name = "measure_time" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51c55d61e72fc3ab704396c5fa16f4c184db37978ae4e94ca8959693a235fc0e" -dependencies = [ - "log", -] - [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" -[[package]] -name = "memmap2" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" -dependencies = [ - "libc", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - [[package]] name = "mio" version = "1.2.0" @@ -1570,54 +705,15 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "murmurhash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2195bf6aa996a481483b29d62a7663eed3fe39600c460e323f8ff41e90bdd89b" - -[[package]] -name = "native-tls" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "nu-ansi-term" version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] -[[package]] -name = "num-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - [[package]] name = "num-traits" version = "0.2.19" @@ -1625,17 +721,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", -] - -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", ] [[package]] @@ -1644,61 +729,6 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "oneshot" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "269bca4c2591a28585d6bf10d9ed0332b7d76900a1b02bec41bdc3a2cdcda107" - -[[package]] -name = "openssl" -version = "0.10.80" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" -dependencies = [ - "bitflags 2.11.1", - "cfg-if", - "foreign-types", - "libc", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "openssl-sys" -version = "0.9.116" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "opentelemetry" version = "0.32.0" @@ -1709,7 +739,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.18", + "thiserror", "tracing", ] @@ -1719,63 +749,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "ownedbytes" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fbd56f7631767e61784dc43f8580f403f4475bd4aaa4da003e6295e1bab4a7e" -dependencies = [ - "stable_deref_trait", -] - -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - -[[package]] -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 0.9.12", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi", -] - -[[package]] -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 0.5.18", - "smallvec", - "windows-link", -] - [[package]] name = "pastey" version = "0.2.2" @@ -1788,51 +761,12 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "pin-project" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "postcard" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" -dependencies = [ - "cobs", - "embedded-io 0.4.0", - "embedded-io 0.6.1", - "heapless", - "serde", -] - [[package]] name = "potential_utf" version = "0.1.5" @@ -1842,21 +776,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "prettyplease" version = "0.2.37" @@ -1864,7 +783,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", ] [[package]] @@ -1876,60 +795,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "prost" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-derive" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" -dependencies = [ - "anyhow", - "itertools", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "prost-types" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" -dependencies = [ - "prost", -] - -[[package]] -name = "qdrant-client" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cef4e669bcf9c07471463adab5ee080dd9bc9381f3652ea4981f6030b2c309" -dependencies = [ - "anyhow", - "derive_builder", - "futures", - "futures-util", - "parking_lot 0.12.5", - "prost", - "prost-types", - "reqwest", - "semver", - "serde", - "serde_json", - "thiserror 1.0.69", - "tokio", - "tonic", -] - [[package]] name = "quinn" version = "0.11.9" @@ -1943,8 +808,8 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.3", - "thiserror 2.0.18", + "socket2 0.5.10", + "thiserror", "tokio", "tracing", "web-time", @@ -1952,20 +817,21 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" dependencies = [ "bytes", - "getrandom 0.3.4", + "getrandom 0.4.2", "lru-slab", - "rand 0.9.4", + "rand", + "rand_pcg", "ring", "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror", "tinyvec", "tracing", "web-time", @@ -1980,7 +846,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.3", + "socket2 0.5.10", "tracing", "windows-sys 0.60.2", ] @@ -1994,12 +860,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - [[package]] name = "r-efi" version = "6.0.0" @@ -2007,138 +867,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -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", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rand_distr" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" -dependencies = [ - "num-traits", - "rand 0.8.6", -] - -[[package]] -name = "rayon" -version = "1.12.0" +name = "rand" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "either", - "rayon-core", + "chacha20", + "getrandom 0.4.2", + "rand_core", ] [[package]] -name = "rayon-core" -version = "1.13.0" +name = "rand_core" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] -name = "reasonkit-mem" -version = "0.1.7" +name = "rand_pcg" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5baf8557e8b9a73636cea0cd6f17d7d06e5d9762d200431b5f7b5f3bae7ab68" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" dependencies = [ - "anyhow", - "async-trait", - "chrono", - "clap", - "crc32fast", - "crossbeam-utils", - "dashmap", - "dirs", - "hex", - "lz4_flex", - "num_cpus", - "postcard", - "qdrant-client", - "rayon", - "reqwest", - "rmp-serde", - "serde", - "serde_bytes", - "serde_json", - "sha2", - "sled", - "tantivy", - "thiserror 1.0.69", - "tokio", - "tracing", - "uuid", + "rand_core", ] [[package]] name = "reasonkit-think-mcp" -version = "0.1.3" +version = "0.2.0" dependencies = [ "anyhow", "chrono", "directories", "once_cell", "opentelemetry", - "reasonkit-mem", "regex", "reqwest", "rmcp", @@ -2151,35 +913,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.11.1", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 1.0.69", -] - [[package]] name = "redox_users" version = "0.5.2" @@ -2188,7 +921,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -2208,7 +941,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2248,22 +981,17 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", - "encoding_rs", "futures-channel", "futures-core", "futures-util", - "h2", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", - "hyper-tls", "hyper-util", "js-sys", "log", - "mime", - "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -2274,16 +1002,13 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-native-tls", "tokio-rustls", - "tokio-util", - "tower 0.5.3", + "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", "web-sys", "webpki-roots", ] @@ -2304,65 +1029,37 @@ dependencies = [ [[package]] name = "rmcp" -version = "1.6.0" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12ca9067b5ebfbd5b3fcdc4acfceb81aa7d5ab2a879dff7cb75d22434276aad" +checksum = "1a15bc53261a9dc37e105df006e4656c598379a8f9581f8950debb130f27a7cf" dependencies = [ - "async-trait", "chrono", "futures", + "indexmap", "pastey", "pin-project-lite", "rmcp-macros", "schemars", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-util", "tracing", + "uuid", ] [[package]] name = "rmcp-macros" -version = "1.6.0" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7caa6743cc0888e433105fe1bc551a7f607940b126a37bc97b478e86064627eb" +checksum = "a85d45508e9b4ba024fe996c2638799635d75b6dd0ba8f32ccf08f8026f0c780" dependencies = [ - "darling 0.23.0", + "darling", "proc-macro2", "quote", "serde_json", - "syn", -] - -[[package]] -name = "rmp" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" -dependencies = [ - "num-traits", -] - -[[package]] -name = "rmp-serde" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" -dependencies = [ - "rmp", - "serde", -] - -[[package]] -name = "rust-stemmers" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" -dependencies = [ - "serde", - "serde_derive", + "syn 3.0.3", ] [[package]] @@ -2371,35 +1068,12 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.11.1", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - [[package]] name = "rustls" version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ - "log", "once_cell", "ring", "rustls-pki-types", @@ -2408,27 +1082,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" version = "1.14.1" @@ -2462,15 +1115,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "schannel" -version = "0.1.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "schemars" version = "1.2.1" @@ -2494,36 +1138,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "security-framework" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" -dependencies = [ - "bitflags 2.11.1", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -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", + "syn 2.0.117", ] [[package]] @@ -2542,16 +1157,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde_bytes" -version = "0.11.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" -dependencies = [ - "serde", - "serde_core", -] - [[package]] name = "serde_core" version = "1.0.228" @@ -2569,7 +1174,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2580,7 +1185,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2608,17 +1213,6 @@ dependencies = [ "serde", ] -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -2634,324 +1228,96 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "simd-adler32" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" - -[[package]] -name = "sketches-ddsketch" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b" -dependencies = [ - "serde", -] - [[package]] name = "slab" version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "sled" -version = "0.34.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" -dependencies = [ - "crc32fast", - "crossbeam-epoch", - "crossbeam-utils", - "fs2", - "fxhash", - "libc", - "log", - "parking_lot 0.11.2", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "system-configuration" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" -dependencies = [ - "bitflags 2.11.1", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tantivy" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502915c7381c5cb2d2781503962610cb880ad8f1a0ca95df1bae645d5ebf2545" -dependencies = [ - "aho-corasick", - "arc-swap", - "base64", - "bitpacking", - "bon", - "byteorder", - "census", - "crc32fast", - "crossbeam-channel", - "downcast-rs", - "fastdivide", - "fnv", - "fs4", - "htmlescape", - "hyperloglogplus", - "itertools", - "levenshtein_automata", - "log", - "lru", - "lz4_flex", - "measure_time", - "memmap2", - "once_cell", - "oneshot", - "rayon", - "regex", - "rust-stemmers", - "rustc-hash", - "serde", - "serde_json", - "sketches-ddsketch", - "smallvec", - "tantivy-bitpacker", - "tantivy-columnar", - "tantivy-common", - "tantivy-fst", - "tantivy-query-grammar", - "tantivy-stacker", - "tantivy-tokenizer-api", - "tempfile", - "thiserror 2.0.18", - "time", - "uuid", - "winapi", -] +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] -name = "tantivy-bitpacker" -version = "0.9.0" +name = "smallvec" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b04eed5108d8283607da6710fe17a7663523440eaf7ea5a1a440d19a1448b6" -dependencies = [ - "bitpacking", -] +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] -name = "tantivy-columnar" -version = "0.6.0" +name = "socket2" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b628488ae936c83e92b5c4056833054ca56f76c0e616aee8339e24ac89119cd" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ - "downcast-rs", - "fastdivide", - "itertools", - "serde", - "tantivy-bitpacker", - "tantivy-common", - "tantivy-sstable", - "tantivy-stacker", + "libc", + "windows-sys 0.52.0", ] [[package]] -name = "tantivy-common" -version = "0.10.0" +name = "socket2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f880aa7cab0c063a47b62596d10991cdd0b6e0e0575d9c5eeb298b307a25de55" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ - "async-trait", - "byteorder", - "ownedbytes", - "serde", - "time", + "libc", + "windows-sys 0.60.2", ] [[package]] -name = "tantivy-fst" -version = "0.5.0" +name = "stable_deref_trait" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18" -dependencies = [ - "byteorder", - "regex-syntax", - "utf8-ranges", -] +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] -name = "tantivy-query-grammar" -version = "0.25.0" +name = "strsim" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "768fccdc84d60d86235d42d7e4c33acf43c418258ff5952abf07bd7837fcd26b" -dependencies = [ - "nom", - "serde", - "serde_json", -] +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] -name = "tantivy-sstable" -version = "0.6.0" +name = "subtle" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8292095d1a8a2c2b36380ec455f910ab52dde516af36321af332c93f20ab7d5" -dependencies = [ - "futures-util", - "itertools", - "tantivy-bitpacker", - "tantivy-common", - "tantivy-fst", - "zstd", -] +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] -name = "tantivy-stacker" -version = "0.6.0" +name = "syn" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d38a379411169f0b3002c9cba61cdfe315f757e9d4f239c00c282497a0749d" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ - "murmurhash32", - "rand_distr", - "tantivy-common", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "tantivy-tokenizer-api" -version = "0.6.0" +name = "syn" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23024f6aeb25ceb1a0e27740c84bdb0fae52626737b7e9a9de6ad5aa25c7b038" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ - "serde", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "tempfile" -version = "3.27.0" +name = "sync_wrapper" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ - "fastrand", - "getrandom 0.4.2", - "once_cell", - "rustix", - "windows-sys 0.61.2", + "futures-core", ] [[package]] -name = "thiserror" -version = "1.0.69" +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ - "thiserror-impl 1.0.69", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -2960,18 +1326,7 @@ version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -2982,7 +1337,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2994,37 +1349,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" -dependencies = [ - "num-conv", - "time-core", -] - [[package]] name = "tinystr" version = "0.8.3" @@ -3059,9 +1383,7 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot 0.12.5", "pin-project-lite", - "signal-hook-registry", "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", @@ -3075,17 +1397,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", + "syn 2.0.117", ] [[package]] @@ -3098,17 +1410,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-util" version = "0.7.18" @@ -3122,60 +1423,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tonic" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" -dependencies = [ - "async-stream", - "async-trait", - "axum", - "base64", - "bytes", - "flate2", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-timeout", - "hyper-util", - "percent-encoding", - "pin-project", - "prost", - "rustls-native-certs", - "rustls-pemfile", - "socket2 0.5.10", - "tokio", - "tokio-rustls", - "tokio-stream", - "tower 0.4.13", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "indexmap 1.9.3", - "pin-project", - "pin-project-lite", - "rand 0.8.6", - "slab", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower" version = "0.5.3" @@ -3197,13 +1444,13 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.11.1", + "bitflags", "bytes", "futures-util", "http", "http-body", "pin-project-lite", - "tower 0.5.3", + "tower", "tower-layer", "tower-service", "url", @@ -3240,7 +1487,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3288,18 +1535,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "twox-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" - -[[package]] -name = "typenum" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" - [[package]] name = "unicode-ident" version = "1.0.24" @@ -3330,24 +1565,12 @@ dependencies = [ "serde", ] -[[package]] -name = "utf8-ranges" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" - [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - [[package]] name = "uuid" version = "1.23.1" @@ -3356,7 +1579,6 @@ checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", - "serde_core", "wasm-bindgen", ] @@ -3366,18 +1588,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "want" version = "0.3.1" @@ -3453,7 +1663,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -3483,33 +1693,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap 2.14.0", + "indexmap", "wasm-encoder", "wasmparser", ] -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "wasmparser" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.1", + "bitflags", "hashbrown 0.15.5", - "indexmap 2.14.0", + "indexmap", "semver", ] @@ -3542,28 +1739,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -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", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-core" version = "0.62.2" @@ -3585,7 +1760,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3596,7 +1771,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3605,17 +1780,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - [[package]] name = "windows-result" version = "0.4.1" @@ -3634,15 +1798,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -3679,21 +1834,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -3727,12 +1867,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -3745,12 +1879,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -3763,12 +1891,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3793,12 +1915,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -3811,12 +1927,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -3829,12 +1939,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -3847,12 +1951,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -3899,9 +1997,9 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap 2.14.0", + "indexmap", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -3917,7 +2015,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -3929,8 +2027,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.1", - "indexmap 2.14.0", + "bitflags", + "indexmap", "log", "serde", "serde_derive", @@ -3949,7 +2047,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.14.0", + "indexmap", "log", "semver", "serde", @@ -3984,30 +2082,10 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] -[[package]] -name = "zerocopy" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zerofrom" version = "0.1.8" @@ -4025,7 +2103,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -4065,7 +2143,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4073,31 +2151,3 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/Cargo.toml b/Cargo.toml index 59e6f65..5472d3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,30 +1,35 @@ [package] name = "reasonkit-think-mcp" -version = "0.1.3" +version = "0.2.0" edition = "2024" # Match repo `rust-toolchain.toml` (latest stable); `rmcp` is edition-2024-friendly. rust-version = "1.95" license = "Apache-2.0" authors = ["Len P. van der Hof "] -description = "Rust MCP server for auditable sequential and tree-of-thoughts reasoning, aligned with ReasonKit skills." +description = "Agent-native MCP server for evidence-gated reasoning, fail-closed verification, and exportable decision audits." repository = "https://github.com/reasonkit/reasonkit-think" documentation = "https://docs.rs/reasonkit-think-mcp" homepage = "https://reasonkit.sh" readme = "README.md" keywords = ["mcp", "reasoning", "llm", "agents", "tree-of-thoughts"] categories = ["development-tools"] -exclude = ["target/", "scripts/__pycache__/"] +include = [ + "/src/**", + "/Cargo.toml", + "/Cargo.lock", + "/README.md", + "/CHANGELOG.md", + "/LICENSE", +] [dependencies] # Official MCP Rust SDK: https://github.com/modelcontextprotocol/rust-sdk — see docs/MCP_STACK_RESEARCH.md -rmcp = { version = "1.6.0", default-features = false, features = [ +rmcp = { version = "3.1.4", default-features = false, features = [ "server", "macros", "schemars", "transport-io", ] } -# Published crates.io dep (path `../reasonkit-mem` for umbrella dev checkouts). -reasonkit-mem = { version = "0.1.7", default-features = false, features = ["compression"] } tokio = { version = "1.52", features = ["io-std", "macros", "rt", "rt-multi-thread"] } anyhow = "1.0" serde = { version = "1.0", features = ["derive"] } diff --git a/README.md b/README.md index a2a1ed0..8f9cd43 100644 --- a/README.md +++ b/README.md @@ -1,285 +1,241 @@

- ReasonKit-think — Advanced Reasoning MCP for AI Coding Agents + ReasonKit Think — auditable decision protocol for MCP agents

-# 🧠 ReasonKit Think MCP +# ReasonKit Think MCP

- Advanced Reasoning MCP for AI Coding Agents
- CoT + ToT + GoT + Verification + Governance Routing + Auditable decisions and fail-closed evidence gates for MCP agents
+ Designed, Not Dreamed. Turn prompts into protocols.

- Rust - MCP - License - Status - CI + + CI status + + + Crates.io version + + + docs.rs + + + Downloads + + Rust 1.95+ + MCP via rmcp + + Apache-2.0 + + + v0.2 release candidate +

-

- GitHub - · - Crates.io - · - reasonkit.sh -

+ReasonKit Think is a Rust stdio MCP server that helps a host agent turn a +decision into an inspectable graph, test critical claims against supplied +evidence, stop when required evidence is missing, and export an audit artifact. + +The host agent remains the semantic engine. ReasonKit Think does not expose a +model's private chain of thought and does not invent model-backed or +retrieval-backed findings. It records only content explicitly submitted through +its tools, labels deterministic heuristics, and fails closed at unsupported +provider boundaries. ---- +- GitHub: +- Crate: +- MCP Registry name: `mcp-name: io.github.reasonkit/reasonkit-think` -## 🎯 Why ReasonKit Think? +## Quick start -Most AI agents today either reason shallowly and move too fast (causing costly mistakes) or reason deeply but become hard to debug, blindly trusting their own hallucinations. +### 1. Install the binary on PATH -**ReasonKit Think** bridges this gap. It provides a structured, auditable, and mathematically grounded reasoning engine directly to your favorite MCP-compatible AI clients (Cursor, Claude Code, Gemini CLI, Copilot). +Prerequisites: Rust 1.95+ and an MCP-compatible host. -We transform abstract AI "thinking" into concrete, inspectable, and governable **Protocols**. +```sh +git clone https://github.com/reasonkit/ReasonKit-think.git +cd ReasonKit-think +cargo install --locked --path . +reasonkit-think-mcp --version +reasonkit-think-mcp doctor --json +``` -### The ReasonKit Advantage: -* **Anti-Hallucination:** Strict evidence-based verification gates. Claims without proof are marked `DATA_DEFICIT` and fail closed. -* **Traceable Auditability:** Every thought, branch, and decision is logged in a stateful DAG (Directed Acyclic Graph) for post-mortem analysis. -* **Dynamic Paradigms:** Seamlessly switch between Chain-of-Thought (CoT), Tree-of-Thoughts (ToT), and Graph-of-Thoughts (GoT) on the fly. -* **Skeleton-of-Thought Phases:** For multi-aspect problems, route into outline-first skeleton branches, elaborate them independently, then merge through GoT checkpoints. -* **Honest Provider Boundaries:** Heuristic outputs are labeled as heuristic; model-backed and retrieval-backed analysis fail closed until real provider bindings are configured and implemented. -* **State Survival & Observability:** Local snapshots survive process restarts, and telemetry resources expose tool latency, verification outcomes, pruning, and graph-depth metrics. -* **Assumption-Led Reasoning:** Critical premises are tracked in an assumption ledger and can block convergence until resolved. -* **Replayable Convergence:** Agents can replay a session timeline, run checkpoints, and converge only after quality, evidence, and assumption gates pass. +This source-checkout command is the presently verifiable `0.2.0` release +candidate path. The Registry metadata is not live yet; after v0.2.0 publication +the equivalent crates.io install will be: ---- +```sh +cargo install --locked --version 0.2.0 reasonkit-think-mcp +``` -## 🧠 Thinking Modes +Do not use an unversioned crates.io install to evaluate this release candidate: +it currently resolves an older published release. `cargo install` places the +executable in Cargo's binary directory, normally `~/.cargo/bin`; add that +directory to `PATH` if the version check cannot find it. -ReasonKit Think now uses accessible user-facing Thinking Modes. The academic method names are still supported internally for compatibility, but most users should start with these names: +### 2. Register the stdio server -| Mode | Internal behavior | Cost | Best for | Aliases | -| --- | --- | --- | --- | --- | -| **Auto** | Hybrid router / adaptive mode selector | Medium | Default entry point for most work | Autopilot, Guided, Smart, Adaptive, Flow, Default | -| **Quick** | CoT-style concise reasoning audit | Low | Fast first pass, simple debugging, concise review | Sprint, Simple, Linear, First Pass, Baseline, Trace | -| **Explore** | Tree-of-Thoughts branching search | High | Options, hypotheses, tradeoffs, comparisons | Branch, Diverse, Brainstorm, Compare, Scenario, Fork | -| **Map** | Graph-of-Thoughts synthesis | High | Dependencies, conflicts, refinements, merged plans | Architect, Synthesis, Weave, Network, Merge, System | -| **Sketch** | Skeleton-of-Thought outline-first expansion | Medium | Multi-section answers, plans, or independent lenses | Outline, Blueprint, Scaffold, Skeleton, Draft, Frame | -| **Test** | CoVe + ReasonKit validation pipeline | Medium to high | Verification, release gates, go/no-go decisions | Verify, Audit, Proof, Guard, Red Team, Check | +Use the executable name, not a machine-specific absolute path. -**Auto is the default.** Tell ReasonKit what you need; it chooses the right reasoning path and returns an auditable result. +```sh +codex mcp add reasonkit-think -- reasonkit-think-mcp +claude mcp add --scope user reasonkit-think -- reasonkit-think-mcp +gemini mcp add --scope user reasonkit-think reasonkit-think-mcp +copilot mcp add reasonkit-think -- reasonkit-think-mcp +``` -Recommended first tool: +For Cursor and other JSON-configured hosts: ```json { - "tool": "run_thinking_mode", - "arguments": { - "mode": "Auto", - "intent": "Compare the migration options, verify risky claims, and return an auditable recommendation." + "mcpServers": { + "reasonkit-think": { + "type": "stdio", + "command": "reasonkit-think-mcp", + "args": [] + } } } ``` -`Test` means claim verification and governance; it does not run your project test suite. - -Clients can discover the full machine-readable mode contract through the native -MCP resource `reasoning://thinking-modes`, including aliases, internal method -mapping, estimated cost, required behavior, safety rules, and recommended tools. - -> **Example Prompt:** -> *"Use Auto mode with reasonkit-think. Compare the migration options, verify risky claims, and return an auditable recommendation with evidence gaps."* +See [client-specific setup](docs/clients/) and the reusable +[configuration examples](examples/client-configs/). -The compatibility mapping is straightforward: +### 3. Run the auditable decision path -- `Quick` maps to `cot`-style linear audit output. -- `Explore` maps to `tot` branching, scoring, and pruning. -- `Map` maps to `got` graph dependencies and synthesis. -- `Sketch` uses a `got` session plus Skeleton-of-Thought tools. -- `Test` uses the ReasonKit verification and governance pipeline. +Ask your host agent: ---- +> Use reasonkit-think Auto mode. Compare the options, state the critical +> assumptions, author the decision nodes, attach explicit evidence to risky +> claims, stop if a critical gap remains, and return the decision plus its +> audit artifact. -## 🛡️ The Verification & Governance Engine +The complete core path is: -Reasoning is useless if it's based on fabricated facts. ReasonKit Think enforces **CoVe (Chain of Verification)** and **Triangulation**. +1. `run_thinking_mode` — start in Auto and create the working decision graph. +2. `add_thought_node` — replace or refine a returned scaffold with the host + agent's actual analysis. Consensus never promotes a heuristic scaffold or + the original goal into a final answer. +3. `verify_thoughts` — submit explicit claims and evidence; missing evidence is + `DATA_DEFICIT`, not a guessed success. +4. `run_reasoning_checkpoint` — evaluate blockers and readiness. +5. `consensus_answer` — produce a decision only when the configured gates allow + it and an agent-authored candidate is on the frontier. +6. `export_reasoning_audit` — return the graph, verification matrix, route, and + recorded limitations. -When you trigger a deep deliberation, the server builds a **Verification Matrix** from claims you supply (or that the **host agent** derives and passes to `verify_thoughts`). The MCP server does not run an LLM to extract claims from thought text. -1. **Registers Claims:** Records factual or architectural assertions with explicit evidence tiers (via `verify_thoughts` / thinking-mode `Test`). -2. **Evaluates Evidence:** Applies tier rules and triangulation heuristics to each claim. -3. **Fails Closed:** If a critical claim has conflicting sources (`SOURCE_CONFLICT`) or missing data (`DATA_DEFICIT`), governance routes return `GATHER_MORE_EVIDENCE` instead of unsafe conclusions. +Use `record_assumption` before verification when a premise must remain visible +and block the decision until resolved. -**Agent-native protocol:** `expand_thoughts` emits labeled **scaffolds**; the host agent must replace them with substantive nodes via `add_thought_node`. Responses include `next_action` coaching, optional ReAct nodes (`record_reasoning_action` / `record_reasoning_observation`), structural `mcts_select_path`, and cross-session `query_failure_memory`. +## Tool packs ---- +`REASONKIT_TOOL_PACK` controls discovery breadth without changing stored data. -## 🚀 Quickstart & Installation - -**Prerequisites:** Rust 1.95+ and an `rmcp`-compatible MCP host. - -### Install via Crates.io (Recommended) -```bash -cargo install reasonkit-think-mcp -``` - -### Build from Source -```bash -git clone https://github.com/reasonkit/reasonkit-think.git -cd reasonkit-think -export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$PWD/target}" -cargo build --release -``` - -To run a quick health check of the server and its tool surface: -```bash -./scripts/smoke_test.py -``` - -To verify the release binary, protocol handshake, schema compatibility, and -local Copilot/Cursor/Claude config paths: -```bash -./scripts/client_compat_check.py -``` - -### Docker - -The image packages the stdio MCP server for supervised MCP hosts: -```bash -docker build -t reasonkit-think-mcp . -docker run --rm -i reasonkit-think-mcp -``` +| Pack | Tools | Intended use | +| --- | ---: | --- | +| `core` | 13 | Default. Complete route → deliberate → evidence → gate → decision → audit path. | +| `standard` | 33 | Adds graph, scoring, pruning, replay, quality, and Skeleton-of-Thought operations. | +| `full` | 47 | compatibility escape hatch for existing clients that depend on the complete legacy inventory. | ---- +Keep the default `core` pack for new installations. Set the environment variable +on the MCP server only when a client needs a broader surface: -## ⚙️ MCP Client Configuration - -Add `reasonkit-think-mcp` to your favorite AI IDE or CLI. **Ensure you provide the ABSOLUTE PATH to the release binary.** If you built from source with the default command above, that path is usually: - -```text -/path/to/reasonkit-think/target/release/reasonkit-think-mcp -``` - -
-Cursor & Cursor Agent - -Add under `mcpServers` in `~/.cursor/mcp.json` or `.cursor/mcp.json`: ```json { "mcpServers": { "reasonkit-think": { "type": "stdio", - "command": "/ABSOLUTE/PATH/TO/reasonkit-think-mcp", + "command": "reasonkit-think-mcp", "args": [], - "env": { "TMPDIR": "/tmp" } + "env": { "REASONKIT_TOOL_PACK": "full" } } } } ``` -Reload MCP from Cursor settings. -
- -
-Claude Code -Add to your `~/.claude.json`: -```json -"reasonkit-think": { - "type": "stdio", - "command": "/ABSOLUTE/PATH/TO/reasonkit-think-mcp", - "args": [], - "env": { "TMPDIR": "/tmp" } -} +The exact lexical inventories are versioned in +[`evals/tool-packs.json`](evals/tool-packs.json) and enforced in CI. + +## Thinking modes + +Auto is the recommended entry point. The other names make routing intent +explicit while retaining compatibility with the underlying reasoning methods. + +| Mode | Behavior | Best for | +| --- | --- | --- | +| **Auto** | Selects an appropriate supported path | Most decisions | +| **Quick** | Concise linear audit | First passes and simple reviews | +| **Explore** | Branch, score, and prune | Options and competing hypotheses | +| **Map** | Link dependencies and synthesize | Architecture and system decisions | +| **Sketch** | Outline, elaborate, then merge | Multi-part plans and documents | +| **Test** | Verification and governance pipeline | Release gates and go/no-go decisions | + +`Test` validates supplied claims; it does not run a repository's test suite. +The machine-readable mode catalog is available at +`reasoning://thinking-modes`. + +## Evidence and audit contract + +- Claims and evidence come from the host agent or user. The server does not run + an LLM to extract facts from hidden reasoning. +- Evidence tiers, independence groups, contradictions, and ambiguity are + evaluated deterministically. +- Critical missing or conflicting evidence routes to `GATHER_MORE_EVIDENCE`. +- Unresolved high/critical assumptions block convergence under the default + policy. +- Heuristic scores are structural proxies with provenance and limitations; they + are not semantic confidence. +- Mutable tool calls persist submitted state locally so sessions can be replayed + and audited. +- The active transport is stdio. No daemon, hosted service, or telemetry export + is required. + +ReasonKit Think cannot read a repository, URL, or `@mention` by itself. Have the +host use its normal filesystem/browser tools, then pass the relevant evidence +packet into ReasonKit Think. + +## Development and verification + +```sh +just check # formatting, clippy, Rust tests, contract unit tests +just build # release binary +just eval # exact pack/protocol matrix plus planted fail-closed cases +just audit # deny vulnerabilities and advisory warnings +just package-check # build and verify the publishable crate archive +just registry-check # validate server.json against the pinned Registry schema +just dist-check # validate the workflow-free cargo-dist manifest ``` -Restart your Claude Code session. -
- -
-Other Clients (Gemini CLI, Codex, Copilot, OpenCode) - -For other clients, configure a standard `stdio` MCP server pointing to the absolute path of `reasonkit-think-mcp`. See the `docs/clients/` folder for comprehensive setup guides for all major AI coding platforms. -
- -Reusable JSON snippets are available in `examples/client-configs/`. - -Workflow request examples are available in `examples/`: -* `run-thinking-mode-auto.json` -* `run-thinking-mode-explore.json` -* `run-thinking-mode-sketch.json` -* `run-thinking-mode-test.json` -* `feature-triage-auto-map.json` +The full-pack end-to-end smoke path is: -If a client reports `ENOENT` or "failed to connect", rebuild the release binary and rerun the compatibility check: - -```bash -export CARGO_TARGET_DIR="$PWD/target" -cargo build --release -./scripts/client_compat_check.py +```sh +python3 scripts/smoke_test.py ``` ---- - -## 🗣️ Natural Wording Triggers - -You do not have to remember exact MCP tool names. `run_thinking_mode`, -`reasoning_intent_router`, and `reasoning_autopilot` understand both the new -mode names and legacy method names: - -* **Auto:** `"auto"`, `"autopilot"`, `"guided"`, `"choose for me"`, `"default"` -* **Quick:** `"quick"`, `"simple"`, `"sprint"`, `"linear reasoning"`, `"first pass"` -* **Explore:** `"explore options"`, `"branch"`, `"brainstorm"`, `"compare"`, `"fork"` -* **Map:** `"map"`, `"architect"`, `"synthesis"`, `"merge paths"`, `"network"` -* **Sketch:** `"sketch"`, `"outline first"`, `"blueprint"`, `"scaffold"`, `"skeleton"` -* **Test:** `"test"`, `"verify"`, `"audit"`, `"proof"`, `"red team"`, `"check"` -* **Support layer:** `"assumption"`, `"checkpoint"`, `"quality gate"`, `"replay"`, `"memory snapshot"`, `"go/no-go"` - -Use `docs/README.md` for the full vocabulary, mode workflows, and tool map. - -Common client argument shapes are accepted: arrays may be passed directly or as -JSON-stringified arrays, numeric fields may be numeric strings, boolean fields -may be boolean-like strings, and graph tools accept friendly node references -such as `frontier`, `root`, and 1-based indexes like `"1"`. +Architecture, tools, prompts, and resources are indexed in +[`docs/README.md`](docs/README.md). The deterministic runtime contract is +described in [`evals/README.md`](evals/README.md). -ReasonKit Think does not inspect local files, repositories, URLs, or `@mentions` -by itself. For requests such as “rank features from `features/todo/`”, first use -your client’s filesystem/search tools to read the files, then pass the extracted -feature packet into `run_thinking_mode` Auto and Map mode. The workflow resource -`reasoning://workflows/feature-triage` is built for that pattern. +## Distribution status ---- +`server.json` and `dist-workspace.toml` describe the `0.2.0` release candidate. +This is not yet a live Registry record: crates.io publication must complete +first, the Registry manifest must resolve that exact package version, and the +post-publish validation must pass. The cargo-dist file is configuration only; +this repository intentionally has no active release workflow, tag, or publish +automation in this tranche. -## Production Contract +## Community -ReasonKit Think is explicit about what it does and does not do: +See [CONTRIBUTING.md](CONTRIBUTING.md) for development and contract checks, +[SECURITY.md](SECURITY.md) for private vulnerability reporting, and +[features/README.md](features/README.md) for the machine-checked planning-record +inventory. -* `heuristic` outputs are deterministic structural proxies with provenance and limitations. -* `model_backed` and `retrieval` provider modes are exposed as fail-closed boundaries in `reasoning://config/providers`; the server does not manufacture semantic confidence when no real provider binding exists. -* `record_assumption` and `set_assumption_status` expose a first-class assumption ledger; unresolved high/critical assumptions gate consensus and convergence. -* `run_reasoning_checkpoint`, `replay_reasoning_session`, and `converge_reasoning` make readiness, replayability, and final synthesis explicit MCP operations. -* `apply_reasoning_pattern` gives agents reusable reasoning modes such as debugging, scientific method, systems mapping, dialectical synthesis, and code reasoning. -* `start_skeleton_of_thought` and `run_skeleton_elaboration` implement a routed SoT workflow. Unsuitable tasks are blocked by default, heuristic mode creates scaffolds/prompt packs only, and model-backed mode fails closed until a real provider exists. -* `plan_tool_sequence` returns stage-aware tool call templates; it plans but never executes tools on behalf of the caller. -* `export_memory_snapshot` returns a memory-ready payload. External sinks are explicit `boundary_unavailable` responses unless a caller persists the payload separately. -* `run_thinking_mode` is the primary operational mode runner for Auto, Quick, Explore, Map, Sketch, and Test. -* `reasoning://thinking-modes` exposes the public Thinking Mode contract used by router/autopilot/run outputs. -* Mutable tools persist state to `~/.local/share/sh.reasonkit.think/state.json` after releasing store locks. -* `reasoning://telemetry/summary` reports in-process counters for MCP clients, alongside OpenTelemetry metric instruments. -* The active transport is `stdio`; `reasoning://config/transports` reports streamable HTTP as not active in this build. +## License ---- - -## 📜 Showcase Prompt (Try this!) - -Want to test the full power of ReasonKit Think? Copy and paste this to your agent: - -> *"Use the reasonkit-think reasoning system to think this through deeply and transparently. Our production database is experiencing 100% CPU utilization, but traffic hasn't spiked. We have 3 theories: a bad index from yesterday's migration, a runaway background job, or a hardware degradation. -> -> Please approach this in a way that is highly auditable: -> 1. Start with a quick first pass to clarify assumptions (CoT). -> 2. Switch to deeper Tree-of-Thought (ToT) reasoning to generate specific diagnostic strategies for all 3 theories. -> 3. Verify critical claims (e.g., 'we can safely kill the background job') with evidence quality in mind. -> 4. Use Graph-of-Thoughts (GoT) to distill the branches into a single, concrete, prioritized action plan. -> 5. Export a traceable reasoning audit showing why this plan is trustworthy."* - ---- +Apache-2.0. See [LICENSE](LICENSE).

Powered by ReasonKit

- -**License:** Apache-2.0 -*Designed, Not Dreamed. Turn Prompts into Protocols.* -[reasonkit.sh](https://reasonkit.sh) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..dc3a4fa --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,28 @@ +# Security Policy + +## Supported versions + +Security fixes target the default branch and, when practical, the latest +published release. The `0.2.0` release candidate is not yet published; older +releases receive fixes on a best-effort basis. + +## Report a vulnerability + +Please report vulnerabilities privately through +[GitHub's private vulnerability form](https://github.com/reasonkit/ReasonKit-think/security/advisories/new). +If that form is unavailable, email `Lenvanderhof@ReasonKit.sh` with the subject +`ReasonKit Think security report`. + +Include the affected version or commit, impact, reproduction steps, and a +minimal proof of concept. Redact credentials, personal data, and unrelated +machine details. Do not disclose the report in a GitHub issue or discussion +before a fix is coordinated. + +The maintainers will acknowledge the report, attempt to reproduce it, assess +affected versions, and coordinate remediation and disclosure. Response times +depend on severity and maintainer availability; no fixed SLA is promised. + +Relevant reports include MCP framing or schema bypasses, fail-open governance, +unsafe local-state handling, command execution, credential exposure, and +dependency vulnerabilities. Ordinary disagreement with a heuristic score is a +quality issue unless it also crosses a documented security or policy boundary. diff --git a/dist-workspace.toml b/dist-workspace.toml new file mode 100644 index 0000000..455f9e4 --- /dev/null +++ b/dist-workspace.toml @@ -0,0 +1,17 @@ +[workspace] +members = ["cargo:."] + +[dist] +cargo-dist-version = "0.32.0" +ci = [] +installers = ["shell", "powershell"] +hosting = ["github"] +targets = [ + "aarch64-apple-darwin", + "aarch64-unknown-linux-gnu", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", + "x86_64-pc-windows-msvc", +] +install-path = "CARGO_HOME" +install-updater = false diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 49b1bcd..a3541ef 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -211,7 +211,7 @@ sequenceDiagram Client->>MCP: run_reasoning_checkpoint / converge_reasoning MCP->>Deliberation: gate convergence on assumptions, verification, quality Client->>MCP: run_reasonkit_pipeline - MCP->>Skills: emit selected heuristic stage markers + MCP->>Skills: emit structured stage contracts, gates, blockers, and agent tasks Client->>MCP: consensus_answer MCP-->>Client: answer + heuristic confidence + route + analysis metadata ``` @@ -256,7 +256,11 @@ Mandatory checks before `consensus_answer` success: ## Operational Resources - `reasoning://thinking-modes` reports the public Thinking Mode catalog and - compatibility mapping for clients. + compatibility mapping filtered to the active tool pack. +- `reasoning://config/tool-packs` reports the startup-fixed `core`, `standard`, + or `full` router surface, exact membership, and the full-pack compatibility + rollback. The default core router exposes 13 of 47 registered routes; rmcp's + stored `ToolRouter` rejects disabled routes as well as hiding them from list. - `reasoning://config/providers` reports active heuristic mode, provider boundaries, required environment variables, and unavailable modes without leaking configured endpoint URLs. @@ -277,6 +281,19 @@ Mandatory checks before `consensus_answer` success: - `reasoning://schemas/{name}` reports Vertex/Gemini-compatible JSON schemas for tools and resources. +`doctor --json` treats an unset or `agent` `BACKEND_MODE` as healthy. Invalid +values and the reserved `external` mode produce a secret-free warning and use +the safe `agent` fallback because no external provider is bound. + +Successful results and handler-generated application errors returned through +ReasonKit's common result helpers use MCP dual-format JSON: the same value is +returned as `structuredContent` and as a pretty JSON text block for legacy +clients. Protocol errors emitted by rmcp before a handler runs, including +argument-deserialization failures, remain framework-generated text errors. +The full-pack `think_query` name remains discoverable for compatibility, but +returns `experimental_memory_unavailable` until a production ingestion and +retrieval provider is explicitly bound; it never simulates a search. + ## Security Controls - Existing prompt-injection scan/redaction remains enabled. @@ -288,4 +305,5 @@ Mandatory checks before `consensus_answer` success: - Keep v1 tools stable and available. - New tools are additive; no breaking rename. -- Documented protocol contract in `docs/research/SOURCES.md` and per-tool docs. +- Documented protocol and ecosystem decisions in `docs/MCP_STACK_RESEARCH.md` + and per-tool docs. diff --git a/docs/MCP_STACK_RESEARCH.md b/docs/MCP_STACK_RESEARCH.md new file mode 100644 index 0000000..5032339 --- /dev/null +++ b/docs/MCP_STACK_RESEARCH.md @@ -0,0 +1,115 @@ +# MCP stack and product opportunity research + +- Status: v0.2 implementation basis +- Reviewed: 2026-08-23 +- Scope: official MCP, Rust SDK, Registry, distribution, and adjacent reference + implementations + +## Product conclusion + +ReasonKit Think should not compete for the longest scratchpad. Its durable OSS +position is the reasoning governance layer for agents: the host authors the +semantic work, while the server records structure, applies evidence and +authorship gates, routes unsafe conclusions away from `PROCEED`, and exports an +artifact another human or agent can inspect. + +The v0.2 release candidate therefore prioritizes a small complete default +surface, protocol compatibility, truthful provider boundaries, transactional +governance, adversarial contract tests, and verifiable packaging. More reasoning +paradigms are lower value until the daily decision workflow is easy to adopt. + +## Primary-source findings + +| Signal | Product implication | v0.2 response | +| --- | --- | --- | +| The official Sequential Thinking reference exposes one tool and describes a dynamic reflective problem-solving process. | One-tool discoverability is the adoption benchmark; copying the scratchpad does not create a wedge. | Default to a complete 13-tool core and keep 33/47-tool packs explicit. | +| MCP tools can return `structuredContent`, while text content remains useful for compatibility. | Governance results should be machine-readable without abandoning older hosts. | Common handler results return the same JSON in structured and text forms; pre-handler SDK errors remain framework-owned. | +| The official Rust SDK is the protocol implementation boundary and evolves with MCP revisions. | SDK drift is product and security debt, not merely dependency hygiene. | Upgrade to `rmcp` 3.1.4 and test five protocol revisions over stdio. | +| The MCP Registry accepts Cargo package metadata and validates `server.json` against a published schema. | Discovery claims should be reproducible before any live Registry claim is made. | Validate the exact 0.2.0 package and Registry manifest in CI; do not claim publication yet. | +| Cargo packages from the manifest include-list, and `cargo publish --dry-run` verifies the packaged crate. | The release artifact, not the worktree, is the supply-chain unit users receive. | Root-anchor the include-list and gate the exact ten-file archive. | +| cargo-dist can generate native release artifacts from checked configuration. | Prebuilt binaries can remove Rust-toolchain friction, but release automation must not be speculative. | Validate a workflow-free cargo-dist manifest now; add publishing only with explicit release authority. | +| MCP Apps provides a standard path for interactive UI attached to MCP tools. | A graph viewer can become a progressive enhancement without turning the project into a separate web product. | Defer to a read-only audit/graph viewer after the protocol is stable. | + +Primary references: + +- [MCP tools specification](https://modelcontextprotocol.io/specification/draft/server/tools) +- [Official MCP Rust SDK](https://github.com/modelcontextprotocol/rust-sdk) +- [Official Sequential Thinking server](https://github.com/modelcontextprotocol/servers/blob/main/src/sequentialthinking/README.md) +- [MCP Registry quickstart](https://modelcontextprotocol.io/registry/quickstart) +- [Registry Cargo package type](https://github.com/modelcontextprotocol/registry/blob/main/docs/modelcontextprotocol-io/package-types.mdx) +- [Cargo package and publish reference](https://doc.rust-lang.org/cargo/reference/publishing.html) +- [cargo-dist documentation](https://axodotdev.github.io/cargo-dist/) +- [MCP Apps SDK](https://github.com/modelcontextprotocol/ext-apps) +- [cargo-audit documentation](https://github.com/rustsec/rustsec/blob/main/cargo-audit/README.md) + +## Verified repository reality + +- The server has 47 compatibility tools, 11 prompts, and native resources. +- The ten-stage ReasonKit pipeline already emits structured contracts, stage + gates, blockers, and host `agent_tasks`; it is not merely a list of labels. +- Semantic model and retrieval execution remain deliberately unavailable unless + a real provider is bound. Heuristic output carries provenance. +- The dominant engineering risk was concentrated state logic in `src/main.rs` + plus wire-level adoption friction, not a lack of reasoning modes. +- The v0.2 contract tests cover exact tool-pack discovery, schema shape, five + protocol versions, and eight planted governance bypasses. + +## Sequenced opportunity map + +### v0.3: evidence-native coding workflows + +1. Add typed evidence packets for `git_diff`, `test_log`, `ci_check`, + `file_excerpt`, `issue_comment`, and `web_source`, with explicit provenance + and conservative defaults. +2. Ship one excellent code-review/release-gate workflow that exports a PR-ready + Markdown audit and refuses approval on unresolved critical claims. +3. Add session import/export and a stable graph resource in JSON, Mermaid, and + DOT forms. +4. Move tool, engine, protocol, and state domains out of `main.rs` without + changing the single-crate or public MCP contracts. +5. Benchmark core-pack tool-call efficiency and governance accuracy against a + no-MCP baseline and a scratchpad baseline using planted failures. + +Exit signal: a new host can complete a source-backed PR go/no-go flow and use +the exported audit without hand-editing it. + +### v0.4: opt-in depth + +1. Bind a real retrieval provider for evidence lookup; never silently fall back + to empty or fabricated results. +2. Add optional embedding-based diversity and model-backed expansion/scoring + with provider identity, cost, and provenance on every output. +3. Add SQLite behind an optional feature for large histories and cross-session + failure-pattern analysis; retain JSON as the zero-dependency default. +4. Prototype a read-only MCP Apps graph/audit viewer. +5. Add streamable HTTP only after multi-user demand justifies authentication, + tenancy, rate-limit, and deployment complexity. + +### v1.0: stable governance protocol + +1. Freeze core tool contracts, resource URIs, error codes, and tool-pack SemVer + rules. +2. Publish a host compatibility matrix and threat model covering prompt + injection, evidence spoofing, redaction, oversized payloads, and replay. +3. Require migration fixtures for persisted sessions and independent release + certification for package, Registry, native binaries, and protocol behavior. + +## Measurement contract + +Track evidence rather than feature count: + +- median install-to-first-audit time; +- tool calls required for the core decision path; +- critical planted claims that remain `DATA_DEFICIT`; +- unsafe consensus attempts rejected with `consensus_blocked`; +- audits usable as PR/ADR artifacts without editing; +- compatibility pass rate by host and MCP protocol revision; +- package-to-source reproducibility and advisory-warning count. + +## Guardrails + +- No implicit server-side LLM. +- No fabricated retrieval or semantic confidence. +- No new paradigm merely to increase tool count. +- No HTTP service before an authenticated multi-user requirement exists. +- No live publication, Registry, or benchmark claim without current evidence. diff --git a/docs/README.md b/docs/README.md index 9d3688f..170ddaf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -247,8 +247,15 @@ The Thinking Modes are supported by reusable layers: | Graph | Preserve dependencies, conflicts, refinements, and merges. | `link_thoughts`, `merge_thought_branches`, `reasoning://session/{id}/graph` | | Audit | Replay and export what happened. | `replay_reasoning_session`, `export_reasoning_audit`, `export_memory_snapshot` | | Mode Catalog | Discover modes, aliases, costs, tools, safety rules, and compatibility values. | `reasoning://thinking-modes` | +| Tool Packs | Discover the active core/standard/full surface and compatibility rollback. | `reasoning://config/tool-packs` | | Aliases | Team-specific natural-language triggers. | `set_reasoning_aliases`, `reasoning://config/aliases` | +The server defaults to the 13-tool `core` pack. Set +`REASONKIT_TOOL_PACK=standard` for 33 everyday workflow tools or +`REASONKIT_TOOL_PACK=full` for the complete 47-tool compatibility surface. +Invalid values warn on stderr and fall back to core. Packs are selected at +startup and require a server restart to change. + ## Evidence Contract ReasonKit Think is intentionally conservative: @@ -286,6 +293,10 @@ DATA_DEFICIT or SOURCE_CONFLICT where needed, and tell me whether we can proceed Core: - `ARCHITECTURE.md` - technical architecture, execution model, provider boundary. +- `MCP_STACK_RESEARCH.md` - primary-source ecosystem findings, product wedge, + and the sequenced post-v0.2 opportunity map. +- `adr/0001-progressive-disclosure-and-governance-integrity.md` - accepted v0.2 + product and protocol decision. Primary tools: diff --git a/docs/adr/0001-progressive-disclosure-and-governance-integrity.md b/docs/adr/0001-progressive-disclosure-and-governance-integrity.md new file mode 100644 index 0000000..d2db9a3 --- /dev/null +++ b/docs/adr/0001-progressive-disclosure-and-governance-integrity.md @@ -0,0 +1,50 @@ +# ADR 0001: Progressive disclosure and governance integrity first + +- Status: Accepted +- Date: 2026-08-23 + +```yaml +decision: > + Make ReasonKit Think the agent-native reasoning governance layer, with a small + complete default tool pack and evidence-gated decisions, before adding more + reasoning paradigms or an embedded model. +context: > + The server already implements rich graph, pipeline, verification, assumption, + checkpoint, ReAct, prompt, and resource surfaces. Its 47-tool default is hard + for hosts to select correctly, while several integrity defects can let later + state erase or bypass earlier blockers. The current SDK pin also trails the + current MCP protocol implementation. +alternatives: + - Keep all tools visible by default and improve descriptions only. + - Reduce the product to a sequential-thinking scratchpad. + - Add an implicit server-side model to produce semantic content. +rationale: > + A complete 13-tool core retains route, deliberate, author, coach, assume, + verify, gate, decide, and audit operations while reducing discovery load by + 72 percent. Fail-closed state transitions and reproducible evaluations turn + the governance claim into observable product behavior. +risks: + - Existing clients may expect a tool outside the default pack. + - Stricter gates can expose workflows that previously passed incorrectly. + - An SDK migration can change wire shapes despite compiling cleanly. +mitigations: + - Preserve REASONKIT_TOOL_PACK=full as the compatibility escape hatch. + - Return actionable structured errors and pack-aware guidance. + - Isolate the SDK migration and test every supported stdio protocol version. +validation: + - Exact 13, 33, and 47 tool inventories are enforced by tests. + - Planted-failure evaluations must remain blocked. + - Current and legacy MCP initialization and tool calls pass smoke tests. + - Formatting, clippy, unit tests, dependency audit, and release build pass. +confidence: 0.9 +``` + +## Boundaries + +- The host model remains the semantic engine; the server coordinates, records, + verifies, and gates host-authored reasoning. +- Stdio remains the default and only transport in this tranche. +- Heuristic outputs remain labeled. No silent model, retrieval, or network + fallback is introduced. +- New academic paradigms, a hosted service, and a standalone UI are deferred + until the adoption and governance contracts have evidence. diff --git a/docs/clients/claude-code.md b/docs/clients/claude-code.md index 968e2ad..e4b2155 100644 --- a/docs/clients/claude-code.md +++ b/docs/clients/claude-code.md @@ -2,35 +2,40 @@ ## Primary references -- https://platform.claude.com/docs/en/agents-and-tools/mcp-connector -- https://modelcontextprotocol.io/docs/develop/connect-local-servers +- +- -## Status +## Register -- Confidence: Medium-high (MCP connector + MCP standard docs). +Install the release-candidate checkout on `PATH` as described in the root +README, then register the stdio server: -## Integration notes - -- Prefer local stdio server registration first. -- Use debug mode if handshake fails. - -### Example (`~/.claude.json` → `mcpServers` entry) +```sh +claude mcp add --scope user reasonkit-think -- reasonkit-think-mcp +claude mcp get reasonkit-think +``` -Merge this object into the existing `mcpServers` map (do not duplicate the outer key): +The equivalent `mcpServers` entry is: ```json -"reasonkit-think": { - "type": "stdio", - "command": "/ABSOLUTE/PATH/TO/reasonkit-think-mcp", - "args": [], - "env": { "TMPDIR": "/tmp" } +{ + "reasonkit-think": { + "type": "stdio", + "command": "reasonkit-think-mcp", + "args": [] + } } ``` -Multi-client reference: root `README.md`. +The default `core` pack exposes the 13-tool audited decision path. Set +`REASONKIT_TOOL_PACK=full` on this server only as a compatibility escape hatch +for a client that requires the complete 47-tool legacy inventory. + +## Verify -## Verification checklist +- The server initializes without text on stdout before the MCP handshake. +- Claude discovers 13 tools under the default pack. +- The Auto → evidence → checkpoint/decision → audit path completes. -- Server initializes successfully. -- Tools and prompt are discovered. -- Deliberation flow returns expected JSON outputs. +If startup reports an executable error, run +`command -v reasonkit-think-mcp` in the environment that launches Claude Code. diff --git a/docs/clients/codex-cli.md b/docs/clients/codex-cli.md index 25a3a4f..d0ea310 100644 --- a/docs/clients/codex-cli.md +++ b/docs/clients/codex-cli.md @@ -2,34 +2,36 @@ ## Official references -- https://developers.openai.com/codex/cli/reference -- https://developers.openai.com/codex/config-reference -- https://github.com/openai/codex/blob/main/docs/config.md +- +- -## Status +## Register -- Confidence: High (official OpenAI docs found). +Install the release-candidate checkout on `PATH` as described in the root +README, then register the stdio server: -## Integration notes - -- Configure MCP server in Codex config. -- Use stdio mode for local server process. +```sh +codex mcp add reasonkit-think -- reasonkit-think-mcp +codex mcp get reasonkit-think +``` -### Example (`~/.codex/config.toml`) +The equivalent `~/.codex/config.toml` entry is: ```toml [mcp_servers.reasonkit-think] -command = "/ABSOLUTE/PATH/TO/reasonkit-think-mcp" +command = "reasonkit-think-mcp" args = [] - -[mcp_servers.reasonkit-think.env] -TMPDIR = "/tmp" ``` -Multi-client reference: root `README.md`. +The default `core` pack exposes the 13-tool audited decision path. Set +`REASONKIT_TOOL_PACK=full` on this server only as a compatibility escape hatch +for a client that requires the complete 47-tool legacy inventory. + +## Verify -## Verification checklist +- `codex mcp get reasonkit-think` reports the server as enabled. +- Tool discovery returns exactly 13 tools under the default pack. +- The Auto → evidence → checkpoint/decision → audit path completes. -- Codex sees server -- `tools/list` returns v1 + v2 tools -- Deliberation commands execute without schema errors +If startup reports an executable error, run +`command -v reasonkit-think-mcp` in the environment that launches Codex. diff --git a/docs/clients/copilot-vscode.md b/docs/clients/copilot-vscode.md index 2120ffc..30a9716 100644 --- a/docs/clients/copilot-vscode.md +++ b/docs/clients/copilot-vscode.md @@ -1,74 +1,61 @@ -# Client Setup: Copilot CLI and VS Code (including Insiders) +# Client Setup: Copilot CLI and VS Code ## Official references -- https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers -- https://docs.github.com/en/copilot/concepts/context/mcp -- https://code.visualstudio.com/docs/copilot/customization/mcp-servers -- https://code.visualstudio.com/docs/copilot/reference/mcp-configuration +- +- +- +- -## Status +## Register in Copilot CLI -- Confidence: High (official GitHub and VS Code docs found). +Install the release-candidate checkout on `PATH` as described in the root +README, then register the local server: -## Integration notes - -- Configure local stdio MCP server in Copilot CLI and VS Code MCP settings. -- Ensure workspace trust before enabling server. -- Use the absolute path to the built release binary, usually - `/path/to/reasonkit-think/target/release/reasonkit-think-mcp`. +```sh +copilot mcp add reasonkit-think -- reasonkit-think-mcp +copilot mcp get reasonkit-think +``` -### Copilot CLI example (`~/.copilot/mcp-config.json` or workspace `.mcp.json`) +The equivalent `.mcp.json` entry is: ```json { "mcpServers": { "reasonkit-think": { "type": "local", - "command": "/ABSOLUTE/PATH/TO/reasonkit-think-mcp", + "command": "reasonkit-think-mcp", "args": [], - "env": { "TMPDIR": "/tmp" }, "tools": ["*"] } } } ``` -### VS Code / VS Code Insiders example (`mcp.json`) +## Register in VS Code -VS Code stores MCP servers under the top-level `servers` key. +VS Code stores MCP servers under the top-level `servers` key: ```json { "servers": { "reasonkit-think": { "type": "stdio", - "command": "/ABSOLUTE/PATH/TO/reasonkit-think-mcp", - "args": [], - "env": { "TMPDIR": "/tmp" } + "command": "reasonkit-think-mcp", + "args": [] } } } ``` -Multi-client reference: root `README.md`. +The default `core` pack exposes exactly 13 tools. Set +`REASONKIT_TOOL_PACK=full` only as the 47-tool compatibility escape hatch. -## Verification checklist +## Verify -- server added and trusted -- tools visible in MCP panel -- prompts visible in MCP panel -- reasoning resources accessible through `list_reasoning_resources` and `read_reasoning_resource` -- `./scripts/client_compat_check.py` reports `status: "ok"` +- The server is trusted and connected. +- Tool discovery returns exactly 13 tools under the default pack. +- The Auto → evidence → checkpoint/decision → audit path completes. -## Troubleshooting - -If Copilot reports `spawn ... ENOENT` or `Failed to connect to MCP server "reasonkit-think"`, the configured binary path does not exist or is not executable. Rebuild and verify: - -```bash -cd /path/to/reasonkit-think -export CARGO_TARGET_DIR="$PWD/target" -cargo build --release -./scripts/client_compat_check.py -copilot mcp get reasonkit-think -``` +If startup reports `ENOENT` or an executable error, run +`command -v reasonkit-think-mcp` in the environment that launches the client. diff --git a/docs/clients/cursor.md b/docs/clients/cursor.md index aab5288..fde72ef 100644 --- a/docs/clients/cursor.md +++ b/docs/clients/cursor.md @@ -2,37 +2,34 @@ ## Official references -- https://cursor.com/docs/mcp -- https://cursor.com/docs/cli/mcp +- +- -## Status +## Register -- Confidence: High (official docs found). - -## Integration notes - -- Configure server command to run `reasonkit-think-mcp` over stdio. -- Validate `tools/list` and `prompts/list` immediately after registration. - -### Example (`~/.cursor/mcp.json`) +Install the release-candidate checkout on `PATH` as described in the root +README, then add this entry to `~/.cursor/mcp.json`: ```json { "mcpServers": { "reasonkit-think": { "type": "stdio", - "command": "/ABSOLUTE/PATH/TO/reasonkit-think-mcp", - "args": [], - "env": { "TMPDIR": "/tmp" } + "command": "reasonkit-think-mcp", + "args": [] } } } ``` -Full multi-client walkthrough: root `README.md` (collapsible sections). +The default `core` pack exposes exactly 13 tools. Set +`REASONKIT_TOOL_PACK=full` only as the 47-tool compatibility escape hatch. + +## Verify -## Verification checklist +- The MCP server appears as connected in Cursor's integrations view. +- Tool discovery returns exactly 13 tools under the default pack. +- The Auto → evidence → checkpoint/decision → audit path completes. -- MCP server appears in Cursor integrations list. -- `sequentialthinking_tools` callable. -- `sequential-thinking-guidance` visible. +If startup reports an executable error, run +`command -v reasonkit-think-mcp` in the environment that launches Cursor. diff --git a/docs/clients/gemini-cli.md b/docs/clients/gemini-cli.md index 74576c3..2cdf852 100644 --- a/docs/clients/gemini-cli.md +++ b/docs/clients/gemini-cli.md @@ -2,38 +2,41 @@ ## Official references -- https://geminicli.com/docs/ -- https://geminicli.com/docs/tools/mcp-server/ -- https://github.com/google-gemini/gemini-cli +- +- -## Status +## Register -- Confidence: High (official docs and repo references). +Install the release-candidate checkout on `PATH` as described in the root +README, then register the stdio server: -## Integration notes - -- Register as stdio MCP server. -- Validate via Gemini CLI MCP diagnostics commands. -- Function declaration schemas are published in a Vertex/Gemini-friendly shape: - optional parameters use an explicit `type` plus `nullable: true`, not nullable - `anyOf` wrappers. If diagnostics fail, inspect `tools/list` and confirm - `consensus_answer.method` is a nullable string enum. +```sh +gemini mcp add --scope user reasonkit-think reasonkit-think-mcp +gemini mcp list +``` -### Example (`~/.gemini/settings.json` → `mcpServers`) +The equivalent `mcpServers` entry is: ```json -"reasonkit-think": { - "command": "/ABSOLUTE/PATH/TO/reasonkit-think-mcp", - "args": [], - "env": { "TMPDIR": "/tmp" }, - "trust": true +{ + "reasonkit-think": { + "command": "reasonkit-think-mcp", + "args": [], + "trust": true + } } ``` -Multi-client reference: root `README.md`. +The server emits Gemini-compatible tool schemas: optional parameters use an +explicit type plus `nullable: true`. The default `core` pack exposes exactly 13 +tools; use `REASONKIT_TOOL_PACK=full` only as the 47-tool compatibility escape +hatch. + +## Verify -## Verification checklist +- `gemini mcp list` shows a connected local server. +- Tool discovery returns exactly 13 tools under the default pack. +- The Auto → evidence → checkpoint/decision → audit path completes. -- server listed -- tools callable -- resources retrievable +If startup reports an executable error, run +`command -v reasonkit-think-mcp` in the environment that launches Gemini CLI. diff --git a/docs/clients/opencode.md b/docs/clients/opencode.md index cc79d83..5e102ee 100644 --- a/docs/clients/opencode.md +++ b/docs/clients/opencode.md @@ -1,43 +1,41 @@ # Client Setup: OpenCode -## References +## Official references -- https://opencode.ai/docs/config/ -- https://opencode.ai/docs/tools/ -- https://opencode.ai/docs/server/ +- +- -## Status +## Register -- Confidence: Medium (official docs found, verify MCP option details against current version before release). - -## Integration notes - -- Add MCP server config under OpenCode config schema (`mcp.` with `"type": "local"` and a `command` array). - -### Example (`~/.opencode/opencode.json` or project `opencode.json`) +Install the release-candidate checkout on `PATH` as described in the root +README, then add this entry to `~/.config/opencode/opencode.json` or a project +`opencode.json`: ```json { "$schema": "https://opencode.ai/config.json", "mcp": { - "reasonkit-think": { - "type": "local", - "command": ["/ABSOLUTE/PATH/TO/reasonkit-think-mcp"], - "enabled": true, - "environment": { "TMPDIR": "/tmp" } + "servers": { + "reasonkit-think": { + "type": "local", + "command": ["reasonkit-think-mcp"] + } } } } ``` -- **Config paths (important):** OpenCode merges multiple layers (see [Config](https://opencode.ai/docs/config/)). A file named `opencode.json` in your **home directory** (`~/opencode.json`) is easy to confuse with the **user/global** file OpenCode tends to ship with installs: **`~/.opencode/opencode.json`**. If you only edit `~/opencode.json`, the UI may still load MCP from `~/.opencode/opencode.json` unless you point **`OPENCODE_CONFIG`** at your preferred file or duplicate entries there. -- **Project repos:** Working inside a Git repo usually picks up **`opencode.json` in that project root** (highest precedence for standard file-based config). Add `reasonkit-think` under `mcp` there too when you develop in a repo that defines its own `mcp` block. -- Validate tool invocation and output rendering. +OpenCode merges configuration layers; a project file can override the user +entry. OpenCode v2 nests named servers under `mcp.servers`; use `disabled: true` +only when intentionally turning one off. The default `core` pack exposes +exactly 13 tools. Set +`REASONKIT_TOOL_PACK=full` only as the 47-tool compatibility escape hatch. -Multi-client reference: root `README.md`. +## Verify -## Verification checklist +- The local server starts without stdout noise before the MCP handshake. +- Tool discovery returns exactly 13 tools under the default pack. +- The Auto → evidence → checkpoint/decision → audit path completes. -- server starts -- tools list loads -- prompt/resource fetch works +If startup reports an executable error, run +`command -v reasonkit-think-mcp` in the environment that launches OpenCode. diff --git a/docs/dependency-audit.md b/docs/dependency-audit.md new file mode 100644 index 0000000..1b8ccc3 --- /dev/null +++ b/docs/dependency-audit.md @@ -0,0 +1,15 @@ +# Dependency audit policy + +CI runs `cargo audit --deny warnings`. Vulnerabilities, unmaintained packages, +unsound advisories, and yanked dependencies all fail the gate. + +The v0.2 dependency cleanup removed the dormant `reasonkit-mem` integration. +Production never populated its hot cache, while the dependency pulled 199 +additional locked packages and all eight prior audit warnings. The +compatibility-only `think_query` tool remains discoverable in the full pack but +now returns `experimental_memory_unavailable`; it performs no retrieval and +cannot create a successful-looking empty result. + +Current lockfile expectation: `cargo audit --deny warnings` exits successfully +with zero warnings. A future memory provider must restore retrieval only with a +real population path, provenance, contract tests, and a warning-clean graph. diff --git a/docs/plans/2026-08-23-next-level-v0-2.md b/docs/plans/2026-08-23-next-level-v0-2.md new file mode 100644 index 0000000..8b93ea2 --- /dev/null +++ b/docs/plans/2026-08-23-next-level-v0-2.md @@ -0,0 +1,59 @@ +# ReasonKit Think v0.2 implementation plan + +## Goal + +Deliver a materially easier and more trustworthy OSS MCP server: a complete +five-minute path from intent to auditable decision, current protocol support, +fail-closed state transitions, repeatable evaluations, and honest release and +discovery metadata. + +## Workstream 1: MCP protocol and progressive disclosure + +1. Upgrade `rmcp` to the current compatible SDK in an isolated commit. +2. Preserve stdio and the existing capability boundary. +3. Add `REASONKIT_TOOL_PACK=core|standard|full`, defaulting to the audited + 13-tool core; preserve exact 33-tool standard and 47-tool full inventories. +4. Store a filtered tool router and expose `reasoning://config/tool-packs`. +5. Make startup instructions and guidance pack-aware. +6. Return both JSON text and MCP `structuredContent` from common result helpers. +7. Test current and legacy protocol negotiation and exact discovery inventories. + +## Workstream 2: Governance integrity + +1. Upsert verification claims without erasing unrelated prior blockers. +2. Require qualifying evidence before an assumption can become verified. +3. Accept observations only for action nodes and complete the referenced action. +4. Reject unknown pipeline stages rather than synthesizing a passing stage. +5. Generate one audit identifier and reuse it in the envelope and payload. +6. Add regression tests for every corrected bypass. + +## Workstream 3: Adoption, evidence, and reproducibility + +1. Rewrite the README golden path around Auto mode, verification, decision, and + audit; move specialist surfaces behind explicit power-user guidance. +2. Add deterministic planted-failure evaluations with machine-readable metrics. +3. Run tests and evaluations in CI, and publish truthful package/registry metadata. +4. Add release automation only when its generated configuration validates. +5. Quarantine incomplete memory/retrieval behavior behind an explicit + experimental feature or remove claims that exceed implemented behavior. +6. Reconcile roadmap files so shipped, partial, and proposed work are distinct. + +## Integration and verification + +1. Land each workstream as independently reviewable commits from isolated + worktrees. +2. Cross-review the merged diff for protocol compatibility, trust regressions, + security, and OSS-boundary violations. +3. Run formatting, clippy with warnings denied, all tests, doctests, release + build, MCP smoke tests, planted-failure evaluations, and `cargo audit`. +4. Verify that the user's original dirty checkout is unchanged. + +## Exit criteria + +- Core discovery exposes only the complete 13-tool golden path. +- `full` restores all existing tools without data migration. +- A critical verification blocker cannot be erased by a later verification call. +- Unsupported stages and evidence-free verification fail closed. +- Planted false-claim and unresolved-assumption evaluations do not proceed. +- The release candidate passes every available local gate; any unavailable gate + is reported with its exact blocker. diff --git a/docs/resources/config-providers.md b/docs/resources/config-providers.md index ef4dda1..b0da320 100644 --- a/docs/resources/config-providers.md +++ b/docs/resources/config-providers.md @@ -18,3 +18,10 @@ boundaries. Configured endpoint URLs are not returned. The resource reports only whether an endpoint is present. + +## Doctor contract + +`reasonkit-think-mcp doctor --json` reports an unset or `agent` `BACKEND_MODE` +as healthy. Invalid values and `external` report a warning and an effective +`backend_mode` of `agent`; `external_provider_bound` remains `false` until a +production provider is bound. Diagnostics never interpolate an invalid value. diff --git a/docs/resources/tool-packs.md b/docs/resources/tool-packs.md new file mode 100644 index 0000000..90d9d42 --- /dev/null +++ b/docs/resources/tool-packs.md @@ -0,0 +1,24 @@ +# Tool Packs + +Resource URI: `reasoning://config/tool-packs` + +ReasonKit Think selects one immutable tool router at process startup: + +| Pack | Count | Purpose | +| --- | ---: | --- | +| `core` | 13 | Default agent-native authoring and fail-closed governance path. | +| `standard` | 33 | Core plus everyday Explore, Map, Sketch, replay, tuning, and resource helpers. | +| `full` | 47 | Complete compatibility surface, including legacy and specialist integrations. | + +Set `REASONKIT_TOOL_PACK` in the MCP server environment. Unset or empty means +`core`. An invalid value writes a warning to stderr and uses the safe core +fallback. Restart the stdio process after changing it. + +Use `REASONKIT_TOOL_PACK=full` to restore the complete pre-pack tool surface. +The resource returns the active/default pack, exact ordered membership of every +pack, counts, restart requirement, and compatibility rollback instruction. It +never returns environment values or secrets. + +The `full` pack retains the experimental `think_query` name for client contract +compatibility. Until a production memory ingestion and retrieval provider is +bound, it returns `experimental_memory_unavailable` and performs no retrieval. diff --git a/docs/tools/advanced_reasoning_operators.md b/docs/tools/advanced_reasoning_operators.md index 880348a..b7e3535 100644 --- a/docs/tools/advanced_reasoning_operators.md +++ b/docs/tools/advanced_reasoning_operators.md @@ -9,6 +9,11 @@ binding supplies real model-backed or retrieval-backed execution. - `record_assumption` stores a premise with status, criticality, optional evidence, and source node links. - `set_assumption_status` updates that premise after evidence is gathered. +- `critical: true` is authoritative when legacy `critical` and `criticality` + fields conflict; callers cannot downgrade a blocker by also sending a lower + criticality. +- A requested `verified` state remains `unresolved` unless the supplied + evidence satisfies the active verification policy. - Critical/high verifiable assumptions that are not `verified` block `consensus_answer`, `converge_reasoning`, and checkpoints under fail-closed policy. diff --git a/docs/tools/consensus_answer.md b/docs/tools/consensus_answer.md index 4a7afb4..5dc28c3 100644 --- a/docs/tools/consensus_answer.md +++ b/docs/tools/consensus_answer.md @@ -19,7 +19,7 @@ semantic synthesis or calibrated confidence. - `require_tier1_unambiguous_for_critical`: boolean - `fail_closed_on_critical_unresolved`: boolean -## Output +## Successful output - `final_answer` - `confidence` @@ -29,15 +29,30 @@ semantic synthesis or calibrated confidence. - `disagreement_report` - `analysis`: provenance and limitations for the consensus run +When governance blocks convergence, the MCP tool returns an application error +with `code: "consensus_blocked"`, the safe route, blocker summary, and a +recommended next action. That blocker response deliberately omits +`final_answer`; clients must not present an unapproved candidate answer as a +decision. + ## Behavior -- Fails closed on unresolved critical verification issues. -- Requires route decision in governed mode. -- Returns `policy_blocked = true` when fail-closed policy blocks the consensus result. +- Fails closed on unresolved critical verification issues or assumptions. +- Requires at least one agent-authored frontier candidate and excludes + heuristic scaffold/root nodes from an allowed final answer. +- Authorship requires a Thought node with server-recorded `agent_authored` + provenance; a caller-added tag on an Action or other heuristic node cannot + satisfy the gate. Consensus provenance lists only the candidates actually + used in the answer. +- Blocks when a pipeline stage failed its gate or the latest checkpoint still + reports blocking gaps. +- Requires a route decision in governed mode. +- A `policy_override` applies only to that call; it does not silently rewrite + the session policy. - `confidence` is a route heuristic derived from verification status, not a model self-rating or historical calibration score. ## Errors - No eligible branches -- Policy gate failure +- Policy, authorship, pipeline, or checkpoint gate failure diff --git a/docs/tools/export_reasoning_audit.md b/docs/tools/export_reasoning_audit.md index 3509ea0..e14414b 100644 --- a/docs/tools/export_reasoning_audit.md +++ b/docs/tools/export_reasoning_audit.md @@ -2,23 +2,33 @@ ## Purpose -Export a complete machine-readable reasoning artifact for downstream agents and compliance review. +Export the canonical reasoning payload plus a deterministic machine- or human-readable artifact for downstream agents and compliance review. ## Input -- `deliberation_id` -- `format`: `json` -- `include_raw_thoughts`: bool +- `deliberation_id` (required) +- `format` (optional): `json` (default) or `markdown` +- `include_raw_thoughts` (optional bool, default `false`) ## Output +- `deliberation_id` - `audit_id` -- `export_path` or `payload` +- `payload`: the unchanged canonical JSON audit object +- `format`: the selected format +- `content_type`: `application/json` or `text/markdown; charset=utf-8` +- `artifact`: deterministic rendering of the canonical audit snapshot ## Includes -- session metadata -- graph snapshot -- verification matrix -- final route decision -- confidence and unresolved risks +- session, mode, goal, and route metadata +- verification statuses and evidence sources +- assumptions, checkpoints, pipeline gates, and blockers +- explicit heuristic/provider provenance +- graph metadata; raw node content only when `include_raw_thoughts=true` + +Markdown tables escape pipes, line breaks, HTML-significant characters, and +active Markdown delimiters for links, images, emphasis, code, and strikethrough. +Map-derived sections are sorted for stable output. JSON remains the +compatibility default, and the canonical `payload`, `audit_id`, and +`deliberation_id` fields remain available in both formats. diff --git a/docs/tools/run_thinking_mode.md b/docs/tools/run_thinking_mode.md index e3133e7..841d6d2 100644 --- a/docs/tools/run_thinking_mode.md +++ b/docs/tools/run_thinking_mode.md @@ -86,6 +86,11 @@ The result always includes: - `policy_blocked`: whether unresolved critical evidence blocked confidence. - `audit_id`: exported audit artifact id. +The returned graph can contain heuristic scaffolds. Follow `next_action` and +submit the host's actual analysis through `add_thought_node` before requesting +an allowed `consensus_answer`; a root goal or scaffold is never a consensus +candidate. + Mode-specific fields appear under `quick`, `explore`, `map`, `sketch`, or `test`. ## Behavior Contract diff --git a/evals/README.md b/evals/README.md new file mode 100644 index 0000000..6f325cb --- /dev/null +++ b/evals/README.md @@ -0,0 +1,28 @@ +# Contract evaluations + +`tool-packs.json` is the exact discovery contract for the lexical `core` (13), +`standard` (33), and `full` (47) tool packs. `run_contract_evals.py` starts a +fresh stdio server for every tool-pack/protocol pair, validates typed schemas, +and then enforces eight adversarial governance checks: evidence-free assumption +verification, conflicting assumption criticality, normalized-claim criticality +downgrades, checkpoint routing, blocker-only consensus errors, unknown pipeline +stages, call-scoped policy overrides, and scaffold-only convergence. + +Run the deterministic unit layer first: + +```sh +just unit +``` + +Run the live release-binary matrix after the protocol and governance changes +are integrated: + +```sh +just eval +``` + +The live matrix covers `2024-11-05`, `2025-03-26`, `2025-06-18`, +`2025-11-25`, and `2026-07-28`. It is intentionally a cross-stream gate: a +baseline binary without `REASONKIT_TOOL_PACK` or the fail-closed governance +repairs will fail. Use `--surface-only` only for focused discovery diagnosis; +CI runs the complete evaluation. diff --git a/evals/__init__.py b/evals/__init__.py new file mode 100644 index 0000000..a9493a2 --- /dev/null +++ b/evals/__init__.py @@ -0,0 +1 @@ +"""Deterministic adoption and governance evaluations for ReasonKit Think.""" diff --git a/evals/mcp_contract.py b/evals/mcp_contract.py new file mode 100644 index 0000000..23000d3 --- /dev/null +++ b/evals/mcp_contract.py @@ -0,0 +1,291 @@ +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + + +CONTRACT_PATH = Path(__file__).with_name("tool-packs.json") +EXPECTED_PACK_COUNTS = {"core": 13, "standard": 33, "full": 47} + + +def load_contract(path: Path | None = None) -> dict[str, Any]: + return json.loads((path or CONTRACT_PATH).read_text(encoding="utf-8")) + + +def validate_contract(contract: dict[str, Any]) -> list[str]: + issues: list[str] = [] + packs = contract.get("toolPacks") + if not isinstance(packs, dict): + return ["toolPacks must be an object"] + + if contract.get("schemaVersion") != 1: + issues.append("schemaVersion must be 1") + if contract.get("defaultToolPack") != "core": + issues.append("defaultToolPack must be core") + if set(packs) != set(EXPECTED_PACK_COUNTS): + issues.append("toolPacks must contain exactly core, standard, and full") + + for name, expected_count in EXPECTED_PACK_COUNTS.items(): + tools = packs.get(name) + if not isinstance(tools, list) or not all(isinstance(tool, str) for tool in tools): + issues.append(f"{name} must be a list of tool names") + continue + if len(tools) != expected_count: + issues.append(f"{name} must contain exactly {expected_count} tools") + if tools != sorted(tools): + issues.append(f"{name} must use lexical router order") + if len(tools) != len(set(tools)): + issues.append(f"{name} contains duplicate tools") + + if all(isinstance(packs.get(name), list) for name in EXPECTED_PACK_COUNTS): + if not set(packs["core"]).issubset(packs["standard"]): + issues.append("core must be a subset of standard") + if not set(packs["standard"]).issubset(packs["full"]): + issues.append("standard must be a subset of full") + + versions = contract.get("protocolVersions") + if not isinstance(versions, list) or len(versions) != len(set(versions or [])): + issues.append("protocolVersions must be a unique list") + return issues + + +def validate_tool_surface( + contract: dict[str, Any], pack: str, tools: list[dict[str, Any] | str] +) -> list[str]: + expected = contract["toolPacks"][pack] + actual = [tool if isinstance(tool, str) else tool.get("name") for tool in tools] + actual_names = [name for name in actual if isinstance(name, str)] + issues: list[str] = [] + + missing = sorted(set(expected) - set(actual_names)) + unexpected = sorted(set(actual_names) - set(expected)) + if missing: + issues.append(f"{pack} missing tools: {', '.join(missing)}") + if unexpected: + issues.append(f"{pack} unexpected tools: {', '.join(unexpected)}") + if actual_names != sorted(actual_names): + issues.append(f"{pack} tool list is not in lexical router order") + if not missing and not unexpected and actual_names != expected: + issues.append(f"{pack} tool order differs from the exact contract") + return issues + + +def schema_type_issues(schema: object, path: str = "$") -> list[str]: + issues: list[str] = [] + if not isinstance(schema, dict): + return issues + + requires_type = any( + key in schema + for key in ( + "properties", + "additionalProperties", + "items", + "anyOf", + "oneOf", + "allOf", + "enum", + "const", + ) + ) + if requires_type and "type" not in schema and "$ref" not in schema: + issues.append(f"{path}: missing type") + + for key, child in schema.items(): + if isinstance(child, dict): + issues.extend(schema_type_issues(child, f"{path}.{key}")) + elif isinstance(child, list): + for index, item in enumerate(child): + issues.extend(schema_type_issues(item, f"{path}.{key}[{index}]")) + return issues + + +def tool_result_is_error(response: dict[str, Any]) -> bool: + result = response.get("result") + if not isinstance(result, dict): + return "error" in response + return result.get("isError") is True or result.get("is_error") is True + + +def tool_payload(response: dict[str, Any]) -> dict[str, Any]: + result = response.get("result", {}) + if not isinstance(result, dict): + return {} + structured = result.get("structuredContent") or result.get("structured_content") + if isinstance(structured, dict): + return structured + for item in result.get("content", []): + if isinstance(item, dict) and isinstance(item.get("text"), str): + try: + payload = json.loads(item["text"]) + except json.JSONDecodeError: + continue + if isinstance(payload, dict): + return payload + return {} + + +def contains_key(value: object, key: str) -> bool: + if isinstance(value, dict): + return key in value or any(contains_key(child, key) for child in value.values()) + if isinstance(value, list): + return any(contains_key(child, key) for child in value) + return False + + +def evaluate_fail_closed( + *, + evidence_free_update: dict[str, Any], + criticality_downgrade: dict[str, Any], + checkpoint: dict[str, Any], + consensus: dict[str, Any], + unknown_stage: dict[str, Any], + pipeline_override: dict[str, Any], + post_pipeline_audit: dict[str, Any], + scaffold_run: dict[str, Any], +) -> dict[str, Any]: + update_payload = tool_payload(evidence_free_update) + update_assumption = update_payload.get("assumption", {}) + update_blocked = ( + isinstance(update_assumption, dict) + and update_assumption.get("status") != "verified" + and bool(update_payload.get("blocking_assumption_ids")) + ) + assumption_criticality_preserved = ( + isinstance(update_assumption, dict) + and update_assumption.get("criticality") == "critical" + ) + + checkpoint_payload = tool_payload(checkpoint).get("checkpoint", {}) + checkpoint_blocked = ( + isinstance(checkpoint_payload, dict) + and checkpoint_payload.get("route_decision") + in {"GATHER_MORE_EVIDENCE", "DEFER_TO_HUMAN", "REJECT_CURRENT_PLAN"} + and bool(checkpoint_payload.get("blocking_gaps")) + ) + + downgrade_matrix = tool_payload(criticality_downgrade).get( + "verification_matrix", [] + ) + criticality_preserved = isinstance(downgrade_matrix, list) and any( + isinstance(entry, dict) + and " ".join(str(entry.get("claim", "")).split()).casefold() + == "release requires approval" + and entry.get("critical") is True + and entry.get("status") == "data_deficit" + for entry in downgrade_matrix + ) + + consensus_payload = tool_payload(consensus) + consensus_blocked = ( + tool_result_is_error(consensus) + and consensus_payload.get("code") == "consensus_blocked" + and not contains_key(consensus, "final_answer") + ) + unknown_stage_rejected = ( + tool_result_is_error(unknown_stage) + and tool_payload(unknown_stage).get("code") == "invalid_input" + ) + + audit_payload = tool_payload(post_pipeline_audit).get("payload", {}) + policy = ( + audit_payload.get("verification_policy", {}) + if isinstance(audit_payload, dict) + else {} + ) + pipeline_policy_call_scoped = ( + not tool_result_is_error(pipeline_override) + and isinstance(policy, dict) + and policy.get("min_independent_groups") == 3 + and policy.get("allow_tier3_for_independence") is False + and policy.get("require_tier1_unambiguous_for_critical") is True + and policy.get("fail_closed_on_critical_unresolved") is True + ) + + scaffold_payload = tool_payload(scaffold_run) + scaffold_authorship = scaffold_payload.get("graph_authorship", {}) + scaffold_route_blocked = ( + isinstance(scaffold_authorship, dict) + and scaffold_authorship.get("agent_authored") == 0 + and scaffold_authorship.get("scaffold_nodes", 0) > 0 + and scaffold_authorship.get("frontier_scaffold_only") is True + and scaffold_payload.get("policy_blocked") is True + and scaffold_payload.get("route_decision") + in {"GATHER_MORE_EVIDENCE", "DEFER_TO_HUMAN", "REJECT_CURRENT_PLAN"} + and scaffold_payload.get("pipeline_route") + in {"GATHER_MORE_EVIDENCE", "DEFER_TO_HUMAN", "REJECT_CURRENT_PLAN"} + ) + checks = { + "evidence_free_verification_blocked": update_blocked, + "assumption_criticality_preserved": assumption_criticality_preserved, + "critical_claim_downgrade_blocked": criticality_preserved, + "critical_checkpoint_blocked": checkpoint_blocked, + "consensus_exact_blocker_only": consensus_blocked, + "unknown_pipeline_stage_invalid_input": unknown_stage_rejected, + "pipeline_policy_override_call_scoped": pipeline_policy_call_scoped, + "scaffold_only_route_blocked": scaffold_route_blocked, + } + return {"passed": all(checks.values()), "checks": checks} + + +def matrix_jobs( + contract: dict[str, Any], + packs: list[str] | None = None, + protocols: list[str] | None = None, +) -> list[tuple[str, str]]: + selected_packs = packs or list(contract["toolPacks"]) + selected_protocols = protocols or contract["protocolVersions"] + unknown_packs = sorted(set(selected_packs) - set(contract["toolPacks"])) + unknown_protocols = sorted( + set(selected_protocols) - set(contract["protocolVersions"]) + ) + if unknown_packs or unknown_protocols: + raise ValueError( + f"unknown packs={unknown_packs!r} protocols={unknown_protocols!r}" + ) + return [ + (pack, protocol) + for pack in selected_packs + for protocol in selected_protocols + ] + + +def validate_runtime_probe( + contract: dict[str, Any], + *, + pack: str, + protocol: str, + initialize: dict[str, Any], + tools: list[dict[str, Any]], +) -> list[str]: + issues: list[str] = [] + result = initialize.get("result") + if not isinstance(result, dict): + return [f"{pack}/{protocol}: initialization failed"] + + server_info = result.get("serverInfo") or result.get("server_info") + server_name = server_info.get("name") if isinstance(server_info, dict) else None + if server_name != "reasonkit-think-mcp": + issues.append( + f"{pack}/{protocol}: server name is {server_name!r}, expected 'reasonkit-think-mcp'" + ) + + negotiated = result.get("protocolVersion") or result.get("protocol_version") + if negotiated != protocol: + issues.append( + f"{pack}/{protocol}: negotiated {negotiated!r}, expected {protocol!r}" + ) + + issues.extend(validate_tool_surface(contract, pack, tools)) + for tool in tools: + name = tool.get("name", "") + schema = tool.get("inputSchema") or tool.get("input_schema") + if not isinstance(schema, dict): + issues.append(f"{pack}/{protocol}: {name} missing input schema") + continue + issues.extend( + f"{pack}/{protocol}: {name} {issue}" + for issue in schema_type_issues(schema) + ) + return issues diff --git a/evals/run_contract_evals.py b/evals/run_contract_evals.py new file mode 100644 index 0000000..dbcdac4 --- /dev/null +++ b/evals/run_contract_evals.py @@ -0,0 +1,427 @@ +#!/usr/bin/env python3 +"""Run deterministic MCP discovery and fail-closed contract evaluations.""" + +from __future__ import annotations + +import argparse +import json +import os +import select +import subprocess +import sys +import tempfile +from pathlib import Path +from typing import Any + +if __package__ in (None, ""): + sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from evals.mcp_contract import ( # noqa: E402 + evaluate_fail_closed, + load_contract, + matrix_jobs, + tool_payload, + validate_contract, + validate_runtime_probe, +) + + +class McpSession: + def __init__(self, binary: Path, pack: str, timeout_seconds: float) -> None: + self.binary = binary + self.pack = pack + self.timeout_seconds = timeout_seconds + self._request_id = 0 + self._state_dir = tempfile.TemporaryDirectory( + prefix=f"reasonkit-think-eval-{pack}-" + ) + env = os.environ.copy() + env.update( + { + "REASONKIT_TOOL_PACK": pack, + "RUST_LOG": "error", + "TMPDIR": "/tmp", + "XDG_DATA_HOME": str(Path(self._state_dir.name) / "data"), + } + ) + self.proc = subprocess.Popen( + [str(binary)], + cwd="/", + env=env, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1, + ) + + def __enter__(self) -> McpSession: + return self + + def __exit__(self, *_exc: object) -> None: + self.close() + + def close(self) -> None: + if self.proc.poll() is None: + self.proc.terminate() + try: + self.proc.wait(timeout=2) + except subprocess.TimeoutExpired: + self.proc.kill() + self.proc.wait(timeout=2) + self._state_dir.cleanup() + + def send(self, payload: dict[str, Any]) -> None: + if self.proc.stdin is None: + raise RuntimeError("MCP stdin is unavailable") + self.proc.stdin.write(json.dumps(payload, separators=(",", ":")) + "\n") + self.proc.stdin.flush() + + def receive(self, request_id: int) -> dict[str, Any]: + if self.proc.stdout is None: + raise RuntimeError("MCP stdout is unavailable") + while True: + ready, _, _ = select.select( + [self.proc.stdout], [], [], self.timeout_seconds + ) + if not ready: + raise TimeoutError( + f"MCP response timed out for request {request_id} ({self.pack})" + ) + line = self.proc.stdout.readline() + if not line: + stderr = self.proc.stderr.read() if self.proc.stderr else "" + raise RuntimeError( + f"MCP server exited before request {request_id}: {stderr[-1000:]}" + ) + response = json.loads(line) + if response.get("id") == request_id: + return response + + def request(self, method: str, params: dict[str, Any]) -> dict[str, Any]: + self._request_id += 1 + request_id = self._request_id + self.send( + { + "jsonrpc": "2.0", + "id": request_id, + "method": method, + "params": params, + } + ) + return self.receive(request_id) + + def initialize(self, protocol: str) -> dict[str, Any]: + response = self.request( + "initialize", + { + "protocolVersion": protocol, + "capabilities": {}, + "clientInfo": { + "name": "reasonkit-contract-eval", + "version": "1.0.0", + }, + }, + ) + if "result" in response: + self.send( + { + "jsonrpc": "2.0", + "method": "notifications/initialized", + "params": {}, + } + ) + return response + + def call_tool(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]: + return self.request("tools/call", {"name": name, "arguments": arguments}) + + +def probe_surface( + binary: Path, + contract: dict[str, Any], + pack: str, + protocol: str, + timeout_seconds: float, +) -> dict[str, Any]: + try: + with McpSession(binary, pack, timeout_seconds) as session: + initialize = session.initialize(protocol) + tools: list[dict[str, Any]] = [] + if "result" in initialize: + listed = session.request("tools/list", {}) + candidate = listed.get("result", {}).get("tools", []) + if isinstance(candidate, list): + tools = [tool for tool in candidate if isinstance(tool, dict)] + issues = validate_runtime_probe( + contract, + pack=pack, + protocol=protocol, + initialize=initialize, + tools=tools, + ) + return { + "pack": pack, + "protocol": protocol, + "toolCount": len(tools), + "status": "passed" if not issues else "failed", + "issues": issues, + } + except (OSError, RuntimeError, TimeoutError, json.JSONDecodeError) as error: + return { + "pack": pack, + "protocol": protocol, + "toolCount": 0, + "status": "failed", + "issues": [str(error)], + } + + +def run_fail_closed_eval( + binary: Path, protocol: str, timeout_seconds: float +) -> dict[str, Any]: + try: + with McpSession(binary, "core", timeout_seconds) as session: + initialized = session.initialize(protocol) + if "result" not in initialized: + return { + "status": "failed", + "passed": False, + "checks": {}, + "issues": ["fail-closed session initialization failed"], + } + + started = session.call_tool( + "start_deliberation", + { + "session_id": "contract-eval-fail-closed", + "mode": "reasonkit", + "goal": "Reject an unsupported critical deployment claim", + }, + ) + deliberation_id = tool_payload(started).get("deliberation_id") + if not isinstance(deliberation_id, str): + return { + "status": "failed", + "passed": False, + "checks": {}, + "issues": ["start_deliberation returned no deliberation_id"], + } + + assumption = session.call_tool( + "record_assumption", + { + "deliberation_id": deliberation_id, + "text": "The deployment is safe despite having no supporting evidence", + "criticality": "medium", + "critical": True, + "status": "verified", + "verifiable": True, + }, + ) + assumption_id = ( + tool_payload(assumption).get("assumption", {}).get("assumption_id") + ) + if not isinstance(assumption_id, str): + return { + "status": "failed", + "passed": False, + "checks": {}, + "issues": ["record_assumption returned no assumption_id"], + } + + evidence_free_update = session.call_tool( + "set_assumption_status", + { + "deliberation_id": deliberation_id, + "assumption_id": assumption_id, + "status": "verified", + "confidence": 0.99, + "notes": "planted evidence-free verification attempt", + }, + ) + session.call_tool( + "verify_thoughts", + { + "deliberation_id": deliberation_id, + "node_ids": [], + "critical_claims": [], + "claims": [ + { + "text": "Release requires approval", + "critical": True, + "evidence": [], + } + ], + "method": "hybrid", + }, + ) + criticality_downgrade = session.call_tool( + "verify_thoughts", + { + "deliberation_id": deliberation_id, + "node_ids": [], + "critical_claims": [], + "claims": [ + { + "text": " Release requires approval ", + "critical": False, + "evidence": [ + { + "source": f"secondary-source-{index}", + "tier": "tier2", + "independence_group": f"secondary-group-{index}", + "supports": True, + "contradictory": False, + "unambiguous": True, + } + for index in range(1, 4) + ], + } + ], + "method": "hybrid", + }, + ) + unknown_stage = session.call_tool( + "run_reasonkit_pipeline", + { + "deliberation_id": deliberation_id, + "stages": ["planted-unknown-stage"], + "policy_override": "relaxed", + }, + ) + pipeline_override = session.call_tool( + "run_reasonkit_pipeline", + { + "deliberation_id": deliberation_id, + "stages": ["causal-modeling"], + "policy_override": "relaxed", + }, + ) + post_pipeline_audit = session.call_tool( + "export_reasoning_audit", + { + "deliberation_id": deliberation_id, + "format": "json", + "include_raw_thoughts": False, + }, + ) + checkpoint = session.call_tool( + "run_reasoning_checkpoint", + { + "deliberation_id": deliberation_id, + "label": "planted critical blocker", + "fail_threshold": 0.0, + }, + ) + consensus = session.call_tool( + "consensus_answer", {"deliberation_id": deliberation_id} + ) + scaffold_run = session.call_tool( + "run_thinking_mode", + { + "session_id": "contract-eval-scaffold-only", + "intent": "Compare several release strategies without host-authored analysis", + "mode": "Explore", + }, + ) + evaluation = evaluate_fail_closed( + evidence_free_update=evidence_free_update, + criticality_downgrade=criticality_downgrade, + checkpoint=checkpoint, + consensus=consensus, + unknown_stage=unknown_stage, + pipeline_override=pipeline_override, + post_pipeline_audit=post_pipeline_audit, + scaffold_run=scaffold_run, + ) + return { + "status": "passed" if evaluation["passed"] else "failed", + **evaluation, + "issues": [] if evaluation["passed"] else ["fail-closed bypass detected"], + } + except (OSError, RuntimeError, TimeoutError, json.JSONDecodeError) as error: + return { + "status": "failed", + "passed": False, + "checks": {}, + "issues": [str(error)], + } + + +def parse_args() -> argparse.Namespace: + contract = load_contract() + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--binary", + type=Path, + default=Path(os.environ.get("REASONKIT_THINK_BIN", "target/release/reasonkit-think-mcp")), + ) + parser.add_argument( + "--packs", + nargs="+", + choices=list(contract["toolPacks"]), + default=list(contract["toolPacks"]), + ) + parser.add_argument( + "--protocols", + nargs="+", + choices=contract["protocolVersions"], + default=contract["protocolVersions"], + ) + parser.add_argument("--timeout", type=float, default=10.0) + parser.add_argument("--surface-only", action="store_true") + parser.add_argument("--output", type=Path) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + contract = load_contract() + contract_issues = validate_contract(contract) + binary = args.binary.expanduser().resolve() + binary_issues: list[str] = [] + if not binary.is_file(): + binary_issues.append(f"binary does not exist: {binary}") + elif not os.access(binary, os.X_OK): + binary_issues.append(f"binary is not executable: {binary}") + + probes: list[dict[str, Any]] = [] + if not contract_issues and not binary_issues: + probes = [ + probe_surface(binary, contract, pack, protocol, args.timeout) + for pack, protocol in matrix_jobs(contract, args.packs, args.protocols) + ] + + fail_closed: dict[str, Any] | None = None + if not args.surface_only and not contract_issues and not binary_issues: + fail_closed = run_fail_closed_eval( + binary, contract["protocolVersions"][-1], args.timeout + ) + + passed = ( + not contract_issues + and not binary_issues + and bool(probes) + and all(probe["status"] == "passed" for probe in probes) + and (fail_closed is None or fail_closed["passed"]) + ) + report = { + "schemaVersion": 1, + "status": "passed" if passed else "failed", + "binary": str(binary), + "contractIssues": contract_issues, + "binaryIssues": binary_issues, + "protocolMatrix": probes, + "failClosed": fail_closed, + } + rendered = json.dumps(report, indent=2, sort_keys=True) + "\n" + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(rendered, encoding="utf-8") + sys.stdout.write(rendered) + return 0 if passed else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/evals/tool-packs.json b/evals/tool-packs.json new file mode 100644 index 0000000..5f9602f --- /dev/null +++ b/evals/tool-packs.json @@ -0,0 +1,112 @@ +{ + "schemaVersion": 1, + "defaultToolPack": "core", + "protocolVersions": [ + "2024-11-05", + "2025-03-26", + "2025-06-18", + "2025-11-25", + "2026-07-28" + ], + "toolPacks": { + "core": [ + "add_thought_node", + "consensus_answer", + "export_reasoning_audit", + "get_reasoning_coaching", + "reasoning_intent_router", + "record_assumption", + "run_reasoning_checkpoint", + "run_reasonkit_pipeline", + "run_thinking_mode", + "set_assumption_status", + "set_verification_policy", + "start_deliberation", + "verify_thoughts" + ], + "standard": [ + "add_thought_node", + "apply_reasoning_pattern", + "consensus_answer", + "converge_reasoning", + "distill_thought_cluster", + "evaluate_reasoning_quality", + "expand_thoughts", + "export_memory_snapshot", + "export_reasoning_audit", + "get_reasoning_coaching", + "get_thinking_history", + "link_thoughts", + "list_reasoning_resources", + "merge_thought_branches", + "plan_tool_sequence", + "prune_thoughts", + "read_reasoning_resource", + "reasoning_intent_router", + "record_assumption", + "refine_thoughts", + "replay_reasoning_session", + "run_reasoning_checkpoint", + "run_reasonkit_pipeline", + "run_skeleton_elaboration", + "run_thinking_mode", + "score_thoughts", + "set_assumption_status", + "set_reasoning_aliases", + "set_scoring_rubric", + "set_verification_policy", + "start_deliberation", + "start_skeleton_of_thought", + "verify_thoughts" + ], + "full": [ + "add_thought_node", + "apply_algorithm_template", + "apply_reasoning_pattern", + "clear_thinking_history", + "consensus_answer", + "converge_reasoning", + "distill_thought_cluster", + "evaluate_reasoning_quality", + "expand_thoughts", + "export_memory_snapshot", + "export_reasoning_audit", + "get_reasoning_coaching", + "get_thinking_history", + "link_thoughts", + "list_reasoning_resources", + "mcts_select_path", + "merge_thought_branches", + "plan_tool_sequence", + "prune_thoughts", + "query_failure_memory", + "read_reasoning_resource", + "reasoning_autopilot", + "reasoning_intent_router", + "record_assumption", + "record_reasoning_action", + "record_reasoning_observation", + "refine_thoughts", + "replay_reasoning_session", + "run_reasoning_checkpoint", + "run_reasonkit_pipeline", + "run_skeleton_elaboration", + "run_thinking_mode", + "score_thoughts", + "sequentialthinking_tools", + "set_assumption_status", + "set_reasoning_aliases", + "set_scoring_rubric", + "set_verification_policy", + "start_deliberation", + "start_skeleton_of_thought", + "think_query", + "verify_thoughts", + "web_research_enqueue", + "web_research_fetch", + "web_research_task_status", + "web_research_triangulate", + "web_research_verify" + ] + } +} diff --git a/examples/client-configs/claude-code.json b/examples/client-configs/claude-code.json index 029df18..dee5c3d 100644 --- a/examples/client-configs/claude-code.json +++ b/examples/client-configs/claude-code.json @@ -1,10 +1,7 @@ { "reasonkit-think": { "type": "stdio", - "command": "/absolute/path/to/reasonkit-think-mcp", - "args": [], - "env": { - "TMPDIR": "/tmp" - } + "command": "reasonkit-think-mcp", + "args": [] } } diff --git a/examples/client-configs/copilot-cli.json b/examples/client-configs/copilot-cli.json index 710b54e..99abe28 100644 --- a/examples/client-configs/copilot-cli.json +++ b/examples/client-configs/copilot-cli.json @@ -2,11 +2,8 @@ "mcpServers": { "reasonkit-think": { "type": "local", - "command": "/absolute/path/to/reasonkit-think-mcp", + "command": "reasonkit-think-mcp", "args": [], - "env": { - "TMPDIR": "/tmp" - }, "tools": ["*"] } } diff --git a/examples/client-configs/cursor.json b/examples/client-configs/cursor.json index f09aaf9..affc72f 100644 --- a/examples/client-configs/cursor.json +++ b/examples/client-configs/cursor.json @@ -2,11 +2,8 @@ "mcpServers": { "reasonkit-think": { "type": "stdio", - "command": "/absolute/path/to/reasonkit-think-mcp", - "args": [], - "env": { - "TMPDIR": "/tmp" - } + "command": "reasonkit-think-mcp", + "args": [] } } } diff --git a/examples/client-configs/generic-stdio.json b/examples/client-configs/generic-stdio.json index 78c9d2f..affc72f 100644 --- a/examples/client-configs/generic-stdio.json +++ b/examples/client-configs/generic-stdio.json @@ -2,12 +2,8 @@ "mcpServers": { "reasonkit-think": { "type": "stdio", - "command": "/absolute/path/to/reasonkit-think-mcp", - "args": [], - "env": { - "TMPDIR": "/tmp", - "RUST_LOG": "info" - } + "command": "reasonkit-think-mcp", + "args": [] } } } diff --git a/examples/client-configs/vscode-mcp.json b/examples/client-configs/vscode-mcp.json index a4305ac..aa14994 100644 --- a/examples/client-configs/vscode-mcp.json +++ b/examples/client-configs/vscode-mcp.json @@ -2,11 +2,8 @@ "servers": { "reasonkit-think": { "type": "stdio", - "command": "/absolute/path/to/reasonkit-think-mcp", - "args": [], - "env": { - "TMPDIR": "/tmp" - } + "command": "reasonkit-think-mcp", + "args": [] } } } diff --git a/features/README.md b/features/README.md new file mode 100644 index 0000000..b7f6957 --- /dev/null +++ b/features/README.md @@ -0,0 +1,31 @@ +# Feature records + +Files under `done/` and `todo/` are planning records. Their directory is the +record's editorial state, not an authoritative claim about the current runtime. +Use this truth order when the sources disagree: + +1. Source behavior and executable tests. +2. Exact discovery surfaces in `evals/tool-packs.json`. +3. These feature records. + +[`manifest.json`](manifest.json) inventories every ID-bearing record. The +community contract test rejects missing paths, duplicate IDs, state-directory +mismatches, and headings that disagree with filenames. + +## Known overlap with the runtime + +| Record | Review | Current boundary | +| --- | --- | --- | +| T2-2A | Partial overlap | `apply_algorithm_template` creates deterministic scaffolds; it is not model-backed AoT. | +| T2-2C | Partial overlap | `mcts_select_path` searches existing scored nodes without LLM rollouts. | +| T2-2D | Partial overlap | `refine_thoughts` applies caller-supplied critique; it is not an autonomous Reflexion loop. | +| T2-2E | Partial overlap | ReAct action and observation records exist, while the host still executes external tools. | +| T2-2F | Proposal | No dedicated articulation or duck-panel tool is currently advertised. | +| T3-3B | Partial overlap | Pipeline critique stages exist, but the proposed shadow-session protocol does not. | +| T3-3C | Partial overlap | The Socratic reasoning pattern exists; dedicated question state and tools do not. | +| T6-6B | Partial overlap | Failure patterns can be detected, persisted, and queried; the broader proposal remains a planning record. | +| T7-P01–P10 | Partial overlap | `run_reasonkit_pipeline` supplies deterministic stage artifacts, not every acceptance criterion in the ten proposals. | + +Before moving a record, compare its acceptance criteria with tests and the +pack-aware runtime contract. A matching tool name alone is not completion +evidence. diff --git a/features/manifest.json b/features/manifest.json new file mode 100644 index 0000000..5b6aac7 --- /dev/null +++ b/features/manifest.json @@ -0,0 +1,150 @@ +{ + "schemaVersion": 1, + "runtimeTruthOrder": [ + "source and tests", + "evals/tool-packs.json", + "feature planning records" + ], + "records": [ + { + "id": "T1-1A", + "path": "features/done/T1-1A-phase-1-complete.md", + "record_state": "done" + }, + { + "id": "T1-1B", + "path": "features/done/T1-1B-semantic-scoring-rubric.md", + "record_state": "done" + }, + { + "id": "T1-1C", + "path": "features/todo/T1-1C-embedding-diversity-pruning.md", + "record_state": "todo" + }, + { + "id": "T2-2A", + "path": "features/todo/T2-2A-algorithm-of-thoughts-aot.md", + "record_state": "todo" + }, + { + "id": "T2-2B", + "path": "features/done/T2-2B-skeleton-of-thoughts-sot.md", + "record_state": "done" + }, + { + "id": "T2-2C", + "path": "features/todo/T2-2C-mcts-deliberation.md", + "record_state": "todo" + }, + { + "id": "T2-2D", + "path": "features/todo/T2-2D-reflexion-self-refine.md", + "record_state": "todo" + }, + { + "id": "T2-2E", + "path": "features/todo/T2-2E-react-tool-augmented-reasoning.md", + "record_state": "todo" + }, + { + "id": "T2-2F", + "path": "features/todo/T2-2F-rubber-duck-articulation.md", + "record_state": "todo" + }, + { + "id": "T3-3A", + "path": "features/done/T3-3A-meta-cognition-tool.md", + "record_state": "done" + }, + { + "id": "T3-3B", + "path": "features/todo/T3-3B-adversarial-deliberation.md", + "record_state": "todo" + }, + { + "id": "T3-3C", + "path": "features/todo/T3-3C-socratic-questioning-engine.md", + "record_state": "todo" + }, + { + "id": "T4-4A", + "path": "features/todo/T4-4A-bayesian-confidence-propagation.md", + "record_state": "todo" + }, + { + "id": "T4-4B", + "path": "features/todo/T4-4B-uncertainty-quantification-tags.md", + "record_state": "todo" + }, + { + "id": "T5-5A", + "path": "features/done/T5-5A-new-edge-types.md", + "record_state": "done" + }, + { + "id": "T5-5B", + "path": "features/todo/T5-5B-pagerank-importance-propagation.md", + "record_state": "todo" + }, + { + "id": "T6-6A", + "path": "features/todo/T6-6A-cross-session-pattern-extraction.md", + "record_state": "todo" + }, + { + "id": "T6-6B", + "path": "features/todo/T6-6B-failure-mode-memory.md", + "record_state": "todo" + }, + { + "id": "T7-P01", + "path": "features/todo/T7-P01-pipeline-constraint-mapping.md", + "record_state": "todo" + }, + { + "id": "T7-P02", + "path": "features/todo/T7-P02-pipeline-diverse-framing.md", + "record_state": "todo" + }, + { + "id": "T7-P03", + "path": "features/todo/T7-P03-pipeline-hypothesis-generation.md", + "record_state": "todo" + }, + { + "id": "T7-P04", + "path": "features/todo/T7-P04-pipeline-evidence-gathering.md", + "record_state": "todo" + }, + { + "id": "T7-P05", + "path": "features/todo/T7-P05-pipeline-tradeoff-analysis.md", + "record_state": "todo" + }, + { + "id": "T7-P06", + "path": "features/todo/T7-P06-pipeline-adversarial-challenge.md", + "record_state": "todo" + }, + { + "id": "T7-P07", + "path": "features/todo/T7-P07-pipeline-synthesis.md", + "record_state": "todo" + }, + { + "id": "T7-P08", + "path": "features/todo/T7-P08-pipeline-verification.md", + "record_state": "todo" + }, + { + "id": "T7-P09", + "path": "features/todo/T7-P09-pipeline-metacognitive-review.md", + "record_state": "todo" + }, + { + "id": "T7-P10", + "path": "features/todo/T7-P10-pipeline-confidence-routing.md", + "record_state": "todo" + } + ] +} diff --git a/features/todo/T2-2E-rubber-duck-articulation.md b/features/todo/T2-2F-rubber-duck-articulation.md similarity index 99% rename from features/todo/T2-2E-rubber-duck-articulation.md rename to features/todo/T2-2F-rubber-duck-articulation.md index 07acd78..ce9cc6b 100644 --- a/features/todo/T2-2E-rubber-duck-articulation.md +++ b/features/todo/T2-2F-rubber-duck-articulation.md @@ -1,4 +1,4 @@ -# T2-2E: Rubber Duck Articulation Tool +# T2-2F: Rubber Duck Articulation Tool **Tier:** 2 — New Reasoning Paradigms **Priority:** P1 (Highest impact on reasoning quality) @@ -381,4 +381,4 @@ PipelineStage::RubberDuckArticulation => { - Rubin, 1977 — "Mnemonic Vocabulary" — original rubber duck reference - Hunt & Thomas, 1999 — *The Pragmatic Programmer* — rubber duck debugging popularized - Madaan et al., 2023 — *Self-Refine* (arXiv:2303.17651) — iterative self-critique -- nesquikm/mcp-rubber-duck — multi-LLM duck panel implementation \ No newline at end of file +- nesquikm/mcp-rubber-duck — multi-LLM duck panel implementation diff --git a/justfile b/justfile index 2087239..d7d95a1 100644 --- a/justfile +++ b/justfile @@ -4,21 +4,43 @@ default: @just --list build: - CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-{{justfile_directory()}}/target}" cargo build --release + CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-{{ justfile_directory() }}/target}" cargo build --release check: - CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-{{justfile_directory()}}/target}" cargo fmt --check - CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-{{justfile_directory()}}/target}" cargo clippy -- -D warnings - CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-{{justfile_directory()}}/target}" cargo test + CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-{{ justfile_directory() }}/target}" cargo fmt --check + CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-{{ justfile_directory() }}/target}" cargo clippy --all-targets --all-features -- -D warnings + just unit + +unit: + CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-{{ justfile_directory() }}/target}" cargo test --all-targets --all-features + python3 -m unittest discover -s tests -p "test_*.py" -v + +eval: build + python3 evals/run_contract_evals.py --binary "${CARGO_TARGET_DIR:-{{ justfile_directory() }}/target}/release/reasonkit-think-mcp" + +audit: + cargo audit --deny warnings + +package-check: + CARGO_TARGET_DIR="{{ justfile_directory() }}/target/package-check" cargo package --locked + +registry-check: + uvx --from check-jsonschema==0.38.0 check-jsonschema --schemafile https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json server.json + +dist-check: + @test "$(dist --version)" = "cargo-dist 0.32.0" + @dist manifest --artifacts=local --output-format=json --no-local-paths > /dev/null + +ci: check eval audit package-check registry-check dist-check fmt: - CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-{{justfile_directory()}}/target}" cargo fmt + CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-{{ justfile_directory() }}/target}" cargo fmt # Rebuild release binary + validate workspace MCP wiring + smoke test. mcp-refresh: #!/usr/bin/env bash set -euo pipefail - ROOT="{{justfile_directory()}}" + ROOT="{{ justfile_directory() }}" CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$ROOT/target}" cargo build --release python3 -m json.tool "$ROOT/../.mcp.json" >/dev/null test -x "$ROOT/target/release/reasonkit-think-mcp" diff --git a/scripts/client_compat_check.py b/scripts/client_compat_check.py index 3b7fef7..32cbb15 100755 --- a/scripts/client_compat_check.py +++ b/scripts/client_compat_check.py @@ -5,13 +5,22 @@ import json import os +import shutil import subprocess import sys import time from pathlib import Path -PROTOCOL_VERSIONS = ("2024-11-05", "2025-03-26", "2025-06-18") +PROTOCOL_VERSIONS = ( + "2024-11-05", + "2025-03-26", + "2025-06-18", + "2025-11-25", + "2026-07-28", +) EXPECTED_SERVER_NAME = "reasonkit-think-mcp" +TOOL_PACK_COUNTS = {"core": 13, "standard": 33, "full": 47} +VERSION_PROBE_TIMEOUT_SECONDS = 3 def send(proc: subprocess.Popen[str], payload: dict) -> None: @@ -56,10 +65,14 @@ def schema_type_issues(schema: object, path: str = "$") -> list[str]: return issues -def probe_protocol(binary: Path, protocol_version: str) -> dict: +def probe_protocol(binary: Path, protocol_version: str, tool_pack: str = "core") -> dict: env = os.environ.copy() env.setdefault("TMPDIR", "/tmp") env.setdefault("RUST_LOG", "warn") + if tool_pack == "core": + env.pop("REASONKIT_TOOL_PACK", None) + else: + env["REASONKIT_TOOL_PACK"] = tool_pack proc = subprocess.Popen( [str(binary)], cwd="/", @@ -109,6 +122,7 @@ def probe_protocol(binary: Path, protocol_version: str) -> dict: server_info = initialize.get("result", {}).get("serverInfo", {}) return { "protocol": protocol_version, + "tool_pack": tool_pack, "init_ms": init_ms, "server_name": server_info.get("name"), "tool_count": len(tools), @@ -125,8 +139,108 @@ def probe_protocol(binary: Path, protocol_version: str) -> dict: proc.wait(timeout=2) +def probe_invalid_tool_pack(binary: Path) -> dict: + env = os.environ.copy() + env.setdefault("TMPDIR", "/tmp") + env["RUST_LOG"] = "warn" + env["REASONKIT_TOOL_PACK"] = "invalid-pack" + proc = subprocess.Popen( + [str(binary)], + cwd="/", + env=env, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + tool_count = 0 + stderr = "" + try: + send( + proc, + { + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-11-25", + "capabilities": {}, + "clientInfo": { + "name": "reasonkit-invalid-pack-check", + "version": "1.0.0", + }, + }, + }, + ) + recv(proc) + send(proc, {"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}) + send(proc, {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}) + tool_count = len(recv(proc).get("result", {}).get("tools", [])) + finally: + proc.terminate() + try: + _, stderr = proc.communicate(timeout=2) + except subprocess.TimeoutExpired: + proc.kill() + _, stderr = proc.communicate(timeout=2) + return { + "requested": "invalid-pack", + "fallback_tool_count": tool_count, + "warning_on_stderr": "invalid REASONKIT_TOOL_PACK" in stderr, + } + + +def resolve_client_command(command: object) -> Path | None: + if not isinstance(command, str) or not command.strip(): + return None + + command = command.strip() + has_path_separator = os.sep in command or bool(os.altsep and os.altsep in command) + if has_path_separator: + return Path(command).expanduser().resolve() + + resolved = shutil.which(command) + return Path(resolved).resolve() if resolved else None + + +def executable_version(binary: Path | None) -> str | None: + if binary is None or not binary.is_file() or not os.access(binary, os.X_OK): + return None + try: + result = subprocess.run( + [str(binary), "--version"], + check=False, + capture_output=True, + text=True, + timeout=VERSION_PROBE_TIMEOUT_SECONDS, + ) + except (OSError, subprocess.TimeoutExpired): + return None + if result.returncode != 0: + return None + + first_line = result.stdout.strip().splitlines() + if not first_line: + return None + prefix = f"{EXPECTED_SERVER_NAME} " + if not first_line[0].startswith(prefix): + return None + return first_line[0].removeprefix(prefix).strip() or None + + +def same_executable(left: Path | None, right: Path) -> bool: + if left is None or not left.exists() or not right.exists(): + return False + try: + return left.samefile(right) + except OSError: + return left.resolve() == right.resolve() + + def client_config_checks(root: Path, binary: Path) -> list[dict]: home = Path.home() + candidate = binary.expanduser().resolve() + candidate_version = executable_version(candidate) configs = { "copilot-user": home / ".copilot" / "mcp-config.json", "copilot-workspace": root.parent / ".mcp.json", @@ -149,14 +263,36 @@ def client_config_checks(root: Path, binary: Path) -> list[dict]: continue command = entry.get("command") type_ok = entry.get("type") in ("stdio", "local") + resolved = resolve_client_command(command) + command_exists = bool(resolved and resolved.is_file()) + command_executable = bool(resolved and os.access(resolved, os.X_OK)) + resolved_version = executable_version(resolved) + resolved_matches_candidate = same_executable(resolved, candidate) + version_matches_candidate = bool( + candidate_version + and resolved_version + and resolved_version == candidate_version + ) checks.append( { "client": name, - "status": "ok" if command == str(binary) and type_ok else "drift", + "status": ( + "ok" + if type_ok + and command_executable + and (resolved_matches_candidate or version_matches_candidate) + else "drift" + ), "type": entry.get("type"), + "configured_command": command, + "resolved_command": str(resolved) if resolved else None, "command_matches_release_binary": command == str(binary), - "command_exists": bool(command and Path(command).exists()), - "command_executable": bool(command and os.access(command, os.X_OK)), + "resolved_matches_release_binary": resolved_matches_candidate, + "candidate_version": candidate_version, + "resolved_version": resolved_version, + "version_matches_release_binary": version_matches_candidate, + "command_exists": command_exists, + "command_executable": command_executable, } ) return checks @@ -171,19 +307,25 @@ def main() -> int: "binary_exists": binary.exists(), "binary_executable": os.access(binary, os.X_OK), "protocols": [], + "invalid_tool_pack": {}, "client_configs": client_config_checks(root, binary), "status": "failed", } if result["binary_exists"] and result["binary_executable"]: - result["protocols"] = [probe_protocol(binary, version) for version in PROTOCOL_VERSIONS] + result["protocols"] = [ + probe_protocol(binary, version, tool_pack) + for version in PROTOCOL_VERSIONS + for tool_pack in TOOL_PACK_COUNTS + ] + result["invalid_tool_pack"] = probe_invalid_tool_pack(binary) ok = ( result["binary_exists"] and result["binary_executable"] and all( protocol["server_name"] == EXPECTED_SERVER_NAME - and protocol["tool_count"] >= 30 + and protocol["tool_count"] == TOOL_PACK_COUNTS[protocol["tool_pack"]] and protocol["prompt_count"] >= 8 and protocol["schema_issue_count"] == 0 for protocol in result["protocols"] @@ -192,6 +334,8 @@ def main() -> int: check["status"] in ("ok", "missing_config") for check in result["client_configs"] ) + and result["invalid_tool_pack"].get("fallback_tool_count") == 13 + and result["invalid_tool_pack"].get("warning_on_stderr") is True ) result["status"] = "ok" if ok else "failed" print(json.dumps(result, indent=2)) diff --git a/scripts/smoke_test.py b/scripts/smoke_test.py index 8fcfbf6..7e54fb0 100755 --- a/scripts/smoke_test.py +++ b/scripts/smoke_test.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""End-to-end MCP stdio smoke test for reasonkit-think-mcp (v1 + v2).""" +"""End-to-end MCP stdio smoke test for the complete full tool pack.""" import json import os @@ -43,11 +43,16 @@ def parse_resource_text(response: dict) -> dict: def main() -> int: root = Path(__file__).resolve().parent.parent + contract = json.loads((root / "evals" / "tool-packs.json").read_text()) + expected_tools = contract["toolPacks"]["full"] for example in root.glob("examples/*.json"): json.loads(example.read_text()) env = os.environ.copy() - env["CARGO_TARGET_DIR"] = str(root / "target") + env.setdefault("CARGO_TARGET_DIR", str(root / "target")) + # The comprehensive smoke exercises every compatibility route. The product + # default is the 13-tool core; full is the explicit 47-tool rollback pack. + env["REASONKIT_TOOL_PACK"] = "full" # Drain neither stderr here; a full `cargo run` build can fill PIPE and deadlock the child. proc = subprocess.Popen( @@ -114,6 +119,24 @@ def main() -> int: ) native_feature_triage = recv(proc) + send( + proc, + { + "jsonrpc": "2.0", + "id": 304, + "method": "resources/read", + "params": {"uri": "reasoning://config/tool-packs"}, + }, + ) + native_tool_packs = recv(proc) + + think_query_unavailable = call_tool( + proc, + 305, + "think_query", + {"query": "must not simulate retrieval"}, + ) + # v1 checks v1_ok = call_tool( proc, @@ -294,7 +317,7 @@ def main() -> int: {"deliberation_id": harden_id}, ) harden_frontier = parse_text_result(harden_boot)["payload"]["frontier"] - call_tool( + harden_expand = call_tool( proc, 21, "expand_thoughts", @@ -304,6 +327,7 @@ def main() -> int: "count": 1, }, ) + harden_scaffold_id = parse_text_result(harden_expand)["created_nodes"][0]["node_id"] harden_verify = call_tool( proc, 22, @@ -349,12 +373,44 @@ def main() -> int: }, }, ) - harden_consensus_relaxed = call_tool( + harden_action_spoof = call_tool( + proc, + 250, + "record_reasoning_action", + { + "deliberation_id": harden_id, + "tool_name": "rg", + "parameters": {"pattern": "release"}, + "tags": ["agent-authored"], + "add_to_frontier": True, + }, + ) + harden_scaffold_consensus = call_tool( proc, 25, "consensus_answer", {"deliberation_id": harden_id}, ) + harden_agent_node = call_tool( + proc, + 251, + "add_thought_node", + { + "deliberation_id": harden_id, + "content": "Agent-authored assessment accepts the residual claim risk under the explicitly relaxed policy.", + "parent_node_id": harden_scaffold_id, + "edge_kind": "refines", + "edge_note": "replace scaffold with host-authored assessment", + "tags": ["smoke", "governance-authorship"], + "add_to_frontier": True, + }, + ) + harden_consensus_relaxed = call_tool( + proc, + 252, + "consensus_answer", + {"deliberation_id": harden_id}, + ) alias_set = call_tool( proc, 26, @@ -579,6 +635,16 @@ def main() -> int: "assumption_id": assumption_payload["assumption"]["assumption_id"], "status": "verified", "confidence": 0.95, + "evidence": [ + { + "source": "smoke-test-harness", + "tier": "tier1", + "independence_group": "smoke-test-harness", + "supports": True, + "contradictory": False, + "unambiguous": True, + } + ], "notes": "smoke-test confirmation", }, ) @@ -782,40 +848,6 @@ def main() -> int: }, ) - expected_tools = { - "sequentialthinking_tools", - "get_thinking_history", - "clear_thinking_history", - "start_deliberation", - "expand_thoughts", - "score_thoughts", - "prune_thoughts", - "start_skeleton_of_thought", - "run_skeleton_elaboration", - "link_thoughts", - "merge_thought_branches", - "distill_thought_cluster", - "refine_thoughts", - "record_assumption", - "set_assumption_status", - "replay_reasoning_session", - "converge_reasoning", - "apply_reasoning_pattern", - "verify_thoughts", - "consensus_answer", - "run_reasonkit_pipeline", - "run_reasoning_checkpoint", - "plan_tool_sequence", - "export_memory_snapshot", - "export_reasoning_audit", - "list_reasoning_resources", - "read_reasoning_resource", - "set_verification_policy", - "set_reasoning_aliases", - "reasoning_autopilot", - "reasoning_intent_router", - "run_thinking_mode", - } expected_prompts = { "sequential-thinking-guidance", "tot-planner-guidance", @@ -839,6 +871,9 @@ def main() -> int: export_payload = parse_text_result(exported) graph_history_payload = parse_text_result(graph_history) harden_verify_payload = parse_text_result(harden_verify) + harden_action_spoof_payload = parse_text_result(harden_action_spoof) + harden_scaffold_consensus_payload = parse_text_result(harden_scaffold_consensus) + harden_consensus_relaxed_payload = parse_text_result(harden_consensus_relaxed) refined_got_payload = parse_text_result(refined_got) resource_list_payload = parse_text_result(resources_list) provider_payload = parse_text_result(providers_resource) @@ -867,21 +902,34 @@ def main() -> int: sot_router_payload = parse_text_result(sot_router) native_thinking_modes_payload = parse_resource_text(native_thinking_modes) native_feature_triage_payload = parse_resource_text(native_feature_triage) + native_tool_packs_payload = parse_resource_text(native_tool_packs) + think_query_unavailable_payload = parse_text_result(think_query_unavailable) run_mode_payload = parse_text_result(run_mode) feature_router_payload = parse_text_result(feature_router) autopilot_map_payload = parse_text_result(autopilot_map) ok = ( "result" in initialize - and expected_tools.issubset(set(tools)) + and tools == expected_tools and expected_prompts.issubset(set(prompts)) and "reasoning://thinking-modes" in native_resources + and "reasoning://config/tool-packs" in native_resources and "reasoning://workflows/feature-triage" in native_resources and native_thinking_modes_payload.get("default_tool") == "run_thinking_mode" and native_thinking_modes_payload.get("default_mode") == "auto" + and native_thinking_modes_payload.get("active_tool_pack") == "full" + and native_tool_packs_payload.get("active") == "full" + and native_tool_packs_payload.get("default") == "core" + and native_tool_packs_payload.get("active_tool_count") == 47 + and think_query_unavailable.get("result", {}).get("isError", False) + and think_query_unavailable_payload.get("code") + == "experimental_memory_unavailable" + and think_query_unavailable.get("result", {}).get("structuredContent") + == think_query_unavailable_payload and "recommended_sequence" in native_feature_triage_payload and "does not read local files" in native_feature_triage_payload.get("critical_boundary", "") and not v1_ok.get("result", {}).get("isError", False) + and v1_ok.get("result", {}).get("structuredContent") == parse_text_result(v1_ok) and not v1_clear.get("result", {}).get("isError", False) and invalid.get("result", {}).get("isError", False) and invalid_payload.get("history_length") == 0 @@ -947,8 +995,17 @@ def main() -> int: and harden_verify_payload["claim_status_summary"]["data_deficit"] >= 1 and harden_consensus.get("result", {}).get("isError", False) and not harden_policy_set.get("result", {}).get("isError", False) + and harden_action_spoof.get("result", {}).get("isError", False) + and harden_action_spoof_payload.get("code") == "invalid_input" + and harden_scaffold_consensus.get("result", {}).get("isError", False) + and harden_scaffold_consensus_payload.get("code") == "consensus_blocked" + and "final_answer" not in harden_scaffold_consensus_payload + and not harden_agent_node.get("result", {}).get("isError", False) and not alias_set.get("result", {}).get("isError", False) and not harden_consensus_relaxed.get("result", {}).get("isError", False) + and "Agent-authored assessment" in harden_consensus_relaxed_payload["final_answer"] + and "SCAFFOLD" not in harden_consensus_relaxed_payload["final_answer"] + and "hard fail check" not in harden_consensus_relaxed_payload["final_answer"] and "aliases" in parse_text_result(aliases_resource) ) @@ -958,6 +1015,8 @@ def main() -> int: "prompts": prompts, "native_resources": native_resources, "native_thinking_modes_default_tool": native_thinking_modes_payload.get("default_tool"), + "native_tool_pack": native_tool_packs_payload.get("active"), + "think_query_error": think_query_unavailable_payload.get("code"), "native_feature_triage_steps": len(native_feature_triage_payload.get("recommended_sequence", [])), "providers_active_mode": provider_payload.get("active_mode"), "telemetry_graph_depth_max": telemetry_payload.get("graph_depth_max"), @@ -976,9 +1035,21 @@ def main() -> int: "harden_data_deficit": harden_verify_payload["claim_status_summary"]["data_deficit"], "harden_consensus_error": harden_consensus.get("result", {}).get("isError"), "harden_policy_set": not harden_policy_set.get("result", {}).get("isError", False), + "harden_action_spoof_error": harden_action_spoof.get("result", {}).get( + "isError" + ), + "harden_scaffold_consensus_error": harden_scaffold_consensus.get( + "result", {} + ).get("isError"), + "harden_agent_node": not harden_agent_node.get("result", {}).get("isError", False), "aliases_set": not alias_set.get("result", {}).get("isError", False), "harden_relaxed_consensus_error": harden_consensus_relaxed.get("result", {}).get("isError"), - "harden_relaxed_consensus_payload": harden_consensus_relaxed.get("result", {}), + "harden_relaxed_consensus_route": harden_consensus_relaxed_payload.get( + "route_decision" + ), + "harden_relaxed_consensus_answer": harden_consensus_relaxed_payload.get( + "final_answer" + ), "sot_phase_id": sot_phase_id, "sot_elaboration_status": sot_elaboration_payload.get("status"), "sot_router_tools": sot_router_payload.get("tool_plan"), diff --git a/server.json b/server.json new file mode 100644 index 0000000..d10b6c5 --- /dev/null +++ b/server.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.reasonkit/reasonkit-think", + "title": "ReasonKit Think", + "description": "Auditable decision protocol and fail-closed evidence gate for MCP agents", + "version": "0.2.0", + "repository": { + "url": "https://github.com/reasonkit/ReasonKit-think", + "source": "github" + }, + "packages": [ + { + "registryType": "cargo", + "identifier": "reasonkit-think-mcp", + "version": "0.2.0", + "transport": { + "type": "stdio" + } + } + ] +} diff --git a/src/agent_protocol.rs b/src/agent_protocol.rs index 551374d..d6b9c11 100644 --- a/src/agent_protocol.rs +++ b/src/agent_protocol.rs @@ -20,6 +20,7 @@ pub struct NodeView { pub score: Option, pub branch_id: Option, pub node_type: ThoughtNodeType, + pub agent_authored_provenance: bool, } #[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)] @@ -134,7 +135,9 @@ const SCAFFOLD_TAGS: &[&str] = &[ ]; pub fn is_agent_authored(node: &NodeView) -> bool { - node.tags.iter().any(|t| t == "agent-authored") + matches!(node.node_type, ThoughtNodeType::Thought) + && node.agent_authored_provenance + && node.tags.iter().any(|t| t == "agent-authored") } pub fn is_scaffold_node(node: &NodeView) -> bool { @@ -356,8 +359,11 @@ pub struct PipelineContext<'a> { pub blocking_assumptions: &'a [String], } -pub fn run_pipeline_stage(stage: &str, ctx: &PipelineContext<'_>) -> PipelineStageArtifact { - match stage { +pub fn run_pipeline_stage( + stage: &str, + ctx: &PipelineContext<'_>, +) -> Result { + let artifact = match stage { "constraint-mapping" => stage_constraint_mapping(ctx), "diverse-thinking" => stage_diverse_thinking(ctx), "first-principles" => stage_first_principles(ctx), @@ -368,8 +374,9 @@ pub fn run_pipeline_stage(stage: &str, ctx: &PipelineContext<'_>) -> PipelineSta "triangulation" => stage_triangulation(ctx), "brutal-honesty" => stage_brutal_honesty(ctx), "calibration-routing" => stage_calibration_routing(ctx), - _ => stage_generic(stage, ctx), - } + _ => return Err(format!("invalid pipeline stage id: {stage}")), + }; + Ok(artifact) } fn stage_constraint_mapping(ctx: &PipelineContext<'_>) -> PipelineStageArtifact { @@ -684,23 +691,6 @@ fn stage_calibration_routing(ctx: &PipelineContext<'_>) -> PipelineStageArtifact } } -fn stage_generic(stage: &str, ctx: &PipelineContext<'_>) -> PipelineStageArtifact { - PipelineStageArtifact { - stage: stage.to_string(), - finding: format!("Stage `{stage}` completed (generic protocol marker)"), - confidence: 0.6, - confidence_basis: "Unknown stage id; minimal structural pass".to_string(), - structured: json!({ "goal": ctx.goal }), - gate: StageGate { - stage: stage.to_string(), - passed: true, - blockers: Vec::new(), - recommended_tools: vec!["add_thought_node".to_string()], - }, - agent_tasks: Vec::new(), - } -} - /// Structural MCTS over existing scored nodes (no LLM rollouts). pub fn mcts_recommend_path( nodes: &HashMap, @@ -907,6 +897,7 @@ mod tests { score: None, branch_id: Some("b1".into()), node_type: ThoughtNodeType::Thought, + agent_authored_provenance: false, }; assert!(is_scaffold_node(&n)); assert!(!is_agent_authored(&n)); diff --git a/src/main.rs b/src/main.rs index f911396..eff3faf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,10 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; use std::time::Duration; -use rmcp::handler::server::{router::prompt::PromptRouter, wrapper::Parameters}; +use rmcp::handler::server::{ + router::{prompt::PromptRouter, tool::ToolRouter}, + wrapper::Parameters, +}; use rmcp::{ ErrorData as McpError, RoleServer, model::*, @@ -39,6 +42,174 @@ const REDACTION: &str = "[redacted: possible prompt-injection text]"; static ID_SEQ: AtomicU64 = AtomicU64::new(1); +const CORE_TOOL_NAMES: &[&str] = &[ + "add_thought_node", + "consensus_answer", + "export_reasoning_audit", + "get_reasoning_coaching", + "reasoning_intent_router", + "record_assumption", + "run_reasoning_checkpoint", + "run_reasonkit_pipeline", + "run_thinking_mode", + "set_assumption_status", + "set_verification_policy", + "start_deliberation", + "verify_thoughts", +]; + +const STANDARD_TOOL_NAMES: &[&str] = &[ + "add_thought_node", + "apply_reasoning_pattern", + "consensus_answer", + "converge_reasoning", + "distill_thought_cluster", + "evaluate_reasoning_quality", + "expand_thoughts", + "export_memory_snapshot", + "export_reasoning_audit", + "get_reasoning_coaching", + "get_thinking_history", + "link_thoughts", + "list_reasoning_resources", + "merge_thought_branches", + "plan_tool_sequence", + "prune_thoughts", + "read_reasoning_resource", + "reasoning_intent_router", + "record_assumption", + "refine_thoughts", + "replay_reasoning_session", + "run_reasoning_checkpoint", + "run_reasonkit_pipeline", + "run_skeleton_elaboration", + "run_thinking_mode", + "score_thoughts", + "set_assumption_status", + "set_reasoning_aliases", + "set_scoring_rubric", + "set_verification_policy", + "start_deliberation", + "start_skeleton_of_thought", + "verify_thoughts", +]; + +const FULL_TOOL_NAMES: &[&str] = &[ + "add_thought_node", + "apply_algorithm_template", + "apply_reasoning_pattern", + "clear_thinking_history", + "consensus_answer", + "converge_reasoning", + "distill_thought_cluster", + "evaluate_reasoning_quality", + "expand_thoughts", + "export_memory_snapshot", + "export_reasoning_audit", + "get_reasoning_coaching", + "get_thinking_history", + "link_thoughts", + "list_reasoning_resources", + "mcts_select_path", + "merge_thought_branches", + "plan_tool_sequence", + "prune_thoughts", + "query_failure_memory", + "read_reasoning_resource", + "reasoning_autopilot", + "reasoning_intent_router", + "record_assumption", + "record_reasoning_action", + "record_reasoning_observation", + "refine_thoughts", + "replay_reasoning_session", + "run_reasoning_checkpoint", + "run_reasonkit_pipeline", + "run_skeleton_elaboration", + "run_thinking_mode", + "score_thoughts", + "sequentialthinking_tools", + "set_assumption_status", + "set_reasoning_aliases", + "set_scoring_rubric", + "set_verification_policy", + "start_deliberation", + "start_skeleton_of_thought", + "think_query", + "verify_thoughts", + "web_research_enqueue", + "web_research_fetch", + "web_research_task_status", + "web_research_triangulate", + "web_research_verify", +]; + +const INVALID_TOOL_PACK_WARNING: &str = + "invalid REASONKIT_TOOL_PACK; expected core, standard, or full; using safe core fallback"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +enum ToolPack { + Core, + Standard, + Full, +} + +impl ToolPack { + fn as_str(self) -> &'static str { + match self { + Self::Core => "core", + Self::Standard => "standard", + Self::Full => "full", + } + } + + fn members(self) -> &'static [&'static str] { + match self { + Self::Core => CORE_TOOL_NAMES, + Self::Standard => STANDARD_TOOL_NAMES, + Self::Full => FULL_TOOL_NAMES, + } + } + + fn contains(self, tool_name: &str) -> bool { + self.members().binary_search(&tool_name).is_ok() + } + + fn minimum_for(tool_name: &str) -> Option { + if Self::Core.contains(tool_name) { + Some(Self::Core) + } else if Self::Standard.contains(tool_name) { + Some(Self::Standard) + } else if Self::Full.contains(tool_name) { + Some(Self::Full) + } else { + None + } + } + + fn resolve(raw: Option<&str>) -> (Self, Option<&'static str>) { + match raw.map(str::trim) { + None | Some("") | Some("core") => (Self::Core, None), + Some("standard") => (Self::Standard, None), + Some("full") => (Self::Full, None), + Some(_) => (Self::Core, Some(INVALID_TOOL_PACK_WARNING)), + } + } + + fn from_env() -> Self { + let (pack, warning) = match std::env::var("REASONKIT_TOOL_PACK") { + Ok(value) => Self::resolve(Some(&value)), + Err(std::env::VarError::NotPresent) => Self::resolve(None), + Err(std::env::VarError::NotUnicode(_)) => (Self::Core, Some(INVALID_TOOL_PACK_WARNING)), + }; + if let Some(message) = warning { + tracing::warn!("{message}"); + } + pack + } +} + macro_rules! lock_or_return { ($lock:expr, $store_name:literal) => { match $lock.lock() { @@ -48,27 +219,114 @@ macro_rules! lock_or_return { }; } -/// Backend mode for reasoning and evaluation. -/// - Agent (default): AI agent does all reasoning locally using built-in capabilities -/// - External (future): AI agent can delegate to external LLM APIs for scoring/peer-review +const INVALID_BACKEND_MODE_WARNING: &str = + "invalid BACKEND_MODE; expected agent or external; using safe agent fallback"; +const EXTERNAL_BACKEND_UNAVAILABLE_WARNING: &str = "BACKEND_MODE=external is unavailable because no external provider is bound; using safe agent fallback"; + +/// Effective backend mode for reasoning and evaluation. +/// The host agent is the only bound backend; reserved external configuration falls back here. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] enum BackendMode { #[serde(rename = "agent")] #[default] Agent, - #[serde(rename = "external")] - External, } impl BackendMode { + fn resolve(raw: Option<&str>) -> (Self, Option<&'static str>) { + match raw.map(str::trim) { + None | Some("agent") => (Self::Agent, None), + Some("external") => (Self::Agent, Some(EXTERNAL_BACKEND_UNAVAILABLE_WARNING)), + Some(_) => (Self::Agent, Some(INVALID_BACKEND_MODE_WARNING)), + } + } + + fn resolve_env() -> (Self, Option<&'static str>) { + match std::env::var("BACKEND_MODE") { + Ok(value) => Self::resolve(Some(&value)), + Err(std::env::VarError::NotPresent) => Self::resolve(None), + Err(std::env::VarError::NotUnicode(_)) => { + (Self::Agent, Some(INVALID_BACKEND_MODE_WARNING)) + } + } + } + fn from_env() -> Self { - match std::env::var("BACKEND_MODE").as_deref() { - Ok("external") => BackendMode::External, - _ => BackendMode::Agent, + let (mode, warning) = Self::resolve_env(); + if let Some(message) = warning { + tracing::warn!("{message}"); + } + mode + } + + fn as_str(self) -> &'static str { + match self { + Self::Agent => "agent", } } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CliAction { + ServeStdio, + Version, + Help, + DoctorJson, +} + +const CLI_HELP: &str = "ReasonKit Think MCP — governed reasoning protocols for AI agents + +Usage: + reasonkit-think-mcp Start the MCP stdio server + reasonkit-think-mcp doctor --json Print a machine-readable installation report + reasonkit-think-mcp --version Print the installed version + reasonkit-think-mcp --help Print this help + +Environment: + REASONKIT_TOOL_PACK=core|standard|full Advertised tool surface (default: core) + BACKEND_MODE=agent|external Reasoning backend; external currently warns and falls back to agent"; + +fn parse_cli_action(args: &[String]) -> Result { + match args { + [] => Ok(CliAction::ServeStdio), + [arg] if arg == "--version" || arg == "-V" => Ok(CliAction::Version), + [arg] if arg == "--help" || arg == "-h" => Ok(CliAction::Help), + [command] if command == "doctor" => Ok(CliAction::DoctorJson), + [command, format] if command == "doctor" && format == "--json" => Ok(CliAction::DoctorJson), + _ => Err(format!( + "unsupported arguments: {}\n\n{CLI_HELP}", + args.join(" ") + )), + } +} + +fn doctor_report( + raw_tool_pack: Option<&str>, + backend_resolution: (BackendMode, Option<&'static str>), +) -> Value { + let (tool_pack, tool_pack_warning) = ToolPack::resolve(raw_tool_pack); + let (backend_mode, backend_warning) = backend_resolution; + let warnings = [tool_pack_warning, backend_warning] + .into_iter() + .flatten() + .collect::>(); + + json!({ + "schema_version": 1, + "status": if warnings.is_empty() { "ok" } else { "warning" }, + "version": env!("CARGO_PKG_VERSION"), + "transport": "stdio", + "active_tool_pack": tool_pack.as_str(), + "tool_count": tool_pack.members().len(), + "semantic_engine": "host_agent", + "backend_mode": backend_mode.as_str(), + "external_provider_bound": false, + "retrieval_provider_bound": false, + "structured_content": true, + "warnings": warnings, + }) +} + #[derive(Debug, Serialize, Deserialize)] struct AppState { thoughts: ThinkingStore, @@ -296,9 +554,10 @@ struct ThinkServer { deliberations: Arc>, failure_memory: Arc>>, session_pattern_notes: Arc>>, - hot_memory: Arc, #[allow(dead_code)] backend_mode: BackendMode, + tool_router: ToolRouter, + tool_pack: ToolPack, #[allow(dead_code)] prompt_router: PromptRouter, obs: Observability, @@ -511,6 +770,10 @@ impl ThinkServer { } fn new(max_history_size: usize) -> Self { + Self::new_with_tool_pack(max_history_size, ToolPack::from_env()) + } + + fn new_with_tool_pack(max_history_size: usize, tool_pack: ToolPack) -> Self { let path = Self::state_path().ok(); let mut thoughts_init = ThinkingStore::new(max_history_size); let mut deliberations_init = DeliberationStore::new(max_history_size); @@ -538,22 +801,61 @@ impl ThinkServer { "ReasonKit-think protocol mode (no server-side LLM reasoning)" ); - let hot_memory = Arc::new(reasonkit_mem::storage::hot::HotMemory::new( - reasonkit_mem::storage::hot::HotMemoryConfig::default(), - )); + let mut tool_router = Self::tool_router(); + for tool in tool_router.list_all() { + if !tool_pack.contains(tool.name.as_ref()) { + let disabled = tool_router.disable_route(tool.name); + debug_assert!(disabled, "generated tool route must exist"); + } + } + assert_eq!( + tool_router.list_all().len(), + tool_pack.members().len(), + "tool-pack contract drift: configured names must match generated routes" + ); + tracing::info!( + tool_pack = tool_pack.as_str(), + tool_count = tool_pack.members().len(), + "ReasonKit-think tool pack selected" + ); Self { thoughts: Arc::new(Mutex::new(thoughts_init)), deliberations: Arc::new(Mutex::new(deliberations_init)), failure_memory: Arc::new(Mutex::new(failure_memory_init)), session_pattern_notes: Arc::new(Mutex::new(session_pattern_notes_init)), - hot_memory, backend_mode, + tool_router, + tool_pack, prompt_router: Self::prompt_router(), obs: Observability::new(), } } + fn with_tool_pack_guidance(&self, text: String, tools: &[&str]) -> String { + let unavailable = tools + .iter() + .filter(|tool| !self.tool_pack.contains(tool)) + .filter_map(|tool| { + ToolPack::minimum_for(tool) + .map(|pack| format!("{tool} requires the {} tool pack", pack.as_str())) + }) + .collect::>(); + let guidance = if unavailable.is_empty() { + format!( + "Active tool pack: {}. All tools named by this prompt are available.", + self.tool_pack.as_str() + ) + } else { + format!( + "Active tool pack: {}. {}. Use run_thinking_mode for the core workflow or restart with the required REASONKIT_TOOL_PACK value.", + self.tool_pack.as_str(), + unavailable.join("; ") + ) + }; + format!("{text}\n\nTool-pack guidance: {guidance}") + } + fn run_thinking_mode_inner( &self, input: RunThinkingModeInput, @@ -814,6 +1116,7 @@ impl ThinkServer { let audit = store.export_audit(ExportReasoningAuditInput { deliberation_id: deliberation_id.clone(), + format: AuditFormat::Json, include_raw_thoughts: Some(false), })?; executed_tools.push("export_reasoning_audit".to_string()); @@ -1181,6 +1484,7 @@ impl ThinkServer { let audit = store.export_audit(ExportReasoningAuditInput { deliberation_id: deliberation_id.clone(), + format: AuditFormat::Json, include_raw_thoughts: Some(true), })?; executed.push("export_reasoning_audit".to_string()); @@ -1920,7 +2224,7 @@ exactly what weights are active after normalization." Ok(out) => { let persist_error = self.save_state().err(); if out.policy_blocked { - json_tool_error(&out) + consensus_blocked_tool_result(&out) } else if let Some(err) = persist_error { json_tool_error(&ErrorEnvelope::new("persistence_error", err, true)) } else { @@ -2011,7 +2315,9 @@ exactly what weights are active after normalization." } } - #[tool(description = "Export a complete reasoning audit artifact for a deliberation.")] + #[tool( + description = "Export the canonical reasoning audit payload plus a deterministic JSON or Markdown artifact. Raw thought content is omitted unless explicitly requested." + )] fn export_reasoning_audit( &self, Parameters(input): Parameters, @@ -2276,6 +2582,9 @@ exactly what weights are active after normalization." if input.uri == "reasoning://config/providers" { return json_tool_success(&provider_capabilities()); } + if input.uri == "reasoning://config/tool-packs" { + return json_tool_success(&tool_pack_resource(self.tool_pack)); + } if input.uri == "reasoning://config/web-research" { return json_tool_success(&json!({ "base_url": Self::web_research_base_url(), @@ -2303,79 +2612,24 @@ exactly what weights are active after normalization." return json_tool_success(&memory_sinks_resource()); } let store = lock_or_return!(self.deliberations, "deliberation"); - match read_reasoning_resource_payload(&store, &input.uri) { + match read_reasoning_resource_payload(&store, &input.uri, self.tool_pack) { Ok(payload) => json_tool_success(&payload), Err(err) => json_tool_error(&ErrorEnvelope::from_store_error(err)), } } #[tool( - description = "Perform a blazing fast semantic query using the hot-tier memory cache, integrating BM25 + vector search and RAPTOR expansion without writing to persistent stores." + description = "Compatibility-only experimental memory query. No production provider is bound, so this tool fails closed with experimental_memory_unavailable and performs no retrieval." )] - fn think_query(&self, Parameters(input): Parameters) -> CallToolResult { + fn think_query(&self, Parameters(_input): Parameters) -> CallToolResult { let started = std::time::Instant::now(); - let cache = &self.hot_memory; - - let embedding = input.query_embedding.unwrap_or_else(|| vec![0.0; 1536]); - let limit = input.limit.unwrap_or(10); - let min_score = input.min_score.unwrap_or(0.0); - - let dense_results = tokio::task::block_in_place(|| { - tokio::runtime::Handle::current().block_on(async { - cache - .search_similar_with_threshold(&embedding, limit, min_score) - .await - }) - }); - - // Initialize fusion engine - use reasonkit_mem::retrieval::fusion::{FusionEngine, to_ranked_results}; - let fusion_engine = FusionEngine::rrf(60); - let mut results_map = std::collections::HashMap::new(); - - // Add dense results - results_map.insert("dense".to_string(), to_ranked_results(dense_results)); - - // TODO: In the future, add sparse (BM25) results here: - // results_map.insert("sparse".to_string(), to_ranked_results(sparse_results)); - - let fused_results = fusion_engine.fuse(results_map).unwrap_or_default(); - - let mut final_results = Vec::new(); - for fused in fused_results.into_iter().take(limit) { - if let Some(entry) = cache.peek(&fused.id) { - final_results.push(HotMemoryEntryResult { - id: entry.id.to_string(), - content: entry.content, - score: fused.fusion_score, - metadata: entry.metadata, - }); - } - } - - let failure_coaching = self - .failure_memory - .lock() - .ok() - .map(|mem| { - let mut patterns: Vec<_> = mem.values().collect(); - patterns.sort_by_key(|b| std::cmp::Reverse(b.occurrence_count)); - patterns - .into_iter() - .take(5) - .map(|p| format!("[{}] {}", p.pattern_id, p.intervention)) - .collect::>() - }) - .unwrap_or_default(); - self.obs .record_tool_latency("think_query", started.elapsed()); - json_tool_success(&ThinkQueryResult { - results: final_results, - latency_ms: started.elapsed().as_millis(), - failure_coaching, - semantic_engine: "host_agent".to_string(), - }) + json_tool_error(&ErrorEnvelope::new( + "experimental_memory_unavailable", + "think_query has no bound production memory provider; no retrieval was performed", + false, + )) } } @@ -2398,10 +2652,8 @@ impl ThinkServer { if let Some(problem) = input.problem { lines.push(format!("Problem: {problem}")); } - GetPromptResult::new(vec![PromptMessage::new_text( - PromptMessageRole::User, - lines.join("\n"), - )]) + let text = self.with_tool_pack_guidance(lines.join("\n"), &["sequentialthinking_tools"]); + GetPromptResult::new(vec![PromptMessage::new_text(Role::User, text)]) } #[prompt( @@ -2418,7 +2670,16 @@ impl ThinkServer { let text = format!( "Plan with explicit branches.\n1) Create 3-5 candidate branches.\n2) Score each on correctness, risk, and evidence.\n3) Prune weak branches but preserve diversity.\n4) Verify critical claims before final answer.\nProblem: {problem}" ); - GetPromptResult::new(vec![PromptMessage::new_text(PromptMessageRole::User, text)]) + let text = self.with_tool_pack_guidance( + text, + &[ + "expand_thoughts", + "score_thoughts", + "prune_thoughts", + "verify_thoughts", + ], + ); + GetPromptResult::new(vec![PromptMessage::new_text(Role::User, text)]) } #[prompt( @@ -2441,7 +2702,20 @@ impl ThinkServer { 5) Score, verify critical claims, run a checkpoint, then converge with GoT merge/distill before consensus.\n\ Problem: {problem}" ); - GetPromptResult::new(vec![PromptMessage::new_text(PromptMessageRole::User, text)]) + let text = self.with_tool_pack_guidance( + text, + &[ + "start_skeleton_of_thought", + "run_skeleton_elaboration", + "add_thought_node", + "score_thoughts", + "verify_thoughts", + "run_reasoning_checkpoint", + "converge_reasoning", + "consensus_answer", + ], + ); + GetPromptResult::new(vec![PromptMessage::new_text(Role::User, text)]) } #[prompt( @@ -2456,7 +2730,8 @@ Problem: {problem}" "Apply these lenses independently then synthesize: Optimist, Pessimist, Systems Thinker, Empiricist, Contrarian, Security Adversary, Simplifier.\nTopic: {}", input.problem.unwrap_or_else(|| "General topic".to_string()) ); - GetPromptResult::new(vec![PromptMessage::new_text(PromptMessageRole::User, text)]) + let text = self.with_tool_pack_guidance(text, &["add_thought_node"]); + GetPromptResult::new(vec![PromptMessage::new_text(Role::User, text)]) } #[prompt( @@ -2471,7 +2746,8 @@ Problem: {problem}" "Use Chain-of-Verification: draft -> verification questions -> independent answers -> revised final.\nTarget: {}", input.problem.unwrap_or_else(|| "Current draft".to_string()) ); - GetPromptResult::new(vec![PromptMessage::new_text(PromptMessageRole::User, text)]) + let text = self.with_tool_pack_guidance(text, &["verify_thoughts"]); + GetPromptResult::new(vec![PromptMessage::new_text(Role::User, text)]) } #[prompt( @@ -2488,7 +2764,8 @@ Problem: {problem}" .problem .unwrap_or_else(|| "Current answer".to_string()) ); - GetPromptResult::new(vec![PromptMessage::new_text(PromptMessageRole::User, text)]) + let text = self.with_tool_pack_guidance(text, &["verify_thoughts"]); + GetPromptResult::new(vec![PromptMessage::new_text(Role::User, text)]) } #[prompt( @@ -2505,7 +2782,9 @@ Problem: {problem}" .problem .unwrap_or_else(|| "Current deliberation".to_string()) ); - GetPromptResult::new(vec![PromptMessage::new_text(PromptMessageRole::User, text)]) + let text = + self.with_tool_pack_guidance(text, &["run_reasoning_checkpoint", "consensus_answer"]); + GetPromptResult::new(vec![PromptMessage::new_text(Role::User, text)]) } #[prompt( @@ -2522,7 +2801,8 @@ Problem: {problem}" .problem .unwrap_or_else(|| "Current deliberation".to_string()) ); - GetPromptResult::new(vec![PromptMessage::new_text(PromptMessageRole::User, text)]) + let text = self.with_tool_pack_guidance(text, &["expand_thoughts", "add_thought_node"]); + GetPromptResult::new(vec![PromptMessage::new_text(Role::User, text)]) } #[prompt( @@ -2539,7 +2819,11 @@ Problem: {problem}" .problem .unwrap_or_else(|| "Current deliberation".to_string()) ); - GetPromptResult::new(vec![PromptMessage::new_text(PromptMessageRole::User, text)]) + let text = self.with_tool_pack_guidance( + text, + &["link_thoughts", "record_assumption", "add_thought_node"], + ); + GetPromptResult::new(vec![PromptMessage::new_text(Role::User, text)]) } #[prompt( @@ -2556,7 +2840,8 @@ Problem: {problem}" .problem .unwrap_or_else(|| "Current deliberation".to_string()) ); - GetPromptResult::new(vec![PromptMessage::new_text(PromptMessageRole::User, text)]) + let text = self.with_tool_pack_guidance(text, &["score_thoughts", "record_assumption"]); + GetPromptResult::new(vec![PromptMessage::new_text(Role::User, text)]) } #[prompt( @@ -2580,11 +2865,12 @@ Problem: {problem}" 6) Final answer must name the winning feature, runner-up, rejected alternatives, exact evidence used, uncertainty, and route decision.\n\ Do not fabricate feature names, TODO status, effort, dependencies, or implementation claims." ); - GetPromptResult::new(vec![PromptMessage::new_text(PromptMessageRole::User, text)]) + let text = self.with_tool_pack_guidance(text, &["run_thinking_mode"]); + GetPromptResult::new(vec![PromptMessage::new_text(Role::User, text)]) } } -#[tool_handler] +#[tool_handler(router = self.tool_router)] #[prompt_handler] impl ServerHandler for ThinkServer { fn get_info(&self) -> ServerInfo { @@ -2601,9 +2887,11 @@ impl ServerHandler for ThinkServer { .build(), ) .with_server_info(server_info) - .with_instructions( - "Use ReasonKit Think for auditable CoT, ToT, GoT, verification, governance, and reasoning-resource workflows over stdio.", - ) + .with_instructions(format!( + "Use ReasonKit Think for auditable reasoning and fail-closed governance over stdio; active tool pack: {} ({} tools). Read reasoning://config/tool-packs for the exact surface. Restart with REASONKIT_TOOL_PACK=full for the 47-tool compatibility surface.", + self.tool_pack.as_str(), + self.tool_pack.members().len() + )) } fn list_resources( @@ -2633,13 +2921,15 @@ impl ServerHandler for ThinkServer { &self, request: ReadResourceRequestParams, _context: RequestContext, - ) -> impl std::future::Future> + MaybeSendFuture + '_ + ) -> impl std::future::Future> + MaybeSendFuture + '_ { let uri = request.uri; let payload = if uri == "reasoning://telemetry/summary" { Ok(self.obs.snapshot()) } else if uri == "reasoning://config/providers" { Ok(provider_capabilities()) + } else if uri == "reasoning://config/tool-packs" { + Ok(tool_pack_resource(self.tool_pack)) } else if uri == "reasoning://config/web-research" { Ok(json!({ "base_url": Self::web_research_base_url(), @@ -2668,7 +2958,7 @@ impl ServerHandler for ThinkServer { self.deliberations .lock() .map_err(|_| "deliberation store lock unavailable".to_string()) - .and_then(|store| read_reasoning_resource_payload(&store, &uri)) + .and_then(|store| read_reasoning_resource_payload(&store, &uri, self.tool_pack)) }; let result = payload.and_then(|payload| { serde_json::to_string_pretty(&payload) @@ -2677,7 +2967,8 @@ impl ServerHandler for ThinkServer { std::future::ready(match result { Ok(text) => Ok(ReadResourceResult::new(vec![ ResourceContents::text(text, uri).with_mime_type("application/json"), - ])), + ]) + .into()), Err(err) => Err(McpError::resource_not_found(err, None)), }) } @@ -3259,26 +3550,34 @@ impl DeliberationStore { } } + let criticality = match (input.criticality, input.critical) { + (_, Some(true)) => AssumptionCriticality::Critical, + (Some(criticality), _) => criticality, + (None, critical) => assumption_criticality_from_text(&text, critical), + }; let now = chrono::Utc::now().to_rfc3339(); let policy = session.verification_policy.clone(); - let status = input.status.unwrap_or_else(|| { - if input.evidence.is_empty() { - AssumptionStatus::Proposed - } else { - match evaluate_claim_status( - &input.evidence, - &policy, - input.critical.unwrap_or(false), - ) { - VerificationStatus::Verified => AssumptionStatus::Verified, - VerificationStatus::SourceConflict => AssumptionStatus::Falsified, - VerificationStatus::DataDeficit => AssumptionStatus::Unresolved, + let evaluated_status = evaluate_claim_status( + &input.evidence, + &policy, + matches!(criticality, AssumptionCriticality::Critical), + ); + let status = match input.status { + Some(AssumptionStatus::Verified) => { + if matches!(evaluated_status, VerificationStatus::Verified) { + AssumptionStatus::Verified + } else { + AssumptionStatus::Unresolved } } - }); - let criticality = input - .criticality - .unwrap_or_else(|| assumption_criticality_from_text(&text, input.critical)); + Some(status) => status, + None if input.evidence.is_empty() => AssumptionStatus::Proposed, + None => match evaluated_status { + VerificationStatus::Verified => AssumptionStatus::Verified, + VerificationStatus::SourceConflict => AssumptionStatus::Falsified, + VerificationStatus::DataDeficit => AssumptionStatus::Unresolved, + }, + }; let assumption_id = make_id("assumption"); let analysis = heuristic_metadata("assumption_ledger_record", source_node_ids.clone()); @@ -3323,6 +3622,7 @@ impl DeliberationStore { input: SetAssumptionStatusInput, ) -> Result { let session = self.get_mut(&input.deliberation_id)?; + let policy = session.verification_policy.clone(); let analysis = heuristic_metadata( "assumption_ledger_status_update", vec![input.assumption_id.clone()], @@ -3332,8 +3632,6 @@ impl DeliberationStore { .get_mut(&input.assumption_id) .ok_or_else(|| format!("unknown assumption_id: {}", input.assumption_id))?; - assumption.status = input.status; - assumption.updated_at = chrono::Utc::now().to_rfc3339(); if let Some(confidence) = input.confidence { assumption.confidence = Some(confidence.clamp(0.0, 1.0)); } @@ -3343,6 +3641,20 @@ impl DeliberationStore { if let Some(evidence) = input.evidence { assumption.evidence = evidence; } + assumption.status = if matches!(input.status, AssumptionStatus::Verified) + && !matches!( + evaluate_claim_status( + &assumption.evidence, + &policy, + matches!(assumption.criticality, AssumptionCriticality::Critical), + ), + VerificationStatus::Verified + ) { + AssumptionStatus::Unresolved + } else { + input.status + }; + assumption.updated_at = chrono::Utc::now().to_rfc3339(); assumption.analysis = analysis.clone(); let updated = assumption.clone(); session.record_analysis(analysis); @@ -4713,7 +5025,7 @@ impl DeliberationStore { .unwrap_or_else(|| session.verification_policy.clone()) .normalize(); session.verification_policy = policy.clone(); - let mut matrix = Vec::new(); + let mut updates = Vec::new(); let explicit_claims = input.claims.unwrap_or_default(); let fallback_claims = if explicit_claims.is_empty() { @@ -4753,8 +5065,8 @@ impl DeliberationStore { } else { "" }; - matrix.push(VerificationEntry { - claim: claim.text, + updates.push(VerificationEntry { + claim: normalize_claim_text(&claim.text), critical: claim.critical, status, evidence, @@ -4766,6 +5078,19 @@ impl DeliberationStore { }); } + let mut matrix = session.verification_matrix.clone(); + for mut update in updates { + if let Some(existing) = matrix + .iter_mut() + .find(|entry| normalize_claim_text(&entry.claim) == update.claim) + { + update.critical = existing.critical || update.critical; + update.status = evaluate_claim_status(&update.evidence, &policy, update.critical); + *existing = update; + } else { + matrix.push(update); + } + } session.verification_matrix = matrix.clone(); session.record_analysis(analysis.clone()); let summary = VerificationSummary::from_entries(&matrix); @@ -4787,17 +5112,22 @@ impl DeliberationStore { fn consensus(&mut self, input: ConsensusAnswerInput) -> Result { let session = self.get_mut(&input.deliberation_id)?; let method = input.method.unwrap_or(ConsensusMethod::SelfConsistency); - if let Some(policy) = input.policy_override { - session.verification_policy = policy.normalize(); - } - let analysis = heuristic_metadata("consensus_route_heuristic", session.frontier.clone()); - + let policy = input + .policy_override + .unwrap_or_else(|| session.verification_policy.clone()) + .normalize(); let mut ranked = session .frontier .iter() .filter_map(|id| session.nodes.get(id)) .cloned() .collect::>(); + let frontier_has_agent_authored = ranked + .iter() + .any(|node| is_agent_authored(&deliberation_node_view(node))); + if frontier_has_agent_authored { + ranked.retain(|node| is_agent_authored(&deliberation_node_view(node))); + } ranked.sort_by(|a, b| { b.score .unwrap_or(0.0) @@ -4809,6 +5139,10 @@ impl DeliberationStore { if top.is_empty() { return Err("no frontier nodes available for consensus".to_string()); } + let analysis = heuristic_metadata( + "consensus_route_heuristic", + top.iter().map(|node| node.node_id.clone()).collect(), + ); let final_answer = top .iter() @@ -4827,14 +5161,25 @@ impl DeliberationStore { .any(|e| e.critical && !matches!(e.status, VerificationStatus::Verified)); let blocking_assumptions = blocking_assumption_ids(session); let assumption_blocked = !blocking_assumptions.is_empty(); - - let route_decision = if critical_unresolved || unresolved > 0 || assumption_blocked { - RouteDecision::GatherMoreEvidence - } else if matches!(session.profile, ReasoningProfile::Paranoid) { - RouteDecision::ProceedWithCaveats - } else { - RouteDecision::Proceed - }; + let authorship_blocked = !frontier_has_agent_authored; + let pipeline_blocked = session + .stage_findings + .iter() + .any(|finding| !finding.gate_passed); + let checkpoint_blocked = session + .checkpoints + .last() + .is_some_and(|checkpoint| !checkpoint.blocking_gaps.is_empty()); + let governance_blocked = authorship_blocked || pipeline_blocked || checkpoint_blocked; + + let route_decision = + if critical_unresolved || unresolved > 0 || assumption_blocked || governance_blocked { + RouteDecision::GatherMoreEvidence + } else if matches!(session.profile, ReasoningProfile::Paranoid) { + RouteDecision::ProceedWithCaveats + } else { + RouteDecision::Proceed + }; session.route_decision = Some(route_decision); let confidence = @@ -4846,10 +5191,9 @@ impl DeliberationStore { blocking_assumptions.len() ); - let policy_blocked = (critical_unresolved || assumption_blocked) - && session - .verification_policy - .fail_closed_on_critical_unresolved; + let policy_blocked = ((critical_unresolved || assumption_blocked) + && policy.fail_closed_on_critical_unresolved) + || governance_blocked; session.record_analysis(analysis.clone()); Ok(ConsensusAnswerResult { @@ -4872,9 +5216,6 @@ impl DeliberationStore { input: RunReasonKitPipelineInput, ) -> Result { let session = self.get_mut(&input.deliberation_id)?; - if let Some(policy) = input.policy_override { - session.verification_policy = policy.normalize(); - } let analysis = heuristic_metadata("reasonkit_pipeline_heuristic", session.frontier.clone()); let stages = input.stages.unwrap_or_else(|| { vec![ @@ -4915,26 +5256,24 @@ impl DeliberationStore { }; let mut pipeline_blockers = Vec::new(); - let findings = stages - .iter() - .map(|stage| { - let artifact: PipelineStageArtifact = run_pipeline_stage(stage, &ctx); - if !artifact.gate.passed { - pipeline_blockers.extend(artifact.gate.blockers.clone()); - } - StageFinding { - stage: artifact.stage, - finding: artifact.finding, - confidence: artifact.confidence, - confidence_basis: artifact.confidence_basis, - structured: artifact.structured, - gate_passed: artifact.gate.passed, - gate_blockers: artifact.gate.blockers, - agent_tasks: artifact.agent_tasks, - analysis: analysis.clone(), - } - }) - .collect::>(); + let mut findings = Vec::with_capacity(stages.len()); + for stage in &stages { + let artifact: PipelineStageArtifact = run_pipeline_stage(stage, &ctx)?; + if !artifact.gate.passed { + pipeline_blockers.extend(artifact.gate.blockers.clone()); + } + findings.push(StageFinding { + stage: artifact.stage, + finding: artifact.finding, + confidence: artifact.confidence, + confidence_basis: artifact.confidence_basis, + structured: artifact.structured, + gate_passed: artifact.gate.passed, + gate_blockers: artifact.gate.blockers, + agent_tasks: artifact.agent_tasks, + analysis: analysis.clone(), + }); + } let has_critical_unresolved = session.verification_matrix.iter().any(|e| { e.critical @@ -5007,9 +5346,10 @@ impl DeliberationStore { .get(&input.deliberation_id) .ok_or_else(|| "unknown deliberation_id".to_string())?; let include_raw = input.include_raw_thoughts.unwrap_or(false); + let audit_id = make_id("audit"); let mut payload = json!({ - "audit_id": make_id("audit"), + "audit_id": audit_id, "deliberation_id": session.deliberation_id, "session_id": session.session_id, "mode": session.mode, @@ -5040,10 +5380,20 @@ impl DeliberationStore { payload["nodes"] = json!(session.nodes); } + let artifact = match input.format { + AuditFormat::Json => render_canonical_json(&payload)?, + AuditFormat::Markdown => { + render_audit_markdown(&audit_id, session, include_raw, &payload) + } + }; + Ok(ExportReasoningAuditResult { deliberation_id: session.deliberation_id.clone(), - audit_id: make_id("audit"), + audit_id, payload, + format: input.format, + content_type: input.format.content_type().to_string(), + artifact, }) } @@ -5321,6 +5671,15 @@ impl DeliberationStore { &mut self, input: RecordReasoningActionInput, ) -> Result { + if input + .tags + .as_ref() + .is_some_and(|tags| tags.iter().any(|tag| tag == "agent-authored")) + { + return Err( + "invalid action tag: `agent-authored` is reserved for add_thought_node".to_string(), + ); + } let session = self.get_mut(&input.deliberation_id)?; if session.nodes.len() >= session.limits.max_nodes as usize { return Err("capacity limit reached: max_nodes exceeded".to_string()); @@ -5384,8 +5743,15 @@ impl DeliberationStore { input: RecordReasoningObservationInput, ) -> Result { let session = self.get_mut(&input.deliberation_id)?; - if !session.nodes.contains_key(&input.action_node_id) { - return Err(format!("unknown action_node_id: {}", input.action_node_id)); + match session.nodes.get(&input.action_node_id) { + None => return Err(format!("unknown action_node_id: {}", input.action_node_id)), + Some(node) if !matches!(node.node_type, ThoughtNodeType::Action { .. }) => { + return Err(format!( + "invalid action_node_id; must reference an Action node: {}", + input.action_node_id + )); + } + Some(_) => {} } if session.nodes.len() >= session.limits.max_nodes as usize { return Err("capacity limit reached: max_nodes exceeded".to_string()); @@ -5416,6 +5782,22 @@ impl DeliberationStore { result: result.clone(), }, }; + let action_node = session + .nodes + .get_mut(&input.action_node_id) + .expect("validated action node must remain present"); + if let ThoughtNodeType::Action { + tool_name, + parameters, + status, + } = &mut action_node.node_type + { + *status = ActionStatus::Complete; + action_node.content = format!( + "[ACTION] tool={} status={:?}\nparams={}", + tool_name, status, parameters + ); + } session.insert_node(node); session.record_analysis(analysis.clone()); Ok(RecordReasoningObservationResult { @@ -7159,74 +7541,353 @@ fn evaluate_claim_status( } } -fn has_any_ci>(text: &str, needles: &[S]) -> bool { - needles - .iter() - .map(|n| n.as_ref().to_lowercase()) - .any(|n| text.contains(&n)) -} - -fn has_any_ci_groups(text: &str, groups: &[&[String]]) -> bool { - groups.iter().any(|group| has_any_ci(text, group)) -} - -fn dedup_in_place(values: &mut Vec) { - let mut seen = HashSet::new(); - values.retain(|v| seen.insert(v.clone())); +fn normalize_claim_text(text: &str) -> String { + text.split_whitespace().collect::>().join(" ") } -fn normalize_alias_list(values: Vec) -> Vec { - let mut out = values - .into_iter() - .map(|v| v.trim().to_lowercase()) - .filter(|v| !v.is_empty()) - .collect::>(); - dedup_in_place(&mut out); - out +fn render_canonical_json(payload: &Value) -> Result { + serde_json::to_string_pretty(&canonicalize_json(payload)) + .map_err(|err| format!("audit JSON rendering failed: {err}")) } -fn extend_unique(dest: &mut Vec, add: &[String]) { - let mut set = dest.iter().cloned().collect::>(); - for value in add { - let value = value.trim().to_lowercase(); - if !value.is_empty() && set.insert(value.clone()) { - dest.push(value); +fn canonicalize_json(value: &Value) -> Value { + match value { + Value::Object(map) => { + let mut keys = map.keys().collect::>(); + keys.sort(); + let mut sorted = Map::new(); + for key in keys { + sorted.insert(key.clone(), canonicalize_json(&map[key])); + } + Value::Object(sorted) } + Value::Array(items) => Value::Array(items.iter().map(canonicalize_json).collect()), + _ => value.clone(), } } -fn explain_tool(tool: &str) -> String { - match tool { - "reasoning_autopilot" => { - "Auto mode: route intent and run the default audited reasoning flow.".to_string() - } - "run_thinking_mode" => { - "Execute Auto, Quick, Explore, Map, Sketch, or Test as one audited workflow." - .to_string() +fn render_audit_markdown( + audit_id: &str, + session: &DeliberationSession, + include_raw_thoughts: bool, + payload: &Value, +) -> String { + let route = session + .route_decision + .as_ref() + .map(audit_label) + .unwrap_or_else(|| "not_recorded".to_string()); + let active_mode = payload + .pointer("/provider_capabilities/active_mode") + .and_then(Value::as_str) + .unwrap_or("unknown"); + let semantic_engine = payload + .pointer("/provider_capabilities/semantic_engine") + .and_then(Value::as_str) + .unwrap_or("unknown"); + let mut providers = Vec::new(); + for analysis in &session.analysis_runs { + if let Some(provider) = &analysis.provider { + providers.push(provider.clone()); } - "reasoning_intent_router" => { - "Translate normal wording into Thinking Modes and MCP tool calls.".to_string() + } + for entry in &session.verification_matrix { + if let Some(provider) = &entry.analysis.provider { + providers.push(provider.clone()); } - "sequentialthinking_tools" => "Quick linear thought capture (CoT baseline).".to_string(), - "get_thinking_history" => "Inspect current quick-thought assumptions.".to_string(), - "start_deliberation" => "Open structured deep reasoning session.".to_string(), - "expand_thoughts" => "Create heuristic alternatives/branches from frontier.".to_string(), - "start_skeleton_of_thought" => { - "Create a routed Skeleton-of-Thought phase with scaffold skeleton nodes.".to_string() + } + for assumption in session.assumptions.values() { + if let Some(provider) = &assumption.analysis.provider { + providers.push(provider.clone()); } - "run_skeleton_elaboration" => { - "Elaborate SoT skeletons into prompt packs or bounded scaffold nodes.".to_string() + } + for stage in &session.stage_findings { + if let Some(provider) = &stage.analysis.provider { + providers.push(provider.clone()); } - "score_thoughts" => "Rank candidates by weighted heuristic proxies.".to_string(), - "prune_thoughts" => "Keep best/diverse candidates, remove weak branches.".to_string(), - "link_thoughts" => "Add typed GoT edges between existing thought nodes.".to_string(), - "merge_thought_branches" => { - "Converge multiple branches into one merged synthesis node.".to_string() + } + for node in session.nodes.values() { + if let Some(provider) = &node.provenance.provider { + providers.push(provider.clone()); } - "distill_thought_cluster" => { - "Compress multiple nodes into a bullet distill with dependency edges.".to_string() + } + providers.sort(); + providers.dedup(); + let providers = if providers.is_empty() { + "none recorded".to_string() + } else { + providers.join(", ") + }; + + let mut out = String::from("# Reasoning Audit\n\n"); + out.push_str("## Audit metadata\n\n"); + out.push_str("| Field | Value |\n| --- | --- |\n"); + push_markdown_row(&mut out, &["Audit ID", audit_id]); + push_markdown_row(&mut out, &["Deliberation ID", &session.deliberation_id]); + push_markdown_row(&mut out, &["Session", &session.session_id]); + push_markdown_row(&mut out, &["Mode", &audit_label(&session.mode)]); + push_markdown_row(&mut out, &["Profile", &audit_label(&session.profile)]); + push_markdown_row(&mut out, &["Goal", &session.goal]); + push_markdown_row(&mut out, &["Route decision", &route]); + + out.push_str("\n## Provenance and provider boundaries\n\n"); + if active_mode == "heuristic" { + out.push_str( + "> **HEURISTIC OUTPUT:** ReasonKit supplied deterministic structural analysis; no model-backed semantic verification is implied.\n\n", + ); + } else { + out.push_str("> **PROVIDER-BACKED OUTPUT:** Review the provider metadata below.\n\n"); + } + out.push_str("| Field | Value |\n| --- | --- |\n"); + push_markdown_row(&mut out, &["Active analysis mode", active_mode]); + push_markdown_row(&mut out, &["Semantic engine", semantic_engine]); + push_markdown_row(&mut out, &["Recorded providers", &providers]); + push_markdown_row( + &mut out, + &[ + "Boundary", + "Model- and retrieval-backed claims remain fail-closed unless a real provider binding is active; canonical provider details remain in payload.provider_capabilities.", + ], + ); + + out.push_str("\n## Verification matrix\n\n"); + out.push_str("| Claim | Critical | Status | Evidence sources |\n| --- | --- | --- | --- |\n"); + let mut verification = session.verification_matrix.iter().collect::>(); + verification.sort_by(|left, right| left.claim.cmp(&right.claim)); + if verification.is_empty() { + push_markdown_row(&mut out, &["none", "no", "not_recorded", "none"]); + } else { + for entry in verification { + let mut sources = entry + .evidence + .iter() + .map(|evidence| format!("{} ({})", evidence.source, audit_label(&evidence.tier))) + .collect::>(); + sources.sort(); + sources.dedup(); + let sources = if sources.is_empty() { + "none".to_string() + } else { + sources.join(", ") + }; + push_markdown_row( + &mut out, + &[ + &entry.claim, + if entry.critical { "yes" } else { "no" }, + &audit_label(&entry.status), + &sources, + ], + ); } - "refine_thoughts" => { + } + + out.push_str("\n## Assumptions\n\n"); + out.push_str("| ID | Assumption | Status | Criticality |\n| --- | --- | --- | --- |\n"); + let mut assumptions = session.assumptions.values().collect::>(); + assumptions.sort_by(|left, right| left.assumption_id.cmp(&right.assumption_id)); + if assumptions.is_empty() { + push_markdown_row(&mut out, &["none", "none", "not_recorded", "none"]); + } else { + for assumption in assumptions { + push_markdown_row( + &mut out, + &[ + &assumption.assumption_id, + &assumption.text, + &audit_label(&assumption.status), + &audit_label(&assumption.criticality), + ], + ); + } + } + + out.push_str("\n## Quality checkpoints\n\n"); + out.push_str("| ID | Label | Route | Blocking gaps |\n| --- | --- | --- | --- |\n"); + let mut checkpoints = session.checkpoints.iter().collect::>(); + checkpoints.sort_by(|left, right| left.checkpoint_id.cmp(&right.checkpoint_id)); + if checkpoints.is_empty() { + push_markdown_row(&mut out, &["none", "none", "not_recorded", "none"]); + } else { + for checkpoint in checkpoints { + push_markdown_row( + &mut out, + &[ + &checkpoint.checkpoint_id, + &checkpoint.label, + &audit_label(&checkpoint.route_decision), + &sorted_markdown_values(&checkpoint.blocking_gaps), + ], + ); + } + } + + out.push_str("\n## Pipeline gates\n\n"); + out.push_str("| Stage | Gate | Blockers |\n| --- | --- | --- |\n"); + let mut stages = session.stage_findings.iter().collect::>(); + stages.sort_by(|left, right| left.stage.cmp(&right.stage)); + if stages.is_empty() { + push_markdown_row(&mut out, &["none", "not_recorded", "none"]); + } else { + for stage in stages { + push_markdown_row( + &mut out, + &[ + &stage.stage, + if stage.gate_passed { + "passed" + } else { + "blocked" + }, + &sorted_markdown_values(&stage.gate_blockers), + ], + ); + } + } + + out.push_str("\n## Raw thoughts\n\n"); + if include_raw_thoughts { + out.push_str("| Node ID | Branch | Provider | Content |\n| --- | --- | --- | --- |\n"); + let mut nodes = session.nodes.values().collect::>(); + nodes.sort_by(|left, right| left.node_id.cmp(&right.node_id)); + for node in nodes { + push_markdown_row( + &mut out, + &[ + &node.node_id, + node.branch_id.as_deref().unwrap_or("none"), + node.provenance.provider.as_deref().unwrap_or("none"), + &node.content, + ], + ); + } + } else { + out.push_str( + "Raw node content omitted. Re-export with `include_raw_thoughts=true` only when disclosure is appropriate.\n", + ); + } + + out +} + +fn audit_label(value: &T) -> String { + serde_json::to_value(value) + .ok() + .and_then(|value| value.as_str().map(str::to_string)) + .unwrap_or_else(|| "unknown".to_string()) +} + +fn sorted_markdown_values(values: &[String]) -> String { + if values.is_empty() { + return "none".to_string(); + } + let mut values = values.to_vec(); + values.sort(); + values.dedup(); + values.join("\n") +} + +fn push_markdown_row(out: &mut String, cells: &[&str]) { + out.push_str("| "); + out.push_str( + &cells + .iter() + .map(|cell| markdown_table_cell(cell)) + .collect::>() + .join(" | "), + ); + out.push_str(" |\n"); +} + +fn markdown_table_cell(value: &str) -> String { + value + .replace("\r\n", "\n") + .replace('\r', "\n") + .replace('\\', "\\\\") + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('|', "\\|") + .replace('!', "!") + .replace('[', "[") + .replace(']', "]") + .replace('(', "(") + .replace(')', ")") + .replace('*', "*") + .replace('_', "_") + .replace('`', "`") + .replace('~', "~") + .replace('\n', "
") +} + +fn has_any_ci>(text: &str, needles: &[S]) -> bool { + needles + .iter() + .map(|n| n.as_ref().to_lowercase()) + .any(|n| text.contains(&n)) +} + +fn has_any_ci_groups(text: &str, groups: &[&[String]]) -> bool { + groups.iter().any(|group| has_any_ci(text, group)) +} + +fn dedup_in_place(values: &mut Vec) { + let mut seen = HashSet::new(); + values.retain(|v| seen.insert(v.clone())); +} + +fn normalize_alias_list(values: Vec) -> Vec { + let mut out = values + .into_iter() + .map(|v| v.trim().to_lowercase()) + .filter(|v| !v.is_empty()) + .collect::>(); + dedup_in_place(&mut out); + out +} + +fn extend_unique(dest: &mut Vec, add: &[String]) { + let mut set = dest.iter().cloned().collect::>(); + for value in add { + let value = value.trim().to_lowercase(); + if !value.is_empty() && set.insert(value.clone()) { + dest.push(value); + } + } +} + +fn explain_tool(tool: &str) -> String { + match tool { + "reasoning_autopilot" => { + "Auto mode: route intent and run the default audited reasoning flow.".to_string() + } + "run_thinking_mode" => { + "Execute Auto, Quick, Explore, Map, Sketch, or Test as one audited workflow." + .to_string() + } + "reasoning_intent_router" => { + "Translate normal wording into Thinking Modes and MCP tool calls.".to_string() + } + "sequentialthinking_tools" => "Quick linear thought capture (CoT baseline).".to_string(), + "get_thinking_history" => "Inspect current quick-thought assumptions.".to_string(), + "start_deliberation" => "Open structured deep reasoning session.".to_string(), + "expand_thoughts" => "Create heuristic alternatives/branches from frontier.".to_string(), + "start_skeleton_of_thought" => { + "Create a routed Skeleton-of-Thought phase with scaffold skeleton nodes.".to_string() + } + "run_skeleton_elaboration" => { + "Elaborate SoT skeletons into prompt packs or bounded scaffold nodes.".to_string() + } + "score_thoughts" => "Rank candidates by weighted heuristic proxies.".to_string(), + "prune_thoughts" => "Keep best/diverse candidates, remove weak branches.".to_string(), + "link_thoughts" => "Add typed GoT edges between existing thought nodes.".to_string(), + "merge_thought_branches" => { + "Converge multiple branches into one merged synthesis node.".to_string() + } + "distill_thought_cluster" => { + "Compress multiple nodes into a bullet distill with dependency edges.".to_string() + } + "refine_thoughts" => { "Create an explicit feedback-loop correction node with a refines edge.".to_string() } "record_assumption" => "Add an explicit premise to the assumption ledger.".to_string(), @@ -7854,17 +8515,38 @@ fn select_thinking_mode(inferred_modes: &[String], aliases: &ReasoningAliases) - } } -fn thinking_modes_resource(aliases: &ReasoningAliases) -> Value { +fn thinking_modes_resource(aliases: &ReasoningAliases, tool_pack: ToolPack) -> Value { let modes = all_thinking_modes() .into_iter() - .map(|mode| thinking_mode_contract(mode, aliases)) + .map(|mode| { + let mut contract = thinking_mode_contract(mode, aliases); + contract + .primary_tools + .retain(|tool| tool_pack.contains(tool)); + contract.compatible_internal_values.retain(|value| { + ToolPack::minimum_for(value).is_none() || tool_pack.contains(value) + }); + contract + }) .collect::>(); + let compatible_tools = |tools: &[&str]| -> Vec { + tools + .iter() + .copied() + .filter(|tool| ToolPack::minimum_for(tool).is_none() || tool_pack.contains(tool)) + .map(str::to_string) + .collect() + }; json!({ "uri": "reasoning://thinking-modes", "version": 1, "default_mode": ThinkingMode::Auto, "default_tool": "run_thinking_mode", - "compatibility_default_tool": "reasoning_autopilot", + "active_tool_pack": tool_pack.as_str(), + "tool_pack_resource": "reasoning://config/tool-packs", + "compatibility_default_tool": tool_pack + .contains("reasoning_autopilot") + .then_some("reasoning_autopilot"), "mode_order": all_thinking_modes() .into_iter() .map(thinking_mode_id) @@ -7874,9 +8556,9 @@ fn thinking_modes_resource(aliases: &ReasoningAliases) -> Value { "quick": ["cot"], "explore": ["tot"], "map": ["got"], - "sketch": ["got", "start_skeleton_of_thought", "run_skeleton_elaboration"], - "test": ["reasonkit", "verify_thoughts", "run_reasonkit_pipeline"], - "auto": ["reasoning_autopilot", "reasoning_intent_router"] + "sketch": compatible_tools(&["got", "start_skeleton_of_thought", "run_skeleton_elaboration"]), + "test": compatible_tools(&["reasonkit", "verify_thoughts", "run_reasonkit_pipeline"]), + "auto": compatible_tools(&["reasoning_autopilot", "reasoning_intent_router"]) }, "resource_contract": { "resources_are_uri_addressed": true, @@ -9026,13 +9708,49 @@ fn parse_int_env(name: &str, fallback: usize) -> usize { } fn json_tool_success(data: &T) -> CallToolResult { - let text = serde_json::to_string_pretty(data).unwrap_or_else(|_| "{}".to_string()); - CallToolResult::success(vec![Content::text(text)]) + json_tool_result(data, false) } fn json_tool_error(data: &T) -> CallToolResult { - let text = serde_json::to_string_pretty(data).unwrap_or_else(|_| "{}".to_string()); - CallToolResult::error(vec![Content::text(text)]) + json_tool_result(data, true) +} + +fn consensus_blocked_tool_result(result: &ConsensusAnswerResult) -> CallToolResult { + json_tool_error(&json!({ + "code": "consensus_blocked", + "error": "consensus is blocked by unresolved governance gates", + "retryable": false, + "deliberation_id": result.deliberation_id, + "route_decision": result.route_decision, + "policy_blocked": true, + "recommended_action": "resolve the reported evidence, assumption, authorship, pipeline, or checkpoint blockers before requesting consensus again", + })) +} + +fn json_tool_result(data: &T, is_error: bool) -> CallToolResult { + match serde_json::to_value(data) { + Ok(value) => json_value_tool_result(value, is_error), + Err(_) => json_value_tool_result( + json!({ + "code": "serialization_error", + "message": "failed to serialize tool result", + "retryable": false, + }), + true, + ), + } +} + +fn json_value_tool_result(value: Value, is_error: bool) -> CallToolResult { + let text = serde_json::to_string_pretty(&value) + .expect("serde_json::Value must always serialize to JSON text"); + let mut result = if is_error { + CallToolResult::structured_error(value) + } else { + CallToolResult::structured(value) + }; + result.content = vec![ContentBlock::text(text)]; + result } fn canonical_token(raw: &str) -> String { @@ -11292,6 +12010,37 @@ fn provider_capabilities() -> Value { }) } +fn tool_pack_resource(active: ToolPack) -> Value { + json!({ + "uri": "reasoning://config/tool-packs", + "version": 1, + "default": ToolPack::Core.as_str(), + "active": active.as_str(), + "active_tool_count": active.members().len(), + "restart_required": true, + "environment_variable": "REASONKIT_TOOL_PACK", + "invalid_value_behavior": "warn on stderr and use the safe core fallback", + "compatibility_rollback": "REASONKIT_TOOL_PACK=full", + "packs": { + "core": { + "tool_count": ToolPack::Core.members().len(), + "purpose": "agent-native governance golden path", + "tools": ToolPack::Core.members(), + }, + "standard": { + "tool_count": ToolPack::Standard.members().len(), + "purpose": "core plus everyday Explore, Map, Sketch, replay, and tuning operators", + "tools": ToolPack::Standard.members(), + }, + "full": { + "tool_count": ToolPack::Full.members().len(), + "purpose": "compatibility surface including legacy, specialist, web-sidecar, and experimental memory tools", + "tools": ToolPack::Full.members(), + } + } + }) +} + fn transport_capabilities() -> Value { json!({ "active_transport": "stdio", @@ -11830,24 +12579,6 @@ struct ThinkQueryInput { min_score: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -struct HotMemoryEntryResult { - id: String, - content: String, - score: f32, - metadata: serde_json::Value, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -struct ThinkQueryResult { - results: Vec, - latency_ms: u128, - /// Cross-session failure-mode hints (host agent protocol; not LLM-generated). - #[serde(default)] - failure_coaching: Vec, - semantic_engine: String, -} - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[schemars(transform = vertex_compat_schema)] struct RunThinkingModeInput { @@ -12567,6 +13298,7 @@ fn deliberation_node_view(node: &DeliberationNode) -> NodeView { score: node.score, branch_id: node.branch_id.clone(), node_type: node.node_type.clone(), + agent_authored_provenance: matches!(node.provenance.mode, AnalysisMode::AgentAuthored), } } @@ -14046,11 +14778,31 @@ struct RunReasonKitPipelineResult { analysis: AnalysisMetadata, } +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[schemars(inline)] +#[serde(rename_all = "lowercase")] +enum AuditFormat { + #[default] + Json, + Markdown, +} + +impl AuditFormat { + fn content_type(self) -> &'static str { + match self { + Self::Json => "application/json", + Self::Markdown => "text/markdown; charset=utf-8", + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[schemars(transform = vertex_compat_schema)] struct ExportReasoningAuditInput { deliberation_id: String, #[serde(default)] + format: AuditFormat, + #[serde(default)] #[serde(deserialize_with = "deserialize_opt_bool")] include_raw_thoughts: Option, } @@ -14181,6 +14933,9 @@ struct ExportReasoningAuditResult { deliberation_id: String, audit_id: String, payload: Value, + format: AuditFormat, + content_type: String, + artifact: String, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] @@ -14400,6 +15155,7 @@ fn reasoning_resource_uris() -> Vec<&'static str> { "reasoning://session/{id}/route-decision", "reasoning://config/aliases", "reasoning://config/providers", + "reasoning://config/tool-packs", "reasoning://config/web-research", "reasoning://config/transports", "reasoning://workflows/feature-triage", @@ -14430,6 +15186,12 @@ fn native_reasoning_resources() -> Vec { "Provider Capabilities", "Configured analysis provider boundaries and limitations.", ), + ( + "reasoning://config/tool-packs", + "tool-packs", + "Tool Packs", + "Active progressive tool surface and exact core, standard, and full memberships.", + ), ( "reasoning://config/web-research", "web-research-config", @@ -14469,13 +15231,10 @@ fn native_reasoning_resources() -> Vec { ] .into_iter() .map(|(uri, name, title, description)| { - Annotated::new( - RawResource::new(uri, name) - .with_title(title) - .with_description(description) - .with_mime_type("application/json"), - None, - ) + Resource::new(uri, name) + .with_title(title) + .with_description(description) + .with_mime_type("application/json") }) .collect() } @@ -14539,20 +15298,25 @@ fn native_reasoning_resource_templates() -> Vec { ] .into_iter() .map(|(uri_template, name, title, description)| { - Annotated::new( - RawResourceTemplate::new(uri_template, name) - .with_title(title) - .with_description(description) - .with_mime_type("application/json"), - None, - ) + ResourceTemplate::new(uri_template, name) + .with_title(title) + .with_description(description) + .with_mime_type("application/json") }) .collect() } -fn read_reasoning_resource_payload(store: &DeliberationStore, uri: &str) -> Result { +fn read_reasoning_resource_payload( + store: &DeliberationStore, + uri: &str, + tool_pack: ToolPack, +) -> Result { if uri == "reasoning://thinking-modes" { - return Ok(thinking_modes_resource(&store.aliases)); + return Ok(thinking_modes_resource(&store.aliases, tool_pack)); + } + + if uri == "reasoning://config/tool-packs" { + return Ok(tool_pack_resource(tool_pack)); } if uri == "reasoning://config/aliases" { @@ -14819,6 +15583,7 @@ mod input_compatibility_tests { })) .expect("audit export should accept boolean-like strings"); assert_eq!(audit.include_raw_thoughts, Some(true)); + assert_eq!(audit.format, AuditFormat::Json); let sot_elaboration: RunSkeletonElaborationInput = serde_json::from_value(json!({ "deliberation_id": "delib-test", @@ -14996,6 +15761,7 @@ mod input_compatibility_tests { let mode_catalog = read_reasoning_resource_payload( &DeliberationStore::new(64), "reasoning://thinking-modes", + ToolPack::Full, ) .expect("thinking modes resource"); assert_eq!(mode_catalog["default_mode"], json!("auto")); @@ -15004,6 +15770,7 @@ mod input_compatibility_tests { let feature_workflow = read_reasoning_resource_payload( &DeliberationStore::new(64), "reasoning://workflows/feature-triage", + ToolPack::Full, ) .expect("feature triage resource"); assert_eq!( @@ -15326,6 +16093,172 @@ mod architecture_contract_tests { use super::*; use std::panic::AssertUnwindSafe; + fn qualifying_tier1_evidence(source: &str) -> EvidenceItem { + EvidenceItem { + source: source.to_string(), + tier: EvidenceTier::Tier1, + independence_group: source.to_string(), + supports: true, + contradictory: false, + unambiguous: true, + } + } + + fn seeded_audit_store() -> (DeliberationStore, String) { + let mut store = DeliberationStore::new(64); + let started = store.start(StartDeliberationInput { + session_id: Some("audit-session".to_string()), + mode: Some(ReasoningMode::Reasonkit), + goal: "Review | release\nwithout claims".to_string(), + constraints: None, + profile: Some(ReasoningProfile::Paranoid), + limits: None, + verification_policy: None, + }); + let analysis = AnalysisMetadata { + mode: AnalysisMode::Heuristic, + method: "audit_fixture".to_string(), + provider: Some("reasonkit-think/heuristic".to_string()), + model: None, + generated_at: "2026-08-23T00:00:00Z".to_string(), + source_node_ids: Vec::new(), + limitations: heuristic_limitations(), + }; + let session = store + .deliberations + .get_mut(&started.deliberation_id) + .expect("audit fixture session"); + session.route_decision = Some(RouteDecision::GatherMoreEvidence); + session.verification_matrix = vec![ + VerificationEntry { + claim: "Zeta claim".to_string(), + critical: false, + status: VerificationStatus::DataDeficit, + evidence: vec![EvidenceItem { + source: "z-source".to_string(), + tier: EvidenceTier::Tier2, + independence_group: "z".to_string(), + supports: true, + contradictory: false, + unambiguous: false, + }], + notes: "needs another source".to_string(), + analysis: analysis.clone(), + }, + VerificationEntry { + claim: "Alpha | claim\nnext ".to_string(), + critical: true, + status: VerificationStatus::Verified, + evidence: vec![ + qualifying_tier1_evidence("z-source"), + qualifying_tier1_evidence("a|source"), + ], + notes: "verified".to_string(), + analysis: analysis.clone(), + }, + ]; + for (id, text, status, criticality) in [ + ( + "assumption-z", + "Zeta assumption", + AssumptionStatus::Proposed, + AssumptionCriticality::Low, + ), + ( + "assumption-a", + "Alpha | assumption\nwith ", + AssumptionStatus::Unresolved, + AssumptionCriticality::Critical, + ), + ] { + session.assumptions.insert( + id.to_string(), + AssumptionRecord { + assumption_id: id.to_string(), + text: text.to_string(), + confidence: None, + criticality, + status, + verifiable: true, + depends_on: Vec::new(), + invalidates: Vec::new(), + source_node_ids: Vec::new(), + evidence: Vec::new(), + created_at: "2026-08-23T00:00:00Z".to_string(), + updated_at: "2026-08-23T00:00:00Z".to_string(), + notes: None, + analysis: analysis.clone(), + }, + ); + } + session.checkpoints = vec![ + QualityCheckpoint { + checkpoint_id: "checkpoint-z".to_string(), + label: "Zeta checkpoint".to_string(), + created_at: "2026-08-23T00:00:00Z".to_string(), + quality_score: 0.4, + quality_tier: QualityTier::Poor, + route_decision: RouteDecision::GatherMoreEvidence, + blocking_gaps: vec!["z checkpoint blocker".to_string()], + recommended_tools: vec!["verify_thoughts".to_string()], + }, + QualityCheckpoint { + checkpoint_id: "checkpoint-a".to_string(), + label: "Alpha | checkpoint".to_string(), + created_at: "2026-08-23T00:00:00Z".to_string(), + quality_score: 0.5, + quality_tier: QualityTier::Adequate, + route_decision: RouteDecision::DeferToHuman, + blocking_gaps: vec!["a | checkpoint blocker".to_string()], + recommended_tools: vec!["consensus_answer".to_string()], + }, + ]; + session.stage_findings = vec![ + StageFinding { + stage: "z-stage".to_string(), + finding: "z finding".to_string(), + confidence: 0.4, + confidence_basis: "fixture".to_string(), + structured: json!({}), + gate_passed: false, + gate_blockers: vec!["z pipeline blocker".to_string()], + agent_tasks: Vec::new(), + analysis: analysis.clone(), + }, + StageFinding { + stage: "a-stage".to_string(), + finding: "a finding".to_string(), + confidence: 0.7, + confidence_basis: "fixture".to_string(), + structured: json!({}), + gate_passed: false, + gate_blockers: vec!["a | pipeline blocker".to_string()], + agent_tasks: Vec::new(), + analysis: analysis.clone(), + }, + ]; + for (id, content) in [ + ("node-z", "RAW_ZETA_NODE_CONTENT"), + ( + "node-a", + "RAW_ALPHA_NODE_CONTENT | line\n", + ), + ] { + session.insert_node(DeliberationNode::thought( + id.to_string(), + Vec::new(), + content.to_string(), + None, + "2026-08-23T00:00:00Z".to_string(), + None, + vec!["fixture".to_string()], + analysis.clone(), + )); + } + + (store, started.deliberation_id) + } + #[test] fn heuristic_outputs_are_labeled_with_provenance_and_no_semantic_confidence() { let mut store = DeliberationStore::new(64); @@ -15403,6 +16336,7 @@ mod architecture_contract_tests { let audit = store .export_audit(ExportReasoningAuditInput { deliberation_id: started.deliberation_id, + format: AuditFormat::Json, include_raw_thoughts: Some(true), }) .expect("audit should work"); @@ -15419,43 +16353,842 @@ mod architecture_contract_tests { } #[test] - fn graph_indexes_track_lineage_and_strict_cycle_rejections() { + fn verification_upserts_normalized_claim_and_preserves_unrelated_blockers() { let mut store = DeliberationStore::new(64); let started = store.start(StartDeliberationInput { - session_id: Some("graph-indexes".to_string()), - mode: Some(ReasoningMode::Got), - goal: "Maintain graph indexes".to_string(), + session_id: Some("verification-upsert".to_string()), + mode: Some(ReasoningMode::Reasonkit), + goal: "Preserve verification history".to_string(), constraints: None, - profile: Some(ReasoningProfile::Balanced), + profile: Some(ReasoningProfile::Paranoid), limits: None, verification_policy: None, }); - let root_id = store - .deliberations - .get(&started.deliberation_id) - .and_then(|session| session.frontier.first()) - .cloned() - .expect("root frontier"); - let expanded = store - .expand(ExpandThoughtsInput { + store + .verify(VerifyThoughtsInput { deliberation_id: started.deliberation_id.clone(), - from_node_ids: vec![root_id.clone()], - strategy: Some(ExpandStrategy::Diverse), - count: Some(3), + node_ids: Vec::new(), + critical_claims: Vec::new(), + claims: Some(vec![ + VerifyClaimInput { + text: "Release requires approval".to_string(), + critical: true, + evidence: Vec::new(), + }, + VerifyClaimInput { + text: "Secondary claim".to_string(), + critical: false, + evidence: Vec::new(), + }, + ]), + method: Some(VerificationMethod::Hybrid), + policy_override: None, }) - .expect("expand should work"); - let first = expanded.created_nodes[0].node_id.clone(); - let second = expanded.created_nodes[1].node_id.clone(); + .expect("initial verification"); - let session = store - .deliberations - .get(&started.deliberation_id) - .expect("session"); - assert!( - session - .children_by_parent - .get(&root_id) + let updated = store + .verify(VerifyThoughtsInput { + deliberation_id: started.deliberation_id, + node_ids: Vec::new(), + critical_claims: Vec::new(), + claims: Some(vec![VerifyClaimInput { + text: " Secondary claim ".to_string(), + critical: false, + evidence: vec![qualifying_tier1_evidence("secondary-source")], + }]), + method: Some(VerificationMethod::Hybrid), + policy_override: None, + }) + .expect("verification upsert"); + + assert_eq!(updated.verification_matrix.len(), 2); + assert!(updated.verification_matrix.iter().any(|entry| { + entry.claim == "Release requires approval" + && entry.critical + && matches!(entry.status, VerificationStatus::DataDeficit) + })); + assert!(updated.verification_matrix.iter().any(|entry| { + entry.claim == "Secondary claim" + && !entry.critical + && matches!(entry.status, VerificationStatus::Verified) + })); + assert_eq!(updated.claim_status_summary.verified, 1); + assert_eq!(updated.claim_status_summary.data_deficit, 1); + } + + #[test] + fn verification_upsert_preserves_criticality_and_rechecks_evidence() { + let mut store = DeliberationStore::new(64); + let started = store.start(StartDeliberationInput { + session_id: Some("verification-criticality-monotonic".to_string()), + mode: Some(ReasoningMode::Reasonkit), + goal: "Keep critical release claims fail closed".to_string(), + constraints: None, + profile: Some(ReasoningProfile::Paranoid), + limits: None, + verification_policy: None, + }); + + store + .verify(VerifyThoughtsInput { + deliberation_id: started.deliberation_id.clone(), + node_ids: Vec::new(), + critical_claims: Vec::new(), + claims: Some(vec![VerifyClaimInput { + text: "Release requires approval".to_string(), + critical: true, + evidence: Vec::new(), + }]), + method: Some(VerificationMethod::Hybrid), + policy_override: None, + }) + .expect("initial critical verification"); + + let evidence = (1..=3) + .map(|index| EvidenceItem { + source: format!("secondary-source-{index}"), + tier: EvidenceTier::Tier2, + independence_group: format!("secondary-group-{index}"), + supports: true, + contradictory: false, + unambiguous: true, + }) + .collect(); + let updated = store + .verify(VerifyThoughtsInput { + deliberation_id: started.deliberation_id.clone(), + node_ids: Vec::new(), + critical_claims: Vec::new(), + claims: Some(vec![VerifyClaimInput { + text: " Release requires approval ".to_string(), + critical: false, + evidence, + }]), + method: Some(VerificationMethod::Hybrid), + policy_override: None, + }) + .expect("same-claim verification upsert"); + + assert_eq!(updated.verification_matrix.len(), 1); + let entry = &updated.verification_matrix[0]; + assert!(entry.critical, "criticality must be monotonic"); + assert!(matches!(entry.status, VerificationStatus::DataDeficit)); + + let consensus = store + .consensus(ConsensusAnswerInput { + deliberation_id: started.deliberation_id, + method: None, + policy_override: None, + }) + .expect("consensus returns a policy-blocked result"); + assert!(consensus.policy_blocked); + assert!(matches!( + consensus.route_decision, + RouteDecision::GatherMoreEvidence + )); + } + + #[test] + fn conflicting_assumption_criticality_cannot_bypass_governance() { + let mut store = DeliberationStore::new(64); + let started = store.start(StartDeliberationInput { + session_id: Some("assumption-criticality-conflict".to_string()), + mode: Some(ReasoningMode::Reasonkit), + goal: "Do not downgrade an explicitly critical assumption".to_string(), + constraints: None, + profile: Some(ReasoningProfile::Balanced), + limits: None, + verification_policy: None, + }); + + let recorded = store + .record_assumption(RecordAssumptionInput { + deliberation_id: started.deliberation_id.clone(), + text: "Deployment safety must be established".to_string(), + source_node_ids: Vec::new(), + confidence: Some(0.99), + criticality: Some(AssumptionCriticality::Medium), + critical: Some(true), + status: Some(AssumptionStatus::Verified), + verifiable: Some(true), + depends_on: Vec::new(), + invalidates: Vec::new(), + evidence: Vec::new(), + notes: Some("conflicting caller fields".to_string()), + }) + .expect("record conflicting criticality input"); + + assert!(matches!( + recorded.assumption.criticality, + AssumptionCriticality::Critical + )); + assert!(matches!( + recorded.assumption.status, + AssumptionStatus::Unresolved + )); + assert_eq!(recorded.blocking_assumption_ids.len(), 1); + + let checkpoint = store + .run_reasoning_checkpoint(RunReasoningCheckpointInput { + deliberation_id: started.deliberation_id.clone(), + label: Some("criticality conflict gate".to_string()), + fail_threshold: Some(0.0), + }) + .expect("checkpoint"); + assert!(matches!( + checkpoint.checkpoint.route_decision, + RouteDecision::GatherMoreEvidence + )); + assert!(!checkpoint.checkpoint.blocking_gaps.is_empty()); + + let consensus = store + .consensus(ConsensusAnswerInput { + deliberation_id: started.deliberation_id, + method: None, + policy_override: None, + }) + .expect("blocked consensus result"); + assert!(consensus.policy_blocked); + assert!(matches!( + consensus.route_decision, + RouteDecision::GatherMoreEvidence + )); + } + + #[test] + fn consensus_blocks_scaffold_only_frontiers_without_persisting_override() { + let mut store = DeliberationStore::new(64); + let started = store.start(StartDeliberationInput { + session_id: Some("scaffold-consensus-gate".to_string()), + mode: Some(ReasoningMode::Reasonkit), + goal: "Require agent-authored reasoning before consensus".to_string(), + constraints: None, + profile: Some(ReasoningProfile::Balanced), + limits: None, + verification_policy: None, + }); + let root = store + .get(&started.deliberation_id) + .and_then(|session| session.frontier.first()) + .cloned() + .expect("root"); + store + .expand(ExpandThoughtsInput { + deliberation_id: started.deliberation_id.clone(), + from_node_ids: vec![root], + strategy: Some(ExpandStrategy::Diverse), + count: Some(2), + }) + .expect("create scaffold-only frontier"); + + let consensus = store + .consensus(ConsensusAnswerInput { + deliberation_id: started.deliberation_id.clone(), + method: None, + policy_override: Some(verification_policy_from_text("relaxed")), + }) + .expect("consensus returns a blocked result"); + + assert!(consensus.policy_blocked); + assert!(matches!( + consensus.route_decision, + RouteDecision::GatherMoreEvidence + )); + let session = store.get(&started.deliberation_id).expect("session"); + assert!( + session + .verification_policy + .fail_closed_on_critical_unresolved + ); + + let wire = serde_json::to_value(consensus_blocked_tool_result(&consensus)) + .expect("blocked consensus wire result"); + assert_eq!(wire["isError"], true); + assert_eq!(wire["structuredContent"]["code"], "consensus_blocked"); + let rendered = serde_json::to_string(&wire).expect("wire JSON"); + assert!(!rendered.contains("final_answer")); + assert!(!rendered.contains("Require agent-authored reasoning before consensus")); + } + + #[test] + fn consensus_requires_an_authored_frontier_and_never_promotes_scaffolds() { + let mut store = DeliberationStore::new(64); + let started = store.start(StartDeliberationInput { + session_id: Some("authored-consensus-candidates".to_string()), + mode: Some(ReasoningMode::Reasonkit), + goal: "Return only host-authored decision content".to_string(), + constraints: None, + profile: Some(ReasoningProfile::Balanced), + limits: None, + verification_policy: None, + }); + + let root_only = store + .consensus(ConsensusAnswerInput { + deliberation_id: started.deliberation_id.clone(), + method: None, + policy_override: None, + }) + .expect("root-only consensus returns a blocked route"); + assert!(root_only.policy_blocked); + + let spoof_error = store + .record_reasoning_action(RecordReasoningActionInput { + deliberation_id: started.deliberation_id.clone(), + tool_name: "rg".to_string(), + parameters: Some(json!({"pattern": "release"})), + parent_node_id: None, + status: Some("planned".to_string()), + branch_id: None, + tags: Some(vec!["agent-authored".to_string()]), + add_to_frontier: Some(true), + }) + .expect_err("agent-authored is a reserved action tag"); + assert!(spoof_error.contains("agent-authored")); + let action_spoof = store + .consensus(ConsensusAnswerInput { + deliberation_id: started.deliberation_id.clone(), + method: None, + policy_override: None, + }) + .expect("rejected action-tag spoof leaves a blocked root-only route"); + assert!(action_spoof.policy_blocked); + + let root = store + .get(&started.deliberation_id) + .and_then(|session| session.frontier.first()) + .cloned() + .expect("root"); + let expanded = store + .expand(ExpandThoughtsInput { + deliberation_id: started.deliberation_id.clone(), + from_node_ids: vec![root], + strategy: Some(ExpandStrategy::Diverse), + count: Some(1), + }) + .expect("scaffold expansion"); + let scaffold_id = expanded.created_nodes[0].node_id.clone(); + let authored = store + .add_thought_node(AddThoughtNodeInput { + deliberation_id: started.deliberation_id.clone(), + content: "Ship only after the signed release evidence is attached.".to_string(), + parent_node_id: Some(scaffold_id), + edge_kind: Some(GraphEdgeKind::Refines), + edge_note: Some("host-authored replacement".to_string()), + tags: None, + branch_id: None, + add_to_frontier: Some(true), + }) + .expect("agent-authored frontier node"); + + let consensus = store + .consensus(ConsensusAnswerInput { + deliberation_id: started.deliberation_id, + method: None, + policy_override: None, + }) + .expect("authored consensus"); + assert!(!consensus.policy_blocked); + assert!(consensus.final_answer.contains("signed release evidence")); + assert!(!consensus.final_answer.contains("SCAFFOLD")); + assert!(!consensus.final_answer.contains("Hypothesis:")); + assert!(!consensus.final_answer.contains("Return only host-authored")); + assert_eq!(consensus.analysis.source_node_ids, vec![authored.node_id]); + } + + #[test] + fn markdown_table_cells_neutralize_active_markdown() { + let escaped = markdown_table_cell( + "![pixel](https://attacker.invalid/audit.png) [click](https://attacker.invalid) `code` *bold* _under_", + ); + for active in ["![", "](", "`code`", "*bold*", "_under_"] { + assert!( + !escaped.contains(active), + "active Markdown remained: {active}" + ); + } + assert!(escaped.contains("![pixel](")); + assert!(escaped.contains("`code`")); + } + + #[test] + fn verified_assumptions_require_qualifying_evidence() { + let mut store = DeliberationStore::new(64); + let started = store.start(StartDeliberationInput { + session_id: Some("assumption-evidence-gate".to_string()), + mode: Some(ReasoningMode::Got), + goal: "Do not self-certify assumptions".to_string(), + constraints: None, + profile: Some(ReasoningProfile::Paranoid), + limits: None, + verification_policy: None, + }); + + let recorded = store + .record_assumption(RecordAssumptionInput { + deliberation_id: started.deliberation_id.clone(), + text: "Critical dependency is available".to_string(), + source_node_ids: Vec::new(), + confidence: Some(0.99), + criticality: Some(AssumptionCriticality::Critical), + critical: Some(true), + status: Some(AssumptionStatus::Verified), + verifiable: Some(true), + depends_on: Vec::new(), + invalidates: Vec::new(), + evidence: Vec::new(), + notes: None, + }) + .expect("record assumption"); + assert!(matches!( + recorded.assumption.status, + AssumptionStatus::Unresolved + )); + assert_eq!(recorded.blocking_assumption_ids.len(), 1); + + let insufficient = store + .set_assumption_status(SetAssumptionStatusInput { + deliberation_id: started.deliberation_id.clone(), + assumption_id: recorded.assumption.assumption_id.clone(), + status: AssumptionStatus::Verified, + confidence: None, + evidence: Some(vec![EvidenceItem { + source: "secondary-source".to_string(), + tier: EvidenceTier::Tier2, + independence_group: "secondary-source".to_string(), + supports: true, + contradictory: false, + unambiguous: true, + }]), + notes: None, + }) + .expect("attempt evidence-deficient verification"); + assert!(matches!( + insufficient.assumption.status, + AssumptionStatus::Unresolved + )); + assert_eq!(insufficient.blocking_assumption_ids.len(), 1); + + let verified = store + .set_assumption_status(SetAssumptionStatusInput { + deliberation_id: started.deliberation_id, + assumption_id: recorded.assumption.assumption_id, + status: AssumptionStatus::Verified, + confidence: Some(0.95), + evidence: Some(vec![qualifying_tier1_evidence("official-source")]), + notes: None, + }) + .expect("evidence-backed verification"); + assert!(matches!( + verified.assumption.status, + AssumptionStatus::Verified + )); + assert!(verified.blocking_assumption_ids.is_empty()); + } + + #[test] + fn reasoning_observation_requires_action_and_completes_it_atomically() { + let mut store = DeliberationStore::new(64); + let started = store.start(StartDeliberationInput { + session_id: Some("react-observation-integrity".to_string()), + mode: Some(ReasoningMode::Got), + goal: "Keep ReAct state consistent".to_string(), + constraints: None, + profile: Some(ReasoningProfile::Balanced), + limits: None, + verification_policy: None, + }); + let root = store + .get(&started.deliberation_id) + .and_then(|session| session.frontier.first()) + .cloned() + .expect("root"); + + let wrong_target = store.record_reasoning_observation(RecordReasoningObservationInput { + deliberation_id: started.deliberation_id.clone(), + action_node_id: root, + result: Some(json!({"ok": false})), + }); + assert!( + wrong_target + .expect_err("thought nodes cannot own observations") + .contains("Action") + ); + + let action = store + .record_reasoning_action(RecordReasoningActionInput { + deliberation_id: started.deliberation_id.clone(), + tool_name: "unit-test".to_string(), + parameters: Some(json!({"input": "value"})), + parent_node_id: None, + status: Some("planned".to_string()), + branch_id: None, + tags: None, + add_to_frontier: Some(false), + }) + .expect("record action"); + + let node_count = store + .get(&started.deliberation_id) + .map(|session| session.nodes.len()) + .expect("session"); + store + .deliberations + .get_mut(&started.deliberation_id) + .expect("session") + .limits + .max_nodes = node_count as i64; + let capacity_failure = + store.record_reasoning_observation(RecordReasoningObservationInput { + deliberation_id: started.deliberation_id.clone(), + action_node_id: action.action_node_id.clone(), + result: Some(json!({"ok": true})), + }); + assert!( + capacity_failure + .expect_err("capacity failure") + .contains("max_nodes") + ); + let action_after_failure = + &store.get(&started.deliberation_id).expect("session").nodes[&action.action_node_id]; + assert!(matches!( + action_after_failure.node_type, + ThoughtNodeType::Action { + status: ActionStatus::Planned, + .. + } + )); + assert!(action_after_failure.content.contains("status=Planned")); + assert_eq!( + store + .get(&started.deliberation_id) + .expect("session") + .nodes + .len(), + node_count + ); + + store + .deliberations + .get_mut(&started.deliberation_id) + .expect("session") + .limits + .max_nodes += 1; + let observation = store + .record_reasoning_observation(RecordReasoningObservationInput { + deliberation_id: started.deliberation_id.clone(), + action_node_id: action.action_node_id.clone(), + result: Some(json!({"ok": true})), + }) + .expect("record observation"); + let session = store.get(&started.deliberation_id).expect("session"); + assert!(session.nodes.contains_key(&observation.observation_node_id)); + assert!(matches!( + session.nodes[&action.action_node_id].node_type, + ThoughtNodeType::Action { + status: ActionStatus::Complete, + .. + } + )); + assert!( + session.nodes[&action.action_node_id] + .content + .contains("status=Complete") + ); + } + + #[test] + fn unknown_pipeline_stage_maps_to_invalid_input() { + let mut store = DeliberationStore::new(64); + let started = store.start(StartDeliberationInput { + session_id: Some("unknown-pipeline-stage".to_string()), + mode: Some(ReasoningMode::Reasonkit), + goal: "Reject misspelled governance stages".to_string(), + constraints: None, + profile: Some(ReasoningProfile::Balanced), + limits: None, + verification_policy: None, + }); + + let error = store + .run_pipeline(RunReasonKitPipelineInput { + deliberation_id: started.deliberation_id, + stages: Some(vec!["triangulaton".to_string()]), + profile: None, + policy_override: None, + }) + .expect_err("unknown stage must fail closed"); + let envelope = ErrorEnvelope::from_store_error(error); + assert_eq!(envelope.code, "invalid_input"); + } + + #[test] + fn rejected_pipeline_call_leaves_session_unchanged() { + let mut store = DeliberationStore::new(64); + let started = store.start(StartDeliberationInput { + session_id: Some("rejected-pipeline-transaction".to_string()), + mode: Some(ReasoningMode::Reasonkit), + goal: "Reject invalid pipeline requests atomically".to_string(), + constraints: None, + profile: Some(ReasoningProfile::Paranoid), + limits: None, + verification_policy: None, + }); + let before = serde_json::to_value( + store + .get(&started.deliberation_id) + .expect("deliberation session"), + ) + .expect("serialize session before rejected call"); + + let error = store + .run_pipeline(RunReasonKitPipelineInput { + deliberation_id: started.deliberation_id.clone(), + stages: Some(vec!["unknown-stage".to_string()]), + profile: None, + policy_override: Some(verification_policy_from_text("relaxed")), + }) + .expect_err("unknown stage must fail closed"); + assert!(error.contains("invalid pipeline stage")); + + let after = serde_json::to_value( + store + .get(&started.deliberation_id) + .expect("deliberation session"), + ) + .expect("serialize session after rejected call"); + assert_eq!(after, before, "failed calls must not retain partial state"); + } + + #[test] + fn pipeline_policy_override_is_call_scoped() { + let mut store = DeliberationStore::new(64); + let started = store.start(StartDeliberationInput { + session_id: Some("pipeline-call-scoped-policy".to_string()), + mode: Some(ReasoningMode::Reasonkit), + goal: "Keep session verification policy explicit".to_string(), + constraints: None, + profile: Some(ReasoningProfile::Paranoid), + limits: None, + verification_policy: None, + }); + let policy_before = serde_json::to_value( + &store + .get(&started.deliberation_id) + .expect("deliberation session") + .verification_policy, + ) + .expect("serialize policy before pipeline call"); + + store + .run_pipeline(RunReasonKitPipelineInput { + deliberation_id: started.deliberation_id.clone(), + stages: Some(vec!["causal-modeling".to_string()]), + profile: None, + policy_override: Some(verification_policy_from_text("relaxed")), + }) + .expect("known pipeline stage"); + + let policy_after = serde_json::to_value( + &store + .get(&started.deliberation_id) + .expect("deliberation session") + .verification_policy, + ) + .expect("serialize policy after pipeline call"); + assert_eq!( + policy_after, policy_before, + "pipeline policy overrides must not persist on the session" + ); + } + + #[test] + fn audit_export_uses_one_id_for_envelope_and_payload() { + let mut store = DeliberationStore::new(64); + let started = store.start(StartDeliberationInput { + session_id: Some("audit-id-integrity".to_string()), + mode: Some(ReasoningMode::Reasonkit), + goal: "Export a correlatable audit".to_string(), + constraints: None, + profile: Some(ReasoningProfile::Balanced), + limits: None, + verification_policy: None, + }); + + let audit = store + .export_audit(ExportReasoningAuditInput { + deliberation_id: started.deliberation_id, + format: AuditFormat::Json, + include_raw_thoughts: Some(false), + }) + .expect("audit export"); + assert_eq!( + audit.payload["audit_id"].as_str(), + Some(audit.audit_id.as_str()) + ); + } + + #[test] + fn audit_export_defaults_to_canonical_json_artifact() { + let (store, deliberation_id) = seeded_audit_store(); + let input: ExportReasoningAuditInput = serde_json::from_value(json!({ + "deliberation_id": deliberation_id, + "include_raw_thoughts": false + })) + .expect("legacy audit input without format"); + + let result = serde_json::to_value(store.export_audit(input).expect("JSON audit")) + .expect("serialize audit result"); + assert_eq!(result["format"], json!("json")); + assert_eq!(result["content_type"], json!("application/json")); + let artifact = result["artifact"].as_str().expect("JSON artifact"); + let artifact_payload: Value = serde_json::from_str(artifact).expect("valid JSON artifact"); + assert_eq!(artifact_payload, result["payload"]); + assert_eq!(result["audit_id"], result["payload"]["audit_id"]); + assert!(result["payload"].get("nodes").is_none()); + assert!(result["payload"].get("format").is_none()); + assert!(result["payload"].get("artifact").is_none()); + } + + #[test] + fn markdown_audit_is_complete_sorted_escaped_and_redacted_by_default() { + let (store, deliberation_id) = seeded_audit_store(); + let input: ExportReasoningAuditInput = serde_json::from_value(json!({ + "deliberation_id": deliberation_id, + "format": "markdown" + })) + .expect("Markdown audit input"); + + let result = serde_json::to_value(store.export_audit(input).expect("Markdown audit")) + .expect("serialize audit result"); + assert_eq!(result["format"], json!("markdown")); + assert_eq!( + result["content_type"], + json!("text/markdown; charset=utf-8") + ); + let artifact = result["artifact"].as_str().expect("Markdown artifact"); + for required in [ + "# Reasoning Audit", + "audit-session", + "reasonkit", + "GATHER_MORE_EVIDENCE", + "## Verification matrix", + "verified", + "data_deficit", + "z-source", + "## Assumptions", + "unresolved", + "critical", + "## Quality checkpoints", + "checkpoint blocker", + "## Pipeline gates", + "pipeline blocker", + "HEURISTIC OUTPUT", + "reasonkit-think/heuristic", + "host_agent", + "Raw node content omitted", + ] { + assert!( + artifact.contains(required), + "missing `{required}`\n{artifact}" + ); + } + assert!(artifact.contains("Alpha \\| claim
next <tag>")); + assert!(artifact.contains("a\\|source")); + assert!(artifact.contains("a \\| checkpoint blocker")); + assert!(artifact.contains("a \\| pipeline blocker")); + assert!(!artifact.contains("RAW_ALPHA_NODE_CONTENT")); + assert!(!artifact.contains("RAW_ZETA_NODE_CONTENT")); + assert!( + artifact.find("assumption-a").expect("assumption-a") + < artifact.find("assumption-z").expect("assumption-z") + ); + assert!( + artifact.find("checkpoint-a").expect("checkpoint-a") + < artifact.find("checkpoint-z").expect("checkpoint-z") + ); + assert!( + artifact.find("a-stage").expect("a-stage") < artifact.find("z-stage").expect("z-stage") + ); + assert!(result["payload"].get("nodes").is_none()); + } + + #[test] + fn markdown_audit_includes_sorted_raw_nodes_only_when_requested() { + let (store, deliberation_id) = seeded_audit_store(); + let input: ExportReasoningAuditInput = serde_json::from_value(json!({ + "deliberation_id": deliberation_id, + "format": "markdown", + "include_raw_thoughts": true + })) + .expect("raw Markdown audit input"); + + let result = serde_json::to_value(store.export_audit(input).expect("raw Markdown audit")) + .expect("serialize audit result"); + let artifact = result["artifact"].as_str().expect("Markdown artifact"); + assert!(result["payload"]["nodes"].is_object()); + assert!( + artifact.contains( + "RAW_ALPHA_NODE_CONTENT \\| line
<script>alert(1)</script>" + ) + ); + assert!(artifact.contains("RAW_ZETA_NODE_CONTENT")); + assert!( + artifact.find("node-a").expect("node-a") < artifact.find("node-z").expect("node-z") + ); + } + + #[test] + fn audit_format_rejects_values_outside_json_and_markdown() { + let parsed = serde_json::from_value::(json!({ + "deliberation_id": "delib-test", + "format": "html" + })); + assert!( + parsed.is_err(), + "unsupported formats must fail input parsing" + ); + } + + #[test] + fn graph_indexes_track_lineage_and_strict_cycle_rejections() { + let mut store = DeliberationStore::new(64); + let started = store.start(StartDeliberationInput { + session_id: Some("graph-indexes".to_string()), + mode: Some(ReasoningMode::Got), + goal: "Maintain graph indexes".to_string(), + constraints: None, + profile: Some(ReasoningProfile::Balanced), + limits: None, + verification_policy: None, + }); + let root_id = store + .deliberations + .get(&started.deliberation_id) + .and_then(|session| session.frontier.first()) + .cloned() + .expect("root frontier"); + + let expanded = store + .expand(ExpandThoughtsInput { + deliberation_id: started.deliberation_id.clone(), + from_node_ids: vec![root_id.clone()], + strategy: Some(ExpandStrategy::Diverse), + count: Some(3), + }) + .expect("expand should work"); + let first = expanded.created_nodes[0].node_id.clone(); + let second = expanded.created_nodes[1].node_id.clone(); + + let session = store + .deliberations + .get(&started.deliberation_id) + .expect("session"); + assert!( + session + .children_by_parent + .get(&root_id) .is_some_and(|children| children.contains(&first)) ); assert!( @@ -15728,6 +17461,7 @@ mod architecture_contract_tests { "reasoning://session/{}/skeleton-phases", started.deliberation_id ), + ToolPack::Full, ) .expect("phases resource"); assert_eq!(phases["phase_count"], json!(1)); @@ -15737,6 +17471,7 @@ mod architecture_contract_tests { "reasoning://session/{}/skeleton-phase/{}", started.deliberation_id, phase_id ), + ToolPack::Full, ) .expect("phase resource"); assert_eq!(phase["phase"]["state"], json!("MERGED")); @@ -15744,6 +17479,7 @@ mod architecture_contract_tests { let audit = store .export_audit(ExportReasoningAuditInput { deliberation_id: started.deliberation_id, + format: AuditFormat::Json, include_raw_thoughts: Some(false), }) .expect("audit"); @@ -15838,18 +17574,58 @@ mod architecture_contract_tests { RouteDecision::GatherMoreEvidence )); + let rejected = store + .set_assumption_status(SetAssumptionStatusInput { + deliberation_id: started.deliberation_id.clone(), + assumption_id: assumption.assumption.assumption_id.clone(), + status: AssumptionStatus::Verified, + confidence: Some(0.95), + evidence: None, + notes: Some("unsupported verification attempt".to_string()), + }) + .expect("unsupported verification remains unresolved"); + assert!(matches!( + rejected.assumption.status, + AssumptionStatus::Unresolved + )); + assert_eq!(rejected.blocking_assumption_ids.len(), 1); + + let still_blocked = store + .consensus(ConsensusAnswerInput { + deliberation_id: started.deliberation_id.clone(), + method: None, + policy_override: None, + }) + .expect("consensus remains policy-blocked"); + assert!(still_blocked.policy_blocked); + let updated = store .set_assumption_status(SetAssumptionStatusInput { deliberation_id: started.deliberation_id.clone(), assumption_id: assumption.assumption.assumption_id, status: AssumptionStatus::Verified, confidence: Some(0.95), - evidence: None, - notes: Some("verified in test".to_string()), + evidence: Some(vec![qualifying_tier1_evidence("official-dependency-check")]), + notes: Some("verified with qualifying evidence".to_string()), }) - .expect("resolve assumption"); + .expect("resolve assumption with evidence"); assert!(updated.blocking_assumption_ids.is_empty()); + store + .add_thought_node(AddThoughtNodeInput { + deliberation_id: started.deliberation_id.clone(), + content: + "Proceed because the official dependency check resolved the critical premise." + .to_string(), + parent_node_id: Some(root), + edge_kind: Some(GraphEdgeKind::Supports), + edge_note: Some("evidence-backed host conclusion".to_string()), + tags: None, + branch_id: None, + add_to_frontier: Some(true), + }) + .expect("add evidence-backed host conclusion"); + let unblocked = store .consensus(ConsensusAnswerInput { deliberation_id: started.deliberation_id, @@ -16988,14 +18764,14 @@ mod thinking_mode_contract_tests { fn native_resource_catalog_includes_modes_and_templates() { let resources = native_reasoning_resources(); assert!(resources.iter().any(|resource| { - resource.raw.uri == "reasoning://thinking-modes" - && resource.raw.mime_type.as_deref() == Some("application/json") + resource.uri == "reasoning://thinking-modes" + && resource.mime_type.as_deref() == Some("application/json") })); let templates = native_reasoning_resource_templates(); assert!( templates .iter() - .any(|template| template.raw.uri_template == "reasoning://session/{id}/graph") + .any(|template| template.uri_template == "reasoning://session/{id}/graph") ); } } @@ -17522,8 +19298,371 @@ mod add_thought_node_tests { } } +#[cfg(test)] +mod tool_pack_contract_tests { + use super::*; + + const CORE: &[&str] = &[ + "add_thought_node", + "consensus_answer", + "export_reasoning_audit", + "get_reasoning_coaching", + "reasoning_intent_router", + "record_assumption", + "run_reasoning_checkpoint", + "run_reasonkit_pipeline", + "run_thinking_mode", + "set_assumption_status", + "set_verification_policy", + "start_deliberation", + "verify_thoughts", + ]; + + const STANDARD: &[&str] = &[ + "add_thought_node", + "apply_reasoning_pattern", + "consensus_answer", + "converge_reasoning", + "distill_thought_cluster", + "evaluate_reasoning_quality", + "expand_thoughts", + "export_memory_snapshot", + "export_reasoning_audit", + "get_reasoning_coaching", + "get_thinking_history", + "link_thoughts", + "list_reasoning_resources", + "merge_thought_branches", + "plan_tool_sequence", + "prune_thoughts", + "read_reasoning_resource", + "reasoning_intent_router", + "record_assumption", + "refine_thoughts", + "replay_reasoning_session", + "run_reasoning_checkpoint", + "run_reasonkit_pipeline", + "run_skeleton_elaboration", + "run_thinking_mode", + "score_thoughts", + "set_assumption_status", + "set_reasoning_aliases", + "set_scoring_rubric", + "set_verification_policy", + "start_deliberation", + "start_skeleton_of_thought", + "verify_thoughts", + ]; + + const FULL: &[&str] = &[ + "add_thought_node", + "apply_algorithm_template", + "apply_reasoning_pattern", + "clear_thinking_history", + "consensus_answer", + "converge_reasoning", + "distill_thought_cluster", + "evaluate_reasoning_quality", + "expand_thoughts", + "export_memory_snapshot", + "export_reasoning_audit", + "get_reasoning_coaching", + "get_thinking_history", + "link_thoughts", + "list_reasoning_resources", + "mcts_select_path", + "merge_thought_branches", + "plan_tool_sequence", + "prune_thoughts", + "query_failure_memory", + "read_reasoning_resource", + "reasoning_autopilot", + "reasoning_intent_router", + "record_assumption", + "record_reasoning_action", + "record_reasoning_observation", + "refine_thoughts", + "replay_reasoning_session", + "run_reasoning_checkpoint", + "run_reasonkit_pipeline", + "run_skeleton_elaboration", + "run_thinking_mode", + "score_thoughts", + "sequentialthinking_tools", + "set_assumption_status", + "set_reasoning_aliases", + "set_scoring_rubric", + "set_verification_policy", + "start_deliberation", + "start_skeleton_of_thought", + "think_query", + "verify_thoughts", + "web_research_enqueue", + "web_research_fetch", + "web_research_task_status", + "web_research_triangulate", + "web_research_verify", + ]; + + fn router_names(server: &ThinkServer) -> Vec { + server + .tool_router + .list_all() + .into_iter() + .map(|tool| tool.name.into_owned()) + .collect() + } + + #[test] + fn tool_pack_members_match_the_adoption_contract() { + assert_eq!(ToolPack::Core.members(), CORE); + assert_eq!(ToolPack::Standard.members(), STANDARD); + assert_eq!(ToolPack::Full.members(), FULL); + assert_eq!(ThinkServer::tool_router().list_all().len(), FULL.len()); + assert!(CORE.iter().all(|tool| STANDARD.contains(tool))); + assert!(STANDARD.iter().all(|tool| FULL.contains(tool))); + } + + #[test] + fn tool_pack_resolution_defaults_and_falls_back_to_core() { + assert_eq!(ToolPack::resolve(None), (ToolPack::Core, None)); + assert_eq!(ToolPack::resolve(Some("")), (ToolPack::Core, None)); + assert_eq!(ToolPack::resolve(Some("core")), (ToolPack::Core, None)); + assert_eq!( + ToolPack::resolve(Some("standard")), + (ToolPack::Standard, None) + ); + assert_eq!(ToolPack::resolve(Some("full")), (ToolPack::Full, None)); + let (pack, warning) = ToolPack::resolve(Some("everything")); + assert_eq!(pack, ToolPack::Core); + assert_eq!( + warning, + Some( + "invalid REASONKIT_TOOL_PACK; expected core, standard, or full; using safe core fallback" + ) + ); + } + + #[test] + fn stored_router_advertises_exact_tools_for_each_pack() { + let core = ThinkServer::new_with_tool_pack(20, ToolPack::Core); + let standard = ThinkServer::new_with_tool_pack(20, ToolPack::Standard); + let full = ThinkServer::new_with_tool_pack(20, ToolPack::Full); + assert_eq!(router_names(&core), CORE); + assert_eq!(router_names(&standard), STANDARD); + assert_eq!(router_names(&full), FULL); + } + + #[test] + fn tool_pack_resource_reports_active_and_exact_memberships() { + let resource = tool_pack_resource(ToolPack::Standard); + assert_eq!(resource["default"], "core"); + assert_eq!(resource["active"], "standard"); + assert_eq!(resource["active_tool_count"], 33); + assert_eq!(resource["restart_required"], true); + assert_eq!(resource["packs"]["core"]["tools"], json!(CORE)); + assert_eq!(resource["packs"]["standard"]["tools"], json!(STANDARD)); + assert_eq!(resource["packs"]["full"]["tools"], json!(FULL)); + } + + #[test] + fn core_guidance_marks_hidden_tools_instead_of_silently_recommending_them() { + let server = ThinkServer::new_with_tool_pack(20, ToolPack::Core); + let info = server.get_info(); + let instructions = info.instructions.expect("server instructions"); + assert!(instructions.contains("active tool pack: core (13 tools)")); + assert!(instructions.contains("REASONKIT_TOOL_PACK=full")); + + let prompt = server.sequential_thinking_guidance(Parameters(GuidancePromptInput { + problem: Some("small task".to_string()), + })); + let prompt_json = serde_json::to_value(prompt).expect("prompt json"); + let prompt_text = prompt_json["messages"][0]["content"]["text"] + .as_str() + .expect("prompt text"); + assert!(prompt_text.contains("sequentialthinking_tools requires the full tool pack")); + assert!(prompt_text.contains("run_thinking_mode")); + + let modes = thinking_modes_resource(&ReasoningAliases::default(), ToolPack::Core); + assert_eq!(modes["active_tool_pack"], "core"); + for mode in modes["modes"].as_array().expect("mode array") { + for tool in mode["primary_tools"].as_array().expect("primary tools") { + assert!(CORE.contains(&tool.as_str().expect("tool name"))); + } + } + assert_eq!(modes["compatibility_default_tool"], Value::Null); + } + + #[test] + fn json_tool_results_are_dual_format_and_fail_closed() { + let payload = json!({"answer": 42, "status": "ok"}); + let success = serde_json::to_value(json_tool_success(&payload)).expect("success wire json"); + assert_eq!(success["structuredContent"], payload); + assert_eq!(success["isError"], false); + let success_text = success["content"][0]["text"] + .as_str() + .expect("success text"); + assert_eq!( + serde_json::from_str::(success_text).unwrap(), + payload + ); + + let failure = serde_json::to_value(json_tool_error(&payload)).expect("error wire json"); + assert_eq!(failure["structuredContent"], payload); + assert_eq!(failure["isError"], true); + let failure_text = failure["content"][0]["text"].as_str().expect("error text"); + assert_eq!( + serde_json::from_str::(failure_text).unwrap(), + payload + ); + + struct BrokenSerialize; + impl Serialize for BrokenSerialize { + fn serialize(&self, _serializer: S) -> Result + where + S: serde::Serializer, + { + Err(serde::ser::Error::custom( + "intentional serialization failure", + )) + } + } + + let broken = + serde_json::to_value(json_tool_success(&BrokenSerialize)).expect("fallback wire json"); + assert_eq!(broken["isError"], true); + assert_eq!(broken["structuredContent"]["code"], "serialization_error"); + let broken_text = broken["content"][0]["text"] + .as_str() + .expect("fallback text"); + assert_eq!( + serde_json::from_str::(broken_text).unwrap(), + broken["structuredContent"] + ); + } +} + +#[cfg(test)] +mod cli_contract_tests { + use super::*; + + #[test] + fn cli_actions_preserve_stdio_default_and_reject_unknown_arguments() { + assert_eq!(parse_cli_action(&[]).unwrap(), CliAction::ServeStdio); + assert_eq!( + parse_cli_action(&["--version".to_string()]).unwrap(), + CliAction::Version + ); + assert_eq!( + parse_cli_action(&["--help".to_string()]).unwrap(), + CliAction::Help + ); + assert_eq!( + parse_cli_action(&["doctor".to_string(), "--json".to_string()]).unwrap(), + CliAction::DoctorJson + ); + assert!(parse_cli_action(&["--unknown".to_string()]).is_err()); + } + + #[test] + fn doctor_report_is_pack_aware_and_exposes_no_secret_values() { + let report = doctor_report(Some("standard"), BackendMode::resolve(None)); + assert_eq!(report["status"], "ok"); + assert_eq!(report["version"], env!("CARGO_PKG_VERSION")); + assert_eq!(report["transport"], "stdio"); + assert_eq!(report["active_tool_pack"], "standard"); + assert_eq!(report["tool_count"], 33); + assert_eq!(report["semantic_engine"], "host_agent"); + assert_eq!(report["backend_mode"], "agent"); + assert_eq!(report["external_provider_bound"], false); + assert_eq!(report["retrieval_provider_bound"], false); + let rendered = serde_json::to_string(&report).expect("doctor JSON"); + for forbidden in ["TOKEN", "API_KEY", "BEARER", "password"] { + assert!(!rendered.contains(forbidden)); + } + + let fallback = doctor_report(Some("misspelled"), BackendMode::resolve(None)); + assert_eq!(fallback["status"], "warning"); + assert_eq!(fallback["active_tool_pack"], "core"); + assert_eq!(fallback["tool_count"], 13); + assert!( + fallback["warnings"][0] + .as_str() + .expect("warning") + .contains("invalid REASONKIT_TOOL_PACK") + ); + } + + #[test] + fn doctor_report_accepts_unset_and_agent_backend_modes() { + for raw_backend_mode in [None, Some("agent")] { + let report = doctor_report(None, BackendMode::resolve(raw_backend_mode)); + assert_eq!(report["status"], "ok"); + assert_eq!(report["backend_mode"], "agent"); + assert_eq!(report["external_provider_bound"], false); + assert_eq!(report["warnings"], json!([])); + } + } + + #[test] + fn doctor_report_warns_and_falls_back_for_invalid_backend_mode_without_echoing_it() { + let invalid = "external-with-secret-7f3a9c"; + let report = doctor_report(None, BackendMode::resolve(Some(invalid))); + + assert_eq!(report["status"], "warning"); + assert_eq!(report["backend_mode"], "agent"); + assert_eq!(report["external_provider_bound"], false); + assert!( + report["warnings"][0] + .as_str() + .expect("warning") + .contains("invalid BACKEND_MODE") + ); + assert!(!serde_json::to_string(&report).unwrap().contains(invalid)); + } + + #[test] + fn doctor_report_warns_and_falls_back_when_external_provider_is_unbound() { + let report = doctor_report(None, BackendMode::resolve(Some("external"))); + + assert_eq!(report["status"], "warning"); + assert_eq!(report["backend_mode"], "agent"); + assert_eq!(report["external_provider_bound"], false); + assert!( + report["warnings"][0] + .as_str() + .expect("warning") + .contains("no external provider is bound") + ); + } +} + #[tokio::main] async fn main() -> anyhow::Result<()> { + let args = std::env::args().skip(1).collect::>(); + match parse_cli_action(&args).map_err(anyhow::Error::msg)? { + CliAction::Version => { + println!("reasonkit-think-mcp {}", env!("CARGO_PKG_VERSION")); + return Ok(()); + } + CliAction::Help => { + println!("{CLI_HELP}"); + return Ok(()); + } + CliAction::DoctorJson => { + let configured_pack = std::env::var("REASONKIT_TOOL_PACK").ok(); + println!( + "{}", + serde_json::to_string_pretty(&doctor_report( + configured_pack.as_deref(), + BackendMode::resolve_env() + ))? + ); + return Ok(()); + } + CliAction::ServeStdio => {} + } + tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() @@ -17553,47 +19692,48 @@ async fn main() -> anyhow::Result<()> { #[cfg(test)] mod reasoning_loop_integration_tests { use super::*; - use reasonkit_mem::storage::hot::HotMemoryEntry; use tokio::runtime::Runtime; #[test] - fn test_reasoning_loop_with_think_query() { - let rt = Runtime::new().unwrap(); - rt.block_on(async { - // Initialize ThinkServer - let server = ThinkServer::new(100); - - // 1. Add some entries to the hot cache directly (simulating prior thoughts being encoded) - let cache = &server.hot_memory; - let entry = HotMemoryEntry::new( - uuid::Uuid::new_v4(), - "Hot cache is blazing fast".to_string(), - vec![0.5; 1536], - serde_json::json!({"test": true}), - ); - cache.put(entry.clone()).await.expect("hot cache put"); - - // 2. Test the think_query fast-path API - let query_input = ThinkQueryInput { + fn think_query_fails_closed_without_a_bound_provider() { + let runtime = Runtime::new().expect("tokio runtime"); + runtime.block_on(async { + let server = ThinkServer::new_with_tool_pack(100, ToolPack::Full); + let query_res = server.think_query(Parameters(ThinkQueryInput { query: "fast cache".to_string(), query_embedding: Some(vec![0.5; 1536]), limit: Some(5), min_score: Some(0.1), - }; - - let query_res = server.think_query(Parameters(query_input)); - assert_ne!(query_res.is_error, Some(true), "think_query should succeed"); + })); - let value = serde_json::to_value(&query_res).expect("result json"); + assert_eq!(query_res.is_error, Some(true)); + let value = serde_json::to_value(query_res).expect("result json"); + assert_eq!( + value["structuredContent"]["code"], + "experimental_memory_unavailable" + ); + assert_eq!(value["structuredContent"]["retryable"], false); + assert!( + value["structuredContent"]["error"] + .as_str() + .expect("error message") + .contains("no retrieval was performed") + ); let text = value["content"][0]["text"].as_str().expect("text payload"); - let json_val = serde_json::from_str::(text).unwrap(); - let results = json_val.get("results").unwrap().as_array().unwrap(); - assert!(!results.is_empty(), "Expected results from think_query"); - let first_result = results[0].as_object().unwrap(); assert_eq!( - first_result.get("content").unwrap().as_str().unwrap(), - "Hot cache is blazing fast" + serde_json::from_str::(text).expect("text JSON"), + value["structuredContent"] ); + + let description = server + .tool_router + .list_all() + .into_iter() + .find(|tool| tool.name == "think_query") + .expect("think_query route") + .description + .expect("think_query description"); + assert!(description.contains("unavailable")); }); } } diff --git a/tests/test_ci_contract.py b/tests/test_ci_contract.py new file mode 100644 index 0000000..9986d81 --- /dev/null +++ b/tests/test_ci_contract.py @@ -0,0 +1,81 @@ +from pathlib import Path +import unittest + + +class CiContractTests(unittest.TestCase): + def test_justfile_exposes_unit_eval_and_audit_gates(self) -> None: + source = Path("justfile").read_text(encoding="utf-8") + self.assertIn("unit:", source) + self.assertIn("eval:", source) + self.assertIn("audit:", source) + self.assertIn("python3 -m unittest discover", source) + self.assertIn("evals/run_contract_evals.py", source) + self.assertIn("cargo audit --deny warnings", source) + + def test_justfile_exposes_config_only_dist_validation(self) -> None: + source = Path("justfile").read_text(encoding="utf-8") + self.assertIn("dist-check:", source) + self.assertIn( + "dist manifest --artifacts=local --output-format=json --no-local-paths", + source, + ) + + def test_justfile_exposes_package_registry_and_dist_release_gates(self) -> None: + source = Path("justfile").read_text(encoding="utf-8") + self.assertIn("package-check:", source) + self.assertIn( + 'CARGO_TARGET_DIR="{{ justfile_directory() }}/target/package-check" cargo package --locked', + source, + ) + self.assertIn("registry-check:", source) + self.assertIn("uvx --from check-jsonschema==0.38.0", source) + self.assertIn( + "ci: check eval audit package-check registry-check dist-check", + source, + ) + + def test_ci_runs_explicit_rust_python_and_runtime_contract_gates(self) -> None: + source = Path(".github/workflows/ci.yml").read_text(encoding="utf-8") + self.assertIn("cargo test", source) + self.assertIn("Contract unit tests", source) + self.assertIn("MCP contract evaluations", source) + self.assertIn("run: cargo audit --deny warnings", source) + + def test_ci_enforces_pinned_package_registry_and_dist_checks(self) -> None: + source = Path(".github/workflows/ci.yml").read_text(encoding="utf-8") + self.assertIn( + "run: CARGO_TARGET_DIR=target/package-check cargo package --locked", + source, + ) + self.assertIn( + "uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9", + source, + ) + self.assertIn('version: "0.12.1"', source) + self.assertIn("uvx --from check-jsonschema==0.38.0", source) + self.assertIn( + "run: cargo install cargo-dist --version 0.32.0 --locked", + source, + ) + self.assertIn( + "dist manifest --artifacts=local --output-format=json --no-local-paths", + source, + ) + + def test_audit_warning_cleanup_is_tracked(self) -> None: + path = Path("docs/dependency-audit.md") + if not path.is_file(): + self.fail("docs/dependency-audit.md must track the warning-clean boundary") + source = path.read_text(encoding="utf-8") + self.assertIn("reasonkit-mem", source) + self.assertIn("experimental_memory_unavailable", source) + self.assertIn("cargo audit --deny warnings", source) + + def test_contributor_audit_copy_matches_the_zero_warning_gate(self) -> None: + source = Path("CONTRIBUTING.md").read_text(encoding="utf-8") + self.assertNotIn("known warning debt", source) + self.assertIn("zero-warning baseline", source) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_client_compat_check.py b/tests/test_client_compat_check.py new file mode 100644 index 0000000..65ea498 --- /dev/null +++ b/tests/test_client_compat_check.py @@ -0,0 +1,135 @@ +from __future__ import annotations + +import json +import os +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +from scripts import client_compat_check + + +class ClientCompatConfigTests(unittest.TestCase): + def _write_binary(self, path: Path, version: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + "#!/bin/sh\n" + f"printf '%s\\n' 'reasonkit-think-mcp {version}'\n", + encoding="utf-8", + ) + path.chmod(0o755) + + def _write_cursor_config(self, home: Path, command: str) -> None: + path = home / ".cursor" / "mcp.json" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + json.dumps( + { + "mcpServers": { + "reasonkit-think": { + "type": "stdio", + "command": command, + "args": [], + } + } + } + ), + encoding="utf-8", + ) + + def test_path_command_resolves_the_exact_candidate(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + home = root / "home" + binary = root / "bin" / "reasonkit-think-mcp" + self._write_binary(binary, "0.2.0") + self._write_cursor_config(home, "reasonkit-think-mcp") + + with ( + patch.object(client_compat_check.Path, "home", return_value=home), + patch.dict(os.environ, {"PATH": str(binary.parent)}), + ): + checks = client_compat_check.client_config_checks(root, binary) + + cursor = next(check for check in checks if check["client"] == "cursor") + self.assertEqual(cursor["status"], "ok") + self.assertEqual(cursor["configured_command"], "reasonkit-think-mcp") + self.assertEqual(cursor["resolved_command"], str(binary.resolve())) + self.assertFalse(cursor["command_matches_release_binary"]) + self.assertTrue(cursor["resolved_matches_release_binary"]) + self.assertEqual(cursor["candidate_version"], "0.2.0") + self.assertEqual(cursor["resolved_version"], "0.2.0") + self.assertTrue(cursor["version_matches_release_binary"]) + + def test_path_install_with_candidate_version_is_accepted_but_not_exact(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + home = root / "home" + binary = root / "candidate" / "reasonkit-think-mcp" + installed = root / "path-bin" / "reasonkit-think-mcp" + self._write_binary(binary, "0.2.0") + self._write_binary(installed, "0.2.0") + self._write_cursor_config(home, "reasonkit-think-mcp") + + with ( + patch.object(client_compat_check.Path, "home", return_value=home), + patch.dict(os.environ, {"PATH": str(installed.parent)}), + ): + checks = client_compat_check.client_config_checks(root, binary) + + cursor = next(check for check in checks if check["client"] == "cursor") + self.assertEqual(cursor["status"], "ok") + self.assertEqual(cursor["resolved_command"], str(installed.resolve())) + self.assertFalse(cursor["resolved_matches_release_binary"]) + self.assertTrue(cursor["version_matches_release_binary"]) + + def test_path_install_with_an_old_version_reports_drift(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + home = root / "home" + binary = root / "candidate" / "reasonkit-think-mcp" + installed = root / "path-bin" / "reasonkit-think-mcp" + self._write_binary(binary, "0.2.0") + self._write_binary(installed, "0.1.1") + self._write_cursor_config(home, "reasonkit-think-mcp") + + with ( + patch.object(client_compat_check.Path, "home", return_value=home), + patch.dict(os.environ, {"PATH": str(installed.parent)}), + ): + checks = client_compat_check.client_config_checks(root, binary) + + cursor = next(check for check in checks if check["client"] == "cursor") + self.assertEqual(cursor["status"], "drift") + self.assertEqual(cursor["resolved_command"], str(installed.resolve())) + self.assertEqual(cursor["candidate_version"], "0.2.0") + self.assertEqual(cursor["resolved_version"], "0.1.1") + self.assertFalse(cursor["version_matches_release_binary"]) + + def test_path_install_requires_reasonkit_version_identity(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + home = root / "home" + binary = root / "candidate" / "reasonkit-think-mcp" + installed = root / "path-bin" / "reasonkit-think-mcp" + self._write_binary(binary, "0.2.0") + installed.parent.mkdir(parents=True, exist_ok=True) + installed.write_text("#!/bin/sh\nprintf '%s\\n' '0.2.0'\n", encoding="utf-8") + installed.chmod(0o755) + self._write_cursor_config(home, "reasonkit-think-mcp") + + with ( + patch.object(client_compat_check.Path, "home", return_value=home), + patch.dict(os.environ, {"PATH": str(installed.parent)}), + ): + checks = client_compat_check.client_config_checks(root, binary) + + cursor = next(check for check in checks if check["client"] == "cursor") + self.assertEqual(cursor["status"], "drift") + self.assertIsNone(cursor["resolved_version"]) + self.assertFalse(cursor["version_matches_release_binary"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_community_contract.py b/tests/test_community_contract.py new file mode 100644 index 0000000..511b518 --- /dev/null +++ b/tests/test_community_contract.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +import json +import re +import unittest +from collections import Counter +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent.parent +FEATURE_ID = re.compile(r"^(T\d+-[A-Z0-9]+)-") + + +class CommunityContractTests(unittest.TestCase): + def test_public_contribution_and_security_paths_exist(self) -> None: + required = [ + "CONTRIBUTING.md", + "SECURITY.md", + ".github/ISSUE_TEMPLATE/bug_report.yml", + ".github/ISSUE_TEMPLATE/feature_request.yml", + ".github/ISSUE_TEMPLATE/config.yml", + ".github/pull_request_template.md", + ] + for relative in required: + path = ROOT / relative + self.assertTrue(path.is_file(), relative) + self.assertTrue(path.read_text(encoding="utf-8").strip(), relative) + + contributing = (ROOT / "CONTRIBUTING.md").read_text(encoding="utf-8") + self.assertIn("just check", contributing) + self.assertIn("evals/tool-packs.json", contributing) + self.assertIn("Conventional Commits", contributing) + + security = (ROOT / "SECURITY.md").read_text(encoding="utf-8") + self.assertIn("privately", security) + self.assertNotIn("public issue", security.lower()) + + def test_feature_record_ids_are_unique_and_match_headings(self) -> None: + records = sorted((ROOT / "features").glob("*/*.md")) + ids: list[str] = [] + for path in records: + match = FEATURE_ID.match(path.name) + if match is None: + continue + feature_id = match.group(1) + heading = path.read_text(encoding="utf-8").splitlines()[0] + self.assertTrue(heading.startswith(f"# {feature_id}:"), path.name) + ids.append(feature_id) + + self.assertTrue(ids) + duplicates = [name for name, count in Counter(ids).items() if count > 1] + self.assertEqual(duplicates, []) + + def test_feature_manifest_distinguishes_plans_from_runtime_truth(self) -> None: + source = (ROOT / "features" / "README.md").read_text(encoding="utf-8") + self.assertIn("planning records", source) + self.assertIn("evals/tool-packs.json", source) + self.assertIn("Partial overlap", source) + self.assertIn("T2-2F", source) + + payload = json.loads( + (ROOT / "features" / "manifest.json").read_text(encoding="utf-8") + ) + records = payload["records"] + manifest_paths = {record["path"] for record in records} + disk_paths = { + path.relative_to(ROOT).as_posix() + for path in (ROOT / "features").glob("*/*.md") + if FEATURE_ID.match(path.name) + } + self.assertEqual(manifest_paths, disk_paths) + self.assertEqual(len(records), len({record["id"] for record in records})) + for record in records: + self.assertEqual(record["record_state"], Path(record["path"]).parent.name) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_distribution_docs.py b/tests/test_distribution_docs.py new file mode 100644 index 0000000..b564418 --- /dev/null +++ b/tests/test_distribution_docs.py @@ -0,0 +1,173 @@ +from __future__ import annotations + +import json +import shutil +import subprocess +import tomllib +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent.parent + + +class DistributionDocsTests(unittest.TestCase): + def test_release_metadata_and_crate_boundary_are_synchronized(self) -> None: + cargo = tomllib.loads((ROOT / "Cargo.toml").read_text(encoding="utf-8")) + registry = json.loads((ROOT / "server.json").read_text(encoding="utf-8")) + package = cargo["package"] + + self.assertEqual(package["version"], "0.2.0") + self.assertEqual(package["version"], registry["version"]) + self.assertNotIn("exclude", package) + self.assertEqual( + package["include"], + [ + "/src/**", + "/Cargo.toml", + "/Cargo.lock", + "/README.md", + "/CHANGELOG.md", + "/LICENSE", + ], + ) + + @unittest.skipUnless(shutil.which("cargo"), "cargo is not installed") + def test_packaged_file_set_excludes_internal_project_material(self) -> None: + result = subprocess.run( + ["cargo", "package", "--list", "--allow-dirty", "--locked"], + cwd=ROOT, + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) + + allowed_root_files = { + ".cargo_vcs_info.json", + "CHANGELOG.md", + "Cargo.lock", + "Cargo.toml", + "Cargo.toml.orig", + "LICENSE", + "README.md", + } + unexpected = [ + path + for path in result.stdout.splitlines() + if path not in allowed_root_files and not path.startswith("src/") + ] + self.assertEqual(unexpected, []) + + def test_readme_has_truthful_path_first_golden_path(self) -> None: + readme = (ROOT / "README.md").read_text(encoding="utf-8") + self.assertIn("## Quick start", readme) + self.assertNotIn("Five-minute path", readme) + self.assertNotIn("tracked warnings stay visible", readme) + self.assertIn("deny vulnerabilities and advisory warnings", readme) + self.assertIn( + "mcp-name: io.github.reasonkit/reasonkit-think", + readme, + ) + self.assertIn("crates.io/crates/reasonkit-think-mcp", readme) + self.assertNotIn('crates.io/crates/reasonkit-think"', readme) + self.assertNotIn("ABSOLUTE PATH", readme) + self.assertIn('"command": "reasonkit-think-mcp"', readme) + self.assertIn("compatibility escape hatch", readme) + self.assertIn("not yet a live Registry record", readme) + self.assertIn("cargo install --locked --path .", readme) + self.assertIn("after v0.2.0 publication", readme) + self.assertIn( + "cargo install --locked --version 0.2.0 reasonkit-think-mcp", + readme, + ) + + sequence = [ + "run_thinking_mode", + "add_thought_node", + "verify_thoughts", + "run_reasoning_checkpoint", + "consensus_answer", + "export_reasoning_audit", + ] + positions = [readme.index(name) for name in sequence] + self.assertEqual(positions, sorted(positions)) + + def test_client_docs_and_examples_use_path_commands(self) -> None: + client_docs = list((ROOT / "docs" / "clients").glob("*.md")) + self.assertTrue(client_docs) + for path in client_docs: + source = path.read_text(encoding="utf-8") + self.assertNotIn("/ABSOLUTE/PATH", source, path.name) + self.assertIn("reasonkit-think-mcp", source, path.name) + + configs = list((ROOT / "examples" / "client-configs").glob("*.json")) + self.assertTrue(configs) + for path in configs: + payload = json.loads(path.read_text(encoding="utf-8")) + rendered = json.dumps(payload) + self.assertNotIn("/absolute/path", rendered, path.name) + self.assertIn("reasonkit-think-mcp", rendered, path.name) + + def test_registry_manifest_targets_the_release_candidate(self) -> None: + path = ROOT / "server.json" + if not path.is_file(): + self.fail("server.json must exist") + manifest = json.loads(path.read_text(encoding="utf-8")) + self.assertEqual( + manifest["$schema"], + "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + ) + self.assertEqual(manifest["name"], "io.github.reasonkit/reasonkit-think") + self.assertEqual(manifest["version"], "0.2.0") + self.assertEqual( + manifest["packages"], + [ + { + "registryType": "cargo", + "identifier": "reasonkit-think-mcp", + "version": "0.2.0", + "transport": {"type": "stdio"}, + } + ], + ) + + def test_cargo_dist_bootstrap_is_config_only(self) -> None: + path = ROOT / "dist-workspace.toml" + if not path.is_file(): + self.fail("dist-workspace.toml must exist") + config = tomllib.loads(path.read_text(encoding="utf-8")) + self.assertEqual(config["dist"]["cargo-dist-version"], "0.32.0") + self.assertEqual(config["dist"]["ci"], []) + self.assertEqual(config["dist"]["hosting"], ["github"]) + self.assertFalse((ROOT / ".github" / "workflows" / "release.yml").exists()) + + @unittest.skipUnless(shutil.which("dist"), "cargo-dist is not installed") + def test_cargo_dist_manifest_needs_no_generated_workflow(self) -> None: + result = subprocess.run( + [ + "dist", + "manifest", + "--artifacts=local", + "--output-format=json", + "--no-local-paths", + ], + cwd=ROOT, + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertFalse((ROOT / ".github" / "workflows" / "release.yml").exists()) + + def test_python_test_caches_are_ignored_repo_wide(self) -> None: + patterns = { + line.strip() + for line in (ROOT / ".gitignore").read_text(encoding="utf-8").splitlines() + } + self.assertIn("**/__pycache__/", patterns) + self.assertIn("*.py[cod]", patterns) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_mcp_contract.py b/tests/test_mcp_contract.py new file mode 100644 index 0000000..16978c7 --- /dev/null +++ b/tests/test_mcp_contract.py @@ -0,0 +1,292 @@ +from __future__ import annotations + +import importlib +import importlib.util +import unittest +from pathlib import Path + + +MODULE_NAME = "evals.mcp_contract" +try: + MODULE_SPEC = importlib.util.find_spec(MODULE_NAME) +except ModuleNotFoundError: + MODULE_SPEC = None + + +class ContractModulePresenceTests(unittest.TestCase): + def test_contract_module_exists(self) -> None: + self.assertIsNotNone( + MODULE_SPEC, + "evals.mcp_contract must provide the executable MCP contract", + ) + + def test_contract_eval_cli_exists(self) -> None: + self.assertTrue( + Path("evals/run_contract_evals.py").is_file(), + "evals/run_contract_evals.py must execute live contract evaluations", + ) + + +@unittest.skipUnless(MODULE_SPEC, "contract module is the TDD implementation target") +class ContractTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.contracts = importlib.import_module(MODULE_NAME) + cls.contract = cls.contracts.load_contract() + + def test_tool_pack_contract_is_exact_nested_and_sorted(self) -> None: + self.assertEqual(self.contract["defaultToolPack"], "core") + self.assertEqual( + {name: len(tools) for name, tools in self.contract["toolPacks"].items()}, + {"core": 13, "standard": 33, "full": 47}, + ) + self.assertEqual(self.contracts.validate_contract(self.contract), []) + + def test_protocol_matrix_includes_current_and_legacy_versions(self) -> None: + self.assertEqual( + self.contract["protocolVersions"], + [ + "2024-11-05", + "2025-03-26", + "2025-06-18", + "2025-11-25", + "2026-07-28", + ], + ) + + def test_tool_surface_reports_missing_unexpected_and_order_drift(self) -> None: + core = self.contract["toolPacks"]["core"] + actual = [{"name": name} for name in core] + self.assertEqual( + self.contracts.validate_tool_surface(self.contract, "core", actual), + [], + ) + + drifted = actual[1:] + [{"name": "unexpected_tool"}] + issues = self.contracts.validate_tool_surface(self.contract, "core", drifted) + self.assertTrue(any("missing" in issue for issue in issues)) + self.assertTrue(any("unexpected" in issue for issue in issues)) + self.assertTrue(any("lexical" in issue for issue in issues)) + + def test_schema_validator_rejects_untyped_composed_shapes(self) -> None: + invalid = { + "properties": { + "mode": {"enum": ["quick", "test"]}, + "limit": {"type": "integer"}, + } + } + issues = self.contracts.schema_type_issues(invalid) + self.assertIn("$: missing type", issues) + self.assertIn("$.properties.mode: missing type", issues) + self.assertNotIn("$.properties.limit: missing type", issues) + + def test_fail_closed_evaluation_accepts_only_blocked_outcomes(self) -> None: + evaluation = self.contracts.evaluate_fail_closed( + evidence_free_update={ + "result": { + "isError": False, + "structuredContent": { + "code": "invalid_input", + "assumption": { + "status": "unresolved", + "criticality": "critical", + }, + "blocking_assumption_ids": ["assumption-1"], + }, + "content": [{"type": "text", "text": "evidence required"}], + } + }, + criticality_downgrade={ + "result": { + "structuredContent": { + "verification_matrix": [ + { + "claim": "Release requires approval", + "critical": True, + "status": "data_deficit", + } + ] + } + } + }, + checkpoint={ + "result": { + "content": [ + { + "type": "text", + "text": '{"checkpoint":{"route_decision":"GATHER_MORE_EVIDENCE","blocking_gaps":["critical"]}}', + } + ] + } + }, + consensus={ + "result": { + "isError": True, + "structuredContent": {"code": "consensus_blocked"}, + "content": [{"type": "text", "text": "blocked"}], + } + }, + unknown_stage={ + "result": { + "isError": True, + "structuredContent": {"code": "invalid_input"}, + "content": [{"type": "text", "text": "unknown stage"}], + } + }, + pipeline_override={"result": {"structuredContent": {"ok": True}}}, + post_pipeline_audit={ + "result": { + "structuredContent": { + "payload": { + "verification_policy": { + "min_independent_groups": 3, + "allow_tier3_for_independence": False, + "require_tier1_unambiguous_for_critical": True, + "fail_closed_on_critical_unresolved": True, + } + } + } + } + }, + scaffold_run={ + "result": { + "structuredContent": { + "policy_blocked": True, + "route_decision": "GATHER_MORE_EVIDENCE", + "pipeline_route": "GATHER_MORE_EVIDENCE", + "graph_authorship": { + "agent_authored": 0, + "scaffold_nodes": 3, + "frontier_scaffold_only": True, + }, + } + } + }, + ) + self.assertTrue(evaluation["passed"]) + self.assertTrue(all(evaluation["checks"].values())) + + def test_fail_closed_evaluation_catches_a_bypass(self) -> None: + bypass = { + "result": { + "isError": False, + "content": [{"type": "text", "text": '{"status":"verified"}'}], + } + } + evaluation = self.contracts.evaluate_fail_closed( + evidence_free_update=bypass, + criticality_downgrade={ + "result": { + "structuredContent": { + "verification_matrix": [ + { + "claim": "Release requires approval", + "critical": False, + "status": "VERIFIED", + } + ] + } + } + }, + checkpoint={ + "result": { + "content": [ + { + "type": "text", + "text": '{"checkpoint":{"route_decision":"PROCEED_WITH_CAVEATS","blocking_gaps":[]}}', + } + ] + } + }, + consensus=bypass, + unknown_stage=bypass, + pipeline_override=bypass, + post_pipeline_audit={ + "result": { + "structuredContent": { + "payload": { + "verification_policy": { + "min_independent_groups": 1, + "allow_tier3_for_independence": True, + "require_tier1_unambiguous_for_critical": False, + "fail_closed_on_critical_unresolved": False, + } + } + } + } + }, + scaffold_run={ + "result": { + "structuredContent": { + "policy_blocked": False, + "route_decision": "PROCEED_WITH_CAVEATS", + "pipeline_route": "GATHER_MORE_EVIDENCE", + "graph_authorship": { + "agent_authored": 0, + "scaffold_nodes": 3, + "frontier_scaffold_only": True, + }, + } + } + }, + ) + self.assertFalse(evaluation["passed"]) + self.assertFalse(any(evaluation["checks"].values())) + + def test_runtime_probe_and_matrix_api_exists(self) -> None: + self.assertTrue( + hasattr(self.contracts, "validate_runtime_probe"), + "contract module must validate a live tools/list response", + ) + self.assertTrue( + hasattr(self.contracts, "matrix_jobs"), + "contract module must generate every pack/protocol pairing", + ) + + def test_matrix_jobs_cover_all_fifteen_pack_protocol_pairs(self) -> None: + jobs = self.contracts.matrix_jobs(self.contract) + self.assertEqual(len(jobs), 15) + self.assertEqual(jobs[0], ("core", "2024-11-05")) + self.assertEqual(jobs[-1], ("full", "2026-07-28")) + + def test_runtime_probe_requires_negotiation_exact_surface_and_typed_schemas(self) -> None: + protocol = "2026-07-28" + tools = [ + { + "name": name, + "inputSchema": {"type": "object", "properties": {}}, + } + for name in self.contract["toolPacks"]["core"] + ] + initialize = { + "result": { + "protocolVersion": protocol, + "serverInfo": {"name": "reasonkit-think-mcp"}, + } + } + self.assertEqual( + self.contracts.validate_runtime_probe( + self.contract, + pack="core", + protocol=protocol, + initialize=initialize, + tools=tools, + ), + [], + ) + + initialize["result"]["serverInfo"]["name"] = "wrong-server" + tools[0]["inputSchema"] = {"properties": {"value": {"enum": ["x"]}}} + issues = self.contracts.validate_runtime_probe( + self.contract, + pack="core", + protocol=protocol, + initialize=initialize, + tools=tools, + ) + self.assertTrue(any("server name" in issue for issue in issues)) + self.assertTrue(any("missing type" in issue for issue in issues)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_smoke_contract.py b/tests/test_smoke_contract.py new file mode 100644 index 0000000..af5f416 --- /dev/null +++ b/tests/test_smoke_contract.py @@ -0,0 +1,14 @@ +from pathlib import Path +import unittest + + +class SmokeContractTests(unittest.TestCase): + def test_smoke_runs_full_pack_and_checks_exact_inventory(self) -> None: + source = Path("scripts/smoke_test.py").read_text(encoding="utf-8") + self.assertIn('env["REASONKIT_TOOL_PACK"] = "full"', source) + self.assertIn('root / "evals" / "tool-packs.json"', source) + self.assertIn("tools == expected_tools", source) + + +if __name__ == "__main__": + unittest.main()