diff --git a/BUCK b/BUCK index 2966e310..de046eb4 100644 --- a/BUCK +++ b/BUCK @@ -51,8 +51,10 @@ rust_library( "//third-party/rust/crates/num_cpus/1.17.0:num_cpus", "//third-party/rust/crates/path-absolutize/3.1.1:path-absolutize", "//third-party/rust/crates/rayon/1.11.0:rayon", + "//third-party/rust/crates/ring/0.17.14:ring", "//third-party/rust/crates/sea-orm/1.1.19:sea-orm", "//third-party/rust/crates/serde/1.0.228:serde", + "//third-party/rust/crates/serde_json/1.0.145:serde_json", "//third-party/rust/crates/sha1/0.10.6:sha1", "//third-party/rust/crates/sha2/0.10.9:sha2", "//third-party/rust/crates/similar/2.7.0:similar", diff --git a/Cargo.lock b/Cargo.lock index db312760..d22ea71b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -896,8 +896,10 @@ dependencies = [ "rand 0.9.2", "rand_chacha 0.9.0", "rayon", + "ring", "sea-orm", "serde", + "serde_json", "sha1", "sha2", "similar", @@ -1935,6 +1937,20 @@ dependencies = [ "bytecheck", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rkyv" version = "0.7.45" @@ -2929,6 +2945,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "unty" version = "0.0.4" @@ -3133,6 +3155,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -3166,6 +3197,22 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -3176,7 +3223,7 @@ dependencies = [ "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.53.1", "windows_i686_msvc 0.53.1", "windows_x86_64_gnu 0.53.1", "windows_x86_64_gnullvm 0.53.1", @@ -3189,6 +3236,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" @@ -3201,6 +3254,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.1" @@ -3213,12 +3272,24 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_gnullvm" version = "0.53.1" @@ -3231,6 +3302,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_i686_msvc" version = "0.53.1" @@ -3243,6 +3320,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnu" version = "0.53.1" @@ -3255,6 +3338,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" @@ -3267,6 +3356,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.1" diff --git a/Cargo.toml b/Cargo.toml index 85a73e52..59a455b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,10 +43,12 @@ sea-orm = { version = "1.1.17", features = ["sqlx-sqlite"] } flate2 = { version = "1.1.8", features = ["zlib"] } serde = { version = "1.0.228", features = ["derive"] } chrono = { version = "0.4.43", features = ["serde"] } -uuid = { version = "1.19.0", features = ["v4"] } +uuid = { version = "1.19.0", features = ["serde", "v4", "v7"] } tokio = { version = "1.49.0", features = ["fs", "io-util"] } bincode = { version = "2.0.1", features = ["serde"] } axum = { version = "0.8.8", features = ["macros", "json"] } +ring = "0.17.8" +serde_json = "1.0.145" [dev-dependencies] tokio = { version = "1.49.0", features = ["full"] } diff --git a/buckal.snap b/buckal.snap index 87b39d22..50003419 100644 --- a/buckal.snap +++ b/buckal.snap @@ -3,7 +3,7 @@ version = 2 [fingerprints] -"path+file://($WORKSPACE)#0.4.1" = "6d91a20f32d73f0ce745fbcccfac002167d699bb7d0ecaff33ad816280104e72" +"path+file://($WORKSPACE)#0.4.1" = "18cbbeebb64505391cc56951ecd06fb5f741ee8f8404ec87a36c248deb142234" "registry+https://github.com/rust-lang/crates.io-index#adler2@2.0.1" = "7081ce7693ca45bff0e855374e6b7f386805bba1de9f7c288b1c706b99abef7f" "registry+https://github.com/rust-lang/crates.io-index#ahash@0.7.8" = "263bbc26dec8ade458658cf9dae3ef8fc606e85f04c7b47abb4f9255319689ac" "registry+https://github.com/rust-lang/crates.io-index#ahash@0.8.12" = "a383722a12d8343396b1965bfe77f1cebeafb12ccc571747b2b54614e3e1a1ea" @@ -203,6 +203,7 @@ version = 2 "registry+https://github.com/rust-lang/crates.io-index#regex-syntax@0.8.8" = "b5bd7a8b232100b181ef43ff406fb2382e503828b02a43cd606f98d6ea0ad950" "registry+https://github.com/rust-lang/crates.io-index#regex@1.12.2" = "10f6a5c57952790e35d0c05df1e7eafd9c894d80edd73975f10517c9b6c1a00a" "registry+https://github.com/rust-lang/crates.io-index#rend@0.4.2" = "e1477adda7718f5953efaa45ef12a2e30c0037c5723d544285f1d21d751a092b" +"registry+https://github.com/rust-lang/crates.io-index#ring@0.17.14" = "151d151dbff71a9fc2ab072a6790aaf76e22bae424931ac216e88a09077ca58c" "registry+https://github.com/rust-lang/crates.io-index#rkyv@0.7.45" = "696526e2a115738bd5270d4fc1fceddf70699a2c77bb1a5f03355cdfb6bdc2a6" "registry+https://github.com/rust-lang/crates.io-index#rkyv_derive@0.7.45" = "ad51df9d2e223d73130300a8075a8eaeed101ff95d4c9ad1252498f7186db950" "registry+https://github.com/rust-lang/crates.io-index#rsa@0.9.10" = "9e4146e51ce7cb3bfeca2da05259109f47b38350679c086df91d3c492e59a808" @@ -289,10 +290,11 @@ version = 2 "registry+https://github.com/rust-lang/crates.io-index#unicode-normalization@0.1.25" = "9665de652cecbf789eaecff8e27c8b2f1b8b11fbe793954a8b061ef703457af1" "registry+https://github.com/rust-lang/crates.io-index#unicode-properties@0.1.4" = "cdf6d930a0dfc1acc3b026018d97c6bf20106eaecbdf5dceb3d7aaef1b4dbeb4" "registry+https://github.com/rust-lang/crates.io-index#unicode-xid@0.2.6" = "04c6fa64d1f26eb2c0594f87108dff265d17d747d21de7a9b240a4675ad7f2ef" +"registry+https://github.com/rust-lang/crates.io-index#untrusted@0.9.0" = "0e5f903c4d858d56b064f2e47fd2b2e6d228f9404ceeef561b5cc4c0d77bec33" "registry+https://github.com/rust-lang/crates.io-index#unty@0.0.4" = "5475cd4e774a5dff1d71ec84f3f46bbf2a1ce7ecae45e15713335645a8e14941" "registry+https://github.com/rust-lang/crates.io-index#url@2.5.7" = "41a06dd6e4a64931308619cd9143392e669fe55fff38850784e1a9a09b49bfc2" "registry+https://github.com/rust-lang/crates.io-index#utf8_iter@1.0.4" = "047f25b86fd5f1351043a8dde0aeed66f85eeca3ac56cbb7bec54ed6c023b16c" -"registry+https://github.com/rust-lang/crates.io-index#uuid@1.19.0" = "8c6965359b43e1c55bae17a14ebaa08c993d1a96c931fac6fcd3e9da638f5837" +"registry+https://github.com/rust-lang/crates.io-index#uuid@1.19.0" = "6527b631b0af7222779488f8c649a9d5f4c4d2180776bf51cfd4973bae8c8824" "registry+https://github.com/rust-lang/crates.io-index#valuable@0.1.1" = "756ad94b5effaecf9973461517320157d1ee5ab3ec28d86e1f99c4915851a45e" "registry+https://github.com/rust-lang/crates.io-index#vcpkg@0.2.15" = "719f7f17537403aaa321d268702fb5039de3e7c1993d88754cca070f8b597174" "registry+https://github.com/rust-lang/crates.io-index#version_check@0.9.5" = "5f3a662254d4a2b2e505904ada0ee24aa8376f7d08d71832a6a0f1ff04f1ad71" @@ -312,24 +314,34 @@ version = 2 "registry+https://github.com/rust-lang/crates.io-index#windows-result@0.4.1" = "d2d6166685cc9d38cea0479daefd31f9153c918d206af155456ce3fbd35c57e1" "registry+https://github.com/rust-lang/crates.io-index#windows-strings@0.5.1" = "81ed7ba4dd1224709e4d536ef0a5a9c0c2281a16a97fc7c65d0e21ba0d6cc886" "registry+https://github.com/rust-lang/crates.io-index#windows-sys@0.48.0" = "50ddce0dedf31ab06f3fe4bf72330b18a914c7cf46975e23479bbee20655d049" +"registry+https://github.com/rust-lang/crates.io-index#windows-sys@0.52.0" = "b41994c3901bf0618e472914a1f1e2aee066b7d366662300d64bb73a32582315" "registry+https://github.com/rust-lang/crates.io-index#windows-sys@0.60.2" = "ee177220dba83367a193ab58fa2ff8b28517f6a1947e94964172abf5b70c74fe" "registry+https://github.com/rust-lang/crates.io-index#windows-sys@0.61.2" = "06567f74041ec4c5a8ffd08e906ac59ff5081935ec4e0fccff9020d608d8befc" "registry+https://github.com/rust-lang/crates.io-index#windows-targets@0.48.5" = "2eeba1fb41ece7e47a512f90244d1a6a33a80cf7d62dac96544325d85092624e" +"registry+https://github.com/rust-lang/crates.io-index#windows-targets@0.52.6" = "fe8dded5952c3df6593dee93b8e6dd3bc2801a7cc135c097e517f37a5a7b9b68" "registry+https://github.com/rust-lang/crates.io-index#windows-targets@0.53.5" = "4143d0d11931f31b5b484a337f69797bfca82416f5b6ddbdd0820ee86c26c17e" "registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_gnullvm@0.48.5" = "38fd7c9d4e11b7d8199855a289813cf45a7c6602023c3db8e908ea0231606b15" +"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_gnullvm@0.52.6" = "ec5913f3b3c7eaf44512f8f22ca7d9fdd33fadb644a04e539367282f0657a6bb" "registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_gnullvm@0.53.1" = "b7aa4557fd735c7160f9c76aa29c5c331b42c93ee85c500a5800aa692685a2db" "registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_msvc@0.48.5" = "22233c89e54334b947c5444d7da34f56bcf2ad220e5afd3c5796262da6a3015c" +"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_msvc@0.52.6" = "d0dff81edc26b7c335981c42a8c924daf003439dcf087909f2f2bd33f3cfff5c" "registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_msvc@0.53.1" = "a6a1d1d3f5c90f821990f8b394e9102b688b5a17ca9677ddad23005f8e04d01b" "registry+https://github.com/rust-lang/crates.io-index#windows_i686_gnu@0.48.5" = "e462d3c14d1663ae20989241c032c6bf9ee91236188c11475318b0a3d1309f30" +"registry+https://github.com/rust-lang/crates.io-index#windows_i686_gnu@0.52.6" = "beda152c20d15df581eb71538b123ffd8dbe7a179ae41178f92a27ac53cc9a88" "registry+https://github.com/rust-lang/crates.io-index#windows_i686_gnu@0.53.1" = "eb2e8306163c6c2562aea56157cc1cac86427cc2a4687d1a28895c32f5a6b70f" +"registry+https://github.com/rust-lang/crates.io-index#windows_i686_gnullvm@0.52.6" = "1ecf22cfbe5422a9ba4fdb6f4b8ac2209336c745fe009f2adb7c02a70aeeacd2" "registry+https://github.com/rust-lang/crates.io-index#windows_i686_gnullvm@0.53.1" = "5c1891f2195599fc638bb25cc9f8ff3dda24df63017f7b36c4125cde787eb27a" "registry+https://github.com/rust-lang/crates.io-index#windows_i686_msvc@0.48.5" = "8021126b4da7fc1fa275a91ebf1e2e4412253062c242f3f43461b9f61c79135c" +"registry+https://github.com/rust-lang/crates.io-index#windows_i686_msvc@0.52.6" = "5f6a41c34c9257f37ee727bfd361ad10def9bd939b40e8cfd5fac442b77d06fe" "registry+https://github.com/rust-lang/crates.io-index#windows_i686_msvc@0.53.1" = "7a5870f49ee76344eadcc7568e98b5a4e0f253744cf8e0e5b788b78835432a22" "registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnu@0.48.5" = "7ace2b609eace2727e85167f6072875941fa46931bd2bda1b1318d45f236d358" +"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnu@0.52.6" = "7a22dc7cd2f859d72c30f639f3272e7f42de40e5a0421fd02d578f86bf51c430" "registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnu@0.53.1" = "66f8f034cfa8e6d1a4ca921622cc60a1fb07188ff5825b9623cd9f692ba41ca9" "registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnullvm@0.48.5" = "e2b273ebf8c4ae5ed82e49a7a3d2a57d3101b2b2b0f915c39d5480b10d8cdb72" +"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnullvm@0.52.6" = "58086d26333764ddeec150e409033f77a2619a50bc3a6332cf16f90fffcf3ffd" "registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnullvm@0.53.1" = "a162c7cceb8d8de4c4c1843df61c422b49d9a7f8324725b7efffdb30c9aa5ada" "registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_msvc@0.48.5" = "81d2d0f5ef71a9f9bfdafeb6b63fd7381b23f33f1755da936dd1f830afb2f2ba" +"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_msvc@0.52.6" = "c2a6bfcc8e073a4dd33a6426fdbd698ac611fe3ec6c179591786efceaa2d6721" "registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_msvc@0.53.1" = "6c1a38bede7258be170d8bfbcd7846282fb443c2d870183bcb2ff0b849d44b0c" "registry+https://github.com/rust-lang/crates.io-index#winnow@0.7.14" = "41164f26911d5b8c9273f13ef71416bf62adbc115d0cbd0a3f4bb1f290f6841d" "registry+https://github.com/rust-lang/crates.io-index#wit-bindgen@0.46.0" = "b1f4351aafc5a8a7f12f71e1e6acebc3d3da7e928331f659e0b60936473fd938" diff --git a/src/hash.rs b/src/hash.rs index b6396198..2fcc75a2 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -239,6 +239,7 @@ impl ObjectHash { } } } + thread_local! { /// Thread-local variable to store the current hash kind. /// This allows different threads to work with different hash algorithms concurrently diff --git a/src/internal/object/context.rs b/src/internal/object/context.rs new file mode 100644 index 00000000..4b91e3bd --- /dev/null +++ b/src/internal/object/context.rs @@ -0,0 +1,159 @@ +//! AI Context Snapshot Definition +//! +//! A `ContextSnapshot` represents the state of the codebase and external resources +//! that an agent uses to perform its task. +//! +//! # Selection Strategy +//! +//! - **Explicit**: User manually selected files. +//! - **Heuristic**: Agent automatically selected files based on relevance. +//! +//! # Integrity +//! +//! Each item in the snapshot has a content hash (`IntegrityHash`). +//! This ensures that if the file changes on disk, we know the snapshot is stale or refers to an older version. + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::{ + header::{ActorRef, AiObjectType, Header}, + integrity::IntegrityHash, +}; + +/// Selection strategy for context snapshots. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum SelectionStrategy { + /// Files explicitly chosen by the user. + Explicit, + /// Files automatically selected by the agent/system. + Heuristic, +} + +/// Context item kind. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ContextItemKind { + /// A regular file in the repository. + File, +} + +/// Context item describing a single input. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContextItem { + pub kind: ContextItemKind, + pub path: String, + pub content_id: IntegrityHash, +} + +impl ContextItem { + pub fn new( + kind: ContextItemKind, + path: impl Into, + content_id: IntegrityHash, + ) -> Result { + let path = path.into(); + if path.trim().is_empty() { + return Err("path cannot be empty".to_string()); + } + Ok(Self { + kind, + path, + content_id, + }) + } +} + +/// Context snapshot describing selected inputs. +/// Captures the selection strategy and content identifiers used by a run. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContextSnapshot { + #[serde(flatten)] + header: Header, + base_commit_sha: IntegrityHash, + selection_strategy: SelectionStrategy, + #[serde(default)] + items: Vec, + summary: Option, +} + +impl ContextSnapshot { + pub fn new( + repo_id: Uuid, + created_by: ActorRef, + base_commit_sha: impl AsRef, + selection_strategy: SelectionStrategy, + ) -> Result { + let base_commit_sha = base_commit_sha.as_ref().parse()?; + Ok(Self { + header: Header::new(AiObjectType::ContextSnapshot, repo_id, created_by)?, + base_commit_sha, + selection_strategy, + items: Vec::new(), + summary: None, + }) + } + + pub fn header(&self) -> &Header { + &self.header + } + + pub fn base_commit_sha(&self) -> &IntegrityHash { + &self.base_commit_sha + } + + pub fn selection_strategy(&self) -> &SelectionStrategy { + &self.selection_strategy + } + + pub fn items(&self) -> &[ContextItem] { + &self.items + } + + pub fn summary(&self) -> Option<&str> { + self.summary.as_deref() + } + + pub fn add_item(&mut self, item: ContextItem) { + self.items.push(item); + } + + pub fn set_summary(&mut self, summary: Option) { + self.summary = summary; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_hash_hex() -> String { + IntegrityHash::compute(b"ai-process-test").to_hex() + } + + #[test] + fn test_context_snapshot_fields() { + let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef); + let actor = ActorRef::agent("test-agent").expect("actor"); + let base_hash = test_hash_hex(); + + let mut snapshot = + ContextSnapshot::new(repo_id, actor, &base_hash, SelectionStrategy::Explicit) + .expect("snapshot"); + snapshot.set_summary(Some("core files".to_string())); + + snapshot.add_item( + ContextItem::new( + ContextItemKind::File, + "src/lib.rs", + IntegrityHash::compute(b"context-item"), + ) + .expect("context item"), + ); + + assert_eq!(snapshot.items().len(), 1); + assert_eq!(snapshot.items()[0].path, "src/lib.rs"); + assert_eq!(snapshot.summary(), Some("core files")); + } +} diff --git a/src/internal/object/decision.rs b/src/internal/object/decision.rs new file mode 100644 index 00000000..6ff275be --- /dev/null +++ b/src/internal/object/decision.rs @@ -0,0 +1,181 @@ +//! AI Decision Definition +//! +//! `Decision` represents the final outcome of an agent's run. It signals whether the proposed +//! changes should be applied, rejected, or if the agent needs human intervention. +//! +//! # Decision Types +//! +//! - **Commit**: Changes are good, apply them. +//! - **Abandon**: Task is impossible or not worth doing. +//! - **Retry**: Something went wrong, try again (with different params/prompt). +//! - **Checkpoint**: Save progress but don't finish yet. +//! - **Rollback**: Revert changes. + +use std::fmt; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::{ + header::{ActorRef, AiObjectType, Header}, + integrity::IntegrityHash, +}; + +/// Type of decision. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum DecisionType { + /// Approve and commit changes. + Commit, + /// Save intermediate progress. + Checkpoint, + /// Give up on the task. + Abandon, + /// Try again (re-run). + Retry, + /// Revert applied changes. + Rollback, + #[serde(untagged)] + Other(String), +} + +impl fmt::Display for DecisionType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DecisionType::Commit => write!(f, "commit"), + DecisionType::Checkpoint => write!(f, "checkpoint"), + DecisionType::Abandon => write!(f, "abandon"), + DecisionType::Retry => write!(f, "retry"), + DecisionType::Rollback => write!(f, "rollback"), + DecisionType::Other(s) => write!(f, "{}", s), + } + } +} + +impl From for DecisionType { + fn from(s: String) -> Self { + match s.as_str() { + "commit" => DecisionType::Commit, + "checkpoint" => DecisionType::Checkpoint, + "abandon" => DecisionType::Abandon, + "retry" => DecisionType::Retry, + "rollback" => DecisionType::Rollback, + _ => DecisionType::Other(s), + } + } +} + +impl From<&str> for DecisionType { + fn from(s: &str) -> Self { + match s { + "commit" => DecisionType::Commit, + "checkpoint" => DecisionType::Checkpoint, + "abandon" => DecisionType::Abandon, + "retry" => DecisionType::Retry, + "rollback" => DecisionType::Rollback, + _ => DecisionType::Other(s.to_string()), + } + } +} + +/// Decision object linking process to outcomes. +/// Records the final outcome of a run. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Decision { + #[serde(flatten)] + header: Header, + run_id: Uuid, + decision_type: DecisionType, + chosen_patchset_id: Option, + result_commit_sha: Option, + checkpoint_id: Option, + rationale: Option, +} + +impl Decision { + /// Create a new decision object + pub fn new( + repo_id: Uuid, + created_by: ActorRef, + run_id: Uuid, + decision_type: impl Into, + ) -> Result { + Ok(Self { + header: Header::new(AiObjectType::Decision, repo_id, created_by)?, + run_id, + decision_type: decision_type.into(), + chosen_patchset_id: None, + result_commit_sha: None, + checkpoint_id: None, + rationale: None, + }) + } + + pub fn header(&self) -> &Header { + &self.header + } + + pub fn run_id(&self) -> Uuid { + self.run_id + } + + pub fn decision_type(&self) -> &DecisionType { + &self.decision_type + } + + pub fn chosen_patchset_id(&self) -> Option { + self.chosen_patchset_id + } + + pub fn result_commit_sha(&self) -> Option<&IntegrityHash> { + self.result_commit_sha.as_ref() + } + + pub fn checkpoint_id(&self) -> Option<&str> { + self.checkpoint_id.as_deref() + } + + pub fn rationale(&self) -> Option<&str> { + self.rationale.as_deref() + } + + pub fn set_chosen_patchset_id(&mut self, chosen_patchset_id: Option) { + self.chosen_patchset_id = chosen_patchset_id; + } + + pub fn set_result_commit_sha(&mut self, result_commit_sha: Option) { + self.result_commit_sha = result_commit_sha; + } + + pub fn set_checkpoint_id(&mut self, checkpoint_id: Option) { + self.checkpoint_id = checkpoint_id; + } + + pub fn set_rationale(&mut self, rationale: Option) { + self.rationale = rationale; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_decision_fields() { + let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef); + let actor = ActorRef::agent("test-agent").expect("actor"); + let run_id = Uuid::from_u128(0x1); + let patchset_id = Uuid::from_u128(0x2); + let expected_hash = IntegrityHash::compute(b"decision-hash"); + + let mut decision = Decision::new(repo_id, actor, run_id, "commit").expect("decision"); + decision.set_chosen_patchset_id(Some(patchset_id)); + decision.set_result_commit_sha(Some(expected_hash)); + decision.set_rationale(Some("tests passed".to_string())); + + assert_eq!(decision.chosen_patchset_id(), Some(patchset_id)); + assert_eq!(decision.result_commit_sha(), Some(&expected_hash)); + assert_eq!(decision.rationale(), Some("tests passed")); + assert_eq!(decision.decision_type(), &DecisionType::Commit); + } +} diff --git a/src/internal/object/evidence.rs b/src/internal/object/evidence.rs new file mode 100644 index 00000000..32e2f010 --- /dev/null +++ b/src/internal/object/evidence.rs @@ -0,0 +1,183 @@ +//! AI Evidence Definition +//! +//! `Evidence` represents the result of a validation or quality assurance step, such as +//! running tests, linting code, or building artifacts. +//! +//! # Purpose +//! +//! - **Validation**: Proves that a patchset works as expected. +//! - **Feedback**: Provides error messages and logs to the agent so it can fix issues. +//! - **Decision Support**: Used by the `Decision` object to justify committing or rejecting changes. + +use std::fmt; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::header::{ActorRef, AiObjectType, ArtifactRef, Header}; + +/// Kind of evidence. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum EvidenceKind { + /// Unit, integration, or e2e tests. + Test, + /// Static analysis results. + Lint, + /// Compilation or build results. + Build, + #[serde(untagged)] + Other(String), +} + +impl fmt::Display for EvidenceKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + EvidenceKind::Test => write!(f, "test"), + EvidenceKind::Lint => write!(f, "lint"), + EvidenceKind::Build => write!(f, "build"), + EvidenceKind::Other(s) => write!(f, "{}", s), + } + } +} + +impl From for EvidenceKind { + fn from(s: String) -> Self { + match s.as_str() { + "test" => EvidenceKind::Test, + "lint" => EvidenceKind::Lint, + "build" => EvidenceKind::Build, + _ => EvidenceKind::Other(s), + } + } +} + +impl From<&str> for EvidenceKind { + fn from(s: &str) -> Self { + match s { + "test" => EvidenceKind::Test, + "lint" => EvidenceKind::Lint, + "build" => EvidenceKind::Build, + _ => EvidenceKind::Other(s.to_string()), + } + } +} + +/// Evidence object for test/lint/build results. +/// Links tooling output back to a run or patchset. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Evidence { + #[serde(flatten)] + header: Header, + run_id: Uuid, + patchset_id: Option, + kind: EvidenceKind, + tool: String, + command: Option, + exit_code: Option, + summary: Option, // passed/failed, error signature + #[serde(default)] + report_artifacts: Vec, +} + +impl Evidence { + pub fn new( + repo_id: Uuid, + created_by: ActorRef, + run_id: Uuid, + kind: impl Into, + tool: impl Into, + ) -> Result { + Ok(Self { + header: Header::new(AiObjectType::Evidence, repo_id, created_by)?, + run_id, + patchset_id: None, + kind: kind.into(), + tool: tool.into(), + command: None, + exit_code: None, + summary: None, + report_artifacts: Vec::new(), + }) + } + + pub fn header(&self) -> &Header { + &self.header + } + + pub fn run_id(&self) -> Uuid { + self.run_id + } + + pub fn patchset_id(&self) -> Option { + self.patchset_id + } + + pub fn kind(&self) -> &EvidenceKind { + &self.kind + } + + pub fn tool(&self) -> &str { + &self.tool + } + + pub fn command(&self) -> Option<&str> { + self.command.as_deref() + } + + pub fn exit_code(&self) -> Option { + self.exit_code + } + + pub fn summary(&self) -> Option<&str> { + self.summary.as_deref() + } + + pub fn report_artifacts(&self) -> &[ArtifactRef] { + &self.report_artifacts + } + + pub fn set_patchset_id(&mut self, patchset_id: Option) { + self.patchset_id = patchset_id; + } + + pub fn set_command(&mut self, command: Option) { + self.command = command; + } + + pub fn set_exit_code(&mut self, exit_code: Option) { + self.exit_code = exit_code; + } + + pub fn set_summary(&mut self, summary: Option) { + self.summary = summary; + } + + pub fn add_report_artifact(&mut self, artifact: ArtifactRef) { + self.report_artifacts.push(artifact); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_evidence_fields() { + let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef); + let actor = ActorRef::agent("test-agent").expect("actor"); + let run_id = Uuid::from_u128(0x1); + let patchset_id = Uuid::from_u128(0x2); + + let mut evidence = + Evidence::new(repo_id, actor, run_id, "test", "cargo").expect("evidence"); + evidence.set_patchset_id(Some(patchset_id)); + evidence.set_exit_code(Some(1)); + evidence.add_report_artifact(ArtifactRef::new("local", "log.txt").expect("artifact")); + + assert_eq!(evidence.patchset_id(), Some(patchset_id)); + assert_eq!(evidence.exit_code(), Some(1)); + assert_eq!(evidence.report_artifacts().len(), 1); + assert_eq!(evidence.kind(), &EvidenceKind::Test); + } +} diff --git a/src/internal/object/header.rs b/src/internal/object/header.rs new file mode 100644 index 00000000..3039b1ff --- /dev/null +++ b/src/internal/object/header.rs @@ -0,0 +1,637 @@ +//! AI Object Header Definition +//! +//! This module defines the common metadata header shared by all AI process objects. +//! It ensures that every object in the AI workflow has a consistent identity, +//! provenance, and integrity verification mechanism. +//! +//! # Core Concepts +//! +//! - **Identity**: Every object has a unique `Uuid` (v7) and `AiObjectType`. +//! - **Provenance**: `ActorRef` tracks who created the object (Human, Agent, System). +//! - **Integrity**: `IntegrityHash` (SHA-256) ensures content has not been tampered with. +//! - **Linking**: Objects reference each other via UUIDs (e.g., Run -> Task). + +use std::{collections::HashMap, fmt}; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::integrity::{IntegrityHash, compute_integrity_hash}; + +/// Visibility of an AI process object. +/// +/// Determines whether the object is accessible only within the project (Private) +/// or can be shared externally (Public). +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum Visibility { + Private, + Public, +} + +/// AI process object type. +/// +/// Enumerates all supported object types in the AI workflow system. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum AiObjectType { + /// A unit of work to be performed. + Task, + /// An execution instance of an agent working on a task. + Run, + /// A set of code changes proposed by an agent. + PatchSet, + /// A snapshot of the codebase context used for a run. + ContextSnapshot, + /// A record of a tool execution (read, write, cmd, etc.). + ToolInvocation, + /// A step-by-step plan generated by the agent. + Plan, + /// Verification results (test output, lint errors). + Evidence, + /// Metadata about the model and provider used. + Provenance, + /// The final outcome of a run (commit, reject, etc.). + Decision, +} + +impl AiObjectType { + pub fn as_str(&self) -> &'static str { + match self { + AiObjectType::Task => "task", + AiObjectType::Run => "run", + AiObjectType::PatchSet => "patchset", + AiObjectType::ContextSnapshot => "context_snapshot", + AiObjectType::ToolInvocation => "tool_invocation", + AiObjectType::Plan => "plan", + AiObjectType::Evidence => "evidence", + AiObjectType::Provenance => "provenance", + AiObjectType::Decision => "decision", + } + } +} + +impl fmt::Display for AiObjectType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +/// Header shared by all AI Process Objects. +/// +/// Contains standard metadata like ID, type, creator, and timestamps. +/// +/// # Usage +/// +/// Every AI object struct should flatten this header: +/// +/// ```rust,ignore +/// #[derive(Serialize, Deserialize)] +/// pub struct MyObject { +/// #[serde(flatten)] +/// header: Header, +/// // specific fields... +/// } +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct Header { + /// Global unique ID (UUID v7) + object_id: Uuid, + /// Object type (task/run/patchset/...) + object_type: AiObjectType, + /// Model version + schema_version: u32, + /// Repository identifier + repo_id: Uuid, + /// Creation time + created_at: DateTime, + /// Creator + created_by: ActorRef, + /// Visibility (fixed to private for Libra) + visibility: Visibility, + /// Search tags + #[serde(default)] + tags: HashMap, + /// External ID mapping + #[serde(default)] + external_ids: HashMap, + /// Content checksum (optional) + #[serde(default)] + checksum: Option, +} + +impl Header { + /// Create a new Header with default values. + /// + /// # Arguments + /// + /// * `object_type` - The specific type of the AI object. + /// * `repo_id` - The UUID of the repository this object belongs to. + /// * `created_by` - The actor (human/agent) creating this object. + pub fn new( + object_type: AiObjectType, + repo_id: Uuid, + created_by: ActorRef, + ) -> Result { + Ok(Self { + object_id: Uuid::now_v7(), + object_type, + schema_version: 1, + repo_id, + created_at: Utc::now(), + created_by, + visibility: Visibility::Private, + tags: HashMap::new(), + external_ids: HashMap::new(), + checksum: None, + }) + } + + pub fn object_id(&self) -> Uuid { + self.object_id + } + + pub fn object_type(&self) -> &AiObjectType { + &self.object_type + } + + pub fn schema_version(&self) -> u32 { + self.schema_version + } + + pub fn repo_id(&self) -> Uuid { + self.repo_id + } + + pub fn created_at(&self) -> DateTime { + self.created_at + } + + pub fn created_by(&self) -> &ActorRef { + &self.created_by + } + + pub fn visibility(&self) -> &Visibility { + &self.visibility + } + + pub fn tags(&self) -> &HashMap { + &self.tags + } + + pub fn tags_mut(&mut self) -> &mut HashMap { + &mut self.tags + } + + pub fn external_ids(&self) -> &HashMap { + &self.external_ids + } + + pub fn external_ids_mut(&mut self) -> &mut HashMap { + &mut self.external_ids + } + + pub fn set_object_id(&mut self, object_id: Uuid) { + self.object_id = object_id; + } + + pub fn set_object_type(&mut self, object_type: AiObjectType) -> Result<(), String> { + self.object_type = object_type; + Ok(()) + } + + pub fn set_schema_version(&mut self, schema_version: u32) -> Result<(), String> { + if schema_version == 0 { + return Err("schema_version must be greater than 0".to_string()); + } + self.schema_version = schema_version; + Ok(()) + } + + pub fn set_created_at(&mut self, created_at: DateTime) { + self.created_at = created_at; + } + + pub fn set_visibility(&mut self, visibility: Visibility) { + self.visibility = visibility; + } + + /// Accessor for checksum + pub fn checksum(&self) -> Option<&IntegrityHash> { + self.checksum.as_ref() + } + + /// Seal the header by calculating and setting the checksum of the provided object. + /// The checksum field is temporarily cleared to keep sealing idempotent. + /// + /// This is typically called just before storing the object to ensure `checksum` matches content. + pub fn seal(&mut self, object: &T) -> Result<(), serde_json::Error> { + let previous = self.checksum.take(); + match compute_integrity_hash(object) { + Ok(checksum) => { + self.checksum = Some(checksum); + Ok(()) + } + Err(err) => { + self.checksum = previous; + Err(err) + } + } + } +} + +/// Actor kind enum +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ActorKind { + Human, + Agent, + System, + McpClient, + #[serde(untagged)] + Other(String), +} + +impl fmt::Display for ActorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ActorKind::Human => write!(f, "human"), + ActorKind::Agent => write!(f, "agent"), + ActorKind::System => write!(f, "system"), + ActorKind::McpClient => write!(f, "mcp_client"), + ActorKind::Other(s) => write!(f, "{}", s), + } + } +} + +impl From for ActorKind { + fn from(s: String) -> Self { + match s.as_str() { + "human" => ActorKind::Human, + "agent" => ActorKind::Agent, + "system" => ActorKind::System, + "mcp_client" => ActorKind::McpClient, + _ => ActorKind::Other(s), + } + } +} + +impl From<&str> for ActorKind { + fn from(s: &str) -> Self { + match s { + "human" => ActorKind::Human, + "agent" => ActorKind::Agent, + "system" => ActorKind::System, + "mcp_client" => ActorKind::McpClient, + _ => ActorKind::Other(s.to_string()), + } + } +} + +/// Actor reference (who created/triggered). +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ActorRef { + /// Kind: human/agent/system/mcp_client + kind: ActorKind, + /// Subject ID (user/agent name or client ID) + id: String, + /// Display name (optional) + display_name: Option, + /// Auth context (optional, Libra usually empty) + auth_context: Option, +} + +impl ActorRef { + /// Create a new ActorRef with validation. + pub fn new(kind: impl Into, id: impl Into) -> Result { + let id_str = id.into(); + if id_str.trim().is_empty() { + return Err("Actor ID cannot be empty".to_string()); + } + Ok(Self { + kind: kind.into(), + id: id_str, + display_name: None, + auth_context: None, + }) + } + + /// Create an MCP client actor reference (MCP writes must use this). + pub fn new_for_mcp(id: impl Into) -> Result { + Self::new(ActorKind::McpClient, id) + } + + /// Validate that this actor is an MCP client. + pub fn ensure_mcp_client(&self) -> Result<(), String> { + if self.kind != ActorKind::McpClient { + return Err("MCP writes must use mcp_client actor kind".to_string()); + } + Ok(()) + } + + pub fn kind(&self) -> &ActorKind { + &self.kind + } + + pub fn id(&self) -> &str { + &self.id + } + + pub fn display_name(&self) -> Option<&str> { + self.display_name.as_deref() + } + + pub fn auth_context(&self) -> Option<&str> { + self.auth_context.as_deref() + } + + pub fn set_display_name(&mut self, display_name: Option) { + self.display_name = display_name; + } + + pub fn set_auth_context(&mut self, auth_context: Option) { + self.auth_context = auth_context; + } + + /// Create a human actor reference. + pub fn human(id: impl Into) -> Result { + Self::new(ActorKind::Human, id) + } + + /// Create an agent actor reference. + pub fn agent(name: impl Into) -> Result { + Self::new(ActorKind::Agent, name) + } + + /// Create a system component actor reference. + pub fn system(component: impl Into) -> Result { + Self::new(ActorKind::System, component) + } + + /// Create an MCP client actor reference. + pub fn mcp_client(client_id: impl Into) -> Result { + Self::new(ActorKind::McpClient, client_id) + } +} + +/// Artifact reference (external content). +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ArtifactRef { + /// Store type: local_fs/s3 + store: String, + /// Storage key (e.g., path or object key) + key: String, + /// MIME type (optional) + content_type: Option, + /// Size in bytes (optional) + size_bytes: Option, + /// Content hash (strongly recommended) + hash: Option, + /// Expiration time (optional) + expires_at: Option>, +} + +impl ArtifactRef { + pub fn new(store: impl Into, key: impl Into) -> Result { + let store = store.into(); + let key = key.into(); + if store.trim().is_empty() { + return Err("store cannot be empty".to_string()); + } + if key.trim().is_empty() { + return Err("key cannot be empty".to_string()); + } + Ok(Self { + store, + key, + content_type: None, + size_bytes: None, + hash: None, + expires_at: None, + }) + } + + pub fn store(&self) -> &str { + &self.store + } + + pub fn key(&self) -> &str { + &self.key + } + + pub fn content_type(&self) -> Option<&str> { + self.content_type.as_deref() + } + + pub fn size_bytes(&self) -> Option { + self.size_bytes + } + + pub fn hash(&self) -> Option<&IntegrityHash> { + self.hash.as_ref() + } + + pub fn expires_at(&self) -> Option> { + self.expires_at + } + + /// Calculate hash for the given content bytes. + pub fn compute_hash(content: &[u8]) -> IntegrityHash { + IntegrityHash::compute(content) + } + + /// Set the hash directly. + pub fn with_hash(mut self, hash: IntegrityHash) -> Self { + self.hash = Some(hash); + self + } + + /// Set the hash from a hex string. + pub fn with_hash_hex(mut self, hash: impl AsRef) -> Result { + let hash = hash.as_ref().parse()?; + self.hash = Some(hash); + Ok(self) + } + + pub fn set_content_type(&mut self, content_type: Option) { + self.content_type = content_type; + } + + pub fn set_size_bytes(&mut self, size_bytes: Option) { + self.size_bytes = size_bytes; + } + + pub fn set_expires_at(&mut self, expires_at: Option>) { + self.expires_at = expires_at; + } + + /// Verify if the provided content matches the stored checksum + #[must_use = "handle integrity verification result"] + pub fn verify_integrity(&self, content: &[u8]) -> Result { + let stored_hash = self + .hash + .as_ref() + .ok_or_else(|| "No hash stored in ArtifactRef".to_string())?; + + Ok(IntegrityHash::compute(content) == *stored_hash) + } + + /// Check if two artifacts have the same content based on checksum + #[must_use] + pub fn content_eq(&self, other: &Self) -> Option { + match (&self.hash, &other.hash) { + (Some(a), Some(b)) => Some(a == b), + _ => None, + } + } + + /// Check if the artifact has expired + #[must_use] + pub fn is_expired(&self) -> bool { + if let Some(expires_at) = self.expires_at { + expires_at < Utc::now() + } else { + false + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_header_serialization() { + let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef); + let actor = ActorRef::human("jackie").expect("actor"); + let header = Header::new(AiObjectType::Task, repo_id, actor).expect("header"); + + let json = serde_json::to_string(&header).unwrap(); + let deserialized: Header = serde_json::from_str(&json).unwrap(); + + assert_eq!(header.object_id(), deserialized.object_id()); + assert_eq!(header.object_type(), deserialized.object_type()); + assert_eq!(header.repo_id(), deserialized.repo_id()); + } + + #[test] + fn test_actor_ref() { + let actor = ActorRef::agent("coder").expect("actor"); + assert_eq!(actor.kind(), &ActorKind::Agent); + assert_eq!(actor.id(), "coder"); + + let sys = ActorRef::system("scheduler").expect("system"); + assert_eq!(sys.kind(), &ActorKind::System); + + let client = ActorRef::mcp_client("vscode").expect("client"); + assert_eq!(client.kind(), &ActorKind::McpClient); + assert!(client.ensure_mcp_client().is_ok()); + + let non_mcp = ActorRef::human("jackie").expect("actor"); + assert!(non_mcp.ensure_mcp_client().is_err()); + } + + #[test] + fn test_actor_kind_serialization() { + let k = ActorKind::McpClient; + let s = serde_json::to_string(&k).unwrap(); + assert_eq!(s, "\"mcp_client\""); + + let k2: ActorKind = serde_json::from_str("\"system\"").unwrap(); + assert_eq!(k2, ActorKind::System); + } + + #[test] + fn test_header_checksum() { + let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef); + let actor = ActorRef::human("jackie").expect("actor"); + let mut header = Header::new(AiObjectType::Task, repo_id, actor).expect("header"); + // Fix time for deterministic checksum + header.set_created_at( + DateTime::parse_from_rfc3339("2026-02-10T00:00:00Z") + .unwrap() + .with_timezone(&Utc), + ); + header.set_object_id(Uuid::from_u128(0x00000000000000000000000000000001)); + + let checksum = compute_integrity_hash(&header).expect("checksum"); + assert_eq!(checksum.to_hex().len(), 64); // SHA256 length + + // Ensure changes change checksum + header + .set_object_type(AiObjectType::Run) + .expect("object_type"); + let checksum2 = compute_integrity_hash(&header).expect("checksum"); + assert_ne!(checksum, checksum2); + } + + #[test] + fn test_artifact_checksum() { + let content = b"hello world"; + let hash = ArtifactRef::compute_hash(content); + // echo -n "hello world" | shasum -a 256 + let expected_str = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"; + assert_eq!(hash.to_hex(), expected_str); + + let artifact = ArtifactRef::new("s3", "key") + .expect("artifact") + .with_hash(hash); + assert_eq!(artifact.hash(), Some(&hash)); + + // Integrity check + assert!(artifact.verify_integrity(content).unwrap()); + assert!(!artifact.verify_integrity(b"wrong").unwrap()); + + // Deduplication + let artifact2 = ArtifactRef::new("local", "other/path") + .expect("artifact") + .with_hash(IntegrityHash::compute(content)); + assert_eq!(artifact.content_eq(&artifact2), Some(true)); + + let artifact3 = ArtifactRef::new("s3", "diff") + .expect("artifact") + .with_hash(ArtifactRef::compute_hash(b"diff")); + assert_eq!(artifact.content_eq(&artifact3), Some(false)); + } + + #[test] + fn test_invalid_checksum() { + let result = ArtifactRef::new("s3", "key") + .expect("artifact") + .with_hash_hex("bad_hash"); + assert!(result.is_err()); + } + + #[test] + fn test_header_seal() { + let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef); + let actor = ActorRef::human("jackie").expect("actor"); + let mut header = Header::new(AiObjectType::Task, repo_id, actor).expect("header"); + + let content = serde_json::json!({"key": "value"}); + header.seal(&content).expect("seal"); + + assert!(header.checksum().is_some()); + let expected = compute_integrity_hash(&content).expect("checksum"); + assert_eq!(header.checksum().expect("checksum"), &expected); + } + + #[test] + fn test_empty_actor_id() { + let result = ActorRef::new(ActorKind::Human, " "); + assert!(result.is_err()); + } + + #[test] + fn test_artifact_expiration() { + let mut artifact = ArtifactRef::new("s3", "key").expect("artifact"); + assert!(!artifact.is_expired()); + + artifact.set_expires_at(Some(Utc::now() - chrono::Duration::hours(1))); + assert!(artifact.is_expired()); + + artifact.set_expires_at(Some(Utc::now() + chrono::Duration::hours(1))); + assert!(!artifact.is_expired()); + } +} diff --git a/src/internal/object/integrity.rs b/src/internal/object/integrity.rs new file mode 100644 index 00000000..99adbc00 --- /dev/null +++ b/src/internal/object/integrity.rs @@ -0,0 +1,151 @@ +//! Hash implementation for AI process objects. +//! +//! This module defines `IntegrityHash`, which is used for integrity verification +//! and deduplication of AI objects (Artifacts, Headers, etc.). +//! +//! # Why not `ObjectHash`? +//! +//! We avoid using `git_internal::hash::ObjectHash` here because: +//! 1. `ObjectHash` implies Git content addressing (SHA-1 or SHA-256 depending on repo config). +//! 2. `IntegrityHash` always uses SHA-256 for consistent integrity checks regardless of the +//! underlying Git repository format. +//! 3. This separation prevents accidental usage of integrity checksums as Git object IDs. + +use std::{fmt, str::FromStr}; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use sha2::{Digest, Sha256}; + +/// A SHA-256 hash used for integrity verification. +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct IntegrityHash([u8; 32]); + +impl IntegrityHash { + /// Create a new hash from raw bytes. + pub fn new(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + /// Compute hash from content bytes. + pub fn compute(content: &[u8]) -> Self { + let mut hasher = Sha256::new(); + hasher.update(content); + let result = hasher.finalize(); + Self(result.into()) + } + + /// Return the hex string representation. + pub fn to_hex(&self) -> String { + hex::encode(self.0) + } + + /// Return the raw bytes. + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +impl fmt::Debug for IntegrityHash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "IntegrityHash({})", self.to_hex()) + } +} + +impl fmt::Display for IntegrityHash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_hex()) + } +} + +impl FromStr for IntegrityHash { + type Err = String; + + fn from_str(s: &str) -> Result { + if s.len() != 64 { + return Err(format!("Invalid hash length: expected 64, got {}", s.len())); + } + let mut bytes = [0u8; 32]; + hex::decode_to_slice(s, &mut bytes).map_err(|e| e.to_string())?; + Ok(Self(bytes)) + } +} + +impl Serialize for IntegrityHash { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_hex()) + } +} + +impl<'de> Deserialize<'de> for IntegrityHash { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Self::from_str(&s).map_err(serde::de::Error::custom) + } +} + +/// Compute canonical JSON hash. +pub fn compute_integrity_hash( + object: &T, +) -> Result { + let mut value = serde_json::to_value(object)?; + canonicalize_json(&mut value); + let content = serde_json::to_vec(&value)?; + Ok(IntegrityHash::compute(&content)) +} + +fn canonicalize_json(value: &mut Value) { + match value { + Value::Array(items) => { + for item in items.iter_mut() { + canonicalize_json(item); + } + } + Value::Object(map) => { + let mut entries: Vec<(String, Value)> = std::mem::take(map).into_iter().collect(); + entries.sort_by(|(a, _), (b, _)| a.cmp(b)); + let mut sorted = serde_json::Map::with_capacity(entries.len()); + for (key, mut value) in entries { + canonicalize_json(&mut value); + sorted.insert(key, value); + } + *map = sorted; + } + _ => {} + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::*; + + #[derive(Serialize)] + struct MapWrapper { + map: HashMap, + } + + #[test] + fn test_integrity_hash_deterministic() { + let mut map_a = HashMap::new(); + map_a.insert("b".to_string(), "2".to_string()); + map_a.insert("a".to_string(), "1".to_string()); + + let mut map_b = HashMap::new(); + map_b.insert("a".to_string(), "1".to_string()); + map_b.insert("b".to_string(), "2".to_string()); + + let hash_a = compute_integrity_hash(&MapWrapper { map: map_a }).expect("checksum"); + let hash_b = compute_integrity_hash(&MapWrapper { map: map_b }).expect("checksum"); + + assert_eq!(hash_a, hash_b); + assert_eq!(hash_a.to_hex().len(), 64); + } +} diff --git a/src/internal/object/mod.rs b/src/internal/object/mod.rs index 7cdbc01a..e7ce1a1e 100644 --- a/src/internal/object/mod.rs +++ b/src/internal/object/mod.rs @@ -1,11 +1,21 @@ //! Object model definitions for Git blobs, trees, commits, tags, and supporting traits that let the //! pack/zlib layers create strongly typed values from raw bytes. - pub mod blob; pub mod commit; +pub mod context; +pub mod decision; +pub mod evidence; +pub mod header; +pub mod integrity; pub mod note; +pub mod patchset; +pub mod plan; +pub mod provenance; +pub mod run; pub mod signature; pub mod tag; +pub mod task; +pub mod tool; pub mod tree; pub mod types; pub mod utils; diff --git a/src/internal/object/patchset.rs b/src/internal/object/patchset.rs new file mode 100644 index 00000000..edefdc15 --- /dev/null +++ b/src/internal/object/patchset.rs @@ -0,0 +1,225 @@ +//! AI PatchSet Definition +//! +//! A `PatchSet` represents a proposed set of code changes (diffs) generated by an agent. +//! +//! # Generations +//! +//! PatchSets are often created in generations (iterations). +//! If a PatchSet fails validation or code review, the agent may generate a new PatchSet (generation N+1) +//! for the same `Run`. +//! +//! # Content +//! +//! The actual diff content is stored as an `ArtifactRef` (e.g., pointing to a file in object storage), +//! while `TouchedFile` provides a lightweight summary for UI/indexing. + +use std::fmt; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::{ + header::{ActorRef, AiObjectType, ArtifactRef, Header}, + integrity::IntegrityHash, +}; + +/// Patch application status. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ApplyStatus { + /// Patch is generated but not yet applied to the repo. + Proposed, + /// Patch has been applied (committed) to the repo. + Applied, + /// Patch was rejected by validation or user. + Rejected, + /// A newer generation has replaced this patch. + Superseded, +} + +impl ApplyStatus { + pub fn as_str(&self) -> &'static str { + match self { + ApplyStatus::Proposed => "proposed", + ApplyStatus::Applied => "applied", + ApplyStatus::Rejected => "rejected", + ApplyStatus::Superseded => "superseded", + } + } +} + +impl fmt::Display for ApplyStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +/// Diff format for patch content. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum DiffFormat { + /// Standard unified diff format. + UnifiedDiff, + /// Git-specific diff format (with binary support etc). + GitDiff, +} + +/// Type of change for a file. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ChangeType { + Add, + Modify, + Delete, + Rename, + Copy, +} + +/// Touched file summary in a patchset. +/// +/// Provides a quick overview of what files are modified without parsing the full diff. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TouchedFile { + pub path: String, + pub change_type: ChangeType, + pub lines_added: u32, + pub lines_deleted: u32, +} + +impl TouchedFile { + pub fn new( + path: impl Into, + change_type: ChangeType, + lines_added: u32, + lines_deleted: u32, + ) -> Result { + let path = path.into(); + if path.trim().is_empty() { + return Err("path cannot be empty".to_string()); + } + Ok(Self { + path, + change_type, + lines_added, + lines_deleted, + }) + } +} + +/// PatchSet object containing a candidate diff. +/// Each generation represents a new candidate diff for the same run. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PatchSet { + #[serde(flatten)] + header: Header, + run_id: Uuid, + generation: u32, + base_commit_sha: IntegrityHash, + diff_format: DiffFormat, + diff_artifact: Option, + #[serde(default)] + touched_files: Vec, + rationale: Option, + apply_status: ApplyStatus, +} + +impl PatchSet { + /// Create a new patchset object + pub fn new( + repo_id: Uuid, + created_by: ActorRef, + run_id: Uuid, + base_commit_sha: impl AsRef, + generation: u32, + ) -> Result { + let base_commit_sha = base_commit_sha.as_ref().parse()?; + Ok(Self { + header: Header::new(AiObjectType::PatchSet, repo_id, created_by)?, + run_id, + generation, + base_commit_sha, + diff_format: DiffFormat::UnifiedDiff, + diff_artifact: None, + touched_files: Vec::new(), + rationale: None, + apply_status: ApplyStatus::Proposed, + }) + } + + pub fn header(&self) -> &Header { + &self.header + } + + pub fn run_id(&self) -> Uuid { + self.run_id + } + + pub fn generation(&self) -> u32 { + self.generation + } + + pub fn base_commit_sha(&self) -> &IntegrityHash { + &self.base_commit_sha + } + + pub fn diff_format(&self) -> &DiffFormat { + &self.diff_format + } + + pub fn diff_artifact(&self) -> Option<&ArtifactRef> { + self.diff_artifact.as_ref() + } + + pub fn touched_files(&self) -> &[TouchedFile] { + &self.touched_files + } + + pub fn rationale(&self) -> Option<&str> { + self.rationale.as_deref() + } + + pub fn apply_status(&self) -> &ApplyStatus { + &self.apply_status + } + + pub fn set_diff_artifact(&mut self, diff_artifact: Option) { + self.diff_artifact = diff_artifact; + } + + pub fn add_touched_file(&mut self, file: TouchedFile) { + self.touched_files.push(file); + } + + pub fn set_rationale(&mut self, rationale: Option) { + self.rationale = rationale; + } + + pub fn set_apply_status(&mut self, apply_status: ApplyStatus) { + self.apply_status = apply_status; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_hash_hex() -> String { + IntegrityHash::compute(b"ai-process-test").to_hex() + } + + #[test] + fn test_patchset_creation() { + let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef); + let actor = ActorRef::agent("test-agent").expect("actor"); + let run_id = Uuid::from_u128(0x1); + let base_hash = test_hash_hex(); + + let patchset = PatchSet::new(repo_id, actor, run_id, &base_hash, 1).expect("patchset"); + + assert_eq!(patchset.header().object_type(), &AiObjectType::PatchSet); + assert_eq!(patchset.generation(), 1); + assert_eq!(patchset.diff_format(), &DiffFormat::UnifiedDiff); + assert_eq!(patchset.apply_status(), &ApplyStatus::Proposed); + assert!(patchset.touched_files().is_empty()); + } +} diff --git a/src/internal/object/plan.rs b/src/internal/object/plan.rs new file mode 100644 index 00000000..3aa6f837 --- /dev/null +++ b/src/internal/object/plan.rs @@ -0,0 +1,159 @@ +//! AI Plan Definition +//! +//! A `Plan` represents a sequence of steps that an agent intends to execute to complete a task. +//! +//! # Versioning +//! +//! Plans are versioned monotonically. As the agent learns more or encounters obstacles, +//! it may update the plan. Each update creates a new `Plan` object with `plan_version = previous + 1`. +//! +//! # Steps +//! +//! Each step has an `intent` (what to do) and a `status` (pending/in_progress/done). +//! Steps can also define expected inputs/outputs for better chain-of-thought tracking. + +use std::fmt; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::header::{ActorRef, AiObjectType, Header}; + +/// Plan step status. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum PlanStatus { + /// Step is waiting to be executed. + Pending, + /// Step is currently being executed. + InProgress, + /// Step finished successfully. + Completed, + /// Step failed. + Failed, + /// Step was skipped (e.g. no longer necessary). + Skipped, +} + +impl PlanStatus { + pub fn as_str(&self) -> &'static str { + match self { + PlanStatus::Pending => "pending", + PlanStatus::InProgress => "in_progress", + PlanStatus::Completed => "completed", + PlanStatus::Failed => "failed", + PlanStatus::Skipped => "skipped", + } + } +} + +impl fmt::Display for PlanStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +/// Plan step with inputs, outputs, and checks. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct PlanStep { + pub intent: String, + pub inputs: Option, + pub outputs: Option, + pub checks: Option, + pub owner_role: Option, + pub status: PlanStatus, +} + +/// Plan object for step decomposition. +/// New versions are created via `new_next` with monotonic versioning. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct Plan { + #[serde(flatten)] + header: Header, + run_id: Uuid, + /// Plan version starts at 1 and must increase by 1 for each update. + plan_version: u32, + #[serde(default)] + steps: Vec, +} + +impl Plan { + /// Create a new plan object (version 1) + pub fn new(repo_id: Uuid, created_by: ActorRef, run_id: Uuid) -> Result { + Ok(Self { + header: Header::new(AiObjectType::Plan, repo_id, created_by)?, + run_id, + plan_version: 1, + steps: Vec::new(), + }) + } + + /// Create the next version of a plan. + /// + /// # Arguments + /// * `previous_version` - The version number of the plan being updated. + pub fn new_next( + repo_id: Uuid, + created_by: ActorRef, + run_id: Uuid, + previous_version: u32, + ) -> Result { + let next_version = previous_version + .checked_add(1) + .ok_or_else(|| "plan_version overflow".to_string())?; + Ok(Self { + header: Header::new(AiObjectType::Plan, repo_id, created_by)?, + run_id, + plan_version: next_version, + steps: Vec::new(), + }) + } + + pub fn header(&self) -> &Header { + &self.header + } + + pub fn run_id(&self) -> Uuid { + self.run_id + } + + pub fn plan_version(&self) -> u32 { + self.plan_version + } + + pub fn steps(&self) -> &[PlanStep] { + &self.steps + } + + pub fn add_step(&mut self, step: PlanStep) { + self.steps.push(step); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_plan_version_ordering() { + let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef); + let actor = ActorRef::human("jackie").expect("actor"); + let run_id = Uuid::from_u128(0x1); + + let plan_v1 = Plan::new(repo_id, actor.clone(), run_id).expect("plan"); + let plan_v2 = + Plan::new_next(repo_id, actor.clone(), run_id, plan_v1.plan_version()).expect("plan"); + let plan_v3 = + Plan::new_next(repo_id, actor.clone(), run_id, plan_v2.plan_version()).expect("plan"); + + let mut plans = [plan_v2.clone(), plan_v1.clone(), plan_v3.clone()]; + plans.sort_by_key(|plan| plan.plan_version()); + + assert_eq!(plans[0].plan_version(), 1); + assert_eq!(plans[1].plan_version(), 2); + assert_eq!(plans[2].plan_version(), 3); + + assert!(plan_v3.plan_version() > plan_v2.plan_version()); + assert!(plan_v2.plan_version() > plan_v1.plan_version()); + } +} diff --git a/src/internal/object/provenance.rs b/src/internal/object/provenance.rs new file mode 100644 index 00000000..1bfffef4 --- /dev/null +++ b/src/internal/object/provenance.rs @@ -0,0 +1,100 @@ +//! AI Provenance Definition +//! +//! `Provenance` captures metadata about *how* a run was executed, specifically focusing on +//! the model (LLM) and provider configuration. +//! +//! # Usage +//! +//! This is critical for: +//! - **Reproducibility**: Knowing which model version produced a result. +//! - **Cost Accounting**: Tracking token usage per run. +//! - **Optimization**: Comparing performance across different models or parameters. + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::header::{ActorRef, AiObjectType, Header}; + +/// Provenance object for model/provider metadata. +/// Captures model/provider settings and usage. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Provenance { + #[serde(flatten)] + header: Header, + run_id: Uuid, + provider: String, + model: String, + parameters: Option, + token_usage: Option, +} + +impl Provenance { + pub fn new( + repo_id: Uuid, + created_by: ActorRef, + run_id: Uuid, + provider: impl Into, + model: impl Into, + ) -> Result { + Ok(Self { + header: Header::new(AiObjectType::Provenance, repo_id, created_by)?, + run_id, + provider: provider.into(), + model: model.into(), + parameters: None, + token_usage: None, + }) + } + + pub fn header(&self) -> &Header { + &self.header + } + + pub fn run_id(&self) -> Uuid { + self.run_id + } + + pub fn provider(&self) -> &str { + &self.provider + } + + pub fn model(&self) -> &str { + &self.model + } + + pub fn parameters(&self) -> Option<&serde_json::Value> { + self.parameters.as_ref() + } + + pub fn token_usage(&self) -> Option<&serde_json::Value> { + self.token_usage.as_ref() + } + + pub fn set_parameters(&mut self, parameters: Option) { + self.parameters = parameters; + } + + pub fn set_token_usage(&mut self, token_usage: Option) { + self.token_usage = token_usage; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_provenance_fields() { + let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef); + let actor = ActorRef::agent("test-agent").expect("actor"); + let run_id = Uuid::from_u128(0x1); + + let mut provenance = + Provenance::new(repo_id, actor, run_id, "openai", "gpt-4").expect("provenance"); + provenance.set_parameters(Some(serde_json::json!({"temperature": 0.2}))); + provenance.set_token_usage(Some(serde_json::json!({"input": 10, "output": 5}))); + + assert!(provenance.parameters().is_some()); + assert!(provenance.token_usage().is_some()); + } +} diff --git a/src/internal/object/run.rs b/src/internal/object/run.rs new file mode 100644 index 00000000..ca9d4793 --- /dev/null +++ b/src/internal/object/run.rs @@ -0,0 +1,229 @@ +//! AI Run Definition +//! +//! A `Run` represents a single execution instance of an AI agent attempting to perform a `Task`. +//! It captures the execution context (environment, agent role) and tracks the progress. +//! +//! # Relationship to Task +//! +//! A `Task` can have multiple `Run`s. This happens when: +//! - An agent fails and retries. +//! - A user requests a different approach. +//! - Multiple agents work on the same task in parallel (future). +//! +//! # Key Fields +//! +//! - `task_id`: Links back to the parent Task. +//! - `base_commit_sha`: The Git commit hash where this run started. +//! - `context_snapshot_id`: Links to the captured context (files, docs) used. + +use std::{collections::HashMap, fmt}; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::{ + header::{ActorRef, AiObjectType, Header}, + integrity::IntegrityHash, +}; + +/// Run lifecycle status. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum RunStatus { + /// Run created, agent not yet started. + Created, + /// Agent is generating patches. + Patching, + /// Agent is running verification tools. + Validating, + /// Agent has finished successfully. + Completed, + /// Agent encountered an unrecoverable error. + Failed, +} + +impl RunStatus { + pub fn as_str(&self) -> &'static str { + match self { + RunStatus::Created => "created", + RunStatus::Patching => "patching", + RunStatus::Validating => "validating", + RunStatus::Completed => "completed", + RunStatus::Failed => "failed", + } + } +} + +impl fmt::Display for RunStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +/// Environment snapshot of the run host. +/// Captured at run creation time. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Environment { + pub os: String, // e.g. "macos", "linux" + pub arch: String, // e.g. "aarch64", "x86_64" + pub cwd: String, // Current working directory + #[serde(flatten)] + pub extra: HashMap, +} + +impl Environment { + /// Create a new environment object from the current system environment + pub fn capture() -> Self { + Self { + os: std::env::consts::OS.to_string(), + arch: std::env::consts::ARCH.to_string(), + cwd: std::env::current_dir() + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_else(|e| { + tracing::warn!("Failed to get current directory: {}", e); + "unknown".to_string() + }), + extra: HashMap::new(), + } + } +} + +/// Agent instance participating in a run. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AgentInstance { + pub role: String, + pub provider_route: Option, +} + +/// Run object for a single orchestration execution. +/// Links a task to execution state and environment. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Run { + #[serde(flatten)] + header: Header, + task_id: Uuid, + orchestrator_version: String, + base_commit_sha: IntegrityHash, + status: RunStatus, + context_snapshot_id: Option, + #[serde(default)] + agent_instances: Vec, + metrics: Option, + error: Option, + environment: Option, +} + +impl Run { + /// Create a new Run. + /// + /// # Arguments + /// * `repo_id` - Repository UUID + /// * `created_by` - Actor (usually the Orchestrator) + /// * `task_id` - The Task this run belongs to + /// * `base_commit_sha` - The Git commit hash of the checkout + pub fn new( + repo_id: Uuid, + created_by: ActorRef, + task_id: Uuid, + base_commit_sha: impl AsRef, + ) -> Result { + let base_commit_sha = base_commit_sha.as_ref().parse()?; + Ok(Self { + header: Header::new(AiObjectType::Run, repo_id, created_by)?, + task_id, + orchestrator_version: "libra-builtin".to_string(), + base_commit_sha, + status: RunStatus::Created, + context_snapshot_id: None, + agent_instances: Vec::new(), + metrics: None, + error: None, + environment: Some(Environment::capture()), + }) + } + + pub fn header(&self) -> &Header { + &self.header + } + + pub fn task_id(&self) -> Uuid { + self.task_id + } + + pub fn orchestrator_version(&self) -> &str { + &self.orchestrator_version + } + + pub fn base_commit_sha(&self) -> &IntegrityHash { + &self.base_commit_sha + } + + pub fn status(&self) -> &RunStatus { + &self.status + } + + pub fn context_snapshot_id(&self) -> Option { + self.context_snapshot_id + } + + pub fn agent_instances(&self) -> &[AgentInstance] { + &self.agent_instances + } + + pub fn metrics(&self) -> Option<&serde_json::Value> { + self.metrics.as_ref() + } + + pub fn error(&self) -> Option<&str> { + self.error.as_deref() + } + + pub fn environment(&self) -> Option<&Environment> { + self.environment.as_ref() + } + + pub fn set_status(&mut self, status: RunStatus) { + self.status = status; + } + + pub fn set_context_snapshot_id(&mut self, context_snapshot_id: Option) { + self.context_snapshot_id = context_snapshot_id; + } + + pub fn add_agent_instance(&mut self, instance: AgentInstance) { + self.agent_instances.push(instance); + } + + pub fn set_metrics(&mut self, metrics: Option) { + self.metrics = metrics; + } + + pub fn set_error(&mut self, error: Option) { + self.error = error; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_hash_hex() -> String { + IntegrityHash::compute(b"ai-process-test").to_hex() + } + + #[test] + fn test_new_objects_creation() { + let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef); + let actor = ActorRef::agent("test-agent").expect("actor"); + let base_hash = test_hash_hex(); + + // Run with environment (auto captured) + let run = Run::new(repo_id, actor.clone(), Uuid::from_u128(0x1), &base_hash).expect("run"); + + let env = run.environment().unwrap(); + // Check if it captured real values (assuming we are running on some OS) + assert!(!env.os.is_empty()); + assert!(!env.arch.is_empty()); + assert!(!env.cwd.is_empty()); + } +} diff --git a/src/internal/object/task.rs b/src/internal/object/task.rs new file mode 100644 index 00000000..66598054 --- /dev/null +++ b/src/internal/object/task.rs @@ -0,0 +1,289 @@ +//! AI Task Definition +//! +//! A `Task` represents a unit of work to be performed by an AI agent. +//! It serves as the root of the AI workflow, defining intent, constraints, and success criteria. +//! +//! # Lifecycle +//! +//! 1. **Draft**: Initial state. Task is being defined. +//! 2. **Running**: An agent (via a `Run` object) has started working on it. +//! 3. **Done**: Work is completed and verified. +//! 4. **Failed**: Work could not be completed. +//! 5. **Cancelled**: User aborted the task. +//! +//! # Relationships +//! +//! - **Parent**: None (Root object). +//! - **Children**: `Run` (1-to-many). A task can have multiple runs (retries). +//! - **Dependencies**: Can depend on other Tasks via `dependencies`. +//! +//! # Example +//! +//! ```rust +//! use git_internal::internal::object::task::{Task, GoalType}; +//! use git_internal::internal::object::header::ActorRef; +//! use uuid::Uuid; +//! +//! let repo_id = Uuid::new_v4(); +//! let actor = ActorRef::human("user").unwrap(); +//! let mut task = Task::new(repo_id, actor, "Refactor Login", Some(GoalType::Refactor)).unwrap(); +//! +//! task.add_constraint("Must use JWT"); +//! task.add_acceptance_criterion("All tests pass"); +//! ``` + +use std::{fmt, str::FromStr}; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::header::{ActorRef, AiObjectType, Header}; + +/// Task lifecycle status. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum TaskStatus { + /// Initial state, definition in progress. + Draft, + /// Agent is actively working on this task. + Running, + /// Task completed successfully. + Done, + /// Task failed to complete. + Failed, + /// Task was cancelled by user. + Cancelled, +} + +impl TaskStatus { + pub fn as_str(&self) -> &'static str { + match self { + TaskStatus::Draft => "draft", + TaskStatus::Running => "running", + TaskStatus::Done => "done", + TaskStatus::Failed => "failed", + TaskStatus::Cancelled => "cancelled", + } + } +} + +impl fmt::Display for TaskStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +/// Task goal category. +/// +/// Helps agents understand the nature of the work. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum GoalType { + Feature, + Bugfix, + Refactor, + Docs, + Perf, + Test, + Chore, + Build, + Ci, + Style, +} + +impl GoalType { + pub fn as_str(&self) -> &'static str { + match self { + GoalType::Feature => "feature", + GoalType::Bugfix => "bugfix", + GoalType::Refactor => "refactor", + GoalType::Docs => "docs", + GoalType::Perf => "perf", + GoalType::Test => "test", + GoalType::Chore => "chore", + GoalType::Build => "build", + GoalType::Ci => "ci", + GoalType::Style => "style", + } + } +} + +impl fmt::Display for GoalType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl FromStr for GoalType { + type Err = String; + + fn from_str(value: &str) -> Result { + match value { + "feature" => Ok(GoalType::Feature), + "bugfix" => Ok(GoalType::Bugfix), + "refactor" => Ok(GoalType::Refactor), + "docs" => Ok(GoalType::Docs), + "perf" => Ok(GoalType::Perf), + "test" => Ok(GoalType::Test), + "chore" => Ok(GoalType::Chore), + "build" => Ok(GoalType::Build), + "ci" => Ok(GoalType::Ci), + "style" => Ok(GoalType::Style), + _ => Err(format!("Invalid goal_type: {}", value)), + } + } +} + +/// Task object describing intent and constraints. +/// Typically created first, then referenced by Run objects. +/// +/// See module documentation for lifecycle details. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Task { + #[serde(flatten)] + header: Header, + title: String, + description: Option, + goal_type: Option, + #[serde(default)] + constraints: Vec, + #[serde(default)] + acceptance_criteria: Vec, + requested_by: Option, + #[serde(default)] + dependencies: Vec, + status: TaskStatus, +} + +impl Task { + /// Create a new Task. + /// + /// # Arguments + /// * `repo_id` - Repository UUID + /// * `created_by` - Actor creating the task + /// * `title` - Short summary of the task + /// * `goal_type` - Optional classification (Feature, Bugfix, etc.) + pub fn new( + repo_id: Uuid, + created_by: ActorRef, + title: impl Into, + goal_type: Option, + ) -> Result { + Ok(Self { + header: Header::new(AiObjectType::Task, repo_id, created_by)?, + title: title.into(), + description: None, + goal_type, + constraints: Vec::new(), + acceptance_criteria: Vec::new(), + requested_by: None, + dependencies: Vec::new(), + status: TaskStatus::Draft, + }) + } + + pub fn header(&self) -> &Header { + &self.header + } + + pub fn title(&self) -> &str { + &self.title + } + + pub fn description(&self) -> Option<&str> { + self.description.as_deref() + } + + pub fn goal_type(&self) -> Option<&GoalType> { + self.goal_type.as_ref() + } + + pub fn constraints(&self) -> &[String] { + &self.constraints + } + + pub fn acceptance_criteria(&self) -> &[String] { + &self.acceptance_criteria + } + + pub fn requested_by(&self) -> Option<&ActorRef> { + self.requested_by.as_ref() + } + + pub fn dependencies(&self) -> &[Uuid] { + &self.dependencies + } + + pub fn status(&self) -> &TaskStatus { + &self.status + } + + pub fn set_description(&mut self, description: Option) { + self.description = description; + } + + pub fn add_constraint(&mut self, constraint: impl Into) { + self.constraints.push(constraint.into()); + } + + pub fn add_acceptance_criterion(&mut self, criterion: impl Into) { + self.acceptance_criteria.push(criterion.into()); + } + + pub fn set_requested_by(&mut self, requested_by: Option) { + self.requested_by = requested_by; + } + + pub fn add_dependency(&mut self, task_id: Uuid) { + self.dependencies.push(task_id); + } + + pub fn set_status(&mut self, status: TaskStatus) { + self.status = status; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::internal::object::header::ActorKind; + + #[test] + fn test_task_creation() { + let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef); + let actor = ActorRef::human("jackie").expect("actor"); + let mut task = Task::new(repo_id, actor, "Fix bug", Some(GoalType::Bugfix)).expect("task"); + + // Test dependencies + let dep_id = Uuid::from_u128(0x00000000000000000000000000000001); + task.add_dependency(dep_id); + + assert_eq!(task.header().object_type(), &AiObjectType::Task); + assert_eq!(task.status(), &TaskStatus::Draft); + assert_eq!(task.goal_type(), Some(&GoalType::Bugfix)); + assert_eq!(task.dependencies().len(), 1); + assert_eq!(task.dependencies()[0], dep_id); + } + + #[test] + fn test_task_goal_type_optional() { + let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef); + let actor = ActorRef::human("jackie").expect("actor"); + let task = Task::new(repo_id, actor, "Write docs", None).expect("task"); + + assert!(task.goal_type().is_none()); + } + + #[test] + fn test_task_requested_by() { + let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef); + let actor = ActorRef::human("jackie").expect("actor"); + let mut task = + Task::new(repo_id, actor.clone(), "Fix bug", Some(GoalType::Bugfix)).expect("task"); + + task.set_requested_by(Some(ActorRef::mcp_client("vscode-client").expect("actor"))); + + assert!(task.requested_by().is_some()); + assert_eq!(task.requested_by().unwrap().kind(), &ActorKind::McpClient); + } +} diff --git a/src/internal/object/tool.rs b/src/internal/object/tool.rs new file mode 100644 index 00000000..6fa1ad08 --- /dev/null +++ b/src/internal/object/tool.rs @@ -0,0 +1,195 @@ +//! AI Tool Invocation Definition +//! +//! A `ToolInvocation` records a specific action taken by an agent, such as reading a file, +//! running a command, or querying a search engine. +//! +//! # Purpose +//! +//! - **Audit Trail**: Allows reconstructing exactly what the agent did. +//! - **Cost Tracking**: Can be used to calculate token/resource usage. +//! - **Debugging**: Helps understand why an agent made a particular decision. +//! +//! # Fields +//! +//! - `tool_name`: The identifier of the tool (e.g., "read_file"). +//! - `args`: JSON arguments passed to the tool. +//! - `io_footprint`: Files read/written during the operation (for dependency tracking). +//! - `status`: Whether the tool call succeeded or failed. + +use std::fmt; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::header::{ActorRef, AiObjectType, ArtifactRef, Header}; + +/// Tool invocation status. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ToolStatus { + /// Tool executed successfully. + Ok, + /// Tool execution failed (returned error). + Error, +} + +impl ToolStatus { + pub fn as_str(&self) -> &'static str { + match self { + ToolStatus::Ok => "ok", + ToolStatus::Error => "error", + } + } +} + +impl fmt::Display for ToolStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +/// IO footprint of a tool invocation. +/// Tracks reads and writes for auditability. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IoFootprint { + #[serde(default)] + pub paths_read: Vec, + #[serde(default)] + pub paths_written: Vec, +} + +/// Tool invocation record. +/// Records a single tool call within a run. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolInvocation { + #[serde(flatten)] + header: Header, + run_id: Uuid, + tool_name: String, + io_footprint: Option, + #[serde(default)] + args: serde_json::Value, + status: ToolStatus, + result_summary: Option, + #[serde(default)] + artifacts: Vec, +} + +impl ToolInvocation { + pub fn new( + repo_id: Uuid, + created_by: ActorRef, + run_id: Uuid, + tool_name: impl Into, + ) -> Result { + Ok(Self { + header: Header::new(AiObjectType::ToolInvocation, repo_id, created_by)?, + run_id, + tool_name: tool_name.into(), + io_footprint: None, + args: serde_json::Value::Null, + status: ToolStatus::Ok, + result_summary: None, + artifacts: Vec::new(), + }) + } + + pub fn header(&self) -> &Header { + &self.header + } + + pub fn run_id(&self) -> Uuid { + self.run_id + } + + pub fn tool_name(&self) -> &str { + &self.tool_name + } + + pub fn io_footprint(&self) -> Option<&IoFootprint> { + self.io_footprint.as_ref() + } + + pub fn args(&self) -> &serde_json::Value { + &self.args + } + + pub fn status(&self) -> &ToolStatus { + &self.status + } + + pub fn result_summary(&self) -> Option<&str> { + self.result_summary.as_deref() + } + + pub fn artifacts(&self) -> &[ArtifactRef] { + &self.artifacts + } + + pub fn set_io_footprint(&mut self, io_footprint: Option) { + self.io_footprint = io_footprint; + } + + pub fn set_args(&mut self, args: serde_json::Value) { + self.args = args; + } + + pub fn set_status(&mut self, status: ToolStatus) { + self.status = status; + } + + pub fn set_result_summary(&mut self, result_summary: Option) { + self.result_summary = result_summary; + } + + pub fn add_artifact(&mut self, artifact: ArtifactRef) { + self.artifacts.push(artifact); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tool_invocation_io_footprint() { + let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef); + let actor = ActorRef::human("jackie").expect("actor"); + let run_id = Uuid::from_u128(0x1); + + let mut tool_inv = + ToolInvocation::new(repo_id, actor, run_id, "read_file").expect("tool_invocation"); + + let footprint = IoFootprint { + paths_read: vec!["src/main.rs".to_string()], + paths_written: vec![], + }; + + tool_inv.set_io_footprint(Some(footprint)); + + assert_eq!(tool_inv.tool_name(), "read_file"); + assert!(tool_inv.io_footprint().is_some()); + assert_eq!( + tool_inv.io_footprint().unwrap().paths_read[0], + "src/main.rs" + ); + } + + #[test] + fn test_tool_invocation_fields() { + let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef); + let actor = ActorRef::human("jackie").expect("actor"); + let run_id = Uuid::from_u128(0x1); + + let mut tool_inv = + ToolInvocation::new(repo_id, actor, run_id, "apply_patch").expect("tool_invocation"); + tool_inv.set_status(ToolStatus::Error); + tool_inv.set_args(serde_json::json!({"path": "src/lib.rs"})); + tool_inv.set_result_summary(Some("failed".to_string())); + tool_inv.add_artifact(ArtifactRef::new("local", "artifact-key").expect("artifact")); + + assert_eq!(tool_inv.status(), &ToolStatus::Error); + assert_eq!(tool_inv.artifacts().len(), 1); + assert_eq!(tool_inv.args()["path"], "src/lib.rs"); + } +} diff --git a/src/lib.rs b/src/lib.rs index 28c094ef..c826f3e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,14 +30,13 @@ //! Test Data //! - Located under `tests/data/`, includes real pack files and object sets. +mod delta; pub mod diff; pub mod errors; pub mod hash; pub mod internal; pub mod protocol; pub mod utils; - -mod delta; mod zstdelta; // Core traits and types that external users need to implement/use diff --git a/third-party/rust/crates/ring/0.17.14/BUCK b/third-party/rust/crates/ring/0.17.14/BUCK new file mode 100644 index 00000000..428bcc53 --- /dev/null +++ b/third-party/rust/crates/ring/0.17.14/BUCK @@ -0,0 +1,78 @@ +# @generated by `cargo buckal` + +load("@buckal//:cargo_manifest.bzl", "cargo_manifest") +load("@buckal//:wrapper.bzl", "buildscript_run", "rust_binary", "rust_library") + +http_archive( + name = "ring-vendor", + urls = ["https://static.crates.io/crates/ring/ring-0.17.14.crate"], + sha256 = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7", + type = "tar.gz", + strip_prefix = "ring-0.17.14", + out = "vendor", +) + +cargo_manifest( + name = "ring-manifest", + vendor = ":ring-vendor", +) + +rust_library( + name = "ring", + srcs = [":ring-vendor"], + crate = "ring", + crate_root = "vendor/src/lib.rs", + edition = "2021", + env = { + "OUT_DIR": "$(location :ring-build-script-run[out_dir])", + }, + features = [ + "alloc", + "default", + "dev_urandom_fallback", + ], + rustc_flags = [ + "@$(location :ring-build-script-run[rustc_flags])", + "@$(location :ring-manifest[env_flags])", + ], + os_deps = { + "macos": ["//third-party/rust/crates/libc/0.2.180:libc"], + }, + visibility = ["PUBLIC"], + deps = [ + "//third-party/rust/crates/cfg-if/1.0.4:cfg-if", + "//third-party/rust/crates/getrandom/0.2.16:getrandom", + "//third-party/rust/crates/untrusted/0.9.0:untrusted", + ], +) + +rust_binary( + name = "ring-build-script-build", + srcs = [":ring-vendor"], + crate = "build_script_build", + crate_root = "vendor/build.rs", + edition = "2021", + features = [ + "alloc", + "default", + "dev_urandom_fallback", + ], + rustc_flags = ["@$(location :ring-manifest[env_flags])"], + visibility = [], + deps = ["//third-party/rust/crates/cc/1.2.49:cc"], +) + +buildscript_run( + name = "ring-build-script-run", + package_name = "ring", + buildscript_rule = ":ring-build-script-build", + env_srcs = [":ring-manifest[env_dict]"], + features = [ + "alloc", + "default", + "dev_urandom_fallback", + ], + version = "0.17.14", + manifest_dir = ":ring-vendor", + visibility = ["PUBLIC"], +) diff --git a/third-party/rust/crates/untrusted/0.9.0/BUCK b/third-party/rust/crates/untrusted/0.9.0/BUCK new file mode 100644 index 00000000..b775a55d --- /dev/null +++ b/third-party/rust/crates/untrusted/0.9.0/BUCK @@ -0,0 +1,28 @@ +# @generated by `cargo buckal` + +load("@buckal//:cargo_manifest.bzl", "cargo_manifest") +load("@buckal//:wrapper.bzl", "rust_library") + +http_archive( + name = "untrusted-vendor", + urls = ["https://static.crates.io/crates/untrusted/untrusted-0.9.0.crate"], + sha256 = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1", + type = "tar.gz", + strip_prefix = "untrusted-0.9.0", + out = "vendor", +) + +cargo_manifest( + name = "untrusted-manifest", + vendor = ":untrusted-vendor", +) + +rust_library( + name = "untrusted", + srcs = [":untrusted-vendor"], + crate = "untrusted", + crate_root = "vendor/src/lib.rs", + edition = "2018", + rustc_flags = ["@$(location :untrusted-manifest[env_flags])"], + visibility = ["PUBLIC"], +) diff --git a/third-party/rust/crates/uuid/1.19.0/BUCK b/third-party/rust/crates/uuid/1.19.0/BUCK index 132f8c48..c7de75ba 100644 --- a/third-party/rust/crates/uuid/1.19.0/BUCK +++ b/third-party/rust/crates/uuid/1.19.0/BUCK @@ -29,6 +29,7 @@ rust_library( "serde", "std", "v4", + "v7", ], rustc_flags = ["@$(location :uuid-manifest[env_flags])"], os_deps = { diff --git a/third-party/rust/crates/windows-sys/0.52.0/BUCK b/third-party/rust/crates/windows-sys/0.52.0/BUCK new file mode 100644 index 00000000..44ba9837 --- /dev/null +++ b/third-party/rust/crates/windows-sys/0.52.0/BUCK @@ -0,0 +1,36 @@ +# @generated by `cargo buckal` + +load("@buckal//:cargo_manifest.bzl", "cargo_manifest") +load("@buckal//:wrapper.bzl", "rust_library") + +http_archive( + name = "windows-sys-vendor", + urls = ["https://static.crates.io/crates/windows-sys/windows-sys-0.52.0.crate"], + sha256 = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d", + type = "tar.gz", + strip_prefix = "windows-sys-0.52.0", + out = "vendor", +) + +cargo_manifest( + name = "windows-sys-manifest", + vendor = ":windows-sys-vendor", +) + +rust_library( + name = "windows-sys", + srcs = [":windows-sys-vendor"], + crate = "windows_sys", + crate_root = "vendor/src/lib.rs", + edition = "2021", + features = [ + "Win32", + "Win32_Foundation", + "Win32_System", + "Win32_System_Threading", + "default", + ], + rustc_flags = ["@$(location :windows-sys-manifest[env_flags])"], + visibility = ["PUBLIC"], + deps = ["//third-party/rust/crates/windows-targets/0.52.6:windows-targets"], +) diff --git a/third-party/rust/crates/windows-targets/0.52.6/BUCK b/third-party/rust/crates/windows-targets/0.52.6/BUCK new file mode 100644 index 00000000..e43c8168 --- /dev/null +++ b/third-party/rust/crates/windows-targets/0.52.6/BUCK @@ -0,0 +1,37 @@ +# @generated by `cargo buckal` + +load("@buckal//:cargo_manifest.bzl", "cargo_manifest") +load("@buckal//:wrapper.bzl", "rust_library") + +http_archive( + name = "windows-targets-vendor", + urls = ["https://static.crates.io/crates/windows-targets/windows-targets-0.52.6.crate"], + sha256 = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973", + type = "tar.gz", + strip_prefix = "windows-targets-0.52.6", + out = "vendor", +) + +cargo_manifest( + name = "windows-targets-manifest", + vendor = ":windows-targets-vendor", +) + +rust_library( + name = "windows-targets", + srcs = [":windows-targets-vendor"], + crate = "windows_targets", + crate_root = "vendor/src/lib.rs", + edition = "2021", + rustc_flags = ["@$(location :windows-targets-manifest[env_flags])"], + os_deps = { + "linux": ["//third-party/rust/crates/windows_x86_64_gnu/0.52.6:windows_x86_64_gnu"], + "windows": ["//third-party/rust/crates/windows_x86_64_msvc/0.52.6:windows_x86_64_msvc"], + }, + visibility = ["PUBLIC"], + deps = [ + "//third-party/rust/crates/windows_aarch64_msvc/0.52.6:windows_aarch64_msvc", + "//third-party/rust/crates/windows_i686_gnu/0.52.6:windows_i686_gnu", + "//third-party/rust/crates/windows_i686_msvc/0.52.6:windows_i686_msvc", + ], +) diff --git a/third-party/rust/crates/windows_aarch64_gnullvm/0.52.6/BUCK b/third-party/rust/crates/windows_aarch64_gnullvm/0.52.6/BUCK new file mode 100644 index 00000000..45e21692 --- /dev/null +++ b/third-party/rust/crates/windows_aarch64_gnullvm/0.52.6/BUCK @@ -0,0 +1,54 @@ +# @generated by `cargo buckal` + +load("@buckal//:cargo_manifest.bzl", "cargo_manifest") +load("@buckal//:wrapper.bzl", "buildscript_run", "rust_binary", "rust_library") + +http_archive( + name = "windows_aarch64_gnullvm-vendor", + urls = ["https://static.crates.io/crates/windows_aarch64_gnullvm/windows_aarch64_gnullvm-0.52.6.crate"], + sha256 = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3", + type = "tar.gz", + strip_prefix = "windows_aarch64_gnullvm-0.52.6", + out = "vendor", +) + +cargo_manifest( + name = "windows_aarch64_gnullvm-manifest", + vendor = ":windows_aarch64_gnullvm-vendor", +) + +rust_library( + name = "windows_aarch64_gnullvm", + srcs = [":windows_aarch64_gnullvm-vendor"], + crate = "windows_aarch64_gnullvm", + crate_root = "vendor/src/lib.rs", + edition = "2021", + env = { + "OUT_DIR": "$(location :windows_aarch64_gnullvm-build-script-run[out_dir])", + }, + rustc_flags = [ + "@$(location :windows_aarch64_gnullvm-build-script-run[rustc_flags])", + "@$(location :windows_aarch64_gnullvm-manifest[env_flags])", + ], + visibility = ["PUBLIC"], +) + +rust_binary( + name = "windows_aarch64_gnullvm-build-script-build", + srcs = [":windows_aarch64_gnullvm-vendor"], + crate = "build_script_build", + crate_root = "vendor/build.rs", + edition = "2021", + rustc_flags = ["@$(location :windows_aarch64_gnullvm-manifest[env_flags])"], + visibility = [], +) + +buildscript_run( + name = "windows_aarch64_gnullvm-build-script-run", + package_name = "windows_aarch64_gnullvm", + buildscript_rule = ":windows_aarch64_gnullvm-build-script-build", + env_srcs = [":windows_aarch64_gnullvm-manifest[env_dict]"], + version = "0.52.6", + manifest_dir = ":windows_aarch64_gnullvm-vendor", + visibility = ["PUBLIC"], +) diff --git a/third-party/rust/crates/windows_aarch64_msvc/0.52.6/BUCK b/third-party/rust/crates/windows_aarch64_msvc/0.52.6/BUCK new file mode 100644 index 00000000..66392f07 --- /dev/null +++ b/third-party/rust/crates/windows_aarch64_msvc/0.52.6/BUCK @@ -0,0 +1,54 @@ +# @generated by `cargo buckal` + +load("@buckal//:cargo_manifest.bzl", "cargo_manifest") +load("@buckal//:wrapper.bzl", "buildscript_run", "rust_binary", "rust_library") + +http_archive( + name = "windows_aarch64_msvc-vendor", + urls = ["https://static.crates.io/crates/windows_aarch64_msvc/windows_aarch64_msvc-0.52.6.crate"], + sha256 = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469", + type = "tar.gz", + strip_prefix = "windows_aarch64_msvc-0.52.6", + out = "vendor", +) + +cargo_manifest( + name = "windows_aarch64_msvc-manifest", + vendor = ":windows_aarch64_msvc-vendor", +) + +rust_library( + name = "windows_aarch64_msvc", + srcs = [":windows_aarch64_msvc-vendor"], + crate = "windows_aarch64_msvc", + crate_root = "vendor/src/lib.rs", + edition = "2021", + env = { + "OUT_DIR": "$(location :windows_aarch64_msvc-build-script-run[out_dir])", + }, + rustc_flags = [ + "@$(location :windows_aarch64_msvc-build-script-run[rustc_flags])", + "@$(location :windows_aarch64_msvc-manifest[env_flags])", + ], + visibility = ["PUBLIC"], +) + +rust_binary( + name = "windows_aarch64_msvc-build-script-build", + srcs = [":windows_aarch64_msvc-vendor"], + crate = "build_script_build", + crate_root = "vendor/build.rs", + edition = "2021", + rustc_flags = ["@$(location :windows_aarch64_msvc-manifest[env_flags])"], + visibility = [], +) + +buildscript_run( + name = "windows_aarch64_msvc-build-script-run", + package_name = "windows_aarch64_msvc", + buildscript_rule = ":windows_aarch64_msvc-build-script-build", + env_srcs = [":windows_aarch64_msvc-manifest[env_dict]"], + version = "0.52.6", + manifest_dir = ":windows_aarch64_msvc-vendor", + visibility = ["PUBLIC"], +) diff --git a/third-party/rust/crates/windows_i686_gnu/0.52.6/BUCK b/third-party/rust/crates/windows_i686_gnu/0.52.6/BUCK new file mode 100644 index 00000000..f51a5d72 --- /dev/null +++ b/third-party/rust/crates/windows_i686_gnu/0.52.6/BUCK @@ -0,0 +1,54 @@ +# @generated by `cargo buckal` + +load("@buckal//:cargo_manifest.bzl", "cargo_manifest") +load("@buckal//:wrapper.bzl", "buildscript_run", "rust_binary", "rust_library") + +http_archive( + name = "windows_i686_gnu-vendor", + urls = ["https://static.crates.io/crates/windows_i686_gnu/windows_i686_gnu-0.52.6.crate"], + sha256 = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b", + type = "tar.gz", + strip_prefix = "windows_i686_gnu-0.52.6", + out = "vendor", +) + +cargo_manifest( + name = "windows_i686_gnu-manifest", + vendor = ":windows_i686_gnu-vendor", +) + +rust_library( + name = "windows_i686_gnu", + srcs = [":windows_i686_gnu-vendor"], + crate = "windows_i686_gnu", + crate_root = "vendor/src/lib.rs", + edition = "2021", + env = { + "OUT_DIR": "$(location :windows_i686_gnu-build-script-run[out_dir])", + }, + rustc_flags = [ + "@$(location :windows_i686_gnu-build-script-run[rustc_flags])", + "@$(location :windows_i686_gnu-manifest[env_flags])", + ], + visibility = ["PUBLIC"], +) + +rust_binary( + name = "windows_i686_gnu-build-script-build", + srcs = [":windows_i686_gnu-vendor"], + crate = "build_script_build", + crate_root = "vendor/build.rs", + edition = "2021", + rustc_flags = ["@$(location :windows_i686_gnu-manifest[env_flags])"], + visibility = [], +) + +buildscript_run( + name = "windows_i686_gnu-build-script-run", + package_name = "windows_i686_gnu", + buildscript_rule = ":windows_i686_gnu-build-script-build", + env_srcs = [":windows_i686_gnu-manifest[env_dict]"], + version = "0.52.6", + manifest_dir = ":windows_i686_gnu-vendor", + visibility = ["PUBLIC"], +) diff --git a/third-party/rust/crates/windows_i686_gnullvm/0.52.6/BUCK b/third-party/rust/crates/windows_i686_gnullvm/0.52.6/BUCK new file mode 100644 index 00000000..3841cce8 --- /dev/null +++ b/third-party/rust/crates/windows_i686_gnullvm/0.52.6/BUCK @@ -0,0 +1,54 @@ +# @generated by `cargo buckal` + +load("@buckal//:cargo_manifest.bzl", "cargo_manifest") +load("@buckal//:wrapper.bzl", "buildscript_run", "rust_binary", "rust_library") + +http_archive( + name = "windows_i686_gnullvm-vendor", + urls = ["https://static.crates.io/crates/windows_i686_gnullvm/windows_i686_gnullvm-0.52.6.crate"], + sha256 = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66", + type = "tar.gz", + strip_prefix = "windows_i686_gnullvm-0.52.6", + out = "vendor", +) + +cargo_manifest( + name = "windows_i686_gnullvm-manifest", + vendor = ":windows_i686_gnullvm-vendor", +) + +rust_library( + name = "windows_i686_gnullvm", + srcs = [":windows_i686_gnullvm-vendor"], + crate = "windows_i686_gnullvm", + crate_root = "vendor/src/lib.rs", + edition = "2021", + env = { + "OUT_DIR": "$(location :windows_i686_gnullvm-build-script-run[out_dir])", + }, + rustc_flags = [ + "@$(location :windows_i686_gnullvm-build-script-run[rustc_flags])", + "@$(location :windows_i686_gnullvm-manifest[env_flags])", + ], + visibility = ["PUBLIC"], +) + +rust_binary( + name = "windows_i686_gnullvm-build-script-build", + srcs = [":windows_i686_gnullvm-vendor"], + crate = "build_script_build", + crate_root = "vendor/build.rs", + edition = "2021", + rustc_flags = ["@$(location :windows_i686_gnullvm-manifest[env_flags])"], + visibility = [], +) + +buildscript_run( + name = "windows_i686_gnullvm-build-script-run", + package_name = "windows_i686_gnullvm", + buildscript_rule = ":windows_i686_gnullvm-build-script-build", + env_srcs = [":windows_i686_gnullvm-manifest[env_dict]"], + version = "0.52.6", + manifest_dir = ":windows_i686_gnullvm-vendor", + visibility = ["PUBLIC"], +) diff --git a/third-party/rust/crates/windows_i686_msvc/0.52.6/BUCK b/third-party/rust/crates/windows_i686_msvc/0.52.6/BUCK new file mode 100644 index 00000000..f5d176ed --- /dev/null +++ b/third-party/rust/crates/windows_i686_msvc/0.52.6/BUCK @@ -0,0 +1,54 @@ +# @generated by `cargo buckal` + +load("@buckal//:cargo_manifest.bzl", "cargo_manifest") +load("@buckal//:wrapper.bzl", "buildscript_run", "rust_binary", "rust_library") + +http_archive( + name = "windows_i686_msvc-vendor", + urls = ["https://static.crates.io/crates/windows_i686_msvc/windows_i686_msvc-0.52.6.crate"], + sha256 = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66", + type = "tar.gz", + strip_prefix = "windows_i686_msvc-0.52.6", + out = "vendor", +) + +cargo_manifest( + name = "windows_i686_msvc-manifest", + vendor = ":windows_i686_msvc-vendor", +) + +rust_library( + name = "windows_i686_msvc", + srcs = [":windows_i686_msvc-vendor"], + crate = "windows_i686_msvc", + crate_root = "vendor/src/lib.rs", + edition = "2021", + env = { + "OUT_DIR": "$(location :windows_i686_msvc-build-script-run[out_dir])", + }, + rustc_flags = [ + "@$(location :windows_i686_msvc-build-script-run[rustc_flags])", + "@$(location :windows_i686_msvc-manifest[env_flags])", + ], + visibility = ["PUBLIC"], +) + +rust_binary( + name = "windows_i686_msvc-build-script-build", + srcs = [":windows_i686_msvc-vendor"], + crate = "build_script_build", + crate_root = "vendor/build.rs", + edition = "2021", + rustc_flags = ["@$(location :windows_i686_msvc-manifest[env_flags])"], + visibility = [], +) + +buildscript_run( + name = "windows_i686_msvc-build-script-run", + package_name = "windows_i686_msvc", + buildscript_rule = ":windows_i686_msvc-build-script-build", + env_srcs = [":windows_i686_msvc-manifest[env_dict]"], + version = "0.52.6", + manifest_dir = ":windows_i686_msvc-vendor", + visibility = ["PUBLIC"], +) diff --git a/third-party/rust/crates/windows_x86_64_gnu/0.52.6/BUCK b/third-party/rust/crates/windows_x86_64_gnu/0.52.6/BUCK new file mode 100644 index 00000000..ba592deb --- /dev/null +++ b/third-party/rust/crates/windows_x86_64_gnu/0.52.6/BUCK @@ -0,0 +1,54 @@ +# @generated by `cargo buckal` + +load("@buckal//:cargo_manifest.bzl", "cargo_manifest") +load("@buckal//:wrapper.bzl", "buildscript_run", "rust_binary", "rust_library") + +http_archive( + name = "windows_x86_64_gnu-vendor", + urls = ["https://static.crates.io/crates/windows_x86_64_gnu/windows_x86_64_gnu-0.52.6.crate"], + sha256 = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78", + type = "tar.gz", + strip_prefix = "windows_x86_64_gnu-0.52.6", + out = "vendor", +) + +cargo_manifest( + name = "windows_x86_64_gnu-manifest", + vendor = ":windows_x86_64_gnu-vendor", +) + +rust_library( + name = "windows_x86_64_gnu", + srcs = [":windows_x86_64_gnu-vendor"], + crate = "windows_x86_64_gnu", + crate_root = "vendor/src/lib.rs", + edition = "2021", + env = { + "OUT_DIR": "$(location :windows_x86_64_gnu-build-script-run[out_dir])", + }, + rustc_flags = [ + "@$(location :windows_x86_64_gnu-build-script-run[rustc_flags])", + "@$(location :windows_x86_64_gnu-manifest[env_flags])", + ], + visibility = ["PUBLIC"], +) + +rust_binary( + name = "windows_x86_64_gnu-build-script-build", + srcs = [":windows_x86_64_gnu-vendor"], + crate = "build_script_build", + crate_root = "vendor/build.rs", + edition = "2021", + rustc_flags = ["@$(location :windows_x86_64_gnu-manifest[env_flags])"], + visibility = [], +) + +buildscript_run( + name = "windows_x86_64_gnu-build-script-run", + package_name = "windows_x86_64_gnu", + buildscript_rule = ":windows_x86_64_gnu-build-script-build", + env_srcs = [":windows_x86_64_gnu-manifest[env_dict]"], + version = "0.52.6", + manifest_dir = ":windows_x86_64_gnu-vendor", + visibility = ["PUBLIC"], +) diff --git a/third-party/rust/crates/windows_x86_64_gnullvm/0.52.6/BUCK b/third-party/rust/crates/windows_x86_64_gnullvm/0.52.6/BUCK new file mode 100644 index 00000000..2bc875c6 --- /dev/null +++ b/third-party/rust/crates/windows_x86_64_gnullvm/0.52.6/BUCK @@ -0,0 +1,54 @@ +# @generated by `cargo buckal` + +load("@buckal//:cargo_manifest.bzl", "cargo_manifest") +load("@buckal//:wrapper.bzl", "buildscript_run", "rust_binary", "rust_library") + +http_archive( + name = "windows_x86_64_gnullvm-vendor", + urls = ["https://static.crates.io/crates/windows_x86_64_gnullvm/windows_x86_64_gnullvm-0.52.6.crate"], + sha256 = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d", + type = "tar.gz", + strip_prefix = "windows_x86_64_gnullvm-0.52.6", + out = "vendor", +) + +cargo_manifest( + name = "windows_x86_64_gnullvm-manifest", + vendor = ":windows_x86_64_gnullvm-vendor", +) + +rust_library( + name = "windows_x86_64_gnullvm", + srcs = [":windows_x86_64_gnullvm-vendor"], + crate = "windows_x86_64_gnullvm", + crate_root = "vendor/src/lib.rs", + edition = "2021", + env = { + "OUT_DIR": "$(location :windows_x86_64_gnullvm-build-script-run[out_dir])", + }, + rustc_flags = [ + "@$(location :windows_x86_64_gnullvm-build-script-run[rustc_flags])", + "@$(location :windows_x86_64_gnullvm-manifest[env_flags])", + ], + visibility = ["PUBLIC"], +) + +rust_binary( + name = "windows_x86_64_gnullvm-build-script-build", + srcs = [":windows_x86_64_gnullvm-vendor"], + crate = "build_script_build", + crate_root = "vendor/build.rs", + edition = "2021", + rustc_flags = ["@$(location :windows_x86_64_gnullvm-manifest[env_flags])"], + visibility = [], +) + +buildscript_run( + name = "windows_x86_64_gnullvm-build-script-run", + package_name = "windows_x86_64_gnullvm", + buildscript_rule = ":windows_x86_64_gnullvm-build-script-build", + env_srcs = [":windows_x86_64_gnullvm-manifest[env_dict]"], + version = "0.52.6", + manifest_dir = ":windows_x86_64_gnullvm-vendor", + visibility = ["PUBLIC"], +) diff --git a/third-party/rust/crates/windows_x86_64_msvc/0.52.6/BUCK b/third-party/rust/crates/windows_x86_64_msvc/0.52.6/BUCK new file mode 100644 index 00000000..56e9f24e --- /dev/null +++ b/third-party/rust/crates/windows_x86_64_msvc/0.52.6/BUCK @@ -0,0 +1,54 @@ +# @generated by `cargo buckal` + +load("@buckal//:cargo_manifest.bzl", "cargo_manifest") +load("@buckal//:wrapper.bzl", "buildscript_run", "rust_binary", "rust_library") + +http_archive( + name = "windows_x86_64_msvc-vendor", + urls = ["https://static.crates.io/crates/windows_x86_64_msvc/windows_x86_64_msvc-0.52.6.crate"], + sha256 = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec", + type = "tar.gz", + strip_prefix = "windows_x86_64_msvc-0.52.6", + out = "vendor", +) + +cargo_manifest( + name = "windows_x86_64_msvc-manifest", + vendor = ":windows_x86_64_msvc-vendor", +) + +rust_library( + name = "windows_x86_64_msvc", + srcs = [":windows_x86_64_msvc-vendor"], + crate = "windows_x86_64_msvc", + crate_root = "vendor/src/lib.rs", + edition = "2021", + env = { + "OUT_DIR": "$(location :windows_x86_64_msvc-build-script-run[out_dir])", + }, + rustc_flags = [ + "@$(location :windows_x86_64_msvc-build-script-run[rustc_flags])", + "@$(location :windows_x86_64_msvc-manifest[env_flags])", + ], + visibility = ["PUBLIC"], +) + +rust_binary( + name = "windows_x86_64_msvc-build-script-build", + srcs = [":windows_x86_64_msvc-vendor"], + crate = "build_script_build", + crate_root = "vendor/build.rs", + edition = "2021", + rustc_flags = ["@$(location :windows_x86_64_msvc-manifest[env_flags])"], + visibility = [], +) + +buildscript_run( + name = "windows_x86_64_msvc-build-script-run", + package_name = "windows_x86_64_msvc", + buildscript_rule = ":windows_x86_64_msvc-build-script-build", + env_srcs = [":windows_x86_64_msvc-manifest[env_dict]"], + version = "0.52.6", + manifest_dir = ":windows_x86_64_msvc-vendor", + visibility = ["PUBLIC"], +)