diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a8001d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +.spy-code/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..9c3c3e1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2328 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +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", +] + +[[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", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "ascii_utils" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" + +[[package]] +name = "async-graphql" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1057a9f7ccf2404d94571dec3451ade1cb524790df6f1ada0d19c2a49f6b0f40" +dependencies = [ + "async-graphql-derive", + "async-graphql-parser", + "async-graphql-value", + "async-io", + "async-trait", + "asynk-strim", + "base64", + "bytes", + "chrono", + "fast_chemail", + "fnv", + "futures-util", + "handlebars", + "http", + "indexmap", + "mime", + "multer", + "num-traits", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "static_assertions_next", + "tempfile", + "thiserror 2.0.18", +] + +[[package]] +name = "async-graphql-axum" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e37c5532e4b686acf45e7162bc93da91fc2c702fb0d465efc2c20c8f973795" +dependencies = [ + "async-graphql", + "axum", + "bytes", + "futures-util", + "serde_json", + "tokio", + "tokio-stream", + "tokio-util", + "tower-service", +] + +[[package]] +name = "async-graphql-derive" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e6cbeadc8515e66450fba0985ce722192e28443697799988265d86304d7cc68" +dependencies = [ + "Inflector", + "async-graphql-parser", + "darling 0.23.0", + "proc-macro-crate", + "proc-macro2", + "quote", + "strum", + "syn", + "thiserror 2.0.18", +] + +[[package]] +name = "async-graphql-parser" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64ef70f77a1c689111e52076da1cd18f91834bcb847de0a9171f83624b07fbf" +dependencies = [ + "async-graphql-value", + "pest", + "serde", + "serde_json", +] + +[[package]] +name = "async-graphql-value" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e3ef112905abea9dea592fc868a6873b10ebd3f983e83308f995d6284e9ba41" +dependencies = [ + "bytes", + "indexmap", + "serde", + "serde_json", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys", +] + +[[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 = "asynk-strim" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52697735bdaac441a29391a9e97102c74c6ef0f9b60a40cf109b1b404e29d2f6" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "base64", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "blake3" +version = "1.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + +[[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 = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +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 = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[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 = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "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", +] + +[[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", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fast_chemail" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" +dependencies = [ + "ascii_utils", +] + +[[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 = "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 = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[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.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "handlebars" +version = "6.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b3f9296c208515b87bd915a2f5d1163d4b3f863ba83337d7713cf478055948e" +dependencies = [ + "derive_builder", + "log", + "num-order", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[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 = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[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 = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "num-modular" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" + +[[package]] +name = "num-order" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" +dependencies = [ + "num-modular", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +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 = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[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", +] + +[[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", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[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.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[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", +] + +[[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 = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +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 = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spy-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-graphql", + "async-graphql-axum", + "axum", + "clap", + "serde", + "serde_json", + "spy-core", + "spy-graph", + "spy-indexer", + "spy-storage", + "tokio", + "tower-http", +] + +[[package]] +name = "spy-core" +version = "0.1.0" +dependencies = [ + "anyhow", + "serde", + "serde_json", + "thiserror 1.0.69", + "tree-sitter", +] + +[[package]] +name = "spy-git" +version = "0.1.0" +dependencies = [ + "anyhow", +] + +[[package]] +name = "spy-graph" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-graphql", + "async-graphql-axum", + "axum", + "serde", + "serde_json", + "spy-core", + "spy-storage", + "tokio", + "tower", + "tower-http", +] + +[[package]] +name = "spy-indexer" +version = "0.1.0" +dependencies = [ + "anyhow", + "blake3", + "spy-core", + "spy-parser", + "spy-resolvers", + "spy-storage", + "walkdir", +] + +[[package]] +name = "spy-mcp" +version = "0.1.0" +dependencies = [ + "anyhow", +] + +[[package]] +name = "spy-parser" +version = "0.1.0" +dependencies = [ + "anyhow", + "spy-core", + "tree-sitter", + "tree-sitter-rust", +] + +[[package]] +name = "spy-resolvers" +version = "0.1.0" +dependencies = [ + "anyhow", + "blake3", + "spy-core", + "spy-parser", + "tree-sitter", + "tree-sitter-rust", +] + +[[package]] +name = "spy-storage" +version = "0.1.0" +dependencies = [ + "anyhow", + "rusqlite", + "serde", + "serde_json", + "spy-core", + "thiserror 1.0.69", +] + +[[package]] +name = "static_assertions_next" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7beae5182595e9a8b683fa98c4317f956c9a2dec3b9716990d20023cc60c766" + +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[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" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +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", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "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.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "http", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tree-sitter" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5387dffa7ffc7d2dae12b50c6f7aab8ff79d6210147c6613561fc3d474c6f75" +dependencies = [ + "cc", + "regex", + "regex-syntax", + "streaming-iterator", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782" + +[[package]] +name = "tree-sitter-rust" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8ccb3e3a3495c8a943f6c3fd24c3804c471fd7f4f16087623c7fa4c0068e8a" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror 2.0.18", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[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 = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[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 = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..17ac2e3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,52 @@ +[workspace] +resolver = "2" +members = [ + "crates/spy-core", + "crates/spy-parser", + "crates/spy-resolvers", + "crates/spy-storage", + "crates/spy-graph", + "crates/spy-git", + "crates/spy-indexer", + "crates/spy-mcp", + "crates/spy-cli", +] + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "MIT" +repository = "https://github.com/yourusername/spy-code" + +[workspace.dependencies] +# Core types +spy-core = { path = "crates/spy-core" } +spy-parser = { path = "crates/spy-parser" } +spy-resolvers = { path = "crates/spy-resolvers" } +spy-storage = { path = "crates/spy-storage" } +spy-graph = { path = "crates/spy-graph" } +spy-git = { path = "crates/spy-git" } +spy-indexer = { path = "crates/spy-indexer" } +spy-mcp = { path = "crates/spy-mcp" } + +# External dependencies +anyhow = "1" +thiserror = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +blake3 = "1" +walkdir = "2" +glob = "0.3" +rusqlite = { version = "0.31", features = ["bundled"] } +tree-sitter = "0.24" +tree-sitter-rust = "0.23" +async-graphql = { version = "7", features = ["chrono"] } +async-graphql-axum = "7" +axum = "0.8" +tokio = { version = "1", features = ["full"] } +tower = "0.5" +tower-http = { version = "0.6", features = ["cors"] } +clap = { version = "4", features = ["derive"] } +chrono = "0.4" +tracing = "0.1" +tracing-subscriber = "0.3" diff --git a/crates/spy-cli/Cargo.toml b/crates/spy-cli/Cargo.toml new file mode 100644 index 0000000..236b722 --- /dev/null +++ b/crates/spy-cli/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "spy-cli" +version.workspace = true +edition.workspace = true +license.workspace = true + +[[bin]] +name = "spy-code" +path = "src/main.rs" + +[dependencies] +spy-core = { workspace = true } +spy-storage = { workspace = true } +spy-indexer = { workspace = true } +spy-graph = { workspace = true } +clap = { workspace = true } +anyhow = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +async-graphql = { workspace = true } +async-graphql-axum = { workspace = true } +axum = { workspace = true } +tower-http = { workspace = true } diff --git a/crates/spy-cli/src/main.rs b/crates/spy-cli/src/main.rs new file mode 100644 index 0000000..eff5c18 --- /dev/null +++ b/crates/spy-cli/src/main.rs @@ -0,0 +1,290 @@ +use anyhow::{Context, Result}; +use clap::{Parser, Subcommand}; +use spy_core::{Config, EdgeKind, NodeKind}; +use spy_indexer::Indexer; +use spy_storage::Storage; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; + +#[derive(Parser)] +#[command(name = "spy-code")] +#[command(version = "0.1.0")] +#[command(about = "GraphQL-style compiler for codebases", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + Init, + Index { + #[arg(long)] + full: bool, + #[arg(long, default_value = ".")] + path: PathBuf, + }, + Query { + query: String, + #[arg(long)] + json: bool, + }, + Get { + node_id: String, + }, + Search { + text: String, + #[arg(long)] + kind: Option, + }, + Callers { + node_id: String, + #[arg(long, default_value = "1")] + depth: i32, + }, + Callees { + node_id: String, + #[arg(long, default_value = "1")] + depth: i32, + }, + Changed { + git_ref: String, + }, + Stats, + Serve { + #[arg(long)] + mcp: bool, + #[arg(long)] + http: bool, + #[arg(long, default_value = "4000")] + port: u16, + }, +} + +#[tokio::main] +async fn main() -> Result<()> { + let cli = Cli::parse(); + + match cli.command { + Commands::Init => cmd_init()?, + Commands::Index { full, path } => cmd_index(full, path)?, + Commands::Query { query, json } => cmd_query(query, json).await?, + Commands::Get { node_id } => cmd_get(node_id).await?, + Commands::Search { text, kind } => cmd_search(text, kind).await?, + Commands::Callers { node_id, depth } => cmd_callers(node_id, depth).await?, + Commands::Callees { node_id, depth } => cmd_callees(node_id, depth).await?, + Commands::Changed { git_ref } => cmd_changed(git_ref).await?, + Commands::Stats => cmd_stats().await?, + Commands::Serve { mcp, http, port } => cmd_serve(mcp, http, port).await?, + } + + Ok(()) +} + +fn cmd_init() -> Result<()> { + let config = Config::default(); + let json = serde_json::to_string_pretty(&config)?; + std::fs::write("spy.config.json", json)?; + println!("Created spy.config.json"); + Ok(()) +} + +fn cmd_index(full: bool, path: PathBuf) -> Result<()> { + let config = load_config()?; + let storage = Storage::open(&config.db_path)?; + let mut indexer = Indexer::new(storage, config); + + println!("Indexing {} (full={})", path.display(), full); + let stats = indexer.index(&path, full)?; + + println!("Indexed {} files", stats.files_scanned); + println!(" Parsed: {}", stats.files_parsed); + println!(" Failed: {}", stats.files_failed); + println!(" Nodes: {}", stats.nodes_extracted); + println!(" Edges: {}", stats.edges_extracted); + + Ok(()) +} + +async fn cmd_query(query_str: String, json: bool) -> Result<()> { + let config = load_config()?; + let storage = Storage::open(&config.db_path)?; + let storage = Arc::new(Mutex::new(storage)); + + let schema = spy_graph::create_schema(storage); + let result = schema.execute(&query_str).await; + + if json { + println!("{}", serde_json::to_string_pretty(&result)?); + } else { + println!("{}", serde_json::to_string_pretty(&result)?); + } + + Ok(()) +} + +async fn cmd_get(node_id: String) -> Result<()> { + let config = load_config()?; + let storage = Storage::open(&config.db_path)?; + + if let Some(node) = storage.get_node(&node_id)? { + println!("Node: {}", node.name); + println!(" ID: {}", node.node_id); + println!(" Kind: {}", node.kind); + println!(" Language: {}", node.language); + println!(" File: {}:{}:{}", node.file_path, node.start_line, node.end_line); + if let Some(desc) = node.description { + println!(" Description: {}", desc); + } + if !node.signatures.is_empty() { + println!(" Signatures:"); + for sig in &node.signatures { + println!(" Params: {:?}", sig.params); + if let Some(ret) = &sig.returns { + println!(" Returns: {}", ret); + } + } + } + } else { + println!("Node not found: {}", node_id); + } + + Ok(()) +} + +async fn cmd_search(text: String, kind: Option) -> Result<()> { + let config = load_config()?; + let storage = Storage::open(&config.db_path)?; + + let results = storage.search_nodes(&text, 20)?; + + let results: Vec<_> = if let Some(kind_str) = kind { + let kind = match kind_str.as_str() { + "function" | "fn" => NodeKind::Function, + "class" => NodeKind::Class, + "constant" | "const" => NodeKind::Constant, + _ => anyhow::bail!("Invalid kind: {}", kind_str), + }; + results + .into_iter() + .filter(|(n, _)| n.kind == kind) + .collect() + } else { + results + }; + + println!("Found {} results:", results.len()); + for (node, score) in results { + println!(" {} ({}) - {} (score: {:.2})", node.node_id, node.kind, node.name, score); + } + + Ok(()) +} + +async fn cmd_callers(node_id: String, _depth: i32) -> Result<()> { + let config = load_config()?; + let storage = Storage::open(&config.db_path)?; + + let edges = storage.get_incoming_edges(&node_id, EdgeKind::Calls)?; + + println!("Callers of {}:", node_id); + for edge in edges { + println!(" {} (confidence: {:.2})", edge.from_id, edge.confidence); + } + + Ok(()) +} + +async fn cmd_callees(node_id: String, _depth: i32) -> Result<()> { + let config = load_config()?; + let storage = Storage::open(&config.db_path)?; + + let edges = storage.get_edges(&node_id, EdgeKind::Calls)?; + + println!("Callees of {}:", node_id); + for edge in edges { + println!(" {} (confidence: {:.2})", edge.to_id, edge.confidence); + } + + Ok(()) +} + +async fn cmd_changed(_git_ref: String) -> Result<()> { + println!("Changed since: Not implemented (git stub)"); + Ok(()) +} + +async fn cmd_stats() -> Result<()> { + let config = load_config()?; + let storage = Storage::open(&config.db_path)?; + + let stats = storage.get_stats()?; + + println!("Index Statistics:"); + println!(" Nodes: {}", stats.node_count); + println!(" Edges: {}", stats.edge_count); + println!(" Files: {}", stats.file_count); + if let Some(sha) = stats.last_git_sha { + println!(" Last Git SHA: {}", sha); + } + + Ok(()) +} + +async fn cmd_serve(mcp: bool, http: bool, port: u16) -> Result<()> { + if mcp { + println!("MCP server: Not implemented (stub)"); + return Ok(()); + } + + if http { + let config = load_config()?; + let storage = Storage::open(&config.db_path)?; + let storage = Arc::new(Mutex::new(storage)); + + let schema = spy_graph::create_schema(storage); + + use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; + use axum::{ + extract::State, + response::{Html, IntoResponse}, + routing::get, + Router, + }; + use tower_http::cors::CorsLayer; + + async fn graphql_handler( + State(schema): State, + req: GraphQLRequest, + ) -> GraphQLResponse { + schema.execute(req.into_inner()).await.into() + } + + async fn graphql_playground() -> impl IntoResponse { + Html(async_graphql::http::playground_source( + async_graphql::http::GraphQLPlaygroundConfig::new("/"), + )) + } + + let app = Router::new() + .route("/", get(graphql_playground).post(graphql_handler)) + .layer(CorsLayer::permissive()) + .with_state(schema); + + let addr = format!("127.0.0.1:{}", port); + println!("GraphQL server listening on http://{}", addr); + println!("Playground: http://{}/", addr); + + let listener = tokio::net::TcpListener::bind(&addr).await?; + axum::serve(listener, app).await?; + } + + Ok(()) +} + +fn load_config() -> Result { + let config_str = std::fs::read_to_string("spy.config.json") + .context("Failed to read spy.config.json. Run 'spy-code init' first.")?; + let config: Config = serde_json::from_str(&config_str)?; + Ok(config) +} diff --git a/crates/spy-core/Cargo.toml b/crates/spy-core/Cargo.toml new file mode 100644 index 0000000..fe70d60 --- /dev/null +++ b/crates/spy-core/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "spy-core" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +tree-sitter = { workspace = true } diff --git a/crates/spy-core/src/lib.rs b/crates/spy-core/src/lib.rs new file mode 100644 index 0000000..054c122 --- /dev/null +++ b/crates/spy-core/src/lib.rs @@ -0,0 +1,418 @@ +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::path::PathBuf; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum SpyError { + #[error("Invalid node ID: {0}")] + InvalidNodeId(String), + #[error("Node ID too long (max 512 chars): {0}")] + NodeIdTooLong(String), + #[error("Parse error: {0}")] + ParseError(String), + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("Other error: {0}")] + Other(String), +} + +pub type Result = std::result::Result; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct NodeId(String); + +impl NodeId { + pub fn new(dir: &str, file: &str, class: &str, symbol: &str) -> Result { + let dir = if dir.is_empty() { "_" } else { dir }; + let file = if file.is_empty() { "_" } else { file }; + let class = if class.is_empty() { "_" } else { class }; + let symbol = if symbol.is_empty() { "_" } else { symbol }; + + let id = format!("{}:{}:{}:{}", dir, file, class, symbol); + + if id.len() > 512 { + return Err(SpyError::NodeIdTooLong(id)); + } + + Ok(NodeId(id)) + } + + pub fn from_string(s: String) -> Result { + if s.len() > 512 { + return Err(SpyError::NodeIdTooLong(s)); + } + let parts: Vec<&str> = s.split(':').collect(); + if parts.len() != 4 { + return Err(SpyError::InvalidNodeId(s)); + } + Ok(NodeId(s)) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn parts(&self) -> (&str, &str, &str, &str) { + let parts: Vec<&str> = self.0.split(':').collect(); + (parts[0], parts[1], parts[2], parts[3]) + } +} + +impl fmt::Display for NodeId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for String { + fn from(id: NodeId) -> String { + id.0 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Language { + Rust, + Python, + TypeScript, + JavaScript, + Go, +} + +impl Language { + pub fn as_str(&self) -> &str { + match self { + Language::Rust => "rust", + Language::Python => "python", + Language::TypeScript => "typescript", + Language::JavaScript => "javascript", + Language::Go => "go", + } + } +} + +impl fmt::Display for Language { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum NodeKind { + Function, + Class, + Constant, +} + +impl NodeKind { + pub fn as_str(&self) -> &str { + match self { + NodeKind::Function => "function", + NodeKind::Class => "class", + NodeKind::Constant => "constant", + } + } +} + +impl fmt::Display for NodeKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum EdgeKind { + Calls, + Imports, + References, +} + +impl EdgeKind { + pub fn as_str(&self) -> &str { + match self { + EdgeKind::Calls => "calls", + EdgeKind::Imports => "imports", + EdgeKind::References => "references", + } + } + + pub fn table_name(&self) -> &str { + match self { + EdgeKind::Calls => "edges_calls", + EdgeKind::Imports => "edges_imports", + EdgeKind::References => "edges_references", + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Param { + pub name: String, + #[serde(rename = "type")] + pub type_: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Signature { + pub params: Vec, + pub returns: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Node { + pub node_id: NodeId, + pub kind: NodeKind, + pub name: String, + pub description: Option, + pub signatures: Vec, + pub language: Language, + pub file_path: String, + pub start_line: u32, + pub end_line: u32, + pub content_hash: String, + pub git_sha: Option, + pub renamed_from: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Edge { + pub from_id: NodeId, + pub to_id: NodeId, + pub kind: EdgeKind, + pub confidence: f64, +} + +pub trait Resolver: Send + Sync { + fn language(&self) -> Language; + fn extensions(&self) -> &[&str]; + fn extract_nodes(&self, ctx: &FileContext) -> anyhow::Result>; + fn extract_edges(&self, ctx: &FileContext, scope: &ProjectScope) -> anyhow::Result>; +} + +pub struct FileContext { + pub tree: tree_sitter::Tree, + pub source: Vec, + pub path: PathBuf, + pub language: Language, +} + +pub struct ProjectScope { + nodes: std::collections::HashMap, +} + +impl ProjectScope { + pub fn new() -> Self { + ProjectScope { + nodes: std::collections::HashMap::new(), + } + } + + pub fn add_node(&mut self, node: Node) { + self.nodes.insert(node.node_id.to_string(), node); + } + + pub fn get_node(&self, node_id: &str) -> Option<&Node> { + self.nodes.get(node_id) + } + + pub fn find_nodes_by_name(&self, name: &str) -> Vec<&Node> { + self.nodes + .values() + .filter(|n| n.name == name) + .collect() + } + + pub fn all_nodes(&self) -> impl Iterator { + self.nodes.values() + } +} + +impl Default for ProjectScope { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Config { + #[serde(default = "default_version")] + pub version: u32, + #[serde(default = "default_db_path")] + pub db_path: String, + #[serde(default)] + pub languages: LanguagesConfig, + #[serde(default)] + pub git: GitConfig, + #[serde(default)] + pub indexing: IndexingConfig, + #[serde(default)] + pub search: SearchConfig, +} + +fn default_version() -> u32 { 1 } +fn default_db_path() -> String { ".spy-code/graph.db".to_string() } + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(deny_unknown_fields)] +pub struct LanguagesConfig { + #[serde(default)] + pub rust: Option, + #[serde(default)] + pub python: Option, + #[serde(default)] + pub typescript: Option, + #[serde(default)] + pub go: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct LanguageConfig { + #[serde(default = "default_enabled")] + pub enabled: bool, + #[serde(default = "default_roots")] + pub roots: Vec, + #[serde(default)] + pub ignore: Vec, + #[serde(default = "default_resolver")] + pub resolver: String, + #[serde(default)] + pub tsconfig: Option, +} + +fn default_enabled() -> bool { true } +fn default_roots() -> Vec { vec!["./".to_string()] } +fn default_resolver() -> String { "builtin".to_string() } + +impl Default for LanguageConfig { + fn default() -> Self { + LanguageConfig { + enabled: true, + roots: vec!["./".to_string()], + ignore: vec![], + resolver: "builtin".to_string(), + tsconfig: None, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct GitConfig { + #[serde(default = "default_enabled")] + pub enabled: bool, + #[serde(default = "default_enabled")] + pub track_renames: bool, + #[serde(default)] + pub follow_symlinks: bool, +} + +impl Default for GitConfig { + fn default() -> Self { + GitConfig { + enabled: true, + track_renames: true, + follow_symlinks: false, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct IndexingConfig { + #[serde(default = "default_max_file_size")] + pub max_file_size_kb: u64, + #[serde(default = "default_parallelism")] + pub parallelism: ParallelismConfig, + #[serde(default)] + pub fail_fast: bool, +} + +fn default_max_file_size() -> u64 { 2048 } +fn default_parallelism() -> ParallelismConfig { ParallelismConfig::Auto } + +impl Default for IndexingConfig { + fn default() -> Self { + IndexingConfig { + max_file_size_kb: 2048, + parallelism: ParallelismConfig::Auto, + fail_fast: false, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ParallelismConfig { + Auto, + Threads(usize), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SearchConfig { + #[serde(default = "default_tokenizer")] + pub fts_tokenizer: String, +} + +fn default_tokenizer() -> String { "unicode61".to_string() } + +impl Default for SearchConfig { + fn default() -> Self { + SearchConfig { + fts_tokenizer: "unicode61".to_string(), + } + } +} + +impl Default for Config { + fn default() -> Self { + Config { + version: 1, + db_path: ".spy-code/graph.db".to_string(), + languages: LanguagesConfig::default(), + git: GitConfig::default(), + indexing: IndexingConfig::default(), + search: SearchConfig::default(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_node_id_format() { + let id = NodeId::new("src", "lib.rs", "_", "parse").unwrap(); + assert_eq!(id.as_str(), "src:lib.rs:_:parse"); + } + + #[test] + fn test_node_id_empty_to_underscore() { + let id = NodeId::new("", "lib.rs", "", "parse").unwrap(); + assert_eq!(id.as_str(), "_:lib.rs:_:parse"); + } + + #[test] + fn test_node_id_max_length() { + let long_name = "a".repeat(600); + let result = NodeId::new(&long_name, "b", "c", "d"); + assert!(result.is_err()); + } + + #[test] + fn test_node_id_parts() { + let id = NodeId::new("src/foo", "bar.rs", "Baz", "qux").unwrap(); + let (dir, file, class, symbol) = id.parts(); + assert_eq!(dir, "src/foo"); + assert_eq!(file, "bar.rs"); + assert_eq!(class, "Baz"); + assert_eq!(symbol, "qux"); + } +} diff --git a/crates/spy-git/Cargo.toml b/crates/spy-git/Cargo.toml new file mode 100644 index 0000000..f2b4542 --- /dev/null +++ b/crates/spy-git/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "spy-git" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow = { workspace = true } diff --git a/crates/spy-git/src/lib.rs b/crates/spy-git/src/lib.rs new file mode 100644 index 0000000..bc63b75 --- /dev/null +++ b/crates/spy-git/src/lib.rs @@ -0,0 +1,3 @@ +pub fn stub() { + println!("spy-git: stub implementation"); +} diff --git a/crates/spy-graph/Cargo.toml b/crates/spy-graph/Cargo.toml new file mode 100644 index 0000000..e619413 --- /dev/null +++ b/crates/spy-graph/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "spy-graph" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +spy-core = { workspace = true } +spy-storage = { workspace = true } +async-graphql = { workspace = true } +async-graphql-axum = { workspace = true } +axum = { workspace = true } +tokio = { workspace = true } +tower = { workspace = true } +tower-http = { workspace = true } +anyhow = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } diff --git a/crates/spy-graph/src/lib.rs b/crates/spy-graph/src/lib.rs new file mode 100644 index 0000000..d8df3ab --- /dev/null +++ b/crates/spy-graph/src/lib.rs @@ -0,0 +1,268 @@ +use async_graphql::{Context, EmptyMutation, EmptySubscription, Object, Schema, SimpleObject}; +use spy_core::{EdgeKind, NodeKind}; +use spy_storage::Storage; +use std::sync::{Arc, Mutex}; + +pub type SpySchema = Schema; + +pub fn create_schema(storage: Arc>) -> SpySchema { + Schema::build(QueryRoot, EmptyMutation, EmptySubscription) + .data(storage) + .finish() +} + +pub struct QueryRoot; + +#[Object] +impl QueryRoot { + async fn node(&self, ctx: &Context<'_>, id: String) -> async_graphql::Result> { + let storage = ctx.data::>>()?; + let storage = storage.lock().unwrap(); + + let node = storage.get_node(&id)?; + Ok(node.map(|n| n.into())) + } + + async fn search( + &self, + ctx: &Context<'_>, + query: String, + kind: Option, + limit: Option, + ) -> async_graphql::Result> { + let storage = ctx.data::>>()?; + let storage = storage.lock().unwrap(); + + let limit = limit.unwrap_or(20) as usize; + let results = storage.search_nodes(&query, limit)?; + + Ok(results + .into_iter() + .filter(|(node, _)| { + if let Some(ref k) = kind { + matches_kind(&node.kind, k) + } else { + true + } + }) + .map(|(node, score)| SearchResult { + node: node.into(), + score, + }) + .collect()) + } + + async fn callers( + &self, + ctx: &Context<'_>, + id: String, + depth: Option, + ) -> async_graphql::Result> { + let storage = ctx.data::>>()?; + let storage = storage.lock().unwrap(); + + let _depth = depth.unwrap_or(1); + let edges = storage.get_incoming_edges(&id, EdgeKind::Calls)?; + + Ok(edges.into_iter().map(|e| e.into()).collect()) + } + + async fn callees( + &self, + ctx: &Context<'_>, + id: String, + depth: Option, + ) -> async_graphql::Result> { + let storage = ctx.data::>>()?; + let storage = storage.lock().unwrap(); + + let _depth = depth.unwrap_or(1); + let edges = storage.get_edges(&id, EdgeKind::Calls)?; + + Ok(edges.into_iter().map(|e| e.into()).collect()) + } + + async fn stats(&self, ctx: &Context<'_>) -> async_graphql::Result { + let storage = ctx.data::>>()?; + let storage = storage.lock().unwrap(); + + let stats = storage.get_stats()?; + Ok(IndexStatsGQL { + node_count: stats.node_count as i32, + edge_count: stats.edge_count as i32, + file_count: stats.file_count as i32, + last_indexed: None, + last_git_sha: stats.last_git_sha, + }) + } + + async fn files(&self, _ctx: &Context<'_>) -> async_graphql::Result> { + Ok(vec![]) + } + + async fn changed_since( + &self, + _ctx: &Context<'_>, + _git_ref: String, + ) -> async_graphql::Result> { + Ok(vec![]) + } +} + +fn matches_kind(node_kind: &NodeKind, gql_kind: &NodeKindGQL) -> bool { + match (node_kind, gql_kind) { + (NodeKind::Function, NodeKindGQL::Function) => true, + (NodeKind::Class, NodeKindGQL::Class) => true, + (NodeKind::Constant, NodeKindGQL::Constant) => true, + _ => false, + } +} + +#[derive(async_graphql::Enum, Copy, Clone, Eq, PartialEq)] +pub enum NodeKindGQL { + Function, + Class, + Constant, +} + +#[derive(async_graphql::Enum, Copy, Clone, Eq, PartialEq)] +pub enum LanguageGQL { + Rust, + Python, + TypeScript, + JavaScript, + Go, +} + +impl From for LanguageGQL { + fn from(lang: spy_core::Language) -> Self { + match lang { + spy_core::Language::Rust => LanguageGQL::Rust, + spy_core::Language::Python => LanguageGQL::Python, + spy_core::Language::TypeScript => LanguageGQL::TypeScript, + spy_core::Language::JavaScript => LanguageGQL::JavaScript, + spy_core::Language::Go => LanguageGQL::Go, + } + } +} + +#[derive(async_graphql::Enum, Copy, Clone, Eq, PartialEq)] +pub enum EdgeKindGQL { + Calls, + Imports, + References, +} + +impl From for EdgeKindGQL { + fn from(kind: spy_core::EdgeKind) -> Self { + match kind { + spy_core::EdgeKind::Calls => EdgeKindGQL::Calls, + spy_core::EdgeKind::Imports => EdgeKindGQL::Imports, + spy_core::EdgeKind::References => EdgeKindGQL::References, + } + } +} + +#[derive(SimpleObject)] +pub struct Param { + name: String, + #[graphql(name = "type")] + type_: Option, +} + +impl From for Param { + fn from(p: spy_core::Param) -> Self { + Param { + name: p.name, + type_: p.type_, + } + } +} + +#[derive(SimpleObject)] +pub struct Signature { + params: Vec, + returns: Option, +} + +impl From for Signature { + fn from(s: spy_core::Signature) -> Self { + Signature { + params: s.params.into_iter().map(Into::into).collect(), + returns: s.returns, + } + } +} + +#[derive(SimpleObject)] +pub struct Node { + id: String, + kind: NodeKindGQL, + name: String, + description: Option, + signatures: Vec, + language: LanguageGQL, + file_path: String, + start_line: i32, + end_line: i32, + git_sha: Option, + renamed_from: Option, +} + +impl From for Node { + fn from(n: spy_core::Node) -> Self { + let kind = match n.kind { + NodeKind::Function => NodeKindGQL::Function, + NodeKind::Class => NodeKindGQL::Class, + NodeKind::Constant => NodeKindGQL::Constant, + }; + + Node { + id: n.node_id.to_string(), + kind, + name: n.name, + description: n.description, + signatures: n.signatures.into_iter().map(Into::into).collect(), + language: n.language.into(), + file_path: n.file_path, + start_line: n.start_line as i32, + end_line: n.end_line as i32, + git_sha: n.git_sha, + renamed_from: n.renamed_from.map(|id| id.to_string()), + } + } +} + +#[derive(SimpleObject)] +pub struct Edge { + from_id: String, + to_id: String, + kind: EdgeKindGQL, + confidence: f64, +} + +impl From for Edge { + fn from(e: spy_core::Edge) -> Self { + Edge { + from_id: e.from_id.to_string(), + to_id: e.to_id.to_string(), + kind: e.kind.into(), + confidence: e.confidence, + } + } +} + +#[derive(SimpleObject)] +pub struct SearchResult { + node: Node, + score: f64, +} + +#[derive(SimpleObject)] +pub struct IndexStatsGQL { + node_count: i32, + edge_count: i32, + file_count: i32, + last_indexed: Option, + last_git_sha: Option, +} diff --git a/crates/spy-indexer/Cargo.toml b/crates/spy-indexer/Cargo.toml new file mode 100644 index 0000000..574e845 --- /dev/null +++ b/crates/spy-indexer/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "spy-indexer" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +spy-core = { workspace = true } +spy-parser = { workspace = true } +spy-resolvers = { workspace = true } +spy-storage = { workspace = true } +anyhow = { workspace = true } +walkdir = { workspace = true } +blake3 = { workspace = true } diff --git a/crates/spy-indexer/src/lib.rs b/crates/spy-indexer/src/lib.rs new file mode 100644 index 0000000..6873b1a --- /dev/null +++ b/crates/spy-indexer/src/lib.rs @@ -0,0 +1,171 @@ +use anyhow::{Context, Result}; +use spy_core::{Config, Language, ProjectScope}; +use spy_storage::{FileRecord, Storage}; +use std::path::Path; +use walkdir::WalkDir; + +pub struct Indexer { + storage: Storage, + #[allow(dead_code)] + config: Config, +} + +impl Indexer { + pub fn new(storage: Storage, config: Config) -> Self { + Indexer { storage, config } + } + + pub fn index(&mut self, root_path: &Path, full: bool) -> Result { + let mut stats = IndexStats::default(); + let mut scope = ProjectScope::new(); + + let files = self.discover_files(root_path)?; + stats.files_scanned = files.len(); + + for file_path in &files { + if let Some(lang) = self.detect_language(file_path) { + let should_parse = if full { + true + } else { + self.should_reparse(file_path)? + }; + + if should_parse { + stats.files_parsed += 1; + + let source = std::fs::read(file_path)?; + let content_hash = compute_file_hash(&source); + + match self.parse_and_extract_nodes(file_path, source.clone(), lang) { + Ok(nodes) => { + for node in nodes { + scope.add_node(node.clone()); + self.storage.upsert_node(&node)?; + stats.nodes_extracted += 1; + } + + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_secs() as i64; + + self.storage.upsert_file(&FileRecord { + path: file_path.to_string_lossy().to_string(), + language: lang.as_str().to_string(), + content_hash, + last_indexed: now, + git_sha: None, + })?; + } + Err(e) => { + eprintln!("Failed to parse {}: {}", file_path.display(), e); + stats.files_failed += 1; + } + } + } + } + } + + for file_path in &files { + if let Some(lang) = self.detect_language(file_path) { + let source = std::fs::read(file_path)?; + match self.extract_edges(file_path, source, lang, &scope) { + Ok(edges) => { + for edge in edges { + self.storage.upsert_edge(&edge)?; + stats.edges_extracted += 1; + } + } + Err(e) => { + eprintln!("Failed to extract edges from {}: {}", file_path.display(), e); + } + } + } + } + + Ok(stats) + } + + fn discover_files(&self, root: &Path) -> Result> { + let mut files = Vec::new(); + + for entry in WalkDir::new(root).follow_links(false) { + let entry = entry?; + if entry.file_type().is_file() { + if let Some(ext) = entry.path().extension() { + if ext == "rs" { + files.push(entry.path().to_path_buf()); + } + } + } + } + + Ok(files) + } + + fn detect_language(&self, path: &Path) -> Option { + path.extension() + .and_then(|ext| ext.to_str()) + .and_then(|ext| match ext { + "rs" => Some(Language::Rust), + "py" => Some(Language::Python), + "ts" => Some(Language::TypeScript), + "js" => Some(Language::JavaScript), + "go" => Some(Language::Go), + _ => None, + }) + } + + fn should_reparse(&self, path: &Path) -> Result { + let source = std::fs::read(path)?; + let current_hash = compute_file_hash(&source); + + if let Some(file_record) = self.storage.get_file(&path.to_string_lossy())? { + Ok(file_record.content_hash != current_hash) + } else { + Ok(true) + } + } + + fn parse_and_extract_nodes( + &self, + path: &Path, + source: Vec, + lang: Language, + ) -> Result> { + let ctx = spy_parser::parse_file(path, source, lang)?; + + let resolver = spy_resolvers::get_resolver(lang) + .context("No resolver available for language")?; + + resolver.extract_nodes(&ctx) + } + + fn extract_edges( + &self, + path: &Path, + source: Vec, + lang: Language, + scope: &ProjectScope, + ) -> Result> { + let ctx = spy_parser::parse_file(path, source, lang)?; + + let resolver = spy_resolvers::get_resolver(lang) + .context("No resolver available for language")?; + + resolver.extract_edges(&ctx, scope) + } +} + +fn compute_file_hash(source: &[u8]) -> String { + let hash = blake3::hash(source); + hash.to_hex().to_string() +} + +#[derive(Debug, Default, Clone)] +pub struct IndexStats { + pub files_scanned: usize, + pub files_parsed: usize, + pub files_failed: usize, + pub nodes_extracted: usize, + pub edges_extracted: usize, +} diff --git a/crates/spy-mcp/Cargo.toml b/crates/spy-mcp/Cargo.toml new file mode 100644 index 0000000..cb9bc81 --- /dev/null +++ b/crates/spy-mcp/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "spy-mcp" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow = { workspace = true } diff --git a/crates/spy-mcp/src/lib.rs b/crates/spy-mcp/src/lib.rs new file mode 100644 index 0000000..df708be --- /dev/null +++ b/crates/spy-mcp/src/lib.rs @@ -0,0 +1,3 @@ +pub fn stub() { + println!("spy-mcp: stub implementation"); +} diff --git a/crates/spy-parser/Cargo.toml b/crates/spy-parser/Cargo.toml new file mode 100644 index 0000000..61e8100 --- /dev/null +++ b/crates/spy-parser/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "spy-parser" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +spy-core = { workspace = true } +tree-sitter = { workspace = true } +tree-sitter-rust = { workspace = true } +anyhow = { workspace = true } diff --git a/crates/spy-parser/src/lib.rs b/crates/spy-parser/src/lib.rs new file mode 100644 index 0000000..4fee625 --- /dev/null +++ b/crates/spy-parser/src/lib.rs @@ -0,0 +1,50 @@ +use anyhow::{Context, Result}; +use spy_core::{FileContext, Language}; +use std::path::Path; +use tree_sitter::Parser; + +pub fn parse_file(path: &Path, source: Vec, language: Language) -> Result { + let mut parser = Parser::new(); + + let ts_lang = match language { + Language::Rust => tree_sitter_rust::LANGUAGE.into(), + _ => anyhow::bail!("Unsupported language: {:?}", language), + }; + + parser.set_language(&ts_lang) + .context("Failed to set parser language")?; + + let tree = parser.parse(&source, None) + .context("Failed to parse source")?; + + Ok(FileContext { + tree, + source, + path: path.to_path_buf(), + language, + }) +} + +pub fn node_text<'a>(node: &tree_sitter::Node, source: &'a [u8]) -> &'a str { + node.utf8_text(source).unwrap_or("") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_rust() -> Result<()> { + let source = b"fn main() {}"; + let ctx = parse_file( + Path::new("test.rs"), + source.to_vec(), + Language::Rust, + )?; + + assert_eq!(ctx.language, Language::Rust); + assert!(ctx.tree.root_node().child_count() > 0); + + Ok(()) + } +} diff --git a/crates/spy-resolvers/Cargo.toml b/crates/spy-resolvers/Cargo.toml new file mode 100644 index 0000000..95c9585 --- /dev/null +++ b/crates/spy-resolvers/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "spy-resolvers" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +spy-core = { workspace = true } +tree-sitter = { workspace = true } +tree-sitter-rust = { workspace = true } +blake3 = { workspace = true } +anyhow = { workspace = true } + +[dev-dependencies] +spy-parser = { workspace = true } diff --git a/crates/spy-resolvers/src/lib.rs b/crates/spy-resolvers/src/lib.rs new file mode 100644 index 0000000..30a78ef --- /dev/null +++ b/crates/spy-resolvers/src/lib.rs @@ -0,0 +1,10 @@ +mod rust; + +pub use rust::RustResolver; + +pub fn get_resolver(lang: spy_core::Language) -> Option> { + match lang { + spy_core::Language::Rust => Some(Box::new(RustResolver)), + _ => None, + } +} diff --git a/crates/spy-resolvers/src/rust.rs b/crates/spy-resolvers/src/rust.rs new file mode 100644 index 0000000..3b51f27 --- /dev/null +++ b/crates/spy-resolvers/src/rust.rs @@ -0,0 +1,362 @@ +use anyhow::Result; +use spy_core::{ + Edge, EdgeKind, FileContext, Language, Node, NodeId, NodeKind, Param, ProjectScope, Resolver, + Signature, +}; +use tree_sitter::Node as TSNode; + +pub struct RustResolver; + +impl Resolver for RustResolver { + fn language(&self) -> Language { + Language::Rust + } + + fn extensions(&self) -> &[&str] { + &["rs"] + } + + fn extract_nodes(&self, ctx: &FileContext) -> Result> { + let mut nodes = Vec::new(); + let root = ctx.tree.root_node(); + + let dir = ctx + .path + .parent() + .and_then(|p| p.to_str()) + .unwrap_or("."); + let file = ctx.path.file_name().and_then(|f| f.to_str()).unwrap_or("_"); + + walk_nodes(&root, &ctx.source, dir, file, "_", &mut nodes, &ctx)?; + + Ok(nodes) + } + + fn extract_edges(&self, ctx: &FileContext, scope: &ProjectScope) -> Result> { + let mut edges = Vec::new(); + let root = ctx.tree.root_node(); + + walk_for_edges(&root, &ctx.source, ctx, scope, &mut edges)?; + + Ok(edges) + } +} + +fn walk_nodes( + node: &TSNode, + source: &[u8], + dir: &str, + file: &str, + parent_class: &str, + nodes: &mut Vec, + ctx: &FileContext, +) -> Result<()> { + match node.kind() { + "function_item" => { + if let Some(name_node) = node.child_by_field_name("name") { + let name = node_text(&name_node, source); + let description = extract_doc_comment(node, source); + let signatures = extract_function_signature(node, source); + let content_hash = compute_hash(node, source); + + let node_id = NodeId::new(dir, file, parent_class, name)?; + + nodes.push(Node { + node_id, + kind: NodeKind::Function, + name: name.to_string(), + description, + signatures: vec![signatures], + language: Language::Rust, + file_path: ctx.path.to_string_lossy().to_string(), + start_line: node.start_position().row as u32 + 1, + end_line: node.end_position().row as u32 + 1, + content_hash, + git_sha: None, + renamed_from: None, + }); + } + } + "struct_item" | "enum_item" | "trait_item" => { + if let Some(name_node) = node.child_by_field_name("name") { + let name = node_text(&name_node, source); + let description = extract_doc_comment(node, source); + let content_hash = compute_hash(node, source); + + let node_id = NodeId::new(dir, file, "_", name)?; + + nodes.push(Node { + node_id, + kind: NodeKind::Class, + name: name.to_string(), + description, + signatures: vec![], + language: Language::Rust, + file_path: ctx.path.to_string_lossy().to_string(), + start_line: node.start_position().row as u32 + 1, + end_line: node.end_position().row as u32 + 1, + content_hash, + git_sha: None, + renamed_from: None, + }); + } + } + "impl_item" => { + if let Some(type_node) = node.child_by_field_name("type") { + let type_name = node_text(&type_node, source); + let class_name = if let Some(trait_node) = node.child_by_field_name("trait") { + let trait_name = node_text(&trait_node, source); + format!("{}<{}>", type_name, trait_name) + } else { + type_name.to_string() + }; + + if let Some(body) = node.child_by_field_name("body") { + let mut cursor = body.walk(); + for child in body.children(&mut cursor) { + if child.kind() == "function_item" { + if let Some(name_node) = child.child_by_field_name("name") { + let name = node_text(&name_node, source); + let description = extract_doc_comment(&child, source); + let signatures = extract_function_signature(&child, source); + let content_hash = compute_hash(&child, source); + + let node_id = NodeId::new(dir, file, &class_name, name)?; + + nodes.push(Node { + node_id, + kind: NodeKind::Function, + name: name.to_string(), + description, + signatures: vec![signatures], + language: Language::Rust, + file_path: ctx.path.to_string_lossy().to_string(), + start_line: child.start_position().row as u32 + 1, + end_line: child.end_position().row as u32 + 1, + content_hash, + git_sha: None, + renamed_from: None, + }); + } + } + } + } + } + } + "const_item" | "static_item" => { + if let Some(name_node) = node.child_by_field_name("name") { + let name = node_text(&name_node, source); + let description = extract_doc_comment(node, source); + let content_hash = compute_hash(node, source); + + let node_id = NodeId::new(dir, file, "_", name)?; + + nodes.push(Node { + node_id, + kind: NodeKind::Constant, + name: name.to_string(), + description, + signatures: vec![], + language: Language::Rust, + file_path: ctx.path.to_string_lossy().to_string(), + start_line: node.start_position().row as u32 + 1, + end_line: node.end_position().row as u32 + 1, + content_hash, + git_sha: None, + renamed_from: None, + }); + } + } + _ => {} + } + + let mut cursor = node.walk(); + for child in node.children(&mut cursor) { + walk_nodes(&child, source, dir, file, parent_class, nodes, ctx)?; + } + + Ok(()) +} + +fn walk_for_edges( + node: &TSNode, + source: &[u8], + ctx: &FileContext, + scope: &ProjectScope, + edges: &mut Vec, +) -> Result<()> { + if node.kind() == "call_expression" { + if let Some(func_node) = node.child_by_field_name("function") { + let func_text = node_text(&func_node, source); + let from_id = infer_containing_function(node, source, ctx)?; + + if let Some(from_id) = from_id { + let candidates = scope.find_nodes_by_name(func_text); + if candidates.len() == 1 { + edges.push(Edge { + from_id, + to_id: candidates[0].node_id.clone(), + kind: EdgeKind::Calls, + confidence: 1.0, + }); + } else if !candidates.is_empty() { + edges.push(Edge { + from_id, + to_id: candidates[0].node_id.clone(), + kind: EdgeKind::Calls, + confidence: 0.4, + }); + } + } + } + } + + let mut cursor = node.walk(); + for child in node.children(&mut cursor) { + walk_for_edges(&child, source, ctx, scope, edges)?; + } + + Ok(()) +} + +fn infer_containing_function( + node: &TSNode, + source: &[u8], + ctx: &FileContext, +) -> Result> { + let mut current = node.parent(); + let dir = ctx + .path + .parent() + .and_then(|p| p.to_str()) + .unwrap_or("."); + let file = ctx.path.file_name().and_then(|f| f.to_str()).unwrap_or("_"); + + while let Some(parent) = current { + if parent.kind() == "function_item" { + if let Some(name_node) = parent.child_by_field_name("name") { + let name = node_text(&name_node, source); + return Ok(Some(NodeId::new(dir, file, "_", name)?)); + } + } else if parent.kind() == "impl_item" { + if let Some(type_node) = parent.child_by_field_name("type") { + let type_name = node_text(&type_node, source); + + if let Some(body) = parent.child_by_field_name("body") { + let mut func_parent = *node; + while let Some(p) = func_parent.parent() { + if p.kind() == "function_item" && p.parent().map(|pp| pp.id()) == Some(body.id()) { + if let Some(name_node) = p.child_by_field_name("name") { + let name = node_text(&name_node, source); + return Ok(Some(NodeId::new(dir, file, type_name, name)?)); + } + } + func_parent = p; + } + } + } + } + current = parent.parent(); + } + + Ok(None) +} + +fn extract_doc_comment(node: &TSNode, source: &[u8]) -> Option { + let mut comments = Vec::new(); + let start_row = node.start_position().row; + + if let Some(parent) = node.parent() { + let mut cursor = parent.walk(); + for sibling in parent.children(&mut cursor) { + if sibling.kind() == "line_comment" && sibling.end_position().row < start_row { + let text = node_text(&sibling, source); + if let Some(stripped) = text.strip_prefix("///") { + comments.push(stripped.trim().to_string()); + } + } + } + } + + if comments.is_empty() { + None + } else { + Some(comments.join(" ")) + } +} + +fn extract_function_signature(node: &TSNode, source: &[u8]) -> Signature { + let mut params = Vec::new(); + + if let Some(params_node) = node.child_by_field_name("parameters") { + let mut cursor = params_node.walk(); + for child in params_node.children(&mut cursor) { + if child.kind() == "parameter" { + let name = child + .child_by_field_name("pattern") + .map(|n| node_text(&n, source).to_string()) + .unwrap_or_else(|| "_".to_string()); + + let type_ = child + .child_by_field_name("type") + .map(|n| node_text(&n, source).to_string()); + + params.push(Param { name, type_ }); + } + } + } + + let returns = node + .child_by_field_name("return_type") + .map(|n| node_text(&n, source).to_string()); + + Signature { params, returns } +} + +fn compute_hash(node: &TSNode, source: &[u8]) -> String { + let start = node.start_byte(); + let end = node.end_byte(); + let slice = &source[start..end]; + let hash = blake3::hash(slice); + hash.to_hex().to_string() +} + +fn node_text<'a>(node: &TSNode, source: &'a [u8]) -> &'a str { + node.utf8_text(source).unwrap_or("") +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + + #[test] + fn test_extract_function() -> Result<()> { + let source = b"fn test() {}"; + let ctx = spy_parser::parse_file(Path::new("test.rs"), source.to_vec(), Language::Rust)?; + + let resolver = RustResolver; + let nodes = resolver.extract_nodes(&ctx)?; + + assert_eq!(nodes.len(), 1); + assert_eq!(nodes[0].name, "test"); + assert_eq!(nodes[0].kind, NodeKind::Function); + + Ok(()) + } + + #[test] + fn test_extract_struct() -> Result<()> { + let source = b"struct Foo {}"; + let ctx = spy_parser::parse_file(Path::new("test.rs"), source.to_vec(), Language::Rust)?; + + let resolver = RustResolver; + let nodes = resolver.extract_nodes(&ctx)?; + + assert_eq!(nodes.len(), 1); + assert_eq!(nodes[0].name, "Foo"); + assert_eq!(nodes[0].kind, NodeKind::Class); + + Ok(()) + } +} diff --git a/crates/spy-storage/Cargo.toml b/crates/spy-storage/Cargo.toml new file mode 100644 index 0000000..f98684b --- /dev/null +++ b/crates/spy-storage/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "spy-storage" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +spy-core = { workspace = true } +rusqlite = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +anyhow = { workspace = true } +thiserror = { workspace = true } diff --git a/crates/spy-storage/src/lib.rs b/crates/spy-storage/src/lib.rs new file mode 100644 index 0000000..584f617 --- /dev/null +++ b/crates/spy-storage/src/lib.rs @@ -0,0 +1,548 @@ +use anyhow::{Context, Result}; +use rusqlite::{params, Connection, OptionalExtension}; +use spy_core::{Edge, EdgeKind, Node, NodeId}; +use std::path::Path; + +pub struct Storage { + conn: Connection, +} + +impl Storage { + pub fn open>(path: P) -> Result { + let path = path.as_ref(); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + + let conn = Connection::open(path) + .context("Failed to open database")?; + + let mut storage = Storage { conn }; + storage.migrate()?; + Ok(storage) + } + + pub fn open_in_memory() -> Result { + let conn = Connection::open_in_memory()?; + let mut storage = Storage { conn }; + storage.migrate()?; + Ok(storage) + } + + fn migrate(&mut self) -> Result<()> { + self.conn.execute_batch( + r#" + CREATE TABLE IF NOT EXISTS nodes ( + node_id TEXT PRIMARY KEY, + kind TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT, + signatures TEXT NOT NULL, + language TEXT NOT NULL, + file_path TEXT NOT NULL, + start_line INTEGER NOT NULL, + end_line INTEGER NOT NULL, + content_hash TEXT NOT NULL, + git_sha TEXT, + renamed_from TEXT + ); + + CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name); + CREATE INDEX IF NOT EXISTS idx_nodes_file ON nodes(file_path); + CREATE INDEX IF NOT EXISTS idx_nodes_kind ON nodes(kind); + + CREATE TABLE IF NOT EXISTS edges_calls ( + from_id TEXT NOT NULL, + to_id TEXT NOT NULL, + confidence REAL NOT NULL DEFAULT 1.0, + PRIMARY KEY (from_id, to_id), + FOREIGN KEY (from_id) REFERENCES nodes(node_id) ON DELETE CASCADE + ); + + CREATE INDEX IF NOT EXISTS idx_calls_to ON edges_calls(to_id); + + CREATE TABLE IF NOT EXISTS edges_imports ( + from_id TEXT NOT NULL, + to_id TEXT NOT NULL, + confidence REAL NOT NULL DEFAULT 1.0, + PRIMARY KEY (from_id, to_id) + ); + + CREATE INDEX IF NOT EXISTS idx_imports_to ON edges_imports(to_id); + + CREATE TABLE IF NOT EXISTS edges_references ( + from_id TEXT NOT NULL, + to_id TEXT NOT NULL, + confidence REAL NOT NULL DEFAULT 1.0, + PRIMARY KEY (from_id, to_id) + ); + + CREATE INDEX IF NOT EXISTS idx_refs_to ON edges_references(to_id); + + CREATE TABLE IF NOT EXISTS files ( + path TEXT PRIMARY KEY, + language TEXT NOT NULL, + content_hash TEXT NOT NULL, + last_indexed INTEGER NOT NULL, + git_sha TEXT + ); + + CREATE TABLE IF NOT EXISTS index_meta ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ); + "#, + )?; + + self.setup_fts()?; + Ok(()) + } + + fn setup_fts(&mut self) -> Result<()> { + let fts_exists: bool = self.conn.query_row( + "SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='table' AND name='nodes_fts'", + [], + |row| row.get(0) + )?; + + if !fts_exists { + self.conn.execute_batch( + r#" + CREATE VIRTUAL TABLE nodes_fts USING fts5( + node_id UNINDEXED, + name, + description, + content=nodes, + content_rowid=rowid + ); + + INSERT INTO nodes_fts(rowid, node_id, name, description) + SELECT rowid, node_id, name, description FROM nodes; + + CREATE TRIGGER nodes_ai AFTER INSERT ON nodes BEGIN + INSERT INTO nodes_fts(rowid, node_id, name, description) + VALUES (NEW.rowid, NEW.node_id, NEW.name, NEW.description); + END; + + CREATE TRIGGER nodes_ad AFTER DELETE ON nodes BEGIN + DELETE FROM nodes_fts WHERE rowid = OLD.rowid; + END; + + CREATE TRIGGER nodes_au AFTER UPDATE ON nodes BEGIN + DELETE FROM nodes_fts WHERE rowid = OLD.rowid; + INSERT INTO nodes_fts(rowid, node_id, name, description) + VALUES (NEW.rowid, NEW.node_id, NEW.name, NEW.description); + END; + "#, + )?; + } + + Ok(()) + } + + pub fn upsert_node(&mut self, node: &Node) -> Result<()> { + let signatures = serde_json::to_string(&node.signatures)?; + + self.conn.execute( + r#" + INSERT INTO nodes ( + node_id, kind, name, description, signatures, language, + file_path, start_line, end_line, content_hash, git_sha, renamed_from + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12) + ON CONFLICT(node_id) DO UPDATE SET + kind = excluded.kind, + name = excluded.name, + description = excluded.description, + signatures = excluded.signatures, + language = excluded.language, + file_path = excluded.file_path, + start_line = excluded.start_line, + end_line = excluded.end_line, + content_hash = excluded.content_hash, + git_sha = excluded.git_sha, + renamed_from = excluded.renamed_from + "#, + params![ + node.node_id.as_str(), + node.kind.as_str(), + &node.name, + node.description.as_ref(), + signatures, + node.language.as_str(), + &node.file_path, + node.start_line, + node.end_line, + &node.content_hash, + node.git_sha.as_ref(), + node.renamed_from.as_ref().map(|id| id.as_str()), + ], + )?; + + Ok(()) + } + + pub fn upsert_edge(&mut self, edge: &Edge) -> Result<()> { + let table = edge.kind.table_name(); + let query = format!( + "INSERT INTO {} (from_id, to_id, confidence) VALUES (?1, ?2, ?3) + ON CONFLICT(from_id, to_id) DO UPDATE SET confidence = excluded.confidence", + table + ); + + self.conn.execute( + &query, + params![ + edge.from_id.as_str(), + edge.to_id.as_str(), + edge.confidence, + ], + )?; + + Ok(()) + } + + pub fn get_node(&self, node_id: &str) -> Result> { + let result = self.conn.query_row( + "SELECT node_id, kind, name, description, signatures, language, + file_path, start_line, end_line, content_hash, git_sha, renamed_from + FROM nodes WHERE node_id = ?1", + params![node_id], + |row| { + let signatures_str: String = row.get(4)?; + let signatures = serde_json::from_str(&signatures_str) + .map_err(|_e| rusqlite::Error::InvalidQuery)?; + + let kind_str: String = row.get(1)?; + let kind = match kind_str.as_str() { + "function" => spy_core::NodeKind::Function, + "class" => spy_core::NodeKind::Class, + "constant" => spy_core::NodeKind::Constant, + _ => return Err(rusqlite::Error::InvalidQuery), + }; + + let lang_str: String = row.get(5)?; + let language = match lang_str.as_str() { + "rust" => spy_core::Language::Rust, + "python" => spy_core::Language::Python, + "typescript" => spy_core::Language::TypeScript, + "javascript" => spy_core::Language::JavaScript, + "go" => spy_core::Language::Go, + _ => return Err(rusqlite::Error::InvalidQuery), + }; + + let renamed_from_str: Option = row.get(11)?; + let renamed_from = renamed_from_str + .map(|s| NodeId::from_string(s)) + .transpose() + .map_err(|_| rusqlite::Error::InvalidQuery)?; + + Ok(Node { + node_id: NodeId::from_string(row.get(0)?) + .map_err(|_| rusqlite::Error::InvalidQuery)?, + kind, + name: row.get(2)?, + description: row.get(3)?, + signatures, + language, + file_path: row.get(6)?, + start_line: row.get(7)?, + end_line: row.get(8)?, + content_hash: row.get(9)?, + git_sha: row.get(10)?, + renamed_from, + }) + }, + ).optional()?; + + Ok(result) + } + + pub fn delete_nodes_for_file(&mut self, file_path: &str) -> Result<()> { + self.conn.execute( + "DELETE FROM nodes WHERE file_path = ?1", + params![file_path], + )?; + Ok(()) + } + + pub fn search_nodes(&self, query: &str, limit: usize) -> Result> { + let mut stmt = self.conn.prepare( + "SELECT n.node_id, n.kind, n.name, n.description, n.signatures, n.language, + n.file_path, n.start_line, n.end_line, n.content_hash, n.git_sha, n.renamed_from, + rank + FROM nodes_fts + JOIN nodes n ON nodes_fts.rowid = n.rowid + WHERE nodes_fts MATCH ?1 + ORDER BY rank + LIMIT ?2" + )?; + + let rows = stmt.query_map(params![query, limit], |row| { + let signatures_str: String = row.get(4)?; + let signatures = serde_json::from_str(&signatures_str) + .map_err(|_| rusqlite::Error::InvalidQuery)?; + + let kind_str: String = row.get(1)?; + let kind = match kind_str.as_str() { + "function" => spy_core::NodeKind::Function, + "class" => spy_core::NodeKind::Class, + "constant" => spy_core::NodeKind::Constant, + _ => return Err(rusqlite::Error::InvalidQuery), + }; + + let lang_str: String = row.get(5)?; + let language = match lang_str.as_str() { + "rust" => spy_core::Language::Rust, + "python" => spy_core::Language::Python, + "typescript" => spy_core::Language::TypeScript, + "javascript" => spy_core::Language::JavaScript, + "go" => spy_core::Language::Go, + _ => return Err(rusqlite::Error::InvalidQuery), + }; + + let renamed_from_str: Option = row.get(11)?; + let renamed_from = renamed_from_str + .map(|s| NodeId::from_string(s)) + .transpose() + .map_err(|_| rusqlite::Error::InvalidQuery)?; + + let rank: f64 = row.get(12)?; + + Ok((Node { + node_id: NodeId::from_string(row.get(0)?) + .map_err(|_| rusqlite::Error::InvalidQuery)?, + kind, + name: row.get(2)?, + description: row.get(3)?, + signatures, + language, + file_path: row.get(6)?, + start_line: row.get(7)?, + end_line: row.get(8)?, + content_hash: row.get(9)?, + git_sha: row.get(10)?, + renamed_from, + }, rank)) + })?; + + let mut results = Vec::new(); + for row in rows { + results.push(row?); + } + Ok(results) + } + + pub fn get_file(&self, path: &str) -> Result> { + let result = self.conn.query_row( + "SELECT path, language, content_hash, last_indexed, git_sha FROM files WHERE path = ?1", + params![path], + |row| { + Ok(FileRecord { + path: row.get(0)?, + language: row.get(1)?, + content_hash: row.get(2)?, + last_indexed: row.get(3)?, + git_sha: row.get(4)?, + }) + }, + ).optional()?; + + Ok(result) + } + + pub fn upsert_file(&mut self, file: &FileRecord) -> Result<()> { + self.conn.execute( + r#" + INSERT INTO files (path, language, content_hash, last_indexed, git_sha) + VALUES (?1, ?2, ?3, ?4, ?5) + ON CONFLICT(path) DO UPDATE SET + language = excluded.language, + content_hash = excluded.content_hash, + last_indexed = excluded.last_indexed, + git_sha = excluded.git_sha + "#, + params![ + &file.path, + &file.language, + &file.content_hash, + file.last_indexed, + file.git_sha.as_ref(), + ], + )?; + Ok(()) + } + + pub fn get_meta(&self, key: &str) -> Result> { + let result = self.conn.query_row( + "SELECT value FROM index_meta WHERE key = ?1", + params![key], + |row| row.get(0), + ).optional()?; + + Ok(result) + } + + pub fn set_meta(&mut self, key: &str, value: &str) -> Result<()> { + self.conn.execute( + "INSERT INTO index_meta (key, value) VALUES (?1, ?2) + ON CONFLICT(key) DO UPDATE SET value = excluded.value", + params![key, value], + )?; + Ok(()) + } + + pub fn get_edges(&self, from_id: &str, kind: EdgeKind) -> Result> { + let table = kind.table_name(); + let query = format!( + "SELECT from_id, to_id, confidence FROM {} WHERE from_id = ?1", + table + ); + + let mut stmt = self.conn.prepare(&query)?; + let rows = stmt.query_map(params![from_id], |row| { + Ok(Edge { + from_id: NodeId::from_string(row.get(0)?) + .map_err(|_| rusqlite::Error::InvalidQuery)?, + to_id: NodeId::from_string(row.get(1)?) + .map_err(|_| rusqlite::Error::InvalidQuery)?, + kind, + confidence: row.get(2)?, + }) + })?; + + let mut results = Vec::new(); + for row in rows { + results.push(row?); + } + Ok(results) + } + + pub fn get_incoming_edges(&self, to_id: &str, kind: EdgeKind) -> Result> { + let table = kind.table_name(); + let query = format!( + "SELECT from_id, to_id, confidence FROM {} WHERE to_id = ?1", + table + ); + + let mut stmt = self.conn.prepare(&query)?; + let rows = stmt.query_map(params![to_id], |row| { + Ok(Edge { + from_id: NodeId::from_string(row.get(0)?) + .map_err(|_| rusqlite::Error::InvalidQuery)?, + to_id: NodeId::from_string(row.get(1)?) + .map_err(|_| rusqlite::Error::InvalidQuery)?, + kind, + confidence: row.get(2)?, + }) + })?; + + let mut results = Vec::new(); + for row in rows { + results.push(row?); + } + Ok(results) + } + + pub fn get_stats(&self) -> Result { + let node_count: i64 = self.conn.query_row("SELECT COUNT(*) FROM nodes", [], |row| row.get(0))?; + let edge_count: i64 = self.conn.query_row( + "SELECT (SELECT COUNT(*) FROM edges_calls) + + (SELECT COUNT(*) FROM edges_imports) + + (SELECT COUNT(*) FROM edges_references)", + [], + |row| row.get(0) + )?; + let file_count: i64 = self.conn.query_row("SELECT COUNT(*) FROM files", [], |row| row.get(0))?; + let last_git_sha = self.get_meta("last_git_sha")?; + + Ok(IndexStats { + node_count: node_count as usize, + edge_count: edge_count as usize, + file_count: file_count as usize, + last_git_sha, + }) + } +} + +#[derive(Debug, Clone)] +pub struct FileRecord { + pub path: String, + pub language: String, + pub content_hash: String, + pub last_indexed: i64, + pub git_sha: Option, +} + +#[derive(Debug, Clone)] +pub struct IndexStats { + pub node_count: usize, + pub edge_count: usize, + pub file_count: usize, + pub last_git_sha: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + use spy_core::{Language, NodeKind, Signature}; + + #[test] + fn test_upsert_and_get_node() -> Result<()> { + let mut storage = Storage::open_in_memory()?; + + let node = Node { + node_id: NodeId::new("src", "lib.rs", "_", "test_fn")?, + kind: NodeKind::Function, + name: "test_fn".to_string(), + description: Some("A test function".to_string()), + signatures: vec![Signature { + params: vec![], + returns: Some("()".to_string()), + }], + language: Language::Rust, + file_path: "src/lib.rs".to_string(), + start_line: 1, + end_line: 5, + content_hash: "abc123".to_string(), + git_sha: None, + renamed_from: None, + }; + + storage.upsert_node(&node)?; + + let retrieved = storage.get_node("src:lib.rs:_:test_fn")?; + assert!(retrieved.is_some()); + let retrieved = retrieved.unwrap(); + assert_eq!(retrieved.name, "test_fn"); + assert_eq!(retrieved.description, Some("A test function".to_string())); + + Ok(()) + } + + #[test] + fn test_search_nodes() -> Result<()> { + let mut storage = Storage::open_in_memory()?; + + let node = Node { + node_id: NodeId::new("src", "lib.rs", "_", "auth_user")?, + kind: NodeKind::Function, + name: "auth_user".to_string(), + description: Some("Authenticate a user".to_string()), + signatures: vec![], + language: Language::Rust, + file_path: "src/lib.rs".to_string(), + start_line: 1, + end_line: 5, + content_hash: "abc123".to_string(), + git_sha: None, + renamed_from: None, + }; + + storage.upsert_node(&node)?; + + let results = storage.search_nodes("auth", 10)?; + assert!(!results.is_empty()); + + Ok(()) + } +} diff --git a/spy.config.json b/spy.config.json new file mode 100644 index 0000000..dabe7b8 --- /dev/null +++ b/spy.config.json @@ -0,0 +1,23 @@ +{ + "version": 1, + "db_path": ".spy-code/graph.db", + "languages": { + "rust": null, + "python": null, + "typescript": null, + "go": null + }, + "git": { + "enabled": true, + "track_renames": true, + "follow_symlinks": false + }, + "indexing": { + "max_file_size_kb": 2048, + "parallelism": null, + "fail_fast": false + }, + "search": { + "fts_tokenizer": "unicode61" + } +} \ No newline at end of file