diff --git a/Cargo.lock b/Cargo.lock index 2799e6377..fc0453e3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,9 +35,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -213,6 +213,15 @@ dependencies = [ "rustversion", ] +[[package]] +name = "arcstr" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03918c3dbd7701a85c6b9887732e2921175f26c350b4563841d0958c21d57e6d" +dependencies = [ + "serde", +] + [[package]] name = "ast-grep-core" version = "0.44.1" @@ -441,6 +450,26 @@ dependencies = [ "serde", ] +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -730,9 +759,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.4" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" +checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" dependencies = [ "clap_builder", "clap_derive", @@ -740,9 +769,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.2" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" +checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" dependencies = [ "anstream", "anstyle", @@ -1155,9 +1184,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" [[package]] name = "defmt" @@ -1274,13 +1303,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -1317,9 +1346,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" [[package]] name = "email_address" @@ -1396,11 +1425,10 @@ checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] @@ -2665,6 +2693,66 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "grafeo-adapters" +version = "0.5.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38cf1373a739aeaa070430588c4026b3c1aa11cd0ad6e6673bda22a390aacda4" +dependencies = [ + "bincode 2.0.1", + "grafeo-common", + "grafeo-core", + "hashbrown 0.17.1", + "parking_lot", + "serde", + "smallvec", + "thiserror", +] + +[[package]] +name = "grafeo-common" +version = "0.5.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f446c25eeedab9cccaabc85060dfa08dac2d1041974244864f36436b81ca5a" +dependencies = [ + "arcstr", + "bincode 2.0.1", + "bumpalo", + "byteorder", + "bytes", + "dashmap", + "foldhash 0.2.0", + "hashbrown 0.17.1", + "indexmap", + "parking_lot", + "serde", + "smallvec", + "thiserror", +] + +[[package]] +name = "grafeo-core" +version = "0.5.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e185dd750843637a2d99e56100c08ef4cc34ce4a7b5f9cb9566f873bbf54c90" +dependencies = [ + "arcstr", + "bincode 2.0.1", + "byteorder", + "bytes", + "crc32fast", + "dashmap", + "foldhash 0.2.0", + "grafeo-common", + "hashbrown 0.17.1", + "indexmap", + "parking_lot", + "serde", + "smallvec", + "thiserror", + "unicode-normalization", +] + [[package]] name = "half" version = "2.7.1" @@ -2718,6 +2806,13 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", + "serde", + "serde_core", +] [[package]] name = "heapless" @@ -2802,9 +2897,9 @@ checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f" [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -2847,9 +2942,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ "typenum", ] @@ -3049,9 +3144,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f8a7b8211e695a1d0cd91cace480d4d0bd57667ab10277cc412c5f7f4884f83" +checksum = "0b17771570a2b94107741a7b033f19132c2eee21d59d21b24d2ced26500bd66e" dependencies = [ "crossbeam-deque", "globset", @@ -3192,9 +3287,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "is-docker" @@ -3258,9 +3353,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e184d09547b80eb7e20d141ba2fb1fbac843ca53f4cf1b31210adc4c1adc6e16" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" dependencies = [ "defmt", "jiff-core", @@ -3284,9 +3379,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323da076b7a6faf914dc677cb05a4b907742ff7375c8322c9e7f5061e5e0e9de" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" dependencies = [ "jiff-core", "proc-macro2", @@ -3358,9 +3453,9 @@ dependencies = [ [[package]] name = "keyring" -version = "4.1.5" +version = "4.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0298a59b384c540e408a600c8b375a09b49c3f97debc080e2c30675d79a6368a" +checksum = "72585bb6cc9bc370d1d545b7e23fcce71dfd4461c5e15275e3cf51bdfd9a980a" dependencies = [ "apple-native-keyring-store", "keyring-core", @@ -3420,9 +3515,9 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libredox" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" dependencies = [ "libc", ] @@ -3513,19 +3608,19 @@ checksum = "e20f57f9918e5bd7bc58c22cdd70a6afc7375d4dd9683af5f2b34bd3d2bba619" [[package]] name = "macro_rules_attribute" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520" +checksum = "b3ae8f6d608c795738406608304d30a2dfbdc8e58e44f7ba43236da5208ded3c" dependencies = [ "macro_rules_attribute-proc_macro", - "paste", + "pastey", ] [[package]] name = "macro_rules_attribute-proc_macro" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" +checksum = "fc04a4c58212d57930a24bf47d3fa87485264a3a054e9c10e042eb373573ad3c" [[package]] name = "matchit" @@ -4469,9 +4564,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "3.0.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3797d226787327b35b0b6f9e878fe55f3af2bb4daf916c0411079885454042f0" +checksum = "ad26b216c966e987e80e86daf784a455c039c43d98575ceed57b8faa259e5695" dependencies = [ "async-trait", "chrono", @@ -4578,9 +4673,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.42" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "log", "once_cell", @@ -4648,9 +4743,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" dependencies = [ "chrono", "dyn-clone", @@ -4662,14 +4757,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -4769,13 +4864,13 @@ dependencies = [ [[package]] name = "serde_derive_internals" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -5142,9 +5237,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.54" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", "num-conv", @@ -5332,13 +5427,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -5379,9 +5474,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.3+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap", "serde_core", @@ -5416,9 +5511,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ "winnow 1.0.4", ] @@ -5480,7 +5575,7 @@ name = "tracedecay" version = "0.0.67" dependencies = [ "axum", - "bincode", + "bincode 1.3.3", "cap-fs-ext", "cap-std", "clap", @@ -5856,6 +5951,9 @@ name = "tracedecay-query" version = "0.1.0" dependencies = [ "fst", + "grafeo-adapters", + "grafeo-common", + "grafeo-core", "hex", "hmac 0.13.0", "roaring", @@ -5877,7 +5975,7 @@ name = "tracedecay-runtime-core" version = "0.1.0" dependencies = [ "amari-holographic", - "bincode", + "bincode 1.3.3", "chrono", "dirs", "fs2", @@ -6285,9 +6383,9 @@ dependencies = [ [[package]] name = "tree-sitter-fsharp" -version = "0.3.1" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3dbb16ab6edea907320c19a75167f05380c8197f6db29ec0f7a0eda8ab993f1" +checksum = "b069005841682d258b95e9ee8cb0c0a0f16b77318ff56000504169c80690c67b" dependencies = [ "cc", "tree-sitter-language", @@ -6699,6 +6797,12 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "ureq" version = "3.3.0" @@ -6802,6 +6906,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + [[package]] name = "vsimd" version = "0.8.0" @@ -7627,9 +7737,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" [[package]] name = "zmij" diff --git a/crates/tracedecay-query/Cargo.toml b/crates/tracedecay-query/Cargo.toml index 662881639..84fbf93c0 100644 --- a/crates/tracedecay-query/Cargo.toml +++ b/crates/tracedecay-query/Cargo.toml @@ -8,6 +8,9 @@ include = ["/src/**"] [dependencies] fst = { version = "=0.4.7", default-features = false, features = ["levenshtein"] } +grafeo-adapters = { version = "=0.5.42", default-features = false, features = ["algos"] } +grafeo-common = { version = "=0.5.42", default-features = false } +grafeo-core = { version = "=0.5.42", default-features = false, features = ["lpg"] } hex = "0.4" hmac = "0.13.0" roaring = { version = "=0.11.4", default-features = false } diff --git a/crates/tracedecay-query/src/retrieval/graph.rs b/crates/tracedecay-query/src/retrieval/graph.rs index 3d06abdf6..429d9e6de 100644 --- a/crates/tracedecay-query/src/retrieval/graph.rs +++ b/crates/tracedecay-query/src/retrieval/graph.rs @@ -11,6 +11,7 @@ use std::collections::{BTreeMap, BTreeSet}; use serde::{Deserialize, Serialize}; +use tracedecay_application::retrieval::MAX_CALLABLE_CODE_DEPTH; use tracedecay_domain::{ CodeGenerationId, CompactCandidate, CursorPayloadDigest, EdgeAuthorityV1, RelationEdgeKindV1, RetrievalBudget, RetrievalError, RetrievalFailure, RetrievalRequest, Retriever, RetrieverBatch, @@ -64,6 +65,11 @@ impl GraphLaneRequest { "graph traversal depth must be positive".to_owned(), )); } + if self.max_depth > MAX_CALLABLE_CODE_DEPTH { + return Err(RetrievalPortError::Contract( + "graph traversal depth exceeds the callable code bound".to_owned(), + )); + } if self.seed_anchors.is_empty() { return Err(RetrievalPortError::Contract( "graph retrieval requires at least one seed anchor".to_owned(), diff --git a/crates/tracedecay-query/src/retrieval/graph/projection.rs b/crates/tracedecay-query/src/retrieval/graph/projection.rs index edd4873a6..685163e08 100644 --- a/crates/tracedecay-query/src/retrieval/graph/projection.rs +++ b/crates/tracedecay-query/src/retrieval/graph/projection.rs @@ -4,9 +4,16 @@ //! identity never becomes product identity; each emitted path re-binds to //! generation-local symbol/file/chunk occurrences from admitted chunks. -use std::collections::{BTreeMap, BTreeSet, VecDeque}; +use std::cmp::Ordering; +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; use std::sync::Arc; +use grafeo_adapters::plugins::algorithms::{Control, TraversalEvent, bfs_with_visitor}; +use grafeo_common::types::{EdgeId, NodeId}; +use grafeo_core::graph::{ + Direction, GraphProjection, GraphStoreSearch, ProjectionSpec, lpg::LpgStore, +}; use tracedecay_domain::{ CanonicalRelationEdgeV1, CodeGenerationId, CodeSearchChunkId, CodeSearchChunkV1, CompactCandidate, ComponentRevision, EdgeAuthorityV1, EvidenceRole, FileOccurrenceId, @@ -22,6 +29,10 @@ use crate::retrieval::ports::{ contract_error, }; +// Domain-scoped labels and edge types let a future Work DAG use the same +// embedded Grafeo substrate through a filtered projection without overlap. +const CODE_SYMBOL_LABEL: &str = "TraceDecayCodeSymbol"; + #[derive(Clone, Debug)] struct SymbolBindingV1 { file: FileOccurrenceId, @@ -30,17 +41,32 @@ struct SymbolBindingV1 { } /// Immutable read port over one published generation's relation evidence. -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct CodeGraphEvidenceAdapterV1 { generation: CodeGenerationId, repository_id: Option, freshness: SourceFreshness, retriever_revision: ComponentRevision, score_domain: ScoreDomainId, - adjacency: Arc>>, + graph: Arc, + nodes: Arc>, + edges: Arc>, symbols: Arc>, } +impl fmt::Debug for CodeGraphEvidenceAdapterV1 { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("CodeGraphEvidenceAdapterV1") + .field("generation", &self.generation) + .field("repository_id", &self.repository_id) + .field("freshness", &self.freshness) + .field("symbols", &self.symbols.len()) + .field("edges", &self.edges.len()) + .finish_non_exhaustive() + } +} + impl CodeGraphEvidenceAdapterV1 { /// Build a generation-bound graph evidence port from published edges and /// the chunks that carry occurrence/file/chunk binding facts. @@ -103,43 +129,68 @@ impl CodeGraphEvidenceAdapterV1 { } } - let mut adjacency: BTreeMap> = - BTreeMap::new(); - for edge in edges { - if !symbols.contains_key(&edge.from_occurrence) { - // An unbound source cannot be reached from an authorized - // seed. A bound source with an unbound target is retained so - // traversal can report the missing target as unknown. - continue; - } - adjacency - .entry(edge.from_occurrence.clone()) - .or_default() - .push(edge.clone()); + let mut retained_edges: Vec<_> = edges + .iter() + .filter(|edge| symbols.contains_key(&edge.from_occurrence)) + .cloned() + .collect(); + retained_edges.sort_by(|left, right| { + ( + &left.from_occurrence, + &left.to_occurrence, + left.kind, + left.authority, + left.evidence_span.start_byte, + left.evidence_span.end_byte, + ) + .cmp(&( + &right.from_occurrence, + &right.to_occurrence, + right.kind, + right.authority, + right.evidence_span.start_byte, + right.evidence_span.end_byte, + )) + }); + retained_edges.dedup(); + + let mut occurrences: BTreeSet<_> = symbols.keys().cloned().collect(); + for edge in &retained_edges { + occurrences.insert(edge.to_occurrence.clone()); } - for neighbors in adjacency.values_mut() { - neighbors.sort_by(|left, right| { - ( - &left.to_occurrence, - left.kind, - left.authority, - left.evidence_span.start_byte, - left.evidence_span.end_byte, + let graph = Arc::new(LpgStore::new().map_err(graph_unavailable)?); + let nodes: BTreeMap<_, _> = occurrences + .into_iter() + .map(|occurrence| { + // Grafeo IDs are disposable projection-local handles. Stable + // authority stays in typed occurrence bindings and edges. + let node = graph.create_node(&[CODE_SYMBOL_LABEL]); + (occurrence, node) + }) + .collect(); + let mut projected_edges = BTreeMap::new(); + for edge in retained_edges { + let from = nodes.get(&edge.from_occurrence).copied().ok_or_else(|| { + RetrievalPortError::Contract( + "graph projection lost a retained source binding".to_owned(), + ) + })?; + let to = nodes.get(&edge.to_occurrence).copied().ok_or_else(|| { + RetrievalPortError::Contract( + "graph projection lost a retained target binding".to_owned(), ) - .cmp(&( - &right.to_occurrence, - right.kind, - right.authority, - right.evidence_span.start_byte, - right.evidence_span.end_byte, - )) - }); - neighbors.dedup_by(|left, right| { - left.to_occurrence == right.to_occurrence - && left.kind == right.kind - && left.authority == right.authority - && left.evidence_span == right.evidence_span - }); + })?; + let edge_id = graph.create_edge(from, to, edge_type(edge.kind)); + projected_edges.insert( + edge_id, + GraphPathSegmentV1 { + from: edge.from_occurrence, + to: edge.to_occurrence, + edge_kind: edge.kind, + authority: edge.authority, + evidence_span: edge.evidence_span, + }, + ); } Ok(Self { @@ -152,7 +203,9 @@ impl CodeGraphEvidenceAdapterV1 { .map_err(contract_error)?, score_domain: ScoreDomainId::new(crate::retrieval::QUERY_GRAPH_SCORE_DOMAIN_V1) .map_err(contract_error)?, - adjacency: Arc::new(adjacency), + graph, + nodes: Arc::new(nodes), + edges: Arc::new(projected_edges), symbols: Arc::new(symbols), }) } @@ -173,128 +226,186 @@ impl CodeGraphEvidenceAdapterV1 { let mut examined = 0u64; let mut excluded = 0u64; let mut unknown = 0u64; + + let graph_store: Arc = self.graph.clone(); + let projection = GraphProjection::new( + graph_store, + ProjectionSpec::new() + .with_node_labels([CODE_SYMBOL_LABEL]) + .with_edge_types(edge_kinds.iter().copied().map(edge_type)), + ); for seed in &request.seed_anchors { let seed_symbol = seed.occurrence.symbol.as_ref().ok_or_else(|| { RetrievalPortError::Contract( "graph seed anchors require a symbol occurrence".to_owned(), ) })?; - if !self.symbols.contains_key(seed_symbol) { + let Some(seed_node) = self.nodes.get(seed_symbol).copied() else { continue; + }; + if !self.symbols.contains_key(seed_symbol) { + return Err(RetrievalPortError::Contract( + "graph projection authorized an unbound seed".to_owned(), + )); + } + + let mut frontiers = BTreeMap::from([(seed_node, vec![FrontierPath::seed()])]); + let traversal_error = + bfs_with_visitor::(&projection, seed_node, |event| { + let result = (|| match event { + TraversalEvent::Discover(node) => { + let paths = projected_frontier(&frontiers, node)?; + let path = paths.first().ok_or_else(|| { + RetrievalPortError::Contract( + "Grafeo returned an empty path frontier".to_owned(), + ) + })?; + let bound = path + .segments + .last() + .is_none_or(|segment| self.symbols.contains_key(&segment.to)); + if path.segments.len() >= request.max_depth as usize || !bound { + Ok(Control::Prune) + } else { + Ok(Control::Continue) + } + } + TraversalEvent::TreeEdge { + source, + target, + edge, + } + | TraversalEvent::NonTreeEdge { + source, + target, + edge, + } => { + let prefixes = projected_frontier(&frontiers, source)?.to_vec(); + let segment = self.edges.get(&edge).ok_or_else(|| { + RetrievalPortError::Contract( + "Grafeo traversal referenced an unknown projected edge" + .to_owned(), + ) + })?; + for prefix in prefixes { + let path = prefix.extended(segment); + admit_frontier_path(frontiers.entry(target).or_default(), path); + } + Ok(Control::Continue) + } + TraversalEvent::Finish(node) => { + let paths = projected_frontier(&frontiers, node)?; + let path = paths.first().ok_or_else(|| { + RetrievalPortError::Contract( + "Grafeo returned an empty path frontier".to_owned(), + ) + })?; + if path.segments.len() < request.max_depth as usize { + let expansions = paths.len() as u64; + for (_, edge_id) in self.graph.edges_from(node, Direction::Outgoing) + { + let segment = self.edges.get(&edge_id).ok_or_else(|| { + RetrievalPortError::Contract( + "Grafeo adjacency referenced an unknown \ + projected edge" + .to_owned(), + ) + })?; + examined = examined.saturating_add(expansions); + if !edge_kinds.contains(&segment.edge_kind) { + excluded = excluded.saturating_add(expansions); + } + } + } + Ok(Control::Continue) + } + TraversalEvent::BackEdge { .. } => Ok(Control::Continue), + })(); + match result { + Ok(control) => control, + Err(error) => Control::Break(error), + } + }); + if let Some(error) = traversal_error { + return Err(error); } - let mut queue = VecDeque::new(); - queue.push_back((seed_symbol.clone(), Vec::::new())); - let mut best_seed_paths = BTreeMap::new(); - best_seed_paths.insert(seed_symbol.clone(), (u64::MAX, Vec::new())); - while let Some((current, path)) = queue.pop_front() { - if path.len() as u32 >= request.max_depth { + + for paths in frontiers.into_values() { + let first = paths.first().ok_or_else(|| { + RetrievalPortError::Contract( + "Grafeo returned an empty path frontier".to_owned(), + ) + })?; + let Some(target) = first.segments.last().map(|segment| segment.to.clone()) else { continue; - } - let Some(neighbors) = self.adjacency.get(¤t) else { + }; + let Some(binding_meta) = self.symbols.get(&target) else { + unknown = unknown.saturating_add(paths.len() as u64); continue; }; - for edge in neighbors { - examined = examined.saturating_add(1); - if !edge_kinds.contains(&edge.kind) { - excluded = excluded.saturating_add(1); - continue; - } - let mut next_path = path.clone(); - next_path.push(GraphPathSegmentV1 { - from: edge.from_occurrence.clone(), - to: edge.to_occurrence.clone(), - edge_kind: edge.kind, - authority: edge.authority, - evidence_span: edge.evidence_span, - }); - let weakest_authority = next_path - .iter() - .map(|segment| segment.authority) - .reduce(EdgeAuthorityV1::weakest) - .unwrap_or_else(|| panic!("path has at least one edge")); - let score_micros = graph_score_micros(next_path.len(), weakest_authority); - let improves_seed_path = match best_seed_paths.get(&edge.to_occurrence) { - None => true, - Some((current_score, current_path)) => { - score_micros > *current_score - || (score_micros == *current_score - && canonical_path_key_from_segments(&next_path) - < canonical_path_key_from_segments(current_path)) - } - }; - if !improves_seed_path { - continue; - } - best_seed_paths.insert( - edge.to_occurrence.clone(), - (score_micros, next_path.clone()), - ); - let Some(binding_meta) = self.symbols.get(&edge.to_occurrence) else { - unknown = unknown.saturating_add(1); - continue; - }; - let occurrence = format!("code-graph:{}", edge.to_occurrence.as_str()); - let evidence_id = format!("code-symbol:{}", edge.to_occurrence.as_str()); - let anchor_id = retrieval_anchor(evidence_id.clone())?; - let logical_evidence_id = - LogicalEvidenceId::new(evidence_id).map_err(contract_error)?; - let candidate = CompactCandidate { - anchor_id, - logical_evidence_id, - source_occurrence_id: SourceOccurrenceId::new(occurrence.clone()) - .map_err(contract_error)?, - file_occurrence_id: Some(binding_meta.file.clone()), - source_namespace: self.freshness.source_namespace.clone(), - repository_id: self.repository_id.clone(), - session_or_thread_id: None, - logical_copy_cluster_id: None, - logical_copy_evidence_anchor: None, - evidence_role: EvidenceRole::Primary, - retriever: RetrieverKind::Graph, - retriever_revision: self.retriever_revision.clone(), - score_domain: self.score_domain.clone(), - raw_score: FixedPointScore(score_micros), - ordinal_rank: 0, - exact_admission_proof: None, - retriever_evidence_anchor: retrieval_anchor(format!( - "evidence.{occurrence}" - ))?, - freshness: self.freshness.clone(), - }; - let evidence = GraphLaneEvidence { - binding: CodeCandidateBindingV1 { - candidate_anchor: candidate.anchor_id.clone(), - occurrence: CodeOccurrenceRefV1 { - generation: self.generation.clone(), - file: binding_meta.file.clone(), - symbol: Some(edge.to_occurrence.clone()), - chunk: binding_meta.chunk.clone(), - }, - language_descriptor_revision: binding_meta - .language_descriptor_revision - .clone(), - matched_term_kinds: Vec::new(), - source_occurrence: candidate.source_occurrence_id.clone(), + let best = best_frontier_path(paths)?; + let weakest_authority = best.weakest.ok_or_else(|| { + RetrievalPortError::Contract("Grafeo returned an empty graph path".to_owned()) + })?; + let score_micros = best.score; + let path = best.segments; + let occurrence = format!("code-graph:{}", target.as_str()); + let evidence_id = format!("code-symbol:{}", target.as_str()); + let anchor_id = retrieval_anchor(evidence_id.clone())?; + let logical_evidence_id = + LogicalEvidenceId::new(evidence_id).map_err(contract_error)?; + let candidate = CompactCandidate { + anchor_id, + logical_evidence_id, + source_occurrence_id: SourceOccurrenceId::new(occurrence.clone()) + .map_err(contract_error)?, + file_occurrence_id: Some(binding_meta.file.clone()), + source_namespace: self.freshness.source_namespace.clone(), + repository_id: self.repository_id.clone(), + session_or_thread_id: None, + logical_copy_cluster_id: None, + logical_copy_evidence_anchor: None, + evidence_role: EvidenceRole::Primary, + retriever: RetrieverKind::Graph, + retriever_revision: self.retriever_revision.clone(), + score_domain: self.score_domain.clone(), + raw_score: FixedPointScore(score_micros), + ordinal_rank: 0, + exact_admission_proof: None, + retriever_evidence_anchor: retrieval_anchor(format!("evidence.{occurrence}"))?, + freshness: self.freshness.clone(), + }; + let evidence = GraphLaneEvidence { + binding: CodeCandidateBindingV1 { + candidate_anchor: candidate.anchor_id.clone(), + occurrence: CodeOccurrenceRefV1 { + generation: self.generation.clone(), + file: binding_meta.file.clone(), + symbol: Some(target), + chunk: binding_meta.chunk.clone(), }, - path: next_path.clone(), - weakest_authority, - }; - match best_by_occurrence.entry(candidate.source_occurrence_id.clone()) { - std::collections::btree_map::Entry::Vacant(entry) => { + language_descriptor_revision: binding_meta + .language_descriptor_revision + .clone(), + matched_term_kinds: Vec::new(), + source_occurrence: candidate.source_occurrence_id.clone(), + }, + path, + weakest_authority, + }; + match best_by_occurrence.entry(candidate.source_occurrence_id.clone()) { + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert((candidate, evidence)); + } + std::collections::btree_map::Entry::Occupied(mut entry) => { + let (current_candidate, current_evidence) = entry.get(); + if candidate.raw_score > current_candidate.raw_score + || (candidate.raw_score == current_candidate.raw_score + && compare_paths(&evidence.path, ¤t_evidence.path).is_lt()) + { entry.insert((candidate, evidence)); } - std::collections::btree_map::Entry::Occupied(mut entry) => { - let (current_candidate, current_evidence) = entry.get(); - if candidate.raw_score > current_candidate.raw_score - || (candidate.raw_score == current_candidate.raw_score - && canonical_path_key(&evidence) - < canonical_path_key(current_evidence)) - { - entry.insert((candidate, evidence)); - } - } } - queue.push_back((edge.to_occurrence.clone(), next_path)); } } } @@ -331,28 +442,115 @@ impl CodeGraphEvidenceAdapterV1 { } } -fn canonical_path_key( - evidence: &GraphLaneEvidence, -) -> Vec<( - &SymbolOccurrenceId, - &SymbolOccurrenceId, - RelationEdgeKindV1, - EdgeAuthorityV1, - tracedecay_domain::SourceSpan, -)> { - canonical_path_key_from_segments(&evidence.path) +#[derive(Clone)] +struct FrontierPath { + segments: Vec, + weakest: Option, + score: u64, } -fn canonical_path_key_from_segments( - path: &[GraphPathSegmentV1], -) -> Vec<( - &SymbolOccurrenceId, - &SymbolOccurrenceId, - RelationEdgeKindV1, - EdgeAuthorityV1, - tracedecay_domain::SourceSpan, -)> { - path.iter() +impl FrontierPath { + fn seed() -> Self { + Self { + segments: Vec::new(), + weakest: None, + score: u64::MAX, + } + } + + fn extended(&self, segment: &GraphPathSegmentV1) -> Self { + let weakest = self.weakest.map_or(segment.authority, |current| { + current.weakest(segment.authority) + }); + let mut segments = self.segments.clone(); + segments.push(segment.clone()); + Self { + score: graph_score_micros(segments.len(), weakest), + segments, + weakest: Some(weakest), + } + } +} + +type PathFrontiers = BTreeMap>; + +fn projected_frontier( + frontiers: &PathFrontiers, + node: NodeId, +) -> Result<&[FrontierPath], RetrievalPortError> { + frontiers + .get(&node) + .filter(|paths| !paths.is_empty()) + .map(Vec::as_slice) + .ok_or_else(|| RetrievalPortError::Contract("Grafeo node has no path frontier".to_owned())) +} + +fn admit_frontier_path(frontier: &mut Vec, candidate: FrontierPath) { + if let Some(depth) = frontier.first().map(|path| path.segments.len()) { + if depth < candidate.segments.len() { + return; + } + if depth > candidate.segments.len() { + frontier.clear(); + } + } + + for current in frontier.iter() { + if current.score >= candidate.score + && !compare_paths(¤t.segments, &candidate.segments).is_gt() + { + return; + } + } + + let mut retained = Vec::with_capacity(frontier.len() + 1); + for current in frontier.drain(..) { + if candidate.score < current.score + || compare_paths(&candidate.segments, ¤t.segments).is_gt() + { + retained.push(current); + } + } + retained.push(candidate); + retained.sort_by(|left, right| compare_paths(&left.segments, &right.segments)); + *frontier = retained; +} + +fn best_frontier_path(paths: Vec) -> Result { + let mut best = None::; + for path in paths { + let improves = best.as_ref().is_none_or(|current| { + path.score > current.score + || (path.score == current.score + && compare_paths(&path.segments, ¤t.segments).is_lt()) + }); + if improves { + best = Some(path); + } + } + best.ok_or_else(|| RetrievalPortError::Contract("Grafeo path frontier is empty".to_owned())) +} + +fn edge_type(kind: RelationEdgeKindV1) -> &'static str { + match kind { + RelationEdgeKindV1::Calls => "CodeCalls", + RelationEdgeKindV1::Uses => "CodeUses", + RelationEdgeKindV1::TypeOf => "CodeTypeOf", + RelationEdgeKindV1::Contains => "CodeContains", + RelationEdgeKindV1::Implements => "CodeImplements", + RelationEdgeKindV1::Extends => "CodeExtends", + RelationEdgeKindV1::Annotates => "CodeAnnotates", + } +} + +fn graph_unavailable(error: impl fmt::Display) -> RetrievalPortError { + RetrievalPortError::AuthorityUnavailable(format!( + "embedded Grafeo projection is unavailable: {error}" + )) +} + +fn compare_paths(left: &[GraphPathSegmentV1], right: &[GraphPathSegmentV1]) -> Ordering { + left.iter() .map(|segment| { ( &segment.from, @@ -362,7 +560,15 @@ fn canonical_path_key_from_segments( segment.evidence_span, ) }) - .collect() + .cmp(right.iter().map(|segment| { + ( + &segment.from, + &segment.to, + segment.edge_kind, + segment.authority, + segment.evidence_span, + ) + })) } impl GraphEvidenceReadPort for CodeGraphEvidenceAdapterV1 { diff --git a/crates/tracedecay-query/src/retrieval/graph/tests.rs b/crates/tracedecay-query/src/retrieval/graph/tests.rs index 1a05a7d9b..489d38aad 100644 --- a/crates/tracedecay-query/src/retrieval/graph/tests.rs +++ b/crates/tracedecay-query/src/retrieval/graph/tests.rs @@ -5,6 +5,7 @@ use std::collections::BTreeMap; use std::fmt; +use tracedecay_application::retrieval::MAX_CALLABLE_CODE_DEPTH; use tracedecay_domain::{ BoundedSanitizedText, CanonicalRelationEdgeV1, ChunkerRevision, CodeSearchChunkAnchorV1, CodeSearchChunkGrainV1, CodeSearchChunkV1, CompactCandidate, ContentDigest, EdgeAuthorityV1, @@ -24,6 +25,9 @@ use crate::retrieval::ports::{ CodeCandidateBindingV1, CodeOccurrenceRefV1, GraphEvidenceReadPort, RetrievalPortError, }; +mod measurement; +mod scale; + fn id(value: &str) -> T where T: TryFrom, @@ -311,6 +315,165 @@ fn result_order(batch: &RetrieverBatch, expected: &[&str]) { assert_eq!(actual, expected); } +#[test] +fn graph_projection_bounds_self_loop_and_cycle_at_depth_with_exact_coverage() { + let request = graph_request(8, 2); + let edge = |from: &str, to: &str, start_byte| CanonicalRelationEdgeV1 { + from_occurrence: id(from), + to_occurrence: id(to), + kind: RelationEdgeKindV1::Calls, + authority: EdgeAuthorityV1::SyntaxExact, + evidence_span: SourceSpan { + start_byte, + end_byte: start_byte + 1, + }, + }; + let edges = vec![ + edge("symbol.seed", "symbol.seed", 0), + edge("symbol.seed", "symbol.middle", 1), + edge("symbol.middle", "symbol.seed", 2), + edge("symbol.middle", "symbol.target", 3), + edge("symbol.target", "symbol.sink", 4), + ]; + + let result = projection_batch( + &request, + &edges, + &[ + "symbol.seed", + "symbol.middle", + "symbol.target", + "symbol.sink", + ], + ); + + result_order( + &result, + &["code-graph:symbol.middle", "code-graph:symbol.target"], + ); + assert_eq!( + result.coverage, + RetrieverCoverage { + examined: 4, + eligible: 2, + excluded: 0, + capped: 0, + unknown: 0, + } + ); + assert_eq!( + result.evidence_by_occurrence[&id("code-graph:symbol.middle")] + .path + .len(), + 1 + ); + assert_eq!( + result.evidence_by_occurrence[&id("code-graph:symbol.target")] + .path + .len(), + 2 + ); + assert!( + !result + .evidence_by_occurrence + .contains_key(&id("code-graph:symbol.sink")) + ); +} + +#[test] +fn graph_projection_deduplicates_identical_edges_byte_for_byte_including_coverage() { + let request = graph_request(8, 1); + let edge = CanonicalRelationEdgeV1 { + from_occurrence: id("symbol.seed"), + to_occurrence: id("symbol.target"), + kind: RelationEdgeKindV1::Calls, + authority: EdgeAuthorityV1::SyntaxExact, + evidence_span: SourceSpan { + start_byte: 0, + end_byte: 1, + }, + }; + let single = projection_batch( + &request, + std::slice::from_ref(&edge), + &["symbol.seed", "symbol.target"], + ); + let duplicate = projection_batch( + &request, + &[edge.clone(), edge], + &["symbol.seed", "symbol.target"], + ); + + assert_eq!(single, duplicate); + assert_eq!( + serde_json::to_vec(&single).expect("serialize single projection"), + serde_json::to_vec(&duplicate).expect("serialize duplicate projection") + ); + assert_eq!( + single.coverage, + RetrieverCoverage { + examined: 1, + eligible: 1, + excluded: 0, + capped: 0, + unknown: 0, + } + ); +} + +#[test] +fn graph_projection_rejects_foreign_generation_chunks() { + let request = graph_request(8, 1); + let mut foreign = projection_chunk(&request, "chunk.foreign", "symbol.foreign"); + foreign.anchor.generation_id = id("generation.foreign"); + + let result = CodeGraphEvidenceAdapterV1::new( + request.generation, + None, + freshness(FreshnessCompatibilityV1::Current), + &[], + &[foreign], + ); + + assert!(matches!( + result, + Err(RetrievalPortError::GenerationMismatch) + )); +} + +#[test] +fn graph_projection_rejects_conflicting_symbol_bindings() { + let request = graph_request(8, 1); + let primary = projection_chunk(&request, "chunk.target.a", "symbol.target"); + let mut conflicting = projection_chunk(&request, "chunk.target.b", "symbol.target"); + conflicting.anchor.file_occurrence_id = id("file.symbol.other"); + + let result = CodeGraphEvidenceAdapterV1::new( + request.generation, + None, + freshness(FreshnessCompatibilityV1::Current), + &[], + &[primary, conflicting], + ); + + assert!(matches!( + result, + Err(RetrievalPortError::Contract(message)) + if message == "one symbol occurrence has conflicting graph candidate bindings" + )); +} + +#[test] +fn graph_lane_rejects_depth_above_the_callable_code_contract() { + let request = graph_request(8, MAX_CALLABLE_CODE_DEPTH + 1); + + assert!(matches!( + request.validate(), + Err(RetrievalPortError::Contract(message)) + if message == "graph traversal depth exceeds the callable code bound" + )); +} + #[test] fn graph_lane_emits_generic_candidates_with_ordered_path_ids_and_weakest_authority() { let request = graph_request(8, 3); diff --git a/crates/tracedecay-query/src/retrieval/graph/tests/measurement.rs b/crates/tracedecay-query/src/retrieval/graph/tests/measurement.rs new file mode 100644 index 000000000..ee27ff6e7 --- /dev/null +++ b/crates/tracedecay-query/src/retrieval/graph/tests/measurement.rs @@ -0,0 +1,115 @@ +use std::time::Instant; + +use super::*; + +#[cfg(target_os = "linux")] +fn process_rss_bytes() -> Option { + let status = std::fs::read_to_string("/proc/self/status").ok()?; + let kibibytes = status + .lines() + .find_map(|line| line.strip_prefix("VmRSS:"))? + .split_whitespace() + .next()? + .parse::() + .ok()?; + kibibytes.checked_mul(1024) +} + +#[cfg(not(target_os = "linux"))] +fn process_rss_bytes() -> Option { + None +} + +#[test] +#[ignore = "manual measurement only; run with --ignored --nocapture"] +fn manual_measure_code_graph_projection_and_traversal() { + const SYMBOL_COUNT: usize = 50_000; + const FANOUT: usize = 2; + let request = graph_request(64, 4); + let symbols: Vec<_> = std::iter::once("symbol.seed".to_owned()) + .chain((1..SYMBOL_COUNT).map(|index| format!("symbol.n{index:05}"))) + .collect(); + let chunks: Vec<_> = symbols + .iter() + .map(|symbol| projection_chunk(&request, &format!("chunk.{symbol}"), symbol)) + .collect(); + let mut edges = Vec::with_capacity(SYMBOL_COUNT * FANOUT); + for (index, from) in symbols.iter().enumerate() { + for step in 1..=FANOUT { + if let Some(to) = symbols.get(index + step) { + edges.push(CanonicalRelationEdgeV1 { + from_occurrence: id(from), + to_occurrence: id(to), + kind: if step == 1 { + RelationEdgeKindV1::Calls + } else { + RelationEdgeKindV1::Uses + }, + authority: EdgeAuthorityV1::SyntaxExact, + evidence_span: SourceSpan { + start_byte: (index * 2 + step) as u64, + end_byte: (index * 2 + step + 1) as u64, + }, + }); + } + } + } + + let rss_before = process_rss_bytes(); + let started = Instant::now(); + let adapter = CodeGraphEvidenceAdapterV1::new( + request.generation.clone(), + None, + freshness(FreshnessCompatibilityV1::Current), + &edges, + &chunks, + ) + .expect("representative projection builds"); + let build_elapsed = started.elapsed(); + let rss_after = process_rss_bytes(); + + let started = Instant::now(); + let batch = complete_batch( + adapter + .read_graph_evidence(&request) + .expect("representative traversal succeeds"), + ); + let traversal_elapsed = started.elapsed(); + result_order( + &batch, + &[ + "code-graph:symbol.n00001", + "code-graph:symbol.n00002", + "code-graph:symbol.n00003", + "code-graph:symbol.n00004", + "code-graph:symbol.n00005", + "code-graph:symbol.n00006", + "code-graph:symbol.n00007", + "code-graph:symbol.n00008", + ], + ); + assert_eq!( + batch.coverage, + RetrieverCoverage { + examined: 14, + eligible: 8, + excluded: 0, + capped: 0, + unknown: 0, + } + ); + + println!( + "manual graph projection: symbols={SYMBOL_COUNT} edges={} build_ms={} \ + traversal_us={} rss_before_bytes={} rss_after_bytes={} rss_delta_bytes={}", + edges.len(), + build_elapsed.as_millis(), + traversal_elapsed.as_micros(), + rss_before.unwrap_or_default(), + rss_after.unwrap_or_default(), + rss_after + .zip(rss_before) + .map(|(after, before)| after.saturating_sub(before)) + .unwrap_or_default(), + ); +} diff --git a/crates/tracedecay-query/src/retrieval/graph/tests/scale.rs b/crates/tracedecay-query/src/retrieval/graph/tests/scale.rs new file mode 100644 index 000000000..66aeeafb8 --- /dev/null +++ b/crates/tracedecay-query/src/retrieval/graph/tests/scale.rs @@ -0,0 +1,161 @@ +use super::*; + +#[test] +fn graph_projection_bounds_convergent_fanout_by_nodes_and_edges() { + let request = graph_request(64, 3); + let first_layer: Vec<_> = (0..16).map(|index| format!("symbol.a{index:02}")).collect(); + let second_layer: Vec<_> = (0..16).map(|index| format!("symbol.b{index:02}")).collect(); + let mut symbols = vec!["symbol.seed".to_owned(), "symbol.sink".to_owned()]; + symbols.extend(first_layer.iter().cloned()); + symbols.extend(second_layer.iter().cloned()); + let chunks: Vec<_> = symbols + .iter() + .map(|symbol| projection_chunk(&request, &format!("chunk.{symbol}"), symbol)) + .collect(); + + let mut edges = Vec::new(); + let mut push_edge = |from: &str, to: &str| { + let offset = edges.len() as u64; + edges.push(CanonicalRelationEdgeV1 { + from_occurrence: id(from), + to_occurrence: id(to), + kind: RelationEdgeKindV1::Calls, + authority: EdgeAuthorityV1::SyntaxExact, + evidence_span: SourceSpan { + start_byte: offset, + end_byte: offset + 1, + }, + }); + }; + for target in &first_layer { + push_edge("symbol.seed", target); + } + for source in &first_layer { + for target in &second_layer { + push_edge(source, target); + } + } + for source in &second_layer { + push_edge(source, "symbol.sink"); + } + + let adapter = CodeGraphEvidenceAdapterV1::new( + request.generation.clone(), + None, + freshness(FreshnessCompatibilityV1::Current), + &edges, + &chunks, + ) + .expect("projection is valid"); + let batch = complete_batch( + adapter + .read_graph_evidence(&request) + .expect("bounded traversal succeeds"), + ); + + assert_eq!(batch.coverage.examined, 288); + assert_eq!(batch.coverage.eligible, 33); + let sink = &batch.evidence_by_occurrence[&id("code-graph:symbol.sink")]; + assert_eq!(sink.path.len(), 3); + assert_eq!(sink.path[0].to.as_str(), "symbol.a00"); + assert_eq!(sink.path[1].to.as_str(), "symbol.b00"); +} + +#[test] +fn graph_projection_preserves_improving_prefixes_through_a_later_bottleneck() { + let request = graph_request(8, 3); + let edge = |from: &str, to: &str, authority, start_byte| CanonicalRelationEdgeV1 { + from_occurrence: id(from), + to_occurrence: id(to), + kind: RelationEdgeKindV1::Calls, + authority, + evidence_span: SourceSpan { + start_byte, + end_byte: start_byte + 1, + }, + }; + let edges = vec![ + edge("symbol.seed", "symbol.a", EdgeAuthorityV1::SyntaxExact, 0), + edge("symbol.seed", "symbol.z", EdgeAuthorityV1::SyntaxExact, 1), + edge( + "symbol.a", + "symbol.join", + EdgeAuthorityV1::HeuristicCandidate, + 2, + ), + edge("symbol.z", "symbol.join", EdgeAuthorityV1::SyntaxExact, 3), + edge( + "symbol.join", + "symbol.target", + EdgeAuthorityV1::HeuristicCandidate, + 4, + ), + ]; + + let batch = projection_batch( + &request, + &edges, + &[ + "symbol.seed", + "symbol.a", + "symbol.z", + "symbol.join", + "symbol.target", + ], + ); + + let target = &batch.evidence_by_occurrence[&id("code-graph:symbol.target")]; + assert_eq!(target.path[0].to.as_str(), "symbol.a"); + assert_eq!(batch.coverage.examined, 6); +} + +#[test] +fn graph_projection_preserves_cross_node_frontiers_through_a_later_bottleneck() { + let request = graph_request(16, 4); + let edge = |from: &str, to: &str, authority, start_byte| CanonicalRelationEdgeV1 { + from_occurrence: id(from), + to_occurrence: id(to), + kind: RelationEdgeKindV1::Calls, + authority, + evidence_span: SourceSpan { + start_byte, + end_byte: start_byte + 1, + }, + }; + let syntax = EdgeAuthorityV1::SyntaxExact; + let edges = vec![ + edge("symbol.seed", "symbol.p1", syntax, 0), + edge("symbol.seed", "symbol.p2", syntax, 1), + edge("symbol.seed", "symbol.p3", syntax, 2), + edge( + "symbol.p1", + "symbol.x", + EdgeAuthorityV1::HeuristicCandidate, + 3, + ), + edge("symbol.p2", "symbol.y", EdgeAuthorityV1::NameResolved, 4), + edge("symbol.p3", "symbol.x", syntax, 5), + edge("symbol.x", "symbol.t", syntax, 6), + edge("symbol.y", "symbol.t", syntax, 7), + edge("symbol.t", "symbol.sink", EdgeAuthorityV1::NameResolved, 8), + ]; + let batch = projection_batch( + &request, + &edges, + &[ + "symbol.seed", + "symbol.p1", + "symbol.p2", + "symbol.p3", + "symbol.x", + "symbol.y", + "symbol.t", + "symbol.sink", + ], + ); + + let sink = &batch.evidence_by_occurrence[&id("code-graph:symbol.sink")]; + assert_eq!(sink.path[0].to.as_str(), "symbol.p2"); + assert_eq!(batch.coverage.examined, 12); + assert_eq!(batch.coverage.eligible, 7); +}