diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml new file mode 100644 index 0000000..2003340 --- /dev/null +++ b/.github/workflows/codspeed.yml @@ -0,0 +1,39 @@ +name: CodSpeed + +on: + push: + branches: ["main"] + pull_request: + # `workflow_dispatch` allows CodSpeed to trigger backtest + # performance analysis in order to generate initial data. + workflow_dispatch: + +permissions: + contents: read + id-token: write # for OpenID Connect authentication with CodSpeed + +env: + CARGO_TERM_COLOR: always + +jobs: + benchmarks: + name: Run benchmarks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust toolchain, cache and cargo-codspeed binary + uses: moonrepo/setup-rust@v0 + with: + channel: stable + cache-target: release + bins: cargo-codspeed + + - name: Build the benchmark targets + run: cargo codspeed build + + - name: Run the benchmarks + uses: CodSpeedHQ/action@v5 + with: + mode: simulation + run: cargo codspeed run diff --git a/Cargo.lock b/Cargo.lock index 9046566..e0a8548 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,6 +114,15 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "arbitrary" version = "1.4.2" @@ -356,6 +365,7 @@ dependencies = [ "anstyle", "clap_lex", "strsim", + "terminal_size", ] [[package]] @@ -383,7 +393,8 @@ dependencies = [ "anyhow", "chrono", "clap", - "colored", + "codspeed-divan-compat", + "colored 2.2.0", "fastembed", "regex", "rusqlite", @@ -394,6 +405,66 @@ dependencies = [ "uuid", ] +[[package]] +name = "codspeed" +version = "5.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0379da47d0de730b2f7900e3f2d9c3b5efc21b46875282305300b4851f03e70" +dependencies = [ + "anyhow", + "cc", + "colored 3.1.1", + "getrandom 0.4.3", + "glob", + "libc", + "nix", + "serde", + "serde_json", + "statrs", +] + +[[package]] +name = "codspeed-divan-compat" +version = "5.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fb193c95eb4f7e393e022c22b174c91a1fa5041b707cd07652dafc921a29ce" +dependencies = [ + "clap", + "codspeed", + "codspeed-divan-compat-macros", + "codspeed-divan-compat-walltime", + "regex", +] + +[[package]] +name = "codspeed-divan-compat-macros" +version = "5.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e522ba05328cbe756ce5165e50863c149417dfc4ecb80c8d8c037a28cdd4a3d1" +dependencies = [ + "divan-macros", + "itertools", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "codspeed-divan-compat-walltime" +version = "5.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8edb055b0842673f227fd0d80a1c0ef67b8575285d135f7ee1b8c4f616dbc07f" +dependencies = [ + "cfg-if", + "clap", + "codspeed", + "condtype", + "divan-macros", + "libc", + "regex-lite", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -416,6 +487,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "compact_str" version = "0.9.1" @@ -431,6 +511,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "condtype" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af" + [[package]] name = "console" version = "0.15.11" @@ -635,6 +721,17 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "divan-macros" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc51d98e636f5e3b0759a39257458b22619cac7e96d932da6eeb052891bb67c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "either" version = "1.17.0" @@ -667,6 +764,12 @@ dependencies = [ "syn 2.0.119", ] +[[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" @@ -910,6 +1013,12 @@ dependencies = [ "weezl", ] +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + [[package]] name = "half" version = "2.7.1" @@ -930,13 +1039,19 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "hashlink" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -1236,6 +1351,16 @@ version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7" +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + [[package]] name = "indicatif" version = "0.17.11" @@ -1526,6 +1651,18 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "no_std_io2" version = "0.9.4" @@ -1795,6 +1932,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -2144,6 +2290,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + [[package]] name = "regex-syntax" version = "0.8.11" @@ -2452,6 +2604,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "statrs" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e" +dependencies = [ + "approx", + "num-traits", +] + [[package]] name = "strsim" version = "0.11.1" @@ -2530,6 +2692,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "thiserror" version = "2.0.20" @@ -2673,6 +2845,36 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.15+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + [[package]] name = "tower" version = "0.5.3" @@ -3248,6 +3450,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.57.1" diff --git a/Cargo.toml b/Cargo.toml index fa0abce..adfd493 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,14 @@ description = "Bagimsiz Rust CLI: Claude Code ortam denetimi, dinamik MCP yoneti license = "MIT" repository = "https://github.com/Ercaner1988/claude-code-setup-rustified" +[lib] +name = "claude_code_setup" +path = "src/lib.rs" + +[[bin]] +name = "claude-code-setup" +path = "src/main.rs" + [dependencies] clap = { version = "4.5", features = ["derive"] } anyhow = "1.0" @@ -21,4 +29,21 @@ tokio = { version = "1", features = ["full"] } uuid = { version = "1.6", features = ["v4", "serde"] } [dev-dependencies] +divan = { version = "5.0.2", package = "codspeed-divan-compat" } tempfile = "3.8" + +[[bench]] +name = "memory_engine" +harness = false + +[[bench]] +name = "memory_search" +harness = false + +[[bench]] +name = "security" +harness = false + +[[bench]] +name = "mcp_protocol" +harness = false diff --git a/README.en.md b/README.en.md index 8704f51..4063ebf 100644 --- a/README.en.md +++ b/README.en.md @@ -5,6 +5,7 @@ [![Rust](https://img.shields.io/badge/Rust%20core-%2591-orange.svg)](https://www.rust-lang.org/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Tests](https://img.shields.io/badge/Tests-30%20Passed-green.svg)]() +[![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://app.codspeed.io/Ercaner1988/claude-code-setup-rustified?utm_source=badge) A local deployment, security auditing, and memory engine (`claude-code-setup`) for managing the **Claude Code** environment. The runtime is a single Rust binary; running it requires no Rust, Python, or Node installation on the host machine. diff --git a/README.md b/README.md index bf04530..ce11667 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![Rust](https://img.shields.io/badge/Rust%20çekirdek-%2591-orange.svg)](https://www.rust-lang.org/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Tests](https://img.shields.io/badge/Tests-30%20Passed-green.svg)]() +[![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://app.codspeed.io/Ercaner1988/claude-code-setup-rustified?utm_source=badge) **Claude Code** ortamını yönetmek için geliştirilmiş yerel dağıtım, güvenlik denetimi ve hafıza motoru (`claude-code-setup`). Çalışma zamanı tek bir Rust ikili dosyasıdır; kullanmak için makinede Rust, Python veya Node kurulu olması gerekmez. diff --git a/benches/mcp_protocol.rs b/benches/mcp_protocol.rs new file mode 100644 index 0000000..4884104 --- /dev/null +++ b/benches/mcp_protocol.rs @@ -0,0 +1,83 @@ +//! MCP sunucusunun JSON-RPC sicak yolu: istek ayristirma, yonlendirme ve +//! yanit serilestirme (handle_request) ile arac -> CLI arguman cevrimi. + +use claude_code_setup::mcp_server::{handle_request, tool_to_cli_args, JsonRpcRequest}; +use serde_json::json; + +fn main() { + divan::main(); +} + +const INITIALIZE: &str = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#; +const TOOLS_LIST: &str = r#"{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}"#; +const RESOURCES_LIST: &str = r#"{"jsonrpc":"2.0","id":3,"method":"resources/list","params":{}}"#; +const UNKNOWN_METHOD: &str = r#"{"jsonrpc":"2.0","id":4,"method":"prompts/list","params":{}}"#; + +fn parse(line: &str) -> JsonRpcRequest { + serde_json::from_str(line).expect("valid json-rpc request") +} + +mod dispatch { + use super::*; + + /// Sunucu yeteneklerini dondurur (her oturumun ilk cagrisi). + #[divan::bench] + fn initialize(bencher: divan::Bencher) { + let request = parse(INITIALIZE); + bencher.bench(|| handle_request(divan::black_box(&request))); + } + + /// En buyuk yanit: tum araclarin JSON Schema'lari serilestirilir. + #[divan::bench] + fn tools_list(bencher: divan::Bencher) { + let request = parse(TOOLS_LIST); + bencher.bench(|| handle_request(divan::black_box(&request))); + } + + #[divan::bench] + fn resources_list(bencher: divan::Bencher) { + let request = parse(RESOURCES_LIST); + bencher.bench(|| handle_request(divan::black_box(&request))); + } + + /// Bilinmeyen metot -> JSON-RPC hata yolu. + #[divan::bench] + fn unknown_method(bencher: divan::Bencher) { + let request = parse(UNKNOWN_METHOD); + bencher.bench(|| handle_request(divan::black_box(&request))); + } +} + +mod parsing { + use super::*; + + /// stdin satirindan JsonRpcRequest ayristirma. + #[divan::bench] + fn parse_request(bencher: divan::Bencher) { + bencher.bench(|| parse(divan::black_box(TOOLS_LIST))); + } +} + +mod tool_args { + use super::*; + + #[divan::bench] + fn mcp_add(bencher: divan::Bencher) { + let args = json!({ + "target": "claude-code", + "name": "filesystem", + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/kullanici"] + }); + bencher.bench(|| tool_to_cli_args(divan::black_box("mcp_add"), divan::black_box(&args))); + } + + #[divan::bench] + fn memory_note(bencher: divan::Bencher) { + let args = json!({ + "text": "Olcum notu\n\nHafiza motoru FTS5 ve gomme vektorlerini birlikte kullanir." + }); + bencher + .bench(|| tool_to_cli_args(divan::black_box("memory_note"), divan::black_box(&args))); + } +} diff --git a/benches/memory_engine.rs b/benches/memory_engine.rs new file mode 100644 index 0000000..8cc7fa1 --- /dev/null +++ b/benches/memory_engine.rs @@ -0,0 +1,152 @@ +//! Hafiza motorunun sicak yollari: gomme (embedding) matematigi, blob +//! donusumleri, icerik parcalama ve markdown/FTS5 metin isleme. + +use claude_code_setup::memory_engine::{ + bytes_to_f32_vec, chunk_content, cosine_similarity, escape_fts5_query, extract_wikilinks, + f32_vec_to_bytes, mean_pool_embeddings, +}; + +fn main() { + divan::main(); +} + +/// MultilingualE5Small gomme boyutu. +const EMBEDDING_DIM: usize = 384; + +/// Deterministik, bagimliliksiz sozde-rastgele uretici (xorshift64*). +struct Rng(u64); + +impl Rng { + fn new(seed: u64) -> Self { + Self(seed | 1) + } + + fn next_f32(&mut self) -> f32 { + self.0 ^= self.0 >> 12; + self.0 ^= self.0 << 25; + self.0 ^= self.0 >> 27; + let bits = self.0.wrapping_mul(0x2545_F491_4F6C_DD1D) >> 40; + (bits as f32 / (1u32 << 24) as f32) * 2.0 - 1.0 + } +} + +fn random_embedding(seed: u64, dim: usize) -> Vec { + let mut rng = Rng::new(seed); + (0..dim).map(|_| rng.next_f32()).collect() +} + +/// Gercekci bir bilgi notu: markdown basliklari, wikilink'ler ve Turkce metin. +fn knowledge_note(paragraphs: usize) -> String { + let mut out = String::from("# Bilgi Notu\n\n"); + for i in 0..paragraphs { + out.push_str(&format!("## Bolum {i}\n\n")); + out.push_str( + "Claude Code ortam denetimi ve MCP yonetimi icin notlar. \ + Semantik arama gomme vektorleri uzerinde kosinus benzerligi kullanir. \ + Ayrintilar icin [[mcp-yonetimi]] ve [[hafiza-motoru.md]] notlarina bakin.\n\n", + ); + out.push_str( + "- FTS5 anahtar kelime aramasi\n- Graf kenarlari ile iliskili notlar\n\ + - Guvenlik denetimi ve gizli anahtar taramasi\n\n", + ); + } + out +} + +mod similarity { + use super::*; + + /// Tek bir not ile sorgu vektorunun karsilastirilmasi. + #[divan::bench] + fn cosine_single(bencher: divan::Bencher) { + let query = random_embedding(1, EMBEDDING_DIM); + let note = random_embedding(2, EMBEDDING_DIM); + bencher.bench(|| cosine_similarity(divan::black_box(&query), divan::black_box(¬e))); + } + + /// Dogrusal tarama: sorguyu tum notlarla karsilastirip en iyileri siralar + /// (search_semantic_vec'in sicak dongusu). + #[divan::bench(args = [64, 512, 4096])] + fn cosine_linear_scan(bencher: divan::Bencher, note_count: usize) { + let query = random_embedding(7, EMBEDDING_DIM); + let notes: Vec> = (0..note_count) + .map(|i| random_embedding(i as u64 + 100, EMBEDDING_DIM)) + .collect(); + + bencher.bench(|| { + let mut scored: Vec = divan::black_box(¬es) + .iter() + .map(|note| cosine_similarity(&query, note) as f64) + .collect(); + scored.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); + scored.truncate(10); + scored + }); + } + + /// Parca gommelerinin ortalamasi (uzun notlar icin mean-pool). + #[divan::bench(args = [4, 32, 256])] + fn mean_pool(bencher: divan::Bencher, chunk_count: usize) { + let embeddings: Vec> = (0..chunk_count) + .map(|i| random_embedding(i as u64 + 5, EMBEDDING_DIM)) + .collect(); + bencher.bench(|| mean_pool_embeddings(divan::black_box(&embeddings))); + } +} + +mod blob_codec { + use super::*; + + /// f32 vektorunu SQLite BLOB'una yaz. + #[divan::bench] + fn encode(bencher: divan::Bencher) { + let embedding = random_embedding(11, EMBEDDING_DIM); + bencher.bench(|| f32_vec_to_bytes(divan::black_box(&embedding))); + } + + /// SQLite BLOB'undan f32 vektorunu oku. + #[divan::bench] + fn decode(bencher: divan::Bencher) { + let bytes = f32_vec_to_bytes(&random_embedding(13, EMBEDDING_DIM)); + bencher.bench(|| bytes_to_f32_vec(divan::black_box(&bytes))); + } + + /// 1000 notluk bir indeksin tamaminin cozulmesi. + #[divan::bench] + fn decode_index(bencher: divan::Bencher) { + let blobs: Vec> = (0..1000) + .map(|i| f32_vec_to_bytes(&random_embedding(i as u64 + 21, EMBEDDING_DIM))) + .collect(); + bencher.bench(|| { + divan::black_box(&blobs) + .iter() + .map(|blob| bytes_to_f32_vec(blob)) + .collect::>() + }); + } +} + +mod text { + use super::*; + + /// Notu satir sinirinda ~512 karakterlik pencerelere bol. + #[divan::bench(args = [8, 64, 512])] + fn chunk(bencher: divan::Bencher, paragraphs: usize) { + let note = knowledge_note(paragraphs); + bencher.bench(|| chunk_content(divan::black_box(¬e), 512)); + } + + /// Wikilink cikarimi (graf kenarlarini besler). + #[divan::bench(args = [8, 64, 512])] + fn wikilinks(bencher: divan::Bencher, paragraphs: usize) { + let note = knowledge_note(paragraphs); + bencher.bench(|| extract_wikilinks(divan::black_box(¬e))); + } + + /// FTS5 sorgu kacirma. + #[divan::bench] + fn fts5_escape(bencher: divan::Bencher) { + let query = "mcp sunucu yapilandirmasi semantik arama gomme vektoru guvenlik denetimi"; + bencher.bench(|| escape_fts5_query(divan::black_box(query))); + } +} diff --git a/benches/memory_search.rs b/benches/memory_search.rs new file mode 100644 index 0000000..a9dc718 --- /dev/null +++ b/benches/memory_search.rs @@ -0,0 +1,214 @@ +//! SQLite destekli arama yollari: FTS5 anahtar kelime aramasi, semantik +//! siralama ve graf uzerinde 2 adimlik BFS. Veritabani bellekte kurulur, +//! boylece olcum disk I/O'suna bagli olmaz. + +use std::cell::RefCell; +use std::collections::{HashSet, VecDeque}; + +use claude_code_setup::memory_engine::{ + bytes_to_f32_vec, cosine_similarity, escape_fts5_query, f32_vec_to_bytes, init_db, +}; +use rusqlite::{params, Connection}; + +fn main() { + divan::main(); +} + +const EMBEDDING_DIM: usize = 384; +const NOTE_COUNT: usize = 500; + +struct Rng(u64); + +impl Rng { + fn new(seed: u64) -> Self { + Self(seed | 1) + } + + fn next_u64(&mut self) -> u64 { + self.0 ^= self.0 >> 12; + self.0 ^= self.0 << 25; + self.0 ^= self.0 >> 27; + self.0.wrapping_mul(0x2545_F491_4F6C_DD1D) + } + + fn next_f32(&mut self) -> f32 { + ((self.next_u64() >> 40) as f32 / (1u32 << 24) as f32) * 2.0 - 1.0 + } +} + +const TOPICS: &[&str] = &[ + "mcp sunucu yapilandirmasi", + "semantik arama gomme vektoru", + "guvenlik denetimi gizli anahtar", + "graf hafiza kenarlari", + "claude code ortam denetimi", +]; + +/// NOTE_COUNT adet sentetik notu FTS5 tablosu, gomme blob'lari ve graf +/// kenarlari ile birlikte bellekteki bir veritabanina yazar. +fn seeded_db() -> Connection { + let conn = Connection::open_in_memory().expect("in-memory sqlite"); + init_db(&conn).expect("schema"); + + let mut rng = Rng::new(0x5EED); + for i in 0..NOTE_COUNT { + let filename = format!("not-{i:04}.md"); + let topic = TOPICS[i % TOPICS.len()]; + let title = format!("Not {i} - {topic}"); + let content = format!( + "# {title}\n\n{topic} hakkinda ayrintili notlar. \ + Iliskili notlar: [[not-{:04}]] ve [[not-{:04}]].\n", + (i + 1) % NOTE_COUNT, + (i + 7) % NOTE_COUNT + ); + let embedding: Vec = (0..EMBEDDING_DIM).map(|_| rng.next_f32()).collect(); + + conn.execute( + "INSERT INTO knowledge_notes (filename, title, content, embedding) VALUES (?1, ?2, ?3, ?4)", + params![filename, title, content, f32_vec_to_bytes(&embedding)], + ) + .expect("insert note"); + conn.execute( + "INSERT INTO knowledge_fts (filename, title, content) VALUES (?1, ?2, ?3)", + params![filename, title, content], + ) + .expect("insert fts"); + + for offset in [1usize, 7] { + conn.execute( + "INSERT OR IGNORE INTO note_edges (src, dst, tur, agirlik) VALUES (?1, ?2, ?3, ?4)", + params![ + filename, + format!("not-{:04}.md", (i + offset) % NOTE_COUNT), + "wikilink", + 1.0_f64 + ], + ) + .expect("insert edge"); + } + } + + conn +} + +thread_local! { + static DB: RefCell> = const { RefCell::new(None) }; +} + +/// Tohumlanmis veritabanini yalnizca bir kez kurar, sonra yeniden kullanir. +fn with_db(f: impl FnOnce(&Connection) -> T) -> T { + DB.with(|cell| { + let mut slot = cell.borrow_mut(); + if slot.is_none() { + *slot = Some(seeded_db()); + } + f(slot.as_ref().expect("db")) + }) +} + +/// FTS5 anahtar kelime aramasi (search_keyword_vec ile ayni sorgu). +#[divan::bench] +fn keyword_search(bencher: divan::Bencher) { + with_db(|conn| { + bencher.bench_local(|| { + let escaped = escape_fts5_query(divan::black_box("semantik arama gomme")); + let mut stmt = conn + .prepare( + "SELECT filename, title, rank FROM knowledge_fts \ + WHERE knowledge_fts MATCH ?1 ORDER BY rank", + ) + .expect("prepare"); + let rows = stmt + .query_map(params![escaped], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, f64>(2)?, + )) + }) + .expect("query"); + let mut out = Vec::new(); + for row in rows { + out.push(row.expect("row")); + if out.len() >= 10 { + break; + } + } + out + }); + }); +} + +/// Semantik siralama: tum gommeleri oku, kosinus ile puanla, en iyi 10'u al +/// (search_semantic_vec'in model cagrisi olmadan olculen kismi). +#[divan::bench] +fn semantic_ranking(bencher: divan::Bencher) { + let mut rng = Rng::new(0xC0FFEE); + let query: Vec = (0..EMBEDDING_DIM).map(|_| rng.next_f32()).collect(); + + with_db(|conn| { + bencher.bench_local(|| { + let mut stmt = conn + .prepare("SELECT filename, title, embedding FROM knowledge_notes") + .expect("prepare"); + let rows = stmt + .query_map([], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, Option>>(2)?, + )) + }) + .expect("query"); + + let mut scored: Vec<(String, f64)> = Vec::new(); + for row in rows { + let (filename, _title, blob) = row.expect("row"); + if let Some(blob) = blob { + let embedding = bytes_to_f32_vec(&blob); + scored.push(( + filename, + cosine_similarity(divan::black_box(&query), &embedding) as f64, + )); + } + } + scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); + scored.truncate(10); + scored + }); + }); +} + +/// Graf uzerinde 2 adimlik BFS (get_related_notes'un gezinme mantigi). +#[divan::bench] +fn graph_related_notes(bencher: divan::Bencher) { + with_db(|conn| { + bencher.bench_local(|| { + let start = divan::black_box("not-0042.md").to_string(); + let mut visited = HashSet::new(); + let mut queue = VecDeque::new(); + queue.push_back((start.clone(), 0usize)); + visited.insert(start); + + let mut stmt = conn + .prepare("SELECT dst, tur, agirlik FROM note_edges WHERE src = ?1") + .expect("prepare"); + + while let Some((curr, dist)) = queue.pop_front() { + if dist >= 2 { + continue; + } + let rows = stmt + .query_map(params![curr], |row| row.get::<_, String>(0)) + .expect("query"); + for row in rows { + let dst = row.expect("row"); + if visited.insert(dst.clone()) { + queue.push_back((dst, dist + 1)); + } + } + } + visited.len() + }); + }); +} diff --git a/benches/security.rs b/benches/security.rs new file mode 100644 index 0000000..524824c --- /dev/null +++ b/benches/security.rs @@ -0,0 +1,57 @@ +//! Guvenlik denetiminin sicak yollari: gizli anahtar deseni taramasi ve +//! dal adi normalizasyonu. + +use claude_code_setup::branch_manager::{is_protected_branch, sanitize_description}; +use claude_code_setup::security::find_secrets; + +fn main() { + divan::main(); +} + +/// Gizli anahtar icermeyen, gercekci bir MCP yapilandirmasi. +fn clean_config(entries: usize) -> String { + let mut out = String::from("{\n \"mcpServers\": {\n"); + for i in 0..entries { + out.push_str(&format!( + " \"sunucu-{i}\": {{ \"command\": \"npx\", \ + \"args\": [\"-y\", \"@modelcontextprotocol/server-filesystem\", \"/home/kullanici\"], \ + \"env\": {{ \"LOG_LEVEL\": \"info\" }} }},\n" + )); + } + out.push_str(" }\n}\n"); + out +} + +/// Icine bilinen desenlerden (gercek olmayan) gizli anahtarlar serpistirilmis +/// yapilandirma — src/security.rs testlerindeki ayni sahte degerler. +fn config_with_secrets(entries: usize) -> String { + let mut out = clean_config(entries); + out.push_str(" token = \"ghp_abcdefghij1234567890abcd\"\n"); + out.push_str(" other = \"sk-proj-abcdef1234567890\"\n"); + out.push_str(" slack = \"xoxb-1234567890-abcdefghij\"\n"); + out.push_str(" aws = \"AKIAIOSFODNN7EXAMPLE\"\n"); + out +} + +#[divan::bench(args = [8, 64, 256])] +fn scan_clean_config(bencher: divan::Bencher, entries: usize) { + let text = clean_config(entries); + bencher.bench(|| find_secrets(divan::black_box(&text))); +} + +#[divan::bench(args = [8, 64, 256])] +fn scan_config_with_secrets(bencher: divan::Bencher, entries: usize) { + let text = config_with_secrets(entries); + bencher.bench(|| find_secrets(divan::black_box(&text))); +} + +#[divan::bench] +fn sanitize_branch_description(bencher: divan::Bencher) { + let description = "Fix Login Bug! (Turkce karakterler: sifre dogrulama & oturum yenileme)"; + bencher.bench(|| sanitize_description(divan::black_box(description))); +} + +#[divan::bench] +fn protected_branch_check(bencher: divan::Bencher) { + bencher.bench(|| is_protected_branch(divan::black_box("feature/olcum"))); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..dba2bc3 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,14 @@ +//! claude-code-setup kutuphane arayuzu. +//! +//! Ikili (binary) hedef ile testler/benchmark'larin ayni modulleri paylasmasi +//! icin moduller burada yayinlanir. Davranis degismez; sadece erisim acilir. + +pub mod agent; +pub mod branch_manager; +pub mod cli; +pub mod installer; +pub mod mcp; +pub mod mcp_server; +pub mod memory_engine; +pub mod security; +pub mod tester; diff --git a/src/main.rs b/src/main.rs index 72da6ff..c9ac05d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,7 @@ -mod agent; -mod branch_manager; -mod cli; -mod installer; -mod mcp; -mod mcp_server; -mod memory_engine; -mod security; -mod tester; - use anyhow::Result; use clap::{CommandFactory, Parser}; -use cli::{Cli, Commands}; +use claude_code_setup::cli::{Cli, Commands}; +use claude_code_setup::{agent, installer, mcp, mcp_server, memory_engine, security, tester}; fn main() -> Result<()> { let cli = Cli::parse(); diff --git a/src/mcp_server.rs b/src/mcp_server.rs index 82286e8..3775ec3 100644 --- a/src/mcp_server.rs +++ b/src/mcp_server.rs @@ -43,7 +43,7 @@ pub fn run_mcp_mode() -> anyhow::Result<()> { Ok(()) } -fn handle_request(req: &JsonRpcRequest) -> String { +pub fn handle_request(req: &JsonRpcRequest) -> String { let response = match req.method.as_str() { "initialize" => initialize(&req.id), "tools/list" => list_tools(&req.id), @@ -228,7 +228,7 @@ fn run_cli(args: &[String]) -> (String, bool) { } /// Arac cagrisini CLI argumanlarina cevirir. -fn tool_to_cli_args(tool: &str, args: &serde_json::Value) -> Result, String> { +pub fn tool_to_cli_args(tool: &str, args: &serde_json::Value) -> Result, String> { let text_of = |key: &str| { args.get(key) .and_then(|v| v.as_str()) diff --git a/src/memory_engine.rs b/src/memory_engine.rs index f9206cc..d97593b 100644 --- a/src/memory_engine.rs +++ b/src/memory_engine.rs @@ -58,7 +58,7 @@ pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 { } } -fn bytes_to_f32_vec(bytes: &[u8]) -> Vec { +pub fn bytes_to_f32_vec(bytes: &[u8]) -> Vec { bytes .as_chunks::<4>() .0 @@ -67,7 +67,7 @@ fn bytes_to_f32_vec(bytes: &[u8]) -> Vec { .collect() } -fn f32_vec_to_bytes(vec: &[f32]) -> Vec { +pub fn f32_vec_to_bytes(vec: &[f32]) -> Vec { let mut bytes = Vec::with_capacity(vec.len() * 4); for &val in vec { bytes.extend_from_slice(&val.to_le_bytes()); @@ -91,7 +91,7 @@ pub fn extract_wikilinks(content: &str) -> Vec { } /// İçeriği ~chunk_size karakterlik pencerelere böl (satır sınırında) -fn chunk_content(content: &str, chunk_size: usize) -> Vec { +pub fn chunk_content(content: &str, chunk_size: usize) -> Vec { if content.len() <= chunk_size { return vec![content.to_string()]; } @@ -119,7 +119,7 @@ fn chunk_content(content: &str, chunk_size: usize) -> Vec { } /// Chunk embedding'lerinin ortalamasını al (mean-pool) -fn mean_pool_embeddings(embeddings: &[Vec]) -> Vec { +pub fn mean_pool_embeddings(embeddings: &[Vec]) -> Vec { if embeddings.is_empty() { return Vec::new(); } @@ -138,7 +138,7 @@ fn mean_pool_embeddings(embeddings: &[Vec]) -> Vec { } /// FTS5 sorgusu için güvenli kaçırma: her sözcüğü çift tırnağa al -fn escape_fts5_query(query: &str) -> String { +pub fn escape_fts5_query(query: &str) -> String { query .split_whitespace() .map(|word| format!("\"{}\"", word))