From d35d5985e80af6fa47a1299bfa2856a63be45968 Mon Sep 17 00:00:00 2001 From: jackie Date: Tue, 10 Feb 2026 18:59:29 +0800 Subject: [PATCH 01/13] add ai_obj and update buck Signed-off-by: jackie --- BUCK | 2 + Cargo.lock | 97 +- Cargo.toml | 4 +- buckal.snap | 16 +- src/ai_process/base.rs | 354 ++++++++ src/ai_process/checksum.rs | 129 +++ src/ai_process/mod.rs | 11 + src/ai_process/objects.rs | 832 ++++++++++++++++++ src/lib.rs | 5 +- third-party/rust/crates/ring/0.17.14/BUCK | 78 ++ third-party/rust/crates/untrusted/0.9.0/BUCK | 28 + third-party/rust/crates/uuid/1.19.0/BUCK | 1 + .../rust/crates/windows-sys/0.52.0/BUCK | 36 + .../rust/crates/windows-targets/0.52.6/BUCK | 37 + .../windows_aarch64_gnullvm/0.52.6/BUCK | 54 ++ .../crates/windows_aarch64_msvc/0.52.6/BUCK | 54 ++ .../rust/crates/windows_i686_gnu/0.52.6/BUCK | 54 ++ .../crates/windows_i686_gnullvm/0.52.6/BUCK | 54 ++ .../rust/crates/windows_i686_msvc/0.52.6/BUCK | 54 ++ .../crates/windows_x86_64_gnu/0.52.6/BUCK | 54 ++ .../crates/windows_x86_64_gnullvm/0.52.6/BUCK | 54 ++ .../crates/windows_x86_64_msvc/0.52.6/BUCK | 54 ++ 22 files changed, 2056 insertions(+), 6 deletions(-) create mode 100644 src/ai_process/base.rs create mode 100644 src/ai_process/checksum.rs create mode 100644 src/ai_process/mod.rs create mode 100644 src/ai_process/objects.rs create mode 100644 third-party/rust/crates/ring/0.17.14/BUCK create mode 100644 third-party/rust/crates/untrusted/0.9.0/BUCK create mode 100644 third-party/rust/crates/windows-sys/0.52.0/BUCK create mode 100644 third-party/rust/crates/windows-targets/0.52.6/BUCK create mode 100644 third-party/rust/crates/windows_aarch64_gnullvm/0.52.6/BUCK create mode 100644 third-party/rust/crates/windows_aarch64_msvc/0.52.6/BUCK create mode 100644 third-party/rust/crates/windows_i686_gnu/0.52.6/BUCK create mode 100644 third-party/rust/crates/windows_i686_gnullvm/0.52.6/BUCK create mode 100644 third-party/rust/crates/windows_i686_msvc/0.52.6/BUCK create mode 100644 third-party/rust/crates/windows_x86_64_gnu/0.52.6/BUCK create mode 100644 third-party/rust/crates/windows_x86_64_gnullvm/0.52.6/BUCK create mode 100644 third-party/rust/crates/windows_x86_64_msvc/0.52.6/BUCK 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/ai_process/base.rs b/src/ai_process/base.rs new file mode 100644 index 00000000..c9ffae1a --- /dev/null +++ b/src/ai_process/base.rs @@ -0,0 +1,354 @@ +use std::{collections::HashMap, fmt, str::FromStr}; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::checksum::Checksum; + +/// Header shared by all AI Process Objects +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct Header { + /// Global unique ID (UUID v7) + pub object_id: Uuid, + /// Object type (task/run/patchset/...) + pub object_type: String, + /// Model version + pub schema_version: u32, + /// Repository identifier + pub repo_id: Uuid, + /// Creation time + pub created_at: DateTime, + /// Creator + pub created_by: ActorRef, + /// Visibility (fixed to private for Libra) + pub visibility: String, + /// Search tags + #[serde(default)] + pub tags: HashMap, + /// External ID mapping + #[serde(default)] + pub external_ids: HashMap, + /// Content checksum (optional) + #[serde(default)] + pub checksum: Option, +} + +impl Header { + pub fn new(object_type: impl Into, repo_id: Uuid, created_by: ActorRef) -> Self { + Self { + object_id: Uuid::now_v7(), + object_type: object_type.into(), + schema_version: 1, + repo_id, + created_at: Utc::now(), + created_by, + visibility: "private".to_string(), + tags: HashMap::new(), + external_ids: HashMap::new(), + checksum: None, + } + } + + /// Accessor for checksum + pub fn checksum(&self) -> Option<&Checksum> { + self.checksum.as_ref() + } + + /// Seal the header by calculating and setting the checksum of the provided object. + /// This is typically called before persisting the object. + pub fn seal(&mut self, object: &T) { + self.checksum = Some(Checksum::compute_json(object)); + } +} + +/// 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 FromStr for ActorKind { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "human" => Ok(ActorKind::Human), + "agent" => Ok(ActorKind::Agent), + "system" => Ok(ActorKind::System), + "mcp_client" => Ok(ActorKind::McpClient), + _ => Ok(ActorKind::Other(s.to_string())), + } + } +} + +impl From for ActorKind { + fn from(s: String) -> Self { + ActorKind::from_str(&s).unwrap() + } +} + +impl From<&str> for ActorKind { + fn from(s: &str) -> Self { + ActorKind::from_str(s).unwrap() + } +} + +/// Actor reference (who created/triggered) +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ActorRef { + /// Kind: human/agent/system/mcp_client + pub kind: ActorKind, + /// Subject ID (user/agent name or client ID) + pub id: String, + /// Display name (optional) + pub display_name: Option, + /// Auth context (optional, Libra usually empty) + pub auth_context: Option, +} + +impl ActorRef { + /// Create a new ActorRef with validation. + pub fn new(kind: impl Into, id: impl Into) -> Self { + let id_str = id.into(); + if id_str.trim().is_empty() { + panic!("Actor ID cannot be empty"); + } + Self { + kind: kind.into(), + id: id_str, + display_name: None, + auth_context: None, + } + } + + /// Create a human actor reference. + pub fn human(id: impl Into) -> Self { + Self::new(ActorKind::Human, id) + } + + /// Create an agent actor reference. + pub fn agent(name: impl Into) -> Self { + Self::new(ActorKind::Agent, name) + } + + /// Create a system component actor reference. + pub fn system(component: impl Into) -> Self { + Self::new(ActorKind::System, component) + } + + /// Create an MCP client actor reference. + pub fn mcp_client(client_id: impl Into) -> Self { + 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 + pub store: String, + /// Storage key (e.g., path or object key) + pub key: String, + /// MIME type (optional) + pub content_type: Option, + /// Size in bytes (optional) + pub size_bytes: Option, + /// SHA256 checksum (strongly recommended) + pub sha256: Option, + /// Expiration time (optional) + pub expires_at: Option>, +} + +impl ArtifactRef { + pub fn new(store: impl Into, key: impl Into) -> Self { + Self { + store: store.into(), + key: key.into(), + content_type: None, + size_bytes: None, + sha256: None, + expires_at: None, + } + } + + /// Calculate SHA256 checksum for the given content bytes + pub fn compute_sha256(content: &[u8]) -> Checksum { + Checksum::compute(content) + } + + /// Set the checksum directly with validation + pub fn with_checksum(mut self, sha256: impl Into) -> Self { + self.sha256 = Some(Checksum::new(sha256).expect("Invalid checksum format")); + self + } + + /// Verify if the provided content matches the stored checksum + pub fn verify_integrity(&self, content: &[u8]) -> Result { + let stored_hash = self + .sha256 + .as_ref() + .ok_or_else(|| "No checksum stored in ArtifactRef".to_string())?; + + Ok(stored_hash.verify(content)) + } + + /// Check if two artifacts have the same content based on checksum + pub fn content_eq(&self, other: &Self) -> Option { + match (&self.sha256, &other.sha256) { + (Some(a), Some(b)) => Some(a == b), + _ => None, + } + } + + /// Check if the artifact has expired + 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::now_v7(); + let actor = ActorRef::human("jackie"); + let header = Header::new("task", repo_id, actor); + + 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"); + assert_eq!(actor.kind, ActorKind::Agent); + assert_eq!(actor.id, "coder"); + + let sys = ActorRef::system("scheduler"); + assert_eq!(sys.kind, ActorKind::System); + + let client = ActorRef::mcp_client("vscode"); + assert_eq!(client.kind, ActorKind::McpClient); + } + + #[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::parse_str("00000000-0000-0000-0000-000000000000").unwrap(); + let actor = ActorRef::human("jackie"); + let mut header = Header::new("task", repo_id, actor); + // Fix time for deterministic checksum + header.created_at = DateTime::parse_from_rfc3339("2026-02-10T00:00:00Z") + .unwrap() + .with_timezone(&Utc); + header.object_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); + + let checksum = Checksum::compute_json(&header); + assert_eq!(checksum.as_str().len(), 64); // SHA256 length + + // Ensure changes change checksum + header.object_type = "run".to_string(); + let checksum2 = Checksum::compute_json(&header); + assert_ne!(checksum, checksum2); + } + + #[test] + fn test_artifact_checksum() { + let content = b"hello world"; + let hash = ArtifactRef::compute_sha256(content); + // echo -n "hello world" | shasum -a 256 + let expected_str = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"; + assert_eq!(hash.as_str(), expected_str); + + let artifact = ArtifactRef::new("s3", "key").with_checksum(hash.as_str()); + assert_eq!(artifact.sha256, Some(hash.clone())); + + // Integrity check + assert!(artifact.verify_integrity(content).unwrap()); + assert!(!artifact.verify_integrity(b"wrong").unwrap()); + + // Deduplication + let artifact2 = ArtifactRef::new("local", "other/path").with_checksum(hash.as_str()); + assert_eq!(artifact.content_eq(&artifact2), Some(true)); + + let artifact3 = ArtifactRef::new("s3", "diff") + .with_checksum(ArtifactRef::compute_sha256(b"diff").as_str()); + assert_eq!(artifact.content_eq(&artifact3), Some(false)); + } + + #[test] + #[should_panic(expected = "Invalid checksum format")] + fn test_invalid_checksum() { + ArtifactRef::new("s3", "key").with_checksum("bad_hash"); + } + + #[test] + fn test_header_seal() { + let repo_id = Uuid::now_v7(); + let actor = ActorRef::human("jackie"); + let mut header = Header::new("task", repo_id, actor); + + let content = serde_json::json!({"key": "value"}); + header.seal(&content); + + assert!(header.checksum.is_some()); + let expected = Checksum::compute_json(&content); + assert_eq!(header.checksum.unwrap(), expected); + } + + #[test] + #[should_panic(expected = "Actor ID cannot be empty")] + fn test_empty_actor_id() { + ActorRef::new(ActorKind::Human, " "); + } + + #[test] + fn test_artifact_expiration() { + let mut artifact = ArtifactRef::new("s3", "key"); + assert!(!artifact.is_expired()); + + artifact.expires_at = Some(Utc::now() - chrono::Duration::hours(1)); + assert!(artifact.is_expired()); + + artifact.expires_at = Some(Utc::now() + chrono::Duration::hours(1)); + assert!(!artifact.is_expired()); + } +} diff --git a/src/ai_process/checksum.rs b/src/ai_process/checksum.rs new file mode 100644 index 00000000..4e3e4c35 --- /dev/null +++ b/src/ai_process/checksum.rs @@ -0,0 +1,129 @@ +use std::fmt; + +use ring::digest; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::Value; + +/// Unified SHA256 Checksum handler +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Checksum(String); + +impl Checksum { + /// Calculate checksum from bytes + pub fn compute(content: &[u8]) -> Self { + let digest = digest::digest(&digest::SHA256, content); + Self(hex::encode(digest.as_ref())) + } + + /// Calculate checksum from a serializable object (deterministic JSON) + pub fn compute_json(object: &T) -> Self { + let mut value = serde_json::to_value(object).unwrap_or(Value::Null); + canonicalize_json(&mut value); + let content = serde_json::to_vec(&value).unwrap_or_default(); + Self::compute(&content) + } + + /// Create from existing hash string with validation + pub fn new(hash: impl Into) -> Result { + let hash = hash.into(); + if !Self::is_valid(&hash) { + return Err(format!("Invalid SHA256 hash format: {}", hash)); + } + Ok(Self(hash)) + } + + /// Verify if content matches this checksum + pub fn verify(&self, content: &[u8]) -> bool { + Self::compute(content) == *self + } + + /// Check valid format (64 hex chars) + pub fn is_valid(hash: &str) -> bool { + hash.len() == 64 && hash.chars().all(|c| c.is_ascii_hexdigit()) + } + + /// Get inner string + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl fmt::Display for Checksum { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for String { + fn from(c: Checksum) -> Self { + c.0 + } +} + +impl Serialize for Checksum { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.0) + } +} + +impl<'de> Deserialize<'de> for Checksum { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Self::new(s).map_err(serde::de::Error::custom) + } +} + +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 ai_process_checksum_deterministic_map() { + 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 = Checksum::compute_json(&MapWrapper { map: map_a }); + let hash_b = Checksum::compute_json(&MapWrapper { map: map_b }); + + assert_eq!(hash_a, hash_b); + } +} diff --git a/src/ai_process/mod.rs b/src/ai_process/mod.rs new file mode 100644 index 00000000..f964113b --- /dev/null +++ b/src/ai_process/mod.rs @@ -0,0 +1,11 @@ +//! AI-powered processes for Git internals. +//! This module provides abstractions and implementations for integrating AI agents +//! into Git workflows, enabling intelligent automation and assistance in handling Git objects +//! and operations. +pub mod base; +pub mod checksum; +pub mod objects; + +pub use base::*; +pub use checksum::*; +pub use objects::*; diff --git a/src/ai_process/objects.rs b/src/ai_process/objects.rs new file mode 100644 index 00000000..8b64ffe9 --- /dev/null +++ b/src/ai_process/objects.rs @@ -0,0 +1,832 @@ +use std::{cmp::Ordering, collections::HashMap, fmt, str::FromStr}; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::base::{ActorRef, ArtifactRef, Header}; + +/// Task Status Enum +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum TaskStatus { + Draft, + Running, + Done, + Failed, + Cancelled, +} + +impl TaskStatus { + 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 Type Enum +#[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 { + 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 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Task { + #[serde(flatten)] + pub header: Header, + pub title: String, + pub description: Option, // Can be text or artifact ref serialized + pub goal_type: Option, // feature/bugfix/refactor/docs/... + #[serde(default)] + pub constraints: Vec, + #[serde(default)] + pub acceptance_criteria: Vec, + pub requested_by: Option, + #[serde(default)] + pub dependencies: Vec, + pub status: TaskStatus, +} + +/// Task Object +impl Task { + pub fn new( + repo_id: Uuid, + created_by: ActorRef, + title: impl Into, + goal_type: Option, + ) -> Self { + Self { + header: Header::new("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, + } + } +} + +/// Run Status Enum +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum RunStatus { + Created, + Patching, + Validating, + Completed, + Failed, +} + +impl RunStatus { + 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()) + } +} + +/// Run Object +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Run { + #[serde(flatten)] + pub header: Header, + pub task_id: Uuid, + pub orchestrator_version: String, + pub base_commit_sha: String, + pub status: RunStatus, + pub context_snapshot_id: Option, + #[serde(default)] + pub agent_instances: Vec, + pub metrics: Option, + pub error: Option, + pub environment: Option, +} + +/// Environment Object +#[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(|_| "unknown".to_string()), + extra: HashMap::new(), + } + } +} + +/// Agent Instance Object +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AgentInstance { + pub role: String, + pub provider_route: Option, +} + +/// Run Object +impl Run { + pub fn new( + repo_id: Uuid, + created_by: ActorRef, + task_id: Uuid, + base_commit_sha: impl Into, + ) -> Self { + Self { + header: Header::new("run", repo_id, created_by), + task_id, + orchestrator_version: "libra-builtin".to_string(), + base_commit_sha: base_commit_sha.into(), + status: RunStatus::Created, + context_snapshot_id: None, + agent_instances: Vec::new(), + metrics: None, + error: None, + environment: Some(Environment::capture()), + } + } +} + +/// Apply Status Enum +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ApplyStatus { + Proposed, + Applied, + Rejected, + Superseded, +} + +impl ApplyStatus { + 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()) + } +} + +/// PatchSet Object +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PatchSet { + #[serde(flatten)] + pub header: Header, + pub run_id: Uuid, + pub generation: u32, + pub base_commit_sha: String, + pub diff_format: String, // unified_diff + pub diff_artifact: Option, + #[serde(default)] + pub touched_files: Vec, + pub rationale: Option, + pub apply_status: ApplyStatus, +} + +/// TouchedFile Object +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TouchedFile { + pub path: String, + pub change_type: String, // modify/add/delete + pub lines_added: u32, + pub lines_deleted: u32, +} + +impl PatchSet { + /// Create a new patchset object + pub fn new( + repo_id: Uuid, + created_by: ActorRef, + run_id: Uuid, + base_commit_sha: impl Into, + generation: u32, + ) -> Self { + Self { + header: Header::new("patchset", repo_id, created_by), + run_id, + generation, + base_commit_sha: base_commit_sha.into(), + diff_format: "unified_diff".to_string(), + diff_artifact: None, + touched_files: Vec::new(), + rationale: None, + apply_status: ApplyStatus::Proposed, + } + } +} + +/// ContextSnapshot Object +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContextSnapshot { + #[serde(flatten)] + pub header: Header, + pub base_commit_sha: String, + pub selection_strategy: String, // explicit/heuristic + #[serde(default)] + pub items: Vec, + pub summary: Option, +} + +/// ContextItem Object +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContextItem { + pub kind: String, // file + pub path: String, + pub content_hash: String, +} + +/// Tool Status Enum +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ToolStatus { + Ok, + Error, +} + +impl ToolStatus { + 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()) + } +} + +/// ToolInvocation Object +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolInvocation { + #[serde(flatten)] + pub header: Header, + pub run_id: Uuid, + pub tool_name: String, + pub io_footprint: Option, + #[serde(default)] + pub args: serde_json::Value, + pub status: ToolStatus, + pub result_summary: Option, + #[serde(default)] + pub artifacts: Vec, +} + +/// IO Footprint Object +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IoFootprint { + #[serde(default)] + pub paths_read: Vec, + #[serde(default)] + pub paths_written: Vec, +} + +impl ToolInvocation { + pub fn new( + repo_id: Uuid, + created_by: ActorRef, + run_id: Uuid, + tool_name: impl Into, + ) -> Self { + Self { + header: Header::new("tool_invocation", 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(), + } + } +} + +/// Plan Status Enum +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum PlanStatus { + Pending, + InProgress, + Completed, + Failed, + Skipped, +} + +impl PlanStatus { + 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 Object +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct Plan { + #[serde(flatten)] + pub header: Header, + pub run_id: Uuid, + pub plan_version: u32, + #[serde(default)] + pub steps: Vec, +} + +impl PartialOrd for Plan { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Plan { + fn cmp(&self, other: &Self) -> Ordering { + self.plan_version.cmp(&other.plan_version) + } +} + +/// Plan Step Object +#[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, +} + +impl Plan { + /// Create a new plan object + pub fn new(repo_id: Uuid, created_by: ActorRef, run_id: Uuid, plan_version: u32) -> Self { + Self { + header: Header::new("plan", repo_id, created_by), + run_id, + plan_version, + steps: Vec::new(), + } + } +} + +/// Evidence Object +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Evidence { + #[serde(flatten)] + pub header: Header, + pub run_id: Uuid, + pub patchset_id: Option, + pub kind: String, // test/lint/build + pub tool: String, + pub command: Option, + pub exit_code: Option, + pub summary: Option, // passed/failed, error signature + #[serde(default)] + pub report_artifacts: Vec, +} + +impl Evidence { + pub fn new( + repo_id: Uuid, + created_by: ActorRef, + run_id: Uuid, + kind: impl Into, + tool: impl Into, + ) -> Self { + Self { + header: Header::new("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(), + } + } +} + +/// Provenance Object +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Provenance { + #[serde(flatten)] + pub header: Header, + pub run_id: Uuid, + pub provider: String, + pub model: String, + pub parameters: Option, + pub token_usage: Option, +} + +impl Provenance { + pub fn new( + repo_id: Uuid, + created_by: ActorRef, + run_id: Uuid, + provider: impl Into, + model: impl Into, + ) -> Self { + Self { + header: Header::new("provenance", repo_id, created_by), + run_id, + provider: provider.into(), + model: model.into(), + parameters: None, + token_usage: None, + } + } +} + +/// Decision Object +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Decision { + #[serde(flatten)] + pub header: Header, + pub run_id: Uuid, + pub decision_type: String, // commit/checkpoint/abandon/retry/rollback + pub chosen_patchset_id: Option, + pub result_commit_sha: Option, + pub checkpoint_id: Option, + pub 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, + ) -> Self { + Self { + header: Header::new("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, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_task_creation() { + let repo_id = Uuid::now_v7(); + let actor = ActorRef::human("jackie"); + let mut task = Task::new(repo_id, actor, "Fix bug", Some(GoalType::Bugfix)); + + // Test dependencies + let dep_id = Uuid::now_v7(); + task.dependencies.push(dep_id); + + assert_eq!(task.header.object_type, "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::now_v7(); + let actor = ActorRef::human("jackie"); + let task = Task::new(repo_id, actor, "Write docs", None); + + assert!(task.goal_type.is_none()); + } + + #[test] + fn test_new_objects_creation() { + let repo_id = Uuid::now_v7(); + let actor = ActorRef::agent("test-agent"); + let run_id = Uuid::now_v7(); + + // Run with environment (auto captured) + let run = Run::new(repo_id, actor.clone(), Uuid::now_v7(), "sha123"); + + let env = run.environment.as_ref().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()); + + // Plan with steps and status + let mut plan = Plan::new(repo_id, actor.clone(), run_id, 1); + plan.steps.push(PlanStep { + intent: "step1".to_string(), + inputs: None, + outputs: None, + checks: None, + owner_role: None, + status: PlanStatus::Pending, + }); + + assert_eq!(plan.header.object_type, "plan"); + assert_eq!(plan.plan_version, 1); + assert_eq!(plan.steps[0].status, PlanStatus::Pending); + + // Evidence + let evidence = Evidence::new(repo_id, actor.clone(), run_id, "test", "cargo"); + assert_eq!(evidence.header.object_type, "evidence"); + assert_eq!(evidence.kind, "test"); + + // Provenance + let provenance = Provenance::new(repo_id, actor.clone(), run_id, "openai", "gpt-4"); + assert_eq!(provenance.header.object_type, "provenance"); + assert_eq!(provenance.provider, "openai"); + + // Decision + let decision = Decision::new(repo_id, actor.clone(), run_id, "commit"); + assert_eq!(decision.header.object_type, "decision"); + assert_eq!(decision.decision_type, "commit"); + } + + #[test] + fn test_task_requested_by() { + let repo_id = Uuid::now_v7(); + let actor = ActorRef::human("jackie"); + let mut task = Task::new(repo_id, actor.clone(), "Fix bug", Some(GoalType::Bugfix)); + + task.requested_by = Some(ActorRef::mcp_client("vscode-client")); + + assert!(task.requested_by.is_some()); + assert_eq!( + task.requested_by.unwrap().kind, + super::super::base::ActorKind::McpClient + ); + } + + #[test] + fn test_tool_invocation_io_footprint() { + let repo_id = Uuid::now_v7(); + let actor = ActorRef::human("jackie"); + let run_id = Uuid::now_v7(); + + let mut tool_inv = ToolInvocation::new(repo_id, actor, run_id, "read_file"); + + let footprint = IoFootprint { + paths_read: vec!["src/main.rs".to_string()], + paths_written: vec![], + }; + + tool_inv.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_patchset_creation() { + let repo_id = Uuid::now_v7(); + let actor = ActorRef::agent("test-agent"); + let run_id = Uuid::now_v7(); + + let patchset = PatchSet::new(repo_id, actor, run_id, "sha123", 1); + + assert_eq!(patchset.header.object_type, "patchset"); + assert_eq!(patchset.generation, 1); + assert_eq!(patchset.diff_format, "unified_diff"); + assert_eq!(patchset.apply_status, ApplyStatus::Proposed); + assert!(patchset.touched_files.is_empty()); + } + + #[test] + fn test_context_snapshot_fields() { + let repo_id = Uuid::now_v7(); + let actor = ActorRef::agent("test-agent"); + + let mut snapshot = ContextSnapshot { + header: Header::new("context_snapshot", repo_id, actor), + base_commit_sha: "sha123".to_string(), + selection_strategy: "explicit".to_string(), + items: Vec::new(), + summary: Some("core files".to_string()), + }; + + snapshot.items.push(ContextItem { + kind: "file".to_string(), + path: "src/lib.rs".to_string(), + content_hash: "abc".to_string(), + }); + + assert_eq!(snapshot.items.len(), 1); + assert_eq!(snapshot.items[0].path, "src/lib.rs"); + assert_eq!(snapshot.summary.as_deref(), Some("core files")); + } + + #[test] + fn test_tool_invocation_fields() { + let repo_id = Uuid::now_v7(); + let actor = ActorRef::human("jackie"); + let run_id = Uuid::now_v7(); + + let mut tool_inv = ToolInvocation::new(repo_id, actor, run_id, "apply_patch"); + tool_inv.status = ToolStatus::Error; + tool_inv.args = serde_json::json!({"path": "src/lib.rs"}); + tool_inv.result_summary = Some("failed".to_string()); + tool_inv + .artifacts + .push(ArtifactRef::new("local", "artifact-key")); + + assert_eq!(tool_inv.status, ToolStatus::Error); + assert_eq!(tool_inv.artifacts.len(), 1); + assert_eq!(tool_inv.args["path"], "src/lib.rs"); + } + + #[test] + fn test_evidence_fields() { + let repo_id = Uuid::now_v7(); + let actor = ActorRef::agent("test-agent"); + let run_id = Uuid::now_v7(); + let patchset_id = Uuid::now_v7(); + + let mut evidence = Evidence::new(repo_id, actor, run_id, "test", "cargo"); + evidence.patchset_id = Some(patchset_id); + evidence.exit_code = Some(1); + evidence + .report_artifacts + .push(ArtifactRef::new("local", "log.txt")); + + assert_eq!(evidence.patchset_id, Some(patchset_id)); + assert_eq!(evidence.exit_code, Some(1)); + assert_eq!(evidence.report_artifacts.len(), 1); + } + + #[test] + fn test_provenance_fields() { + let repo_id = Uuid::now_v7(); + let actor = ActorRef::agent("test-agent"); + let run_id = Uuid::now_v7(); + + let mut provenance = Provenance::new(repo_id, actor, run_id, "openai", "gpt-4"); + provenance.parameters = Some(serde_json::json!({"temperature": 0.2})); + provenance.token_usage = Some(serde_json::json!({"input": 10, "output": 5})); + + assert!(provenance.parameters.is_some()); + assert!(provenance.token_usage.is_some()); + } + + #[test] + fn test_decision_fields() { + let repo_id = Uuid::now_v7(); + let actor = ActorRef::agent("test-agent"); + let run_id = Uuid::now_v7(); + let patchset_id = Uuid::now_v7(); + + let mut decision = Decision::new(repo_id, actor, run_id, "commit"); + decision.chosen_patchset_id = Some(patchset_id); + decision.result_commit_sha = Some("abc123".to_string()); + decision.rationale = Some("tests passed".to_string()); + + assert_eq!(decision.chosen_patchset_id, Some(patchset_id)); + assert_eq!(decision.result_commit_sha.as_deref(), Some("abc123")); + assert_eq!(decision.rationale.as_deref(), Some("tests passed")); + } + + #[test] + fn test_plan_version_ordering() { + let repo_id = Uuid::now_v7(); + let actor = ActorRef::human("jackie"); + let run_id = Uuid::now_v7(); + + let plan_v1 = Plan::new(repo_id, actor.clone(), run_id, 1); + let plan_v2 = Plan::new(repo_id, actor.clone(), run_id, 2); + let plan_v3 = Plan::new(repo_id, actor.clone(), run_id, 3); + + let mut plans = [plan_v2.clone(), plan_v1.clone(), plan_v3.clone()]; + plans.sort(); + + 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_v2); + assert!(plan_v2 > plan_v1); + } + + #[test] + fn ai_process_tool_invocation_artifacts_default() { + let repo_id = Uuid::now_v7(); + let actor = ActorRef::human("jackie"); + let run_id = Uuid::now_v7(); + + let tool_inv = ToolInvocation::new(repo_id, actor, run_id, "read_file"); + let mut value = serde_json::to_value(&tool_inv).unwrap(); + + if let serde_json::Value::Object(ref mut map) = value { + map.remove("artifacts"); + } + + let deserialized: ToolInvocation = serde_json::from_value(value).unwrap(); + assert!(deserialized.artifacts.is_empty()); + } +} diff --git a/src/lib.rs b/src/lib.rs index 28c094ef..caaf9a98 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,7 @@ //! - `internal::pack`: decode/encode, caches, waitlists, parallel pipelines, helpers. //! - `internal::object`: Blob/Tree/Commit/Tag/Note objects, type enum, object trait. //! - `internal::zlib`: compression/decompression stream utilities. +//! - `ai_process`: AI process objects for agent runs and provenance. //! - `delta` and `zstdelta`: delta algorithms and rebuild helpers. //! - `errors`: unified error types. //! - `hash`: Hash helpers. @@ -30,14 +31,14 @@ //! Test Data //! - Located under `tests/data/`, includes real pack files and object sets. +pub mod ai_process; +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"], +) From 258c5591a01a00b676fd2fb673aea154e199032d Mon Sep 17 00:00:00 2001 From: jackie Date: Tue, 10 Feb 2026 20:54:43 +0800 Subject: [PATCH 02/13] refactor project and fix some bugs Signed-off-by: jackie --- src/ai_process/base.rs | 354 ------------ src/ai_process/checksum.rs | 129 ----- src/hash.rs | 5 + src/internal/object/ai_process/base.rs | 531 ++++++++++++++++++ src/internal/object/ai_process/checksum.rs | 76 +++ src/{ => internal/object}/ai_process/mod.rs | 0 .../object}/ai_process/objects.rs | 364 +++++++----- src/internal/object/mod.rs | 2 +- src/lib.rs | 2 - 9 files changed, 841 insertions(+), 622 deletions(-) delete mode 100644 src/ai_process/base.rs delete mode 100644 src/ai_process/checksum.rs create mode 100644 src/internal/object/ai_process/base.rs create mode 100644 src/internal/object/ai_process/checksum.rs rename src/{ => internal/object}/ai_process/mod.rs (100%) rename src/{ => internal/object}/ai_process/objects.rs (69%) diff --git a/src/ai_process/base.rs b/src/ai_process/base.rs deleted file mode 100644 index c9ffae1a..00000000 --- a/src/ai_process/base.rs +++ /dev/null @@ -1,354 +0,0 @@ -use std::{collections::HashMap, fmt, str::FromStr}; - -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use super::checksum::Checksum; - -/// Header shared by all AI Process Objects -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct Header { - /// Global unique ID (UUID v7) - pub object_id: Uuid, - /// Object type (task/run/patchset/...) - pub object_type: String, - /// Model version - pub schema_version: u32, - /// Repository identifier - pub repo_id: Uuid, - /// Creation time - pub created_at: DateTime, - /// Creator - pub created_by: ActorRef, - /// Visibility (fixed to private for Libra) - pub visibility: String, - /// Search tags - #[serde(default)] - pub tags: HashMap, - /// External ID mapping - #[serde(default)] - pub external_ids: HashMap, - /// Content checksum (optional) - #[serde(default)] - pub checksum: Option, -} - -impl Header { - pub fn new(object_type: impl Into, repo_id: Uuid, created_by: ActorRef) -> Self { - Self { - object_id: Uuid::now_v7(), - object_type: object_type.into(), - schema_version: 1, - repo_id, - created_at: Utc::now(), - created_by, - visibility: "private".to_string(), - tags: HashMap::new(), - external_ids: HashMap::new(), - checksum: None, - } - } - - /// Accessor for checksum - pub fn checksum(&self) -> Option<&Checksum> { - self.checksum.as_ref() - } - - /// Seal the header by calculating and setting the checksum of the provided object. - /// This is typically called before persisting the object. - pub fn seal(&mut self, object: &T) { - self.checksum = Some(Checksum::compute_json(object)); - } -} - -/// 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 FromStr for ActorKind { - type Err = (); - - fn from_str(s: &str) -> Result { - match s { - "human" => Ok(ActorKind::Human), - "agent" => Ok(ActorKind::Agent), - "system" => Ok(ActorKind::System), - "mcp_client" => Ok(ActorKind::McpClient), - _ => Ok(ActorKind::Other(s.to_string())), - } - } -} - -impl From for ActorKind { - fn from(s: String) -> Self { - ActorKind::from_str(&s).unwrap() - } -} - -impl From<&str> for ActorKind { - fn from(s: &str) -> Self { - ActorKind::from_str(s).unwrap() - } -} - -/// Actor reference (who created/triggered) -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ActorRef { - /// Kind: human/agent/system/mcp_client - pub kind: ActorKind, - /// Subject ID (user/agent name or client ID) - pub id: String, - /// Display name (optional) - pub display_name: Option, - /// Auth context (optional, Libra usually empty) - pub auth_context: Option, -} - -impl ActorRef { - /// Create a new ActorRef with validation. - pub fn new(kind: impl Into, id: impl Into) -> Self { - let id_str = id.into(); - if id_str.trim().is_empty() { - panic!("Actor ID cannot be empty"); - } - Self { - kind: kind.into(), - id: id_str, - display_name: None, - auth_context: None, - } - } - - /// Create a human actor reference. - pub fn human(id: impl Into) -> Self { - Self::new(ActorKind::Human, id) - } - - /// Create an agent actor reference. - pub fn agent(name: impl Into) -> Self { - Self::new(ActorKind::Agent, name) - } - - /// Create a system component actor reference. - pub fn system(component: impl Into) -> Self { - Self::new(ActorKind::System, component) - } - - /// Create an MCP client actor reference. - pub fn mcp_client(client_id: impl Into) -> Self { - 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 - pub store: String, - /// Storage key (e.g., path or object key) - pub key: String, - /// MIME type (optional) - pub content_type: Option, - /// Size in bytes (optional) - pub size_bytes: Option, - /// SHA256 checksum (strongly recommended) - pub sha256: Option, - /// Expiration time (optional) - pub expires_at: Option>, -} - -impl ArtifactRef { - pub fn new(store: impl Into, key: impl Into) -> Self { - Self { - store: store.into(), - key: key.into(), - content_type: None, - size_bytes: None, - sha256: None, - expires_at: None, - } - } - - /// Calculate SHA256 checksum for the given content bytes - pub fn compute_sha256(content: &[u8]) -> Checksum { - Checksum::compute(content) - } - - /// Set the checksum directly with validation - pub fn with_checksum(mut self, sha256: impl Into) -> Self { - self.sha256 = Some(Checksum::new(sha256).expect("Invalid checksum format")); - self - } - - /// Verify if the provided content matches the stored checksum - pub fn verify_integrity(&self, content: &[u8]) -> Result { - let stored_hash = self - .sha256 - .as_ref() - .ok_or_else(|| "No checksum stored in ArtifactRef".to_string())?; - - Ok(stored_hash.verify(content)) - } - - /// Check if two artifacts have the same content based on checksum - pub fn content_eq(&self, other: &Self) -> Option { - match (&self.sha256, &other.sha256) { - (Some(a), Some(b)) => Some(a == b), - _ => None, - } - } - - /// Check if the artifact has expired - 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::now_v7(); - let actor = ActorRef::human("jackie"); - let header = Header::new("task", repo_id, actor); - - 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"); - assert_eq!(actor.kind, ActorKind::Agent); - assert_eq!(actor.id, "coder"); - - let sys = ActorRef::system("scheduler"); - assert_eq!(sys.kind, ActorKind::System); - - let client = ActorRef::mcp_client("vscode"); - assert_eq!(client.kind, ActorKind::McpClient); - } - - #[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::parse_str("00000000-0000-0000-0000-000000000000").unwrap(); - let actor = ActorRef::human("jackie"); - let mut header = Header::new("task", repo_id, actor); - // Fix time for deterministic checksum - header.created_at = DateTime::parse_from_rfc3339("2026-02-10T00:00:00Z") - .unwrap() - .with_timezone(&Utc); - header.object_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); - - let checksum = Checksum::compute_json(&header); - assert_eq!(checksum.as_str().len(), 64); // SHA256 length - - // Ensure changes change checksum - header.object_type = "run".to_string(); - let checksum2 = Checksum::compute_json(&header); - assert_ne!(checksum, checksum2); - } - - #[test] - fn test_artifact_checksum() { - let content = b"hello world"; - let hash = ArtifactRef::compute_sha256(content); - // echo -n "hello world" | shasum -a 256 - let expected_str = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"; - assert_eq!(hash.as_str(), expected_str); - - let artifact = ArtifactRef::new("s3", "key").with_checksum(hash.as_str()); - assert_eq!(artifact.sha256, Some(hash.clone())); - - // Integrity check - assert!(artifact.verify_integrity(content).unwrap()); - assert!(!artifact.verify_integrity(b"wrong").unwrap()); - - // Deduplication - let artifact2 = ArtifactRef::new("local", "other/path").with_checksum(hash.as_str()); - assert_eq!(artifact.content_eq(&artifact2), Some(true)); - - let artifact3 = ArtifactRef::new("s3", "diff") - .with_checksum(ArtifactRef::compute_sha256(b"diff").as_str()); - assert_eq!(artifact.content_eq(&artifact3), Some(false)); - } - - #[test] - #[should_panic(expected = "Invalid checksum format")] - fn test_invalid_checksum() { - ArtifactRef::new("s3", "key").with_checksum("bad_hash"); - } - - #[test] - fn test_header_seal() { - let repo_id = Uuid::now_v7(); - let actor = ActorRef::human("jackie"); - let mut header = Header::new("task", repo_id, actor); - - let content = serde_json::json!({"key": "value"}); - header.seal(&content); - - assert!(header.checksum.is_some()); - let expected = Checksum::compute_json(&content); - assert_eq!(header.checksum.unwrap(), expected); - } - - #[test] - #[should_panic(expected = "Actor ID cannot be empty")] - fn test_empty_actor_id() { - ActorRef::new(ActorKind::Human, " "); - } - - #[test] - fn test_artifact_expiration() { - let mut artifact = ArtifactRef::new("s3", "key"); - assert!(!artifact.is_expired()); - - artifact.expires_at = Some(Utc::now() - chrono::Duration::hours(1)); - assert!(artifact.is_expired()); - - artifact.expires_at = Some(Utc::now() + chrono::Duration::hours(1)); - assert!(!artifact.is_expired()); - } -} diff --git a/src/ai_process/checksum.rs b/src/ai_process/checksum.rs deleted file mode 100644 index 4e3e4c35..00000000 --- a/src/ai_process/checksum.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::fmt; - -use ring::digest; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use serde_json::Value; - -/// Unified SHA256 Checksum handler -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Checksum(String); - -impl Checksum { - /// Calculate checksum from bytes - pub fn compute(content: &[u8]) -> Self { - let digest = digest::digest(&digest::SHA256, content); - Self(hex::encode(digest.as_ref())) - } - - /// Calculate checksum from a serializable object (deterministic JSON) - pub fn compute_json(object: &T) -> Self { - let mut value = serde_json::to_value(object).unwrap_or(Value::Null); - canonicalize_json(&mut value); - let content = serde_json::to_vec(&value).unwrap_or_default(); - Self::compute(&content) - } - - /// Create from existing hash string with validation - pub fn new(hash: impl Into) -> Result { - let hash = hash.into(); - if !Self::is_valid(&hash) { - return Err(format!("Invalid SHA256 hash format: {}", hash)); - } - Ok(Self(hash)) - } - - /// Verify if content matches this checksum - pub fn verify(&self, content: &[u8]) -> bool { - Self::compute(content) == *self - } - - /// Check valid format (64 hex chars) - pub fn is_valid(hash: &str) -> bool { - hash.len() == 64 && hash.chars().all(|c| c.is_ascii_hexdigit()) - } - - /// Get inner string - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl fmt::Display for Checksum { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for String { - fn from(c: Checksum) -> Self { - c.0 - } -} - -impl Serialize for Checksum { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.0) - } -} - -impl<'de> Deserialize<'de> for Checksum { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - Self::new(s).map_err(serde::de::Error::custom) - } -} - -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 ai_process_checksum_deterministic_map() { - 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 = Checksum::compute_json(&MapWrapper { map: map_a }); - let hash_b = Checksum::compute_json(&MapWrapper { map: map_b }); - - assert_eq!(hash_a, hash_b); - } -} diff --git a/src/hash.rs b/src/hash.rs index b6396198..007ae494 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -239,6 +239,11 @@ impl ObjectHash { } } } + +/// Compute SHA256 hash and return hex string. +pub(crate) fn sha256_hex(data: &[u8]) -> String { + hex::encode(sha2::Sha256::digest(data)) +} 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/ai_process/base.rs b/src/internal/object/ai_process/base.rs new file mode 100644 index 00000000..13450104 --- /dev/null +++ b/src/internal/object/ai_process/base.rs @@ -0,0 +1,531 @@ +use std::{collections::HashMap, fmt}; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::checksum::{ + compute_json_sha256_hex, compute_sha256_hex, is_valid_sha256_hex, verify_sha256_hex, +}; + +/// Visibility of an AI process object. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum Visibility { + Private, + Public, +} + +/// Header shared by all AI Process Objects. +#[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: String, + /// 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 { + pub fn new( + object_type: impl Into, + repo_id: Uuid, + created_by: ActorRef, + ) -> Result { + let object_type = object_type.into(); + if object_type.trim().is_empty() { + return Err("object_type cannot be empty".to_string()); + } + 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) -> &str { + &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: impl Into) -> Result<(), String> { + let object_type = object_type.into(); + if object_type.trim().is_empty() { + return Err("object_type cannot be empty".to_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<&str> { + self.checksum.as_deref() + } + + /// Seal the header by calculating and setting the checksum of the provided object. + /// This is typically called before persisting the object. + pub fn seal(&mut self, object: &T) -> Result<(), serde_json::Error> { + self.checksum = Some(compute_json_sha256_hex(object)?); + Ok(()) + } +} + +/// 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, + }) + } + + 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, + /// SHA256 checksum (strongly recommended) + sha256: 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, + sha256: 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 sha256(&self) -> Option<&str> { + self.sha256.as_deref() + } + + pub fn expires_at(&self) -> Option> { + self.expires_at + } + + /// Calculate SHA256 checksum for the given content bytes + pub fn compute_sha256(content: &[u8]) -> String { + compute_sha256_hex(content) + } + + /// Set the checksum directly with validation + pub fn with_checksum(mut self, sha256: impl Into) -> Result { + let sha256 = sha256.into(); + if !is_valid_sha256_hex(&sha256) { + return Err(format!("Invalid SHA256 hash format: {}", sha256)); + } + self.sha256 = Some(sha256); + 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 + .sha256 + .as_ref() + .ok_or_else(|| "No checksum stored in ArtifactRef".to_string())?; + + Ok(verify_sha256_hex(stored_hash, content)) + } + + /// Check if two artifacts have the same content based on checksum + #[must_use] + pub fn content_eq(&self, other: &Self) -> Option { + match (&self.sha256, &other.sha256) { + (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::now_v7(); + let actor = ActorRef::human("jackie").expect("actor"); + let header = Header::new("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); + } + + #[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::parse_str("00000000-0000-0000-0000-000000000000").unwrap(); + let actor = ActorRef::human("jackie").expect("actor"); + let mut header = Header::new("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::parse_str("00000000-0000-0000-0000-000000000001").unwrap()); + + let checksum = compute_json_sha256_hex(&header).expect("checksum"); + assert_eq!(checksum.len(), 64); // SHA256 length + + // Ensure changes change checksum + header.set_object_type("run").expect("object_type"); + let checksum2 = compute_json_sha256_hex(&header).expect("checksum"); + assert_ne!(checksum, checksum2); + } + + #[test] + fn test_artifact_checksum() { + let content = b"hello world"; + let hash = ArtifactRef::compute_sha256(content); + // echo -n "hello world" | shasum -a 256 + let expected_str = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"; + assert_eq!(hash.as_str(), expected_str); + + let artifact = ArtifactRef::new("s3", "key") + .expect("artifact") + .with_checksum(hash.as_str()) + .expect("checksum"); + assert_eq!(artifact.sha256(), Some(hash.as_str())); + + // 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_checksum(hash.as_str()) + .expect("checksum"); + assert_eq!(artifact.content_eq(&artifact2), Some(true)); + + let artifact3 = ArtifactRef::new("s3", "diff") + .expect("artifact") + .with_checksum(ArtifactRef::compute_sha256(b"diff").as_str()) + .expect("checksum"); + assert_eq!(artifact.content_eq(&artifact3), Some(false)); + } + + #[test] + fn test_invalid_checksum() { + let result = ArtifactRef::new("s3", "key") + .expect("artifact") + .with_checksum("bad_hash"); + assert!(result.is_err()); + } + + #[test] + fn test_header_seal() { + let repo_id = Uuid::now_v7(); + let actor = ActorRef::human("jackie").expect("actor"); + let mut header = Header::new("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_json_sha256_hex(&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/ai_process/checksum.rs b/src/internal/object/ai_process/checksum.rs new file mode 100644 index 00000000..1b88f812 --- /dev/null +++ b/src/internal/object/ai_process/checksum.rs @@ -0,0 +1,76 @@ +use serde::Serialize; +use serde_json::Value; + +use crate::hash::sha256_hex; + +/// Calculate SHA256 hex string from bytes. +pub fn compute_sha256_hex(content: &[u8]) -> String { + sha256_hex(content) +} + +/// Calculate SHA256 hex string from a serializable object (deterministic JSON). +pub fn compute_json_sha256_hex(object: &T) -> Result { + let mut value = serde_json::to_value(object)?; + canonicalize_json(&mut value); + let content = serde_json::to_vec(&value)?; + Ok(sha256_hex(&content)) +} + +/// Check valid format (64 hex chars). +pub fn is_valid_sha256_hex(hash: &str) -> bool { + hash.len() == 64 && hash.chars().all(|c| c.is_ascii_hexdigit()) +} + +/// Verify content against the expected SHA256 hex string. +pub fn verify_sha256_hex(expected: &str, content: &[u8]) -> bool { + compute_sha256_hex(content) == expected +} + +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 ai_process_checksum_deterministic_map() { + 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_json_sha256_hex(&MapWrapper { map: map_a }).expect("checksum"); + let hash_b = compute_json_sha256_hex(&MapWrapper { map: map_b }).expect("checksum"); + + assert_eq!(hash_a, hash_b); + } +} diff --git a/src/ai_process/mod.rs b/src/internal/object/ai_process/mod.rs similarity index 100% rename from src/ai_process/mod.rs rename to src/internal/object/ai_process/mod.rs diff --git a/src/ai_process/objects.rs b/src/internal/object/ai_process/objects.rs similarity index 69% rename from src/ai_process/objects.rs rename to src/internal/object/ai_process/objects.rs index 8b64ffe9..29bf81d9 100644 --- a/src/ai_process/objects.rs +++ b/src/internal/object/ai_process/objects.rs @@ -1,11 +1,11 @@ -use std::{cmp::Ordering, collections::HashMap, fmt, str::FromStr}; +use std::{collections::HashMap, fmt, str::FromStr}; use serde::{Deserialize, Serialize}; use uuid::Uuid; use super::base::{ActorRef, ArtifactRef, Header}; -/// Task Status Enum +/// Task lifecycle status. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum TaskStatus { @@ -17,7 +17,7 @@ pub enum TaskStatus { } impl TaskStatus { - fn as_str(&self) -> &'static str { + pub fn as_str(&self) -> &'static str { match self { TaskStatus::Draft => "draft", TaskStatus::Running => "running", @@ -34,7 +34,7 @@ impl fmt::Display for TaskStatus { } } -/// Task Goal Type Enum +/// Task goal category. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum GoalType { @@ -51,7 +51,7 @@ pub enum GoalType { } impl GoalType { - fn as_str(&self) -> &'static str { + pub fn as_str(&self) -> &'static str { match self { GoalType::Feature => "feature", GoalType::Bugfix => "bugfix", @@ -93,7 +93,7 @@ impl FromStr for GoalType { } } -/// Task Object +/// Task object describing intent and constraints. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Task { #[serde(flatten)] @@ -118,9 +118,9 @@ impl Task { created_by: ActorRef, title: impl Into, goal_type: Option, - ) -> Self { - Self { - header: Header::new("task", repo_id, created_by), + ) -> Result { + Ok(Self { + header: Header::new("task", repo_id, created_by)?, title: title.into(), description: None, goal_type, @@ -129,11 +129,11 @@ impl Task { requested_by: None, dependencies: Vec::new(), status: TaskStatus::Draft, - } + }) } } -/// Run Status Enum +/// Run lifecycle status. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum RunStatus { @@ -145,7 +145,7 @@ pub enum RunStatus { } impl RunStatus { - fn as_str(&self) -> &'static str { + pub fn as_str(&self) -> &'static str { match self { RunStatus::Created => "created", RunStatus::Patching => "patching", @@ -162,7 +162,7 @@ impl fmt::Display for RunStatus { } } -/// Run Object +/// Run object for a single orchestration execution. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Run { #[serde(flatten)] @@ -179,7 +179,7 @@ pub struct Run { pub environment: Option, } -/// Environment Object +/// Environment snapshot of the run host. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Environment { pub os: String, // e.g. "macos", "linux" @@ -197,13 +197,16 @@ impl Environment { arch: std::env::consts::ARCH.to_string(), cwd: std::env::current_dir() .map(|p| p.to_string_lossy().to_string()) - .unwrap_or_else(|_| "unknown".to_string()), + .unwrap_or_else(|e| { + tracing::warn!("Failed to get current directory: {}", e); + "unknown".to_string() + }), extra: HashMap::new(), } } } -/// Agent Instance Object +/// Agent instance participating in a run. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AgentInstance { pub role: String, @@ -217,9 +220,9 @@ impl Run { created_by: ActorRef, task_id: Uuid, base_commit_sha: impl Into, - ) -> Self { - Self { - header: Header::new("run", repo_id, created_by), + ) -> Result { + Ok(Self { + header: Header::new("run", repo_id, created_by)?, task_id, orchestrator_version: "libra-builtin".to_string(), base_commit_sha: base_commit_sha.into(), @@ -229,11 +232,11 @@ impl Run { metrics: None, error: None, environment: Some(Environment::capture()), - } + }) } } -/// Apply Status Enum +/// Patch application status. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum ApplyStatus { @@ -244,7 +247,7 @@ pub enum ApplyStatus { } impl ApplyStatus { - fn as_str(&self) -> &'static str { + pub fn as_str(&self) -> &'static str { match self { ApplyStatus::Proposed => "proposed", ApplyStatus::Applied => "applied", @@ -260,7 +263,15 @@ impl fmt::Display for ApplyStatus { } } -/// PatchSet Object +/// Diff format for patch content. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum DiffFormat { + UnifiedDiff, + GitDiff, +} + +/// PatchSet object containing a candidate diff. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PatchSet { #[serde(flatten)] @@ -268,7 +279,7 @@ pub struct PatchSet { pub run_id: Uuid, pub generation: u32, pub base_commit_sha: String, - pub diff_format: String, // unified_diff + pub diff_format: DiffFormat, pub diff_artifact: Option, #[serde(default)] pub touched_files: Vec, @@ -276,7 +287,7 @@ pub struct PatchSet { pub apply_status: ApplyStatus, } -/// TouchedFile Object +/// Touched file summary in a patchset. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TouchedFile { pub path: String, @@ -285,6 +296,26 @@ pub struct TouchedFile { pub lines_deleted: u32, } +impl TouchedFile { + pub fn new( + path: impl Into, + change_type: impl Into, + 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: change_type.into(), + lines_added, + lines_deleted, + }) + } +} + impl PatchSet { /// Create a new patchset object pub fn new( @@ -293,42 +324,92 @@ impl PatchSet { run_id: Uuid, base_commit_sha: impl Into, generation: u32, - ) -> Self { - Self { - header: Header::new("patchset", repo_id, created_by), + ) -> Result { + Ok(Self { + header: Header::new("patchset", repo_id, created_by)?, run_id, generation, base_commit_sha: base_commit_sha.into(), - diff_format: "unified_diff".to_string(), + diff_format: DiffFormat::UnifiedDiff, diff_artifact: None, touched_files: Vec::new(), rationale: None, apply_status: ApplyStatus::Proposed, - } + }) } } -/// ContextSnapshot Object +/// Selection strategy for context snapshots. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum SelectionStrategy { + Explicit, + Heuristic, +} + +/// Context snapshot describing selected inputs. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ContextSnapshot { #[serde(flatten)] pub header: Header, pub base_commit_sha: String, - pub selection_strategy: String, // explicit/heuristic + pub selection_strategy: SelectionStrategy, #[serde(default)] pub items: Vec, pub summary: Option, } -/// ContextItem Object +impl ContextSnapshot { + pub fn new( + repo_id: Uuid, + created_by: ActorRef, + base_commit_sha: impl Into, + selection_strategy: SelectionStrategy, + ) -> Result { + Ok(Self { + header: Header::new("context_snapshot", repo_id, created_by)?, + base_commit_sha: base_commit_sha.into(), + selection_strategy, + items: Vec::new(), + summary: None, + }) + } +} + +/// Context item kind. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ContextItemKind { + File, +} + +/// Context item describing a single input. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ContextItem { - pub kind: String, // file + pub kind: ContextItemKind, pub path: String, pub content_hash: String, } -/// Tool Status Enum +impl ContextItem { + pub fn new( + kind: ContextItemKind, + path: impl Into, + content_hash: impl Into, + ) -> Result { + let path = path.into(); + if path.trim().is_empty() { + return Err("path cannot be empty".to_string()); + } + Ok(Self { + kind, + path, + content_hash: content_hash.into(), + }) + } +} + +/// Tool invocation status. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum ToolStatus { @@ -337,7 +418,7 @@ pub enum ToolStatus { } impl ToolStatus { - fn as_str(&self) -> &'static str { + pub fn as_str(&self) -> &'static str { match self { ToolStatus::Ok => "ok", ToolStatus::Error => "error", @@ -351,7 +432,7 @@ impl fmt::Display for ToolStatus { } } -/// ToolInvocation Object +/// Tool invocation record. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolInvocation { #[serde(flatten)] @@ -367,7 +448,7 @@ pub struct ToolInvocation { pub artifacts: Vec, } -/// IO Footprint Object +/// IO footprint of a tool invocation. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IoFootprint { #[serde(default)] @@ -382,9 +463,9 @@ impl ToolInvocation { created_by: ActorRef, run_id: Uuid, tool_name: impl Into, - ) -> Self { - Self { - header: Header::new("tool_invocation", repo_id, created_by), + ) -> Result { + Ok(Self { + header: Header::new("tool_invocation", repo_id, created_by)?, run_id, tool_name: tool_name.into(), io_footprint: None, @@ -392,11 +473,11 @@ impl ToolInvocation { status: ToolStatus::Ok, result_summary: None, artifacts: Vec::new(), - } + }) } } -/// Plan Status Enum +/// Plan step status. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum PlanStatus { @@ -408,7 +489,7 @@ pub enum PlanStatus { } impl PlanStatus { - fn as_str(&self) -> &'static str { + pub fn as_str(&self) -> &'static str { match self { PlanStatus::Pending => "pending", PlanStatus::InProgress => "in_progress", @@ -425,30 +506,19 @@ impl fmt::Display for PlanStatus { } } -/// Plan Object +/// Plan object for step decomposition. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Plan { #[serde(flatten)] pub header: Header, pub run_id: Uuid, + /// Plan version starts at 1 and must increase by 1 for each update. pub plan_version: u32, #[serde(default)] pub steps: Vec, } -impl PartialOrd for Plan { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Plan { - fn cmp(&self, other: &Self) -> Ordering { - self.plan_version.cmp(&other.plan_version) - } -} - -/// Plan Step Object +/// Plan step with inputs, outputs, and checks. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct PlanStep { pub intent: String, @@ -461,17 +531,34 @@ pub struct PlanStep { impl Plan { /// Create a new plan object - pub fn new(repo_id: Uuid, created_by: ActorRef, run_id: Uuid, plan_version: u32) -> Self { - Self { - header: Header::new("plan", repo_id, created_by), + pub fn new(repo_id: Uuid, created_by: ActorRef, run_id: Uuid) -> Result { + Ok(Self { + header: Header::new("plan", repo_id, created_by)?, run_id, - plan_version, + plan_version: 1, steps: Vec::new(), - } + }) + } + + 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("plan", repo_id, created_by)?, + run_id, + plan_version: next_version, + steps: Vec::new(), + }) } } -/// Evidence Object +/// Evidence object for test/lint/build results. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Evidence { #[serde(flatten)] @@ -494,9 +581,9 @@ impl Evidence { run_id: Uuid, kind: impl Into, tool: impl Into, - ) -> Self { - Self { - header: Header::new("evidence", repo_id, created_by), + ) -> Result { + Ok(Self { + header: Header::new("evidence", repo_id, created_by)?, run_id, patchset_id: None, kind: kind.into(), @@ -505,11 +592,11 @@ impl Evidence { exit_code: None, summary: None, report_artifacts: Vec::new(), - } + }) } } -/// Provenance Object +/// Provenance object for model/provider metadata. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Provenance { #[serde(flatten)] @@ -528,19 +615,19 @@ impl Provenance { run_id: Uuid, provider: impl Into, model: impl Into, - ) -> Self { - Self { - header: Header::new("provenance", repo_id, created_by), + ) -> Result { + Ok(Self { + header: Header::new("provenance", repo_id, created_by)?, run_id, provider: provider.into(), model: model.into(), parameters: None, token_usage: None, - } + }) } } -/// Decision Object +/// Decision object linking process to outcomes. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Decision { #[serde(flatten)] @@ -560,16 +647,16 @@ impl Decision { created_by: ActorRef, run_id: Uuid, decision_type: impl Into, - ) -> Self { - Self { - header: Header::new("decision", repo_id, created_by), + ) -> Result { + Ok(Self { + header: Header::new("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, - } + }) } } @@ -580,14 +667,14 @@ mod tests { #[test] fn test_task_creation() { let repo_id = Uuid::now_v7(); - let actor = ActorRef::human("jackie"); - let mut task = Task::new(repo_id, actor, "Fix bug", Some(GoalType::Bugfix)); + 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::now_v7(); task.dependencies.push(dep_id); - assert_eq!(task.header.object_type, "task"); + assert_eq!(task.header.object_type(), "task"); assert_eq!(task.status, TaskStatus::Draft); assert_eq!(task.goal_type, Some(GoalType::Bugfix)); assert_eq!(task.dependencies.len(), 1); @@ -597,8 +684,8 @@ mod tests { #[test] fn test_task_goal_type_optional() { let repo_id = Uuid::now_v7(); - let actor = ActorRef::human("jackie"); - let task = Task::new(repo_id, actor, "Write docs", None); + 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()); } @@ -606,11 +693,11 @@ mod tests { #[test] fn test_new_objects_creation() { let repo_id = Uuid::now_v7(); - let actor = ActorRef::agent("test-agent"); + let actor = ActorRef::agent("test-agent").expect("actor"); let run_id = Uuid::now_v7(); // Run with environment (auto captured) - let run = Run::new(repo_id, actor.clone(), Uuid::now_v7(), "sha123"); + let run = Run::new(repo_id, actor.clone(), Uuid::now_v7(), "sha123").expect("run"); let env = run.environment.as_ref().unwrap(); // Check if it captured real values (assuming we are running on some OS) @@ -619,7 +706,7 @@ mod tests { assert!(!env.cwd.is_empty()); // Plan with steps and status - let mut plan = Plan::new(repo_id, actor.clone(), run_id, 1); + let mut plan = Plan::new(repo_id, actor.clone(), run_id).expect("plan"); plan.steps.push(PlanStep { intent: "step1".to_string(), inputs: None, @@ -629,48 +716,52 @@ mod tests { status: PlanStatus::Pending, }); - assert_eq!(plan.header.object_type, "plan"); + assert_eq!(plan.header.object_type(), "plan"); assert_eq!(plan.plan_version, 1); assert_eq!(plan.steps[0].status, PlanStatus::Pending); // Evidence - let evidence = Evidence::new(repo_id, actor.clone(), run_id, "test", "cargo"); - assert_eq!(evidence.header.object_type, "evidence"); + let evidence = + Evidence::new(repo_id, actor.clone(), run_id, "test", "cargo").expect("evidence"); + assert_eq!(evidence.header.object_type(), "evidence"); assert_eq!(evidence.kind, "test"); // Provenance - let provenance = Provenance::new(repo_id, actor.clone(), run_id, "openai", "gpt-4"); - assert_eq!(provenance.header.object_type, "provenance"); + let provenance = + Provenance::new(repo_id, actor.clone(), run_id, "openai", "gpt-4").expect("provenance"); + assert_eq!(provenance.header.object_type(), "provenance"); assert_eq!(provenance.provider, "openai"); // Decision - let decision = Decision::new(repo_id, actor.clone(), run_id, "commit"); - assert_eq!(decision.header.object_type, "decision"); + let decision = Decision::new(repo_id, actor.clone(), run_id, "commit").expect("decision"); + assert_eq!(decision.header.object_type(), "decision"); assert_eq!(decision.decision_type, "commit"); } #[test] fn test_task_requested_by() { let repo_id = Uuid::now_v7(); - let actor = ActorRef::human("jackie"); - let mut task = Task::new(repo_id, actor.clone(), "Fix bug", Some(GoalType::Bugfix)); + let actor = ActorRef::human("jackie").expect("actor"); + let mut task = + Task::new(repo_id, actor.clone(), "Fix bug", Some(GoalType::Bugfix)).expect("task"); - task.requested_by = Some(ActorRef::mcp_client("vscode-client")); + task.requested_by = Some(ActorRef::mcp_client("vscode-client").expect("actor")); assert!(task.requested_by.is_some()); assert_eq!( - task.requested_by.unwrap().kind, - super::super::base::ActorKind::McpClient + task.requested_by.unwrap().kind(), + &super::super::base::ActorKind::McpClient ); } #[test] fn test_tool_invocation_io_footprint() { let repo_id = Uuid::now_v7(); - let actor = ActorRef::human("jackie"); + let actor = ActorRef::human("jackie").expect("actor"); let run_id = Uuid::now_v7(); - let mut tool_inv = ToolInvocation::new(repo_id, actor, run_id, "read_file"); + 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()], @@ -687,14 +778,14 @@ mod tests { #[test] fn test_patchset_creation() { let repo_id = Uuid::now_v7(); - let actor = ActorRef::agent("test-agent"); + let actor = ActorRef::agent("test-agent").expect("actor"); let run_id = Uuid::now_v7(); - let patchset = PatchSet::new(repo_id, actor, run_id, "sha123", 1); + let patchset = PatchSet::new(repo_id, actor, run_id, "sha123", 1).expect("patchset"); - assert_eq!(patchset.header.object_type, "patchset"); + assert_eq!(patchset.header.object_type(), "patchset"); assert_eq!(patchset.generation, 1); - assert_eq!(patchset.diff_format, "unified_diff"); + assert_eq!(patchset.diff_format, DiffFormat::UnifiedDiff); assert_eq!(patchset.apply_status, ApplyStatus::Proposed); assert!(patchset.touched_files.is_empty()); } @@ -702,21 +793,16 @@ mod tests { #[test] fn test_context_snapshot_fields() { let repo_id = Uuid::now_v7(); - let actor = ActorRef::agent("test-agent"); + let actor = ActorRef::agent("test-agent").expect("actor"); - let mut snapshot = ContextSnapshot { - header: Header::new("context_snapshot", repo_id, actor), - base_commit_sha: "sha123".to_string(), - selection_strategy: "explicit".to_string(), - items: Vec::new(), - summary: Some("core files".to_string()), - }; + let mut snapshot = + ContextSnapshot::new(repo_id, actor, "sha123", SelectionStrategy::Explicit) + .expect("snapshot"); + snapshot.summary = Some("core files".to_string()); - snapshot.items.push(ContextItem { - kind: "file".to_string(), - path: "src/lib.rs".to_string(), - content_hash: "abc".to_string(), - }); + snapshot.items.push( + ContextItem::new(ContextItemKind::File, "src/lib.rs", "abc").expect("context item"), + ); assert_eq!(snapshot.items.len(), 1); assert_eq!(snapshot.items[0].path, "src/lib.rs"); @@ -726,16 +812,17 @@ mod tests { #[test] fn test_tool_invocation_fields() { let repo_id = Uuid::now_v7(); - let actor = ActorRef::human("jackie"); + let actor = ActorRef::human("jackie").expect("actor"); let run_id = Uuid::now_v7(); - let mut tool_inv = ToolInvocation::new(repo_id, actor, run_id, "apply_patch"); + let mut tool_inv = + ToolInvocation::new(repo_id, actor, run_id, "apply_patch").expect("tool_invocation"); tool_inv.status = ToolStatus::Error; tool_inv.args = serde_json::json!({"path": "src/lib.rs"}); tool_inv.result_summary = Some("failed".to_string()); tool_inv .artifacts - .push(ArtifactRef::new("local", "artifact-key")); + .push(ArtifactRef::new("local", "artifact-key").expect("artifact")); assert_eq!(tool_inv.status, ToolStatus::Error); assert_eq!(tool_inv.artifacts.len(), 1); @@ -745,16 +832,17 @@ mod tests { #[test] fn test_evidence_fields() { let repo_id = Uuid::now_v7(); - let actor = ActorRef::agent("test-agent"); + let actor = ActorRef::agent("test-agent").expect("actor"); let run_id = Uuid::now_v7(); let patchset_id = Uuid::now_v7(); - let mut evidence = Evidence::new(repo_id, actor, run_id, "test", "cargo"); + let mut evidence = + Evidence::new(repo_id, actor, run_id, "test", "cargo").expect("evidence"); evidence.patchset_id = Some(patchset_id); evidence.exit_code = Some(1); evidence .report_artifacts - .push(ArtifactRef::new("local", "log.txt")); + .push(ArtifactRef::new("local", "log.txt").expect("artifact")); assert_eq!(evidence.patchset_id, Some(patchset_id)); assert_eq!(evidence.exit_code, Some(1)); @@ -764,10 +852,11 @@ mod tests { #[test] fn test_provenance_fields() { let repo_id = Uuid::now_v7(); - let actor = ActorRef::agent("test-agent"); + let actor = ActorRef::agent("test-agent").expect("actor"); let run_id = Uuid::now_v7(); - let mut provenance = Provenance::new(repo_id, actor, run_id, "openai", "gpt-4"); + let mut provenance = + Provenance::new(repo_id, actor, run_id, "openai", "gpt-4").expect("provenance"); provenance.parameters = Some(serde_json::json!({"temperature": 0.2})); provenance.token_usage = Some(serde_json::json!({"input": 10, "output": 5})); @@ -778,11 +867,11 @@ mod tests { #[test] fn test_decision_fields() { let repo_id = Uuid::now_v7(); - let actor = ActorRef::agent("test-agent"); + let actor = ActorRef::agent("test-agent").expect("actor"); let run_id = Uuid::now_v7(); let patchset_id = Uuid::now_v7(); - let mut decision = Decision::new(repo_id, actor, run_id, "commit"); + let mut decision = Decision::new(repo_id, actor, run_id, "commit").expect("decision"); decision.chosen_patchset_id = Some(patchset_id); decision.result_commit_sha = Some("abc123".to_string()); decision.rationale = Some("tests passed".to_string()); @@ -795,31 +884,34 @@ mod tests { #[test] fn test_plan_version_ordering() { let repo_id = Uuid::now_v7(); - let actor = ActorRef::human("jackie"); + let actor = ActorRef::human("jackie").expect("actor"); let run_id = Uuid::now_v7(); - let plan_v1 = Plan::new(repo_id, actor.clone(), run_id, 1); - let plan_v2 = Plan::new(repo_id, actor.clone(), run_id, 2); - let plan_v3 = Plan::new(repo_id, actor.clone(), run_id, 3); + 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(); + 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_v2); - assert!(plan_v2 > plan_v1); + assert!(plan_v3.plan_version > plan_v2.plan_version); + assert!(plan_v2.plan_version > plan_v1.plan_version); } #[test] fn ai_process_tool_invocation_artifacts_default() { let repo_id = Uuid::now_v7(); - let actor = ActorRef::human("jackie"); + let actor = ActorRef::human("jackie").expect("actor"); let run_id = Uuid::now_v7(); - let tool_inv = ToolInvocation::new(repo_id, actor, run_id, "read_file"); + let tool_inv = + ToolInvocation::new(repo_id, actor, run_id, "read_file").expect("tool_invocation"); let mut value = serde_json::to_value(&tool_inv).unwrap(); if let serde_json::Value::Object(ref mut map) = value { diff --git a/src/internal/object/mod.rs b/src/internal/object/mod.rs index 7cdbc01a..72518499 100644 --- a/src/internal/object/mod.rs +++ b/src/internal/object/mod.rs @@ -1,6 +1,6 @@ //! 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 ai_process; pub mod blob; pub mod commit; pub mod note; diff --git a/src/lib.rs b/src/lib.rs index caaf9a98..c826f3e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,6 @@ //! - `internal::pack`: decode/encode, caches, waitlists, parallel pipelines, helpers. //! - `internal::object`: Blob/Tree/Commit/Tag/Note objects, type enum, object trait. //! - `internal::zlib`: compression/decompression stream utilities. -//! - `ai_process`: AI process objects for agent runs and provenance. //! - `delta` and `zstdelta`: delta algorithms and rebuild helpers. //! - `errors`: unified error types. //! - `hash`: Hash helpers. @@ -31,7 +30,6 @@ //! Test Data //! - Located under `tests/data/`, includes real pack files and object sets. -pub mod ai_process; mod delta; pub mod diff; pub mod errors; From a8cd5c71a6a12a1844f179be71beb5b72721a2b8 Mon Sep 17 00:00:00 2001 From: jackie Date: Tue, 10 Feb 2026 21:23:21 +0800 Subject: [PATCH 03/13] restore hash Signed-off-by: jackie --- src/hash.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/hash.rs b/src/hash.rs index 007ae494..2fcc75a2 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -240,10 +240,6 @@ impl ObjectHash { } } -/// Compute SHA256 hash and return hex string. -pub(crate) fn sha256_hex(data: &[u8]) -> String { - hex::encode(sha2::Sha256::digest(data)) -} thread_local! { /// Thread-local variable to store the current hash kind. /// This allows different threads to work with different hash algorithms concurrently From 332149ab1a2d4be808432bcff30e0cc250e632f8 Mon Sep 17 00:00:00 2001 From: jackie Date: Tue, 10 Feb 2026 21:24:39 +0800 Subject: [PATCH 04/13] update checksum Signed-off-by: jackie --- src/internal/object/ai_process/checksum.rs | 40 +++++++++++++--------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/internal/object/ai_process/checksum.rs b/src/internal/object/ai_process/checksum.rs index 1b88f812..bb0b2ed5 100644 --- a/src/internal/object/ai_process/checksum.rs +++ b/src/internal/object/ai_process/checksum.rs @@ -1,29 +1,35 @@ +use std::str::FromStr; + use serde::Serialize; use serde_json::Value; -use crate::hash::sha256_hex; +use crate::hash::{ObjectHash, get_hash_kind}; -/// Calculate SHA256 hex string from bytes. -pub fn compute_sha256_hex(content: &[u8]) -> String { - sha256_hex(content) +/// Calculate hash from bytes using the repository hash kind. +pub fn compute_hash(content: &[u8]) -> ObjectHash { + ObjectHash::new(content) } -/// Calculate SHA256 hex string from a serializable object (deterministic JSON). -pub fn compute_json_sha256_hex(object: &T) -> Result { +/// Calculate hash from a serializable object (deterministic JSON). +pub fn compute_json_hash(object: &T) -> Result { let mut value = serde_json::to_value(object)?; canonicalize_json(&mut value); let content = serde_json::to_vec(&value)?; - Ok(sha256_hex(&content)) -} - -/// Check valid format (64 hex chars). -pub fn is_valid_sha256_hex(hash: &str) -> bool { - hash.len() == 64 && hash.chars().all(|c| c.is_ascii_hexdigit()) + Ok(ObjectHash::new(&content)) } -/// Verify content against the expected SHA256 hex string. -pub fn verify_sha256_hex(expected: &str, content: &[u8]) -> bool { - compute_sha256_hex(content) == expected +/// Parse a hex string into `ObjectHash` for the current repository hash kind. +pub fn parse_object_hash(value: &str) -> Result { + if value.len() != get_hash_kind().hex_len() { + return Err(format!( + "Invalid hash hex string length (expected {})", + get_hash_kind().hex_len() + )); + } + if !value.chars().all(|c| c.is_ascii_hexdigit()) { + return Err("Invalid hash hex string".to_string()); + } + ObjectHash::from_str(value) } fn canonicalize_json(value: &mut Value) { @@ -68,8 +74,8 @@ mod tests { map_b.insert("a".to_string(), "1".to_string()); map_b.insert("b".to_string(), "2".to_string()); - let hash_a = compute_json_sha256_hex(&MapWrapper { map: map_a }).expect("checksum"); - let hash_b = compute_json_sha256_hex(&MapWrapper { map: map_b }).expect("checksum"); + let hash_a = compute_json_hash(&MapWrapper { map: map_a }).expect("checksum"); + let hash_b = compute_json_hash(&MapWrapper { map: map_b }).expect("checksum"); assert_eq!(hash_a, hash_b); } From 352250e58d7b410b3722edd89d41da6b7ed6db60 Mon Sep 17 00:00:00 2001 From: jackie Date: Tue, 10 Feb 2026 21:36:28 +0800 Subject: [PATCH 05/13] update base object Signed-off-by: jackie --- src/internal/object/ai_process/base.rs | 165 ++++++++++++++++--------- 1 file changed, 108 insertions(+), 57 deletions(-) diff --git a/src/internal/object/ai_process/base.rs b/src/internal/object/ai_process/base.rs index 13450104..3f7410cd 100644 --- a/src/internal/object/ai_process/base.rs +++ b/src/internal/object/ai_process/base.rs @@ -4,9 +4,8 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use super::checksum::{ - compute_json_sha256_hex, compute_sha256_hex, is_valid_sha256_hex, verify_sha256_hex, -}; +use super::checksum::{compute_hash as compute_object_hash, compute_json_hash, parse_object_hash}; +use crate::hash::ObjectHash; /// Visibility of an AI process object. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -16,13 +15,50 @@ pub enum Visibility { Public, } +/// AI process object type. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum AiObjectType { + Task, + Run, + PatchSet, + ContextSnapshot, + ToolInvocation, + Plan, + Evidence, + Provenance, + 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. #[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: String, + object_type: AiObjectType, /// Model version schema_version: u32, /// Repository identifier @@ -41,19 +77,15 @@ pub struct Header { external_ids: HashMap, /// Content checksum (optional) #[serde(default)] - checksum: Option, + checksum: Option, } impl Header { pub fn new( - object_type: impl Into, + object_type: AiObjectType, repo_id: Uuid, created_by: ActorRef, ) -> Result { - let object_type = object_type.into(); - if object_type.trim().is_empty() { - return Err("object_type cannot be empty".to_string()); - } Ok(Self { object_id: Uuid::now_v7(), object_type, @@ -72,7 +104,7 @@ impl Header { self.object_id } - pub fn object_type(&self) -> &str { + pub fn object_type(&self) -> &AiObjectType { &self.object_type } @@ -116,11 +148,7 @@ impl Header { self.object_id = object_id; } - pub fn set_object_type(&mut self, object_type: impl Into) -> Result<(), String> { - let object_type = object_type.into(); - if object_type.trim().is_empty() { - return Err("object_type cannot be empty".to_string()); - } + pub fn set_object_type(&mut self, object_type: AiObjectType) -> Result<(), String> { self.object_type = object_type; Ok(()) } @@ -142,14 +170,14 @@ impl Header { } /// Accessor for checksum - pub fn checksum(&self) -> Option<&str> { - self.checksum.as_deref() + pub fn checksum(&self) -> Option<&ObjectHash> { + self.checksum.as_ref() } /// Seal the header by calculating and setting the checksum of the provided object. /// This is typically called before persisting the object. pub fn seal(&mut self, object: &T) -> Result<(), serde_json::Error> { - self.checksum = Some(compute_json_sha256_hex(object)?); + self.checksum = Some(compute_json_hash(object)?); Ok(()) } } @@ -230,6 +258,19 @@ impl ActorRef { }) } + /// 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 } @@ -286,8 +327,8 @@ pub struct ArtifactRef { content_type: Option, /// Size in bytes (optional) size_bytes: Option, - /// SHA256 checksum (strongly recommended) - sha256: Option, + /// Content hash (strongly recommended) + hash: Option, /// Expiration time (optional) expires_at: Option>, } @@ -307,7 +348,7 @@ impl ArtifactRef { key, content_type: None, size_bytes: None, - sha256: None, + hash: None, expires_at: None, }) } @@ -328,26 +369,29 @@ impl ArtifactRef { self.size_bytes } - pub fn sha256(&self) -> Option<&str> { - self.sha256.as_deref() + pub fn hash(&self) -> Option<&ObjectHash> { + self.hash.as_ref() } pub fn expires_at(&self) -> Option> { self.expires_at } - /// Calculate SHA256 checksum for the given content bytes - pub fn compute_sha256(content: &[u8]) -> String { - compute_sha256_hex(content) + /// Calculate hash for the given content bytes. + pub fn compute_hash(content: &[u8]) -> ObjectHash { + compute_object_hash(content) } - /// Set the checksum directly with validation - pub fn with_checksum(mut self, sha256: impl Into) -> Result { - let sha256 = sha256.into(); - if !is_valid_sha256_hex(&sha256) { - return Err(format!("Invalid SHA256 hash format: {}", sha256)); - } - self.sha256 = Some(sha256); + /// Set the hash directly. + pub fn with_hash(mut self, hash: ObjectHash) -> 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 = parse_object_hash(hash.as_ref())?; + self.hash = Some(hash); Ok(self) } @@ -367,17 +411,17 @@ impl ArtifactRef { #[must_use = "handle integrity verification result"] pub fn verify_integrity(&self, content: &[u8]) -> Result { let stored_hash = self - .sha256 + .hash .as_ref() - .ok_or_else(|| "No checksum stored in ArtifactRef".to_string())?; + .ok_or_else(|| "No hash stored in ArtifactRef".to_string())?; - Ok(verify_sha256_hex(stored_hash, content)) + Ok(compute_object_hash(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.sha256, &other.sha256) { + match (&self.hash, &other.hash) { (Some(a), Some(b)) => Some(a == b), _ => None, } @@ -397,12 +441,13 @@ impl ArtifactRef { #[cfg(test)] mod tests { use super::*; + use crate::hash::{HashKind, ObjectHash, set_hash_kind_for_test}; #[test] fn test_header_serialization() { let repo_id = Uuid::now_v7(); let actor = ActorRef::human("jackie").expect("actor"); - let header = Header::new("task", repo_id, actor).expect("header"); + 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(); @@ -423,6 +468,10 @@ mod tests { 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] @@ -437,9 +486,10 @@ mod tests { #[test] fn test_header_checksum() { + let _guard = set_hash_kind_for_test(HashKind::Sha256); let repo_id = Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap(); let actor = ActorRef::human("jackie").expect("actor"); - let mut header = Header::new("task", repo_id, actor).expect("header"); + 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") @@ -448,28 +498,30 @@ mod tests { ); header.set_object_id(Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap()); - let checksum = compute_json_sha256_hex(&header).expect("checksum"); - assert_eq!(checksum.len(), 64); // SHA256 length + let checksum = compute_json_hash(&header).expect("checksum"); + assert_eq!(checksum.to_string().len(), 64); // SHA256 length // Ensure changes change checksum - header.set_object_type("run").expect("object_type"); - let checksum2 = compute_json_sha256_hex(&header).expect("checksum"); + header + .set_object_type(AiObjectType::Run) + .expect("object_type"); + let checksum2 = compute_json_hash(&header).expect("checksum"); assert_ne!(checksum, checksum2); } #[test] fn test_artifact_checksum() { + let _guard = set_hash_kind_for_test(HashKind::Sha256); let content = b"hello world"; - let hash = ArtifactRef::compute_sha256(content); + let hash = ArtifactRef::compute_hash(content); // echo -n "hello world" | shasum -a 256 let expected_str = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"; - assert_eq!(hash.as_str(), expected_str); + assert_eq!(hash.to_string(), expected_str); let artifact = ArtifactRef::new("s3", "key") .expect("artifact") - .with_checksum(hash.as_str()) - .expect("checksum"); - assert_eq!(artifact.sha256(), Some(hash.as_str())); + .with_hash(hash); + assert_eq!(artifact.hash(), Some(&hash)); // Integrity check assert!(artifact.verify_integrity(content).unwrap()); @@ -478,14 +530,12 @@ mod tests { // Deduplication let artifact2 = ArtifactRef::new("local", "other/path") .expect("artifact") - .with_checksum(hash.as_str()) - .expect("checksum"); + .with_hash(ObjectHash::new(content)); assert_eq!(artifact.content_eq(&artifact2), Some(true)); let artifact3 = ArtifactRef::new("s3", "diff") .expect("artifact") - .with_checksum(ArtifactRef::compute_sha256(b"diff").as_str()) - .expect("checksum"); + .with_hash(ArtifactRef::compute_hash(b"diff")); assert_eq!(artifact.content_eq(&artifact3), Some(false)); } @@ -493,22 +543,23 @@ mod tests { fn test_invalid_checksum() { let result = ArtifactRef::new("s3", "key") .expect("artifact") - .with_checksum("bad_hash"); + .with_hash_hex("bad_hash"); assert!(result.is_err()); } #[test] fn test_header_seal() { + let _guard = set_hash_kind_for_test(HashKind::Sha256); let repo_id = Uuid::now_v7(); let actor = ActorRef::human("jackie").expect("actor"); - let mut header = Header::new("task", repo_id, actor).expect("header"); + 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_json_sha256_hex(&content).expect("checksum"); - assert_eq!(header.checksum().expect("checksum"), expected); + let expected = compute_json_hash(&content).expect("checksum"); + assert_eq!(header.checksum().expect("checksum"), &expected); } #[test] From 40f7340ccfa69d3408a349e4e5892d05f1e774f9 Mon Sep 17 00:00:00 2001 From: jackie Date: Tue, 10 Feb 2026 21:37:31 +0800 Subject: [PATCH 06/13] update process object Signed-off-by: jackie --- src/internal/object/ai_process/objects.rs | 97 ++++++++++++++--------- 1 file changed, 61 insertions(+), 36 deletions(-) diff --git a/src/internal/object/ai_process/objects.rs b/src/internal/object/ai_process/objects.rs index 29bf81d9..a77b30db 100644 --- a/src/internal/object/ai_process/objects.rs +++ b/src/internal/object/ai_process/objects.rs @@ -3,7 +3,11 @@ use std::{collections::HashMap, fmt, str::FromStr}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use super::base::{ActorRef, ArtifactRef, Header}; +use super::{ + base::{ActorRef, AiObjectType, ArtifactRef, Header}, + checksum::parse_object_hash, +}; +use crate::hash::ObjectHash; /// Task lifecycle status. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -120,7 +124,7 @@ impl Task { goal_type: Option, ) -> Result { Ok(Self { - header: Header::new("task", repo_id, created_by)?, + header: Header::new(AiObjectType::Task, repo_id, created_by)?, title: title.into(), description: None, goal_type, @@ -169,7 +173,7 @@ pub struct Run { pub header: Header, pub task_id: Uuid, pub orchestrator_version: String, - pub base_commit_sha: String, + pub base_commit_sha: ObjectHash, pub status: RunStatus, pub context_snapshot_id: Option, #[serde(default)] @@ -219,13 +223,14 @@ impl Run { repo_id: Uuid, created_by: ActorRef, task_id: Uuid, - base_commit_sha: impl Into, + base_commit_sha: impl AsRef, ) -> Result { + let base_commit_sha = parse_object_hash(base_commit_sha.as_ref())?; Ok(Self { - header: Header::new("run", repo_id, created_by)?, + header: Header::new(AiObjectType::Run, repo_id, created_by)?, task_id, orchestrator_version: "libra-builtin".to_string(), - base_commit_sha: base_commit_sha.into(), + base_commit_sha, status: RunStatus::Created, context_snapshot_id: None, agent_instances: Vec::new(), @@ -278,7 +283,7 @@ pub struct PatchSet { pub header: Header, pub run_id: Uuid, pub generation: u32, - pub base_commit_sha: String, + pub base_commit_sha: ObjectHash, pub diff_format: DiffFormat, pub diff_artifact: Option, #[serde(default)] @@ -322,14 +327,15 @@ impl PatchSet { repo_id: Uuid, created_by: ActorRef, run_id: Uuid, - base_commit_sha: impl Into, + base_commit_sha: impl AsRef, generation: u32, ) -> Result { + let base_commit_sha = parse_object_hash(base_commit_sha.as_ref())?; Ok(Self { - header: Header::new("patchset", repo_id, created_by)?, + header: Header::new(AiObjectType::PatchSet, repo_id, created_by)?, run_id, generation, - base_commit_sha: base_commit_sha.into(), + base_commit_sha, diff_format: DiffFormat::UnifiedDiff, diff_artifact: None, touched_files: Vec::new(), @@ -352,7 +358,7 @@ pub enum SelectionStrategy { pub struct ContextSnapshot { #[serde(flatten)] pub header: Header, - pub base_commit_sha: String, + pub base_commit_sha: ObjectHash, pub selection_strategy: SelectionStrategy, #[serde(default)] pub items: Vec, @@ -363,12 +369,13 @@ impl ContextSnapshot { pub fn new( repo_id: Uuid, created_by: ActorRef, - base_commit_sha: impl Into, + base_commit_sha: impl AsRef, selection_strategy: SelectionStrategy, ) -> Result { + let base_commit_sha = parse_object_hash(base_commit_sha.as_ref())?; Ok(Self { - header: Header::new("context_snapshot", repo_id, created_by)?, - base_commit_sha: base_commit_sha.into(), + header: Header::new(AiObjectType::ContextSnapshot, repo_id, created_by)?, + base_commit_sha, selection_strategy, items: Vec::new(), summary: None, @@ -388,14 +395,14 @@ pub enum ContextItemKind { pub struct ContextItem { pub kind: ContextItemKind, pub path: String, - pub content_hash: String, + pub content_id: ObjectHash, } impl ContextItem { pub fn new( kind: ContextItemKind, path: impl Into, - content_hash: impl Into, + content_id: ObjectHash, ) -> Result { let path = path.into(); if path.trim().is_empty() { @@ -404,7 +411,7 @@ impl ContextItem { Ok(Self { kind, path, - content_hash: content_hash.into(), + content_id, }) } } @@ -465,7 +472,7 @@ impl ToolInvocation { tool_name: impl Into, ) -> Result { Ok(Self { - header: Header::new("tool_invocation", repo_id, created_by)?, + header: Header::new(AiObjectType::ToolInvocation, repo_id, created_by)?, run_id, tool_name: tool_name.into(), io_footprint: None, @@ -533,7 +540,7 @@ impl Plan { /// Create a new plan object pub fn new(repo_id: Uuid, created_by: ActorRef, run_id: Uuid) -> Result { Ok(Self { - header: Header::new("plan", repo_id, created_by)?, + header: Header::new(AiObjectType::Plan, repo_id, created_by)?, run_id, plan_version: 1, steps: Vec::new(), @@ -550,7 +557,7 @@ impl Plan { .checked_add(1) .ok_or_else(|| "plan_version overflow".to_string())?; Ok(Self { - header: Header::new("plan", repo_id, created_by)?, + header: Header::new(AiObjectType::Plan, repo_id, created_by)?, run_id, plan_version: next_version, steps: Vec::new(), @@ -583,7 +590,7 @@ impl Evidence { tool: impl Into, ) -> Result { Ok(Self { - header: Header::new("evidence", repo_id, created_by)?, + header: Header::new(AiObjectType::Evidence, repo_id, created_by)?, run_id, patchset_id: None, kind: kind.into(), @@ -617,7 +624,7 @@ impl Provenance { model: impl Into, ) -> Result { Ok(Self { - header: Header::new("provenance", repo_id, created_by)?, + header: Header::new(AiObjectType::Provenance, repo_id, created_by)?, run_id, provider: provider.into(), model: model.into(), @@ -635,7 +642,7 @@ pub struct Decision { pub run_id: Uuid, pub decision_type: String, // commit/checkpoint/abandon/retry/rollback pub chosen_patchset_id: Option, - pub result_commit_sha: Option, + pub result_commit_sha: Option, pub checkpoint_id: Option, pub rationale: Option, } @@ -649,7 +656,7 @@ impl Decision { decision_type: impl Into, ) -> Result { Ok(Self { - header: Header::new("decision", repo_id, created_by)?, + header: Header::new(AiObjectType::Decision, repo_id, created_by)?, run_id, decision_type: decision_type.into(), chosen_patchset_id: None, @@ -663,6 +670,11 @@ impl Decision { #[cfg(test)] mod tests { use super::*; + use crate::hash::{HashKind, set_hash_kind_for_test}; + + fn test_hash_hex() -> String { + ObjectHash::new(b"ai-process-test").to_string() + } #[test] fn test_task_creation() { @@ -674,7 +686,7 @@ mod tests { let dep_id = Uuid::now_v7(); task.dependencies.push(dep_id); - assert_eq!(task.header.object_type(), "task"); + 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); @@ -692,12 +704,14 @@ mod tests { #[test] fn test_new_objects_creation() { + let _guard = set_hash_kind_for_test(HashKind::Sha256); let repo_id = Uuid::now_v7(); let actor = ActorRef::agent("test-agent").expect("actor"); let run_id = Uuid::now_v7(); + let base_hash = test_hash_hex(); // Run with environment (auto captured) - let run = Run::new(repo_id, actor.clone(), Uuid::now_v7(), "sha123").expect("run"); + let run = Run::new(repo_id, actor.clone(), Uuid::now_v7(), &base_hash).expect("run"); let env = run.environment.as_ref().unwrap(); // Check if it captured real values (assuming we are running on some OS) @@ -716,25 +730,25 @@ mod tests { status: PlanStatus::Pending, }); - assert_eq!(plan.header.object_type(), "plan"); + assert_eq!(plan.header.object_type(), &AiObjectType::Plan); assert_eq!(plan.plan_version, 1); assert_eq!(plan.steps[0].status, PlanStatus::Pending); // Evidence let evidence = Evidence::new(repo_id, actor.clone(), run_id, "test", "cargo").expect("evidence"); - assert_eq!(evidence.header.object_type(), "evidence"); + assert_eq!(evidence.header.object_type(), &AiObjectType::Evidence); assert_eq!(evidence.kind, "test"); // Provenance let provenance = Provenance::new(repo_id, actor.clone(), run_id, "openai", "gpt-4").expect("provenance"); - assert_eq!(provenance.header.object_type(), "provenance"); + assert_eq!(provenance.header.object_type(), &AiObjectType::Provenance); assert_eq!(provenance.provider, "openai"); // Decision let decision = Decision::new(repo_id, actor.clone(), run_id, "commit").expect("decision"); - assert_eq!(decision.header.object_type(), "decision"); + assert_eq!(decision.header.object_type(), &AiObjectType::Decision); assert_eq!(decision.decision_type, "commit"); } @@ -777,13 +791,15 @@ mod tests { #[test] fn test_patchset_creation() { + let _guard = set_hash_kind_for_test(HashKind::Sha256); let repo_id = Uuid::now_v7(); let actor = ActorRef::agent("test-agent").expect("actor"); let run_id = Uuid::now_v7(); + let base_hash = test_hash_hex(); - let patchset = PatchSet::new(repo_id, actor, run_id, "sha123", 1).expect("patchset"); + let patchset = PatchSet::new(repo_id, actor, run_id, &base_hash, 1).expect("patchset"); - assert_eq!(patchset.header.object_type(), "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); @@ -792,16 +808,23 @@ mod tests { #[test] fn test_context_snapshot_fields() { + let _guard = set_hash_kind_for_test(HashKind::Sha256); let repo_id = Uuid::now_v7(); let actor = ActorRef::agent("test-agent").expect("actor"); + let base_hash = test_hash_hex(); let mut snapshot = - ContextSnapshot::new(repo_id, actor, "sha123", SelectionStrategy::Explicit) + ContextSnapshot::new(repo_id, actor, &base_hash, SelectionStrategy::Explicit) .expect("snapshot"); snapshot.summary = Some("core files".to_string()); snapshot.items.push( - ContextItem::new(ContextItemKind::File, "src/lib.rs", "abc").expect("context item"), + ContextItem::new( + ContextItemKind::File, + "src/lib.rs", + ObjectHash::new(b"context-item"), + ) + .expect("context item"), ); assert_eq!(snapshot.items.len(), 1); @@ -866,18 +889,20 @@ mod tests { #[test] fn test_decision_fields() { + let _guard = set_hash_kind_for_test(HashKind::Sha256); let repo_id = Uuid::now_v7(); let actor = ActorRef::agent("test-agent").expect("actor"); let run_id = Uuid::now_v7(); let patchset_id = Uuid::now_v7(); + let expected_hash = ObjectHash::new(b"decision-hash"); let mut decision = Decision::new(repo_id, actor, run_id, "commit").expect("decision"); decision.chosen_patchset_id = Some(patchset_id); - decision.result_commit_sha = Some("abc123".to_string()); + decision.result_commit_sha = Some(expected_hash); decision.rationale = Some("tests passed".to_string()); assert_eq!(decision.chosen_patchset_id, Some(patchset_id)); - assert_eq!(decision.result_commit_sha.as_deref(), Some("abc123")); + assert_eq!(decision.result_commit_sha, Some(expected_hash)); assert_eq!(decision.rationale.as_deref(), Some("tests passed")); } From 9b13c9e5ff80f373785411fedf21385136c5eae9 Mon Sep 17 00:00:00 2001 From: jackie Date: Tue, 10 Feb 2026 21:40:22 +0800 Subject: [PATCH 07/13] update doc Signed-off-by: jackie --- src/internal/object/ai_process/objects.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/internal/object/ai_process/objects.rs b/src/internal/object/ai_process/objects.rs index a77b30db..8cc0e946 100644 --- a/src/internal/object/ai_process/objects.rs +++ b/src/internal/object/ai_process/objects.rs @@ -98,6 +98,7 @@ impl FromStr for GoalType { } /// Task object describing intent and constraints. +/// Typically created first, then referenced by Run objects. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Task { #[serde(flatten)] @@ -167,6 +168,7 @@ impl fmt::Display for RunStatus { } /// 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)] @@ -184,6 +186,7 @@ pub struct Run { } /// 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" @@ -277,6 +280,7 @@ pub enum DiffFormat { } /// 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)] @@ -354,6 +358,7 @@ pub enum SelectionStrategy { } /// 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)] @@ -440,6 +445,7 @@ impl fmt::Display for ToolStatus { } /// Tool invocation record. +/// Records a single tool call within a run. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolInvocation { #[serde(flatten)] @@ -456,6 +462,7 @@ pub struct ToolInvocation { } /// IO footprint of a tool invocation. +/// Tracks reads and writes for auditability. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IoFootprint { #[serde(default)] @@ -514,6 +521,7 @@ impl fmt::Display for 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)] @@ -566,6 +574,7 @@ impl Plan { } /// 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)] @@ -604,6 +613,7 @@ impl Evidence { } /// Provenance object for model/provider metadata. +/// Captures model/provider settings and usage. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Provenance { #[serde(flatten)] @@ -635,6 +645,7 @@ impl Provenance { } /// Decision object linking process to outcomes. +/// Records the final outcome of a run. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Decision { #[serde(flatten)] @@ -670,7 +681,7 @@ impl Decision { #[cfg(test)] mod tests { use super::*; - use crate::hash::{HashKind, set_hash_kind_for_test}; + use crate::hash::{set_hash_kind_for_test, HashKind}; fn test_hash_hex() -> String { ObjectHash::new(b"ai-process-test").to_string() From 32e927daa37613fed9bfe105ba7a9c04b5eae487 Mon Sep 17 00:00:00 2001 From: jackie Date: Tue, 10 Feb 2026 21:46:30 +0800 Subject: [PATCH 08/13] update seal Signed-off-by: jackie --- src/internal/object/ai_process/base.rs | 20 +++++++++++++++++--- src/internal/object/ai_process/objects.rs | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/internal/object/ai_process/base.rs b/src/internal/object/ai_process/base.rs index 3f7410cd..e6e30a09 100644 --- a/src/internal/object/ai_process/base.rs +++ b/src/internal/object/ai_process/base.rs @@ -175,10 +175,19 @@ impl Header { } /// Seal the header by calculating and setting the checksum of the provided object. - /// This is typically called before persisting the object. + /// The checksum field is temporarily cleared to keep sealing idempotent. pub fn seal(&mut self, object: &T) -> Result<(), serde_json::Error> { - self.checksum = Some(compute_json_hash(object)?); - Ok(()) + let previous = self.checksum.take(); + match compute_json_hash(object) { + Ok(checksum) => { + self.checksum = Some(checksum); + Ok(()) + } + Err(err) => { + self.checksum = previous; + Err(err) + } + } } } @@ -560,6 +569,11 @@ mod tests { assert!(header.checksum().is_some()); let expected = compute_json_hash(&content).expect("checksum"); assert_eq!(header.checksum().expect("checksum"), &expected); + + let first = header.checksum().cloned().expect("checksum"); + header.seal(&content).expect("seal"); + let second = header.checksum().cloned().expect("checksum"); + assert_eq!(first, second); } #[test] diff --git a/src/internal/object/ai_process/objects.rs b/src/internal/object/ai_process/objects.rs index 8cc0e946..042c557f 100644 --- a/src/internal/object/ai_process/objects.rs +++ b/src/internal/object/ai_process/objects.rs @@ -681,7 +681,7 @@ impl Decision { #[cfg(test)] mod tests { use super::*; - use crate::hash::{set_hash_kind_for_test, HashKind}; + use crate::hash::{HashKind, set_hash_kind_for_test}; fn test_hash_hex() -> String { ObjectHash::new(b"ai-process-test").to_string() From 86058fb4c56b85318e124469c737fd883a19e60b Mon Sep 17 00:00:00 2001 From: jackie Date: Wed, 11 Feb 2026 13:18:33 +0800 Subject: [PATCH 09/13] refactor base Signed-off-by: jackie --- src/internal/object/ai_header.rs | 589 +++++++++++++++++++++++++++++++ src/internal/object/mod.rs | 12 +- 2 files changed, 600 insertions(+), 1 deletion(-) create mode 100644 src/internal/object/ai_header.rs diff --git a/src/internal/object/ai_header.rs b/src/internal/object/ai_header.rs new file mode 100644 index 00000000..8a941467 --- /dev/null +++ b/src/internal/object/ai_header.rs @@ -0,0 +1,589 @@ +use std::{collections::HashMap, fmt}; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::ai_hash::{IntegrityHash, compute_integrity_hash}; + +/// Visibility of an AI process object. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum Visibility { + Private, + Public, +} + +/// AI process object type. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum AiObjectType { + Task, + Run, + PatchSet, + ContextSnapshot, + ToolInvocation, + Plan, + Evidence, + Provenance, + 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. +#[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. + 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. + 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/mod.rs b/src/internal/object/mod.rs index 72518499..bb09ab17 100644 --- a/src/internal/object/mod.rs +++ b/src/internal/object/mod.rs @@ -1,6 +1,16 @@ //! 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 ai_process; +pub mod ai_context; +pub mod ai_decision; +pub mod ai_evidence; +pub mod ai_hash; +pub mod ai_header; +pub mod ai_patchset; +pub mod ai_plan; +pub mod ai_provenance; +pub mod ai_run; +pub mod ai_task; +pub mod ai_tool; pub mod blob; pub mod commit; pub mod note; From 75d13622fa843e28fd4fc06f644bb44f787c2e0f Mon Sep 17 00:00:00 2001 From: jackie Date: Wed, 11 Feb 2026 13:19:01 +0800 Subject: [PATCH 10/13] update ai_hash Signed-off-by: jackie --- src/internal/object/ai_hash.rs | 151 +++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 src/internal/object/ai_hash.rs diff --git a/src/internal/object/ai_hash.rs b/src/internal/object/ai_hash.rs new file mode 100644 index 00000000..99adbc00 --- /dev/null +++ b/src/internal/object/ai_hash.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); + } +} From 630a939b0abe577687d92fbe7226d2562b47451b Mon Sep 17 00:00:00 2001 From: jackie Date: Wed, 11 Feb 2026 13:22:10 +0800 Subject: [PATCH 11/13] splite objects Signed-off-by: jackie --- src/internal/object/ai_context.rs | 141 +++ src/internal/object/ai_decision.rs | 163 ++++ src/internal/object/ai_evidence.rs | 169 ++++ src/internal/object/ai_patchset.rs | 202 +++++ src/internal/object/ai_plan.rs | 136 +++ src/internal/object/ai_process/base.rs | 596 ------------- src/internal/object/ai_process/checksum.rs | 82 -- src/internal/object/ai_process/mod.rs | 11 - src/internal/object/ai_process/objects.rs | 960 --------------------- src/internal/object/ai_provenance.rs | 88 ++ src/internal/object/ai_run.rs | 199 +++++ src/internal/object/ai_task.rs | 244 ++++++ src/internal/object/ai_tool.rs | 175 ++++ 13 files changed, 1517 insertions(+), 1649 deletions(-) create mode 100644 src/internal/object/ai_context.rs create mode 100644 src/internal/object/ai_decision.rs create mode 100644 src/internal/object/ai_evidence.rs create mode 100644 src/internal/object/ai_patchset.rs create mode 100644 src/internal/object/ai_plan.rs delete mode 100644 src/internal/object/ai_process/base.rs delete mode 100644 src/internal/object/ai_process/checksum.rs delete mode 100644 src/internal/object/ai_process/mod.rs delete mode 100644 src/internal/object/ai_process/objects.rs create mode 100644 src/internal/object/ai_provenance.rs create mode 100644 src/internal/object/ai_run.rs create mode 100644 src/internal/object/ai_task.rs create mode 100644 src/internal/object/ai_tool.rs diff --git a/src/internal/object/ai_context.rs b/src/internal/object/ai_context.rs new file mode 100644 index 00000000..618b0e8e --- /dev/null +++ b/src/internal/object/ai_context.rs @@ -0,0 +1,141 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::{ + ai_hash::IntegrityHash, + ai_header::{ActorRef, AiObjectType, Header}, +}; + +/// Selection strategy for context snapshots. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum SelectionStrategy { + Explicit, + Heuristic, +} + +/// Context item kind. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ContextItemKind { + 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/ai_decision.rs b/src/internal/object/ai_decision.rs new file mode 100644 index 00000000..38f58676 --- /dev/null +++ b/src/internal/object/ai_decision.rs @@ -0,0 +1,163 @@ +use std::fmt; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::{ + ai_hash::IntegrityHash, + ai_header::{ActorRef, AiObjectType, Header}, +}; + +/// Type of decision. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum DecisionType { + Commit, + Checkpoint, + Abandon, + Retry, + 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/ai_evidence.rs b/src/internal/object/ai_evidence.rs new file mode 100644 index 00000000..60b1c448 --- /dev/null +++ b/src/internal/object/ai_evidence.rs @@ -0,0 +1,169 @@ +use std::fmt; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::ai_header::{ActorRef, AiObjectType, ArtifactRef, Header}; + +/// Kind of evidence. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum EvidenceKind { + Test, + Lint, + 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/ai_patchset.rs b/src/internal/object/ai_patchset.rs new file mode 100644 index 00000000..f8b88895 --- /dev/null +++ b/src/internal/object/ai_patchset.rs @@ -0,0 +1,202 @@ +use std::fmt; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::{ + ai_hash::IntegrityHash, + ai_header::{ActorRef, AiObjectType, ArtifactRef, Header}, +}; + +/// Patch application status. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ApplyStatus { + Proposed, + Applied, + Rejected, + 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 { + UnifiedDiff, + 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. +#[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/ai_plan.rs b/src/internal/object/ai_plan.rs new file mode 100644 index 00000000..368dedd8 --- /dev/null +++ b/src/internal/object/ai_plan.rs @@ -0,0 +1,136 @@ +use std::fmt; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::ai_header::{ActorRef, AiObjectType, Header}; + +/// Plan step status. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum PlanStatus { + Pending, + InProgress, + Completed, + Failed, + 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 + 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(), + }) + } + + 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/ai_process/base.rs b/src/internal/object/ai_process/base.rs deleted file mode 100644 index e6e30a09..00000000 --- a/src/internal/object/ai_process/base.rs +++ /dev/null @@ -1,596 +0,0 @@ -use std::{collections::HashMap, fmt}; - -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use super::checksum::{compute_hash as compute_object_hash, compute_json_hash, parse_object_hash}; -use crate::hash::ObjectHash; - -/// Visibility of an AI process object. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum Visibility { - Private, - Public, -} - -/// AI process object type. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum AiObjectType { - Task, - Run, - PatchSet, - ContextSnapshot, - ToolInvocation, - Plan, - Evidence, - Provenance, - 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. -#[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 { - 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<&ObjectHash> { - 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. - pub fn seal(&mut self, object: &T) -> Result<(), serde_json::Error> { - let previous = self.checksum.take(); - match compute_json_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<&ObjectHash> { - 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]) -> ObjectHash { - compute_object_hash(content) - } - - /// Set the hash directly. - pub fn with_hash(mut self, hash: ObjectHash) -> 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 = parse_object_hash(hash.as_ref())?; - 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(compute_object_hash(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::*; - use crate::hash::{HashKind, ObjectHash, set_hash_kind_for_test}; - - #[test] - fn test_header_serialization() { - let repo_id = Uuid::now_v7(); - 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 _guard = set_hash_kind_for_test(HashKind::Sha256); - let repo_id = Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap(); - 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::parse_str("00000000-0000-0000-0000-000000000001").unwrap()); - - let checksum = compute_json_hash(&header).expect("checksum"); - assert_eq!(checksum.to_string().len(), 64); // SHA256 length - - // Ensure changes change checksum - header - .set_object_type(AiObjectType::Run) - .expect("object_type"); - let checksum2 = compute_json_hash(&header).expect("checksum"); - assert_ne!(checksum, checksum2); - } - - #[test] - fn test_artifact_checksum() { - let _guard = set_hash_kind_for_test(HashKind::Sha256); - 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_string(), 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(ObjectHash::new(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 _guard = set_hash_kind_for_test(HashKind::Sha256); - let repo_id = Uuid::now_v7(); - 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_json_hash(&content).expect("checksum"); - assert_eq!(header.checksum().expect("checksum"), &expected); - - let first = header.checksum().cloned().expect("checksum"); - header.seal(&content).expect("seal"); - let second = header.checksum().cloned().expect("checksum"); - assert_eq!(first, second); - } - - #[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/ai_process/checksum.rs b/src/internal/object/ai_process/checksum.rs deleted file mode 100644 index bb0b2ed5..00000000 --- a/src/internal/object/ai_process/checksum.rs +++ /dev/null @@ -1,82 +0,0 @@ -use std::str::FromStr; - -use serde::Serialize; -use serde_json::Value; - -use crate::hash::{ObjectHash, get_hash_kind}; - -/// Calculate hash from bytes using the repository hash kind. -pub fn compute_hash(content: &[u8]) -> ObjectHash { - ObjectHash::new(content) -} - -/// Calculate hash from a serializable object (deterministic JSON). -pub fn compute_json_hash(object: &T) -> Result { - let mut value = serde_json::to_value(object)?; - canonicalize_json(&mut value); - let content = serde_json::to_vec(&value)?; - Ok(ObjectHash::new(&content)) -} - -/// Parse a hex string into `ObjectHash` for the current repository hash kind. -pub fn parse_object_hash(value: &str) -> Result { - if value.len() != get_hash_kind().hex_len() { - return Err(format!( - "Invalid hash hex string length (expected {})", - get_hash_kind().hex_len() - )); - } - if !value.chars().all(|c| c.is_ascii_hexdigit()) { - return Err("Invalid hash hex string".to_string()); - } - ObjectHash::from_str(value) -} - -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 ai_process_checksum_deterministic_map() { - 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_json_hash(&MapWrapper { map: map_a }).expect("checksum"); - let hash_b = compute_json_hash(&MapWrapper { map: map_b }).expect("checksum"); - - assert_eq!(hash_a, hash_b); - } -} diff --git a/src/internal/object/ai_process/mod.rs b/src/internal/object/ai_process/mod.rs deleted file mode 100644 index f964113b..00000000 --- a/src/internal/object/ai_process/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! AI-powered processes for Git internals. -//! This module provides abstractions and implementations for integrating AI agents -//! into Git workflows, enabling intelligent automation and assistance in handling Git objects -//! and operations. -pub mod base; -pub mod checksum; -pub mod objects; - -pub use base::*; -pub use checksum::*; -pub use objects::*; diff --git a/src/internal/object/ai_process/objects.rs b/src/internal/object/ai_process/objects.rs deleted file mode 100644 index 042c557f..00000000 --- a/src/internal/object/ai_process/objects.rs +++ /dev/null @@ -1,960 +0,0 @@ -use std::{collections::HashMap, fmt, str::FromStr}; - -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use super::{ - base::{ActorRef, AiObjectType, ArtifactRef, Header}, - checksum::parse_object_hash, -}; -use crate::hash::ObjectHash; - -/// Task lifecycle status. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum TaskStatus { - Draft, - Running, - Done, - Failed, - 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. -#[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. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Task { - #[serde(flatten)] - pub header: Header, - pub title: String, - pub description: Option, // Can be text or artifact ref serialized - pub goal_type: Option, // feature/bugfix/refactor/docs/... - #[serde(default)] - pub constraints: Vec, - #[serde(default)] - pub acceptance_criteria: Vec, - pub requested_by: Option, - #[serde(default)] - pub dependencies: Vec, - pub status: TaskStatus, -} - -/// Task Object -impl Task { - 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, - }) - } -} - -/// Run lifecycle status. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum RunStatus { - Created, - Patching, - Validating, - Completed, - 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()) - } -} - -/// 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)] - pub header: Header, - pub task_id: Uuid, - pub orchestrator_version: String, - pub base_commit_sha: ObjectHash, - pub status: RunStatus, - pub context_snapshot_id: Option, - #[serde(default)] - pub agent_instances: Vec, - pub metrics: Option, - pub error: Option, - pub environment: Option, -} - -/// 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 -impl Run { - pub fn new( - repo_id: Uuid, - created_by: ActorRef, - task_id: Uuid, - base_commit_sha: impl AsRef, - ) -> Result { - let base_commit_sha = parse_object_hash(base_commit_sha.as_ref())?; - 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()), - }) - } -} - -/// Patch application status. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum ApplyStatus { - Proposed, - Applied, - Rejected, - 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 { - UnifiedDiff, - GitDiff, -} - -/// 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)] - pub header: Header, - pub run_id: Uuid, - pub generation: u32, - pub base_commit_sha: ObjectHash, - pub diff_format: DiffFormat, - pub diff_artifact: Option, - #[serde(default)] - pub touched_files: Vec, - pub rationale: Option, - pub apply_status: ApplyStatus, -} - -/// Touched file summary in a patchset. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TouchedFile { - pub path: String, - pub change_type: String, // modify/add/delete - pub lines_added: u32, - pub lines_deleted: u32, -} - -impl TouchedFile { - pub fn new( - path: impl Into, - change_type: impl Into, - 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: change_type.into(), - lines_added, - lines_deleted, - }) - } -} - -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 = parse_object_hash(base_commit_sha.as_ref())?; - 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, - }) - } -} - -/// Selection strategy for context snapshots. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum SelectionStrategy { - Explicit, - Heuristic, -} - -/// 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)] - pub header: Header, - pub base_commit_sha: ObjectHash, - pub selection_strategy: SelectionStrategy, - #[serde(default)] - pub items: Vec, - pub 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 = parse_object_hash(base_commit_sha.as_ref())?; - Ok(Self { - header: Header::new(AiObjectType::ContextSnapshot, repo_id, created_by)?, - base_commit_sha, - selection_strategy, - items: Vec::new(), - summary: None, - }) - } -} - -/// Context item kind. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum ContextItemKind { - File, -} - -/// Context item describing a single input. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ContextItem { - pub kind: ContextItemKind, - pub path: String, - pub content_id: ObjectHash, -} - -impl ContextItem { - pub fn new( - kind: ContextItemKind, - path: impl Into, - content_id: ObjectHash, - ) -> Result { - let path = path.into(); - if path.trim().is_empty() { - return Err("path cannot be empty".to_string()); - } - Ok(Self { - kind, - path, - content_id, - }) - } -} - -/// Tool invocation status. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum ToolStatus { - Ok, - 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()) - } -} - -/// Tool invocation record. -/// Records a single tool call within a run. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ToolInvocation { - #[serde(flatten)] - pub header: Header, - pub run_id: Uuid, - pub tool_name: String, - pub io_footprint: Option, - #[serde(default)] - pub args: serde_json::Value, - pub status: ToolStatus, - pub result_summary: Option, - #[serde(default)] - pub artifacts: Vec, -} - -/// 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, -} - -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(), - }) - } -} - -/// Plan step status. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum PlanStatus { - Pending, - InProgress, - Completed, - Failed, - 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 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)] - pub header: Header, - pub run_id: Uuid, - /// Plan version starts at 1 and must increase by 1 for each update. - pub plan_version: u32, - #[serde(default)] - pub steps: Vec, -} - -/// 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, -} - -impl Plan { - /// Create a new plan object - 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(), - }) - } - - 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(), - }) - } -} - -/// 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)] - pub header: Header, - pub run_id: Uuid, - pub patchset_id: Option, - pub kind: String, // test/lint/build - pub tool: String, - pub command: Option, - pub exit_code: Option, - pub summary: Option, // passed/failed, error signature - #[serde(default)] - pub 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(), - }) - } -} - -/// Provenance object for model/provider metadata. -/// Captures model/provider settings and usage. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Provenance { - #[serde(flatten)] - pub header: Header, - pub run_id: Uuid, - pub provider: String, - pub model: String, - pub parameters: Option, - pub 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, - }) - } -} - -/// Decision object linking process to outcomes. -/// Records the final outcome of a run. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Decision { - #[serde(flatten)] - pub header: Header, - pub run_id: Uuid, - pub decision_type: String, // commit/checkpoint/abandon/retry/rollback - pub chosen_patchset_id: Option, - pub result_commit_sha: Option, - pub checkpoint_id: Option, - pub 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, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::hash::{HashKind, set_hash_kind_for_test}; - - fn test_hash_hex() -> String { - ObjectHash::new(b"ai-process-test").to_string() - } - - #[test] - fn test_task_creation() { - let repo_id = Uuid::now_v7(); - 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::now_v7(); - task.dependencies.push(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::now_v7(); - 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_new_objects_creation() { - let _guard = set_hash_kind_for_test(HashKind::Sha256); - let repo_id = Uuid::now_v7(); - let actor = ActorRef::agent("test-agent").expect("actor"); - let run_id = Uuid::now_v7(); - let base_hash = test_hash_hex(); - - // Run with environment (auto captured) - let run = Run::new(repo_id, actor.clone(), Uuid::now_v7(), &base_hash).expect("run"); - - let env = run.environment.as_ref().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()); - - // Plan with steps and status - let mut plan = Plan::new(repo_id, actor.clone(), run_id).expect("plan"); - plan.steps.push(PlanStep { - intent: "step1".to_string(), - inputs: None, - outputs: None, - checks: None, - owner_role: None, - status: PlanStatus::Pending, - }); - - assert_eq!(plan.header.object_type(), &AiObjectType::Plan); - assert_eq!(plan.plan_version, 1); - assert_eq!(plan.steps[0].status, PlanStatus::Pending); - - // Evidence - let evidence = - Evidence::new(repo_id, actor.clone(), run_id, "test", "cargo").expect("evidence"); - assert_eq!(evidence.header.object_type(), &AiObjectType::Evidence); - assert_eq!(evidence.kind, "test"); - - // Provenance - let provenance = - Provenance::new(repo_id, actor.clone(), run_id, "openai", "gpt-4").expect("provenance"); - assert_eq!(provenance.header.object_type(), &AiObjectType::Provenance); - assert_eq!(provenance.provider, "openai"); - - // Decision - let decision = Decision::new(repo_id, actor.clone(), run_id, "commit").expect("decision"); - assert_eq!(decision.header.object_type(), &AiObjectType::Decision); - assert_eq!(decision.decision_type, "commit"); - } - - #[test] - fn test_task_requested_by() { - let repo_id = Uuid::now_v7(); - let actor = ActorRef::human("jackie").expect("actor"); - let mut task = - Task::new(repo_id, actor.clone(), "Fix bug", Some(GoalType::Bugfix)).expect("task"); - - task.requested_by = Some(ActorRef::mcp_client("vscode-client").expect("actor")); - - assert!(task.requested_by.is_some()); - assert_eq!( - task.requested_by.unwrap().kind(), - &super::super::base::ActorKind::McpClient - ); - } - - #[test] - fn test_tool_invocation_io_footprint() { - let repo_id = Uuid::now_v7(); - let actor = ActorRef::human("jackie").expect("actor"); - let run_id = Uuid::now_v7(); - - 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.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_patchset_creation() { - let _guard = set_hash_kind_for_test(HashKind::Sha256); - let repo_id = Uuid::now_v7(); - let actor = ActorRef::agent("test-agent").expect("actor"); - let run_id = Uuid::now_v7(); - 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()); - } - - #[test] - fn test_context_snapshot_fields() { - let _guard = set_hash_kind_for_test(HashKind::Sha256); - let repo_id = Uuid::now_v7(); - 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.summary = Some("core files".to_string()); - - snapshot.items.push( - ContextItem::new( - ContextItemKind::File, - "src/lib.rs", - ObjectHash::new(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.as_deref(), Some("core files")); - } - - #[test] - fn test_tool_invocation_fields() { - let repo_id = Uuid::now_v7(); - let actor = ActorRef::human("jackie").expect("actor"); - let run_id = Uuid::now_v7(); - - let mut tool_inv = - ToolInvocation::new(repo_id, actor, run_id, "apply_patch").expect("tool_invocation"); - tool_inv.status = ToolStatus::Error; - tool_inv.args = serde_json::json!({"path": "src/lib.rs"}); - tool_inv.result_summary = Some("failed".to_string()); - tool_inv - .artifacts - .push(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"); - } - - #[test] - fn test_evidence_fields() { - let repo_id = Uuid::now_v7(); - let actor = ActorRef::agent("test-agent").expect("actor"); - let run_id = Uuid::now_v7(); - let patchset_id = Uuid::now_v7(); - - let mut evidence = - Evidence::new(repo_id, actor, run_id, "test", "cargo").expect("evidence"); - evidence.patchset_id = Some(patchset_id); - evidence.exit_code = Some(1); - evidence - .report_artifacts - .push(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); - } - - #[test] - fn test_provenance_fields() { - let repo_id = Uuid::now_v7(); - let actor = ActorRef::agent("test-agent").expect("actor"); - let run_id = Uuid::now_v7(); - - let mut provenance = - Provenance::new(repo_id, actor, run_id, "openai", "gpt-4").expect("provenance"); - provenance.parameters = Some(serde_json::json!({"temperature": 0.2})); - provenance.token_usage = Some(serde_json::json!({"input": 10, "output": 5})); - - assert!(provenance.parameters.is_some()); - assert!(provenance.token_usage.is_some()); - } - - #[test] - fn test_decision_fields() { - let _guard = set_hash_kind_for_test(HashKind::Sha256); - let repo_id = Uuid::now_v7(); - let actor = ActorRef::agent("test-agent").expect("actor"); - let run_id = Uuid::now_v7(); - let patchset_id = Uuid::now_v7(); - let expected_hash = ObjectHash::new(b"decision-hash"); - - let mut decision = Decision::new(repo_id, actor, run_id, "commit").expect("decision"); - decision.chosen_patchset_id = Some(patchset_id); - decision.result_commit_sha = Some(expected_hash); - decision.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.as_deref(), Some("tests passed")); - } - - #[test] - fn test_plan_version_ordering() { - let repo_id = Uuid::now_v7(); - let actor = ActorRef::human("jackie").expect("actor"); - let run_id = Uuid::now_v7(); - - 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); - } - - #[test] - fn ai_process_tool_invocation_artifacts_default() { - let repo_id = Uuid::now_v7(); - let actor = ActorRef::human("jackie").expect("actor"); - let run_id = Uuid::now_v7(); - - let tool_inv = - ToolInvocation::new(repo_id, actor, run_id, "read_file").expect("tool_invocation"); - let mut value = serde_json::to_value(&tool_inv).unwrap(); - - if let serde_json::Value::Object(ref mut map) = value { - map.remove("artifacts"); - } - - let deserialized: ToolInvocation = serde_json::from_value(value).unwrap(); - assert!(deserialized.artifacts.is_empty()); - } -} diff --git a/src/internal/object/ai_provenance.rs b/src/internal/object/ai_provenance.rs new file mode 100644 index 00000000..434fbbcf --- /dev/null +++ b/src/internal/object/ai_provenance.rs @@ -0,0 +1,88 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::ai_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/ai_run.rs b/src/internal/object/ai_run.rs new file mode 100644 index 00000000..bcff8162 --- /dev/null +++ b/src/internal/object/ai_run.rs @@ -0,0 +1,199 @@ +use std::{collections::HashMap, fmt}; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::{ + ai_hash::IntegrityHash, + ai_header::{ActorRef, AiObjectType, Header}, +}; + +/// Run lifecycle status. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum RunStatus { + Created, + Patching, + Validating, + Completed, + 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 { + 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/ai_task.rs b/src/internal/object/ai_task.rs new file mode 100644 index 00000000..16968678 --- /dev/null +++ b/src/internal/object/ai_task.rs @@ -0,0 +1,244 @@ +use std::{fmt, str::FromStr}; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::ai_header::{ActorRef, AiObjectType, Header}; + +/// Task lifecycle status. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum TaskStatus { + Draft, + Running, + Done, + Failed, + 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. +#[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. +/// +/// # Lifecycle +/// 1. Created with `TaskStatus::Draft` +/// 2. Moves to `Running` when an agent starts working +/// 3. Ends in `Done`, `Failed`, or `Cancelled` +#[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 { + 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::ai_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/ai_tool.rs b/src/internal/object/ai_tool.rs new file mode 100644 index 00000000..d2e7e711 --- /dev/null +++ b/src/internal/object/ai_tool.rs @@ -0,0 +1,175 @@ +use std::fmt; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::ai_header::{ActorRef, AiObjectType, ArtifactRef, Header}; + +/// Tool invocation status. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ToolStatus { + Ok, + 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"); + } +} From 09e3fe308d80311257b2b85d72a14b3a261672da Mon Sep 17 00:00:00 2001 From: jackie Date: Wed, 11 Feb 2026 13:29:51 +0800 Subject: [PATCH 12/13] update docs Signed-off-by: jackie --- src/internal/object/ai_context.rs | 18 ++++++++++ src/internal/object/ai_decision.rs | 18 ++++++++++ src/internal/object/ai_evidence.rs | 14 ++++++++ src/internal/object/ai_header.rs | 48 +++++++++++++++++++++++++ src/internal/object/ai_patchset.rs | 23 ++++++++++++ src/internal/object/ai_plan.rs | 25 ++++++++++++- src/internal/object/ai_provenance.rs | 12 +++++++ src/internal/object/ai_run.rs | 30 ++++++++++++++++ src/internal/object/ai_task.rs | 53 +++++++++++++++++++++++++--- src/internal/object/ai_tool.rs | 20 +++++++++++ 10 files changed, 256 insertions(+), 5 deletions(-) diff --git a/src/internal/object/ai_context.rs b/src/internal/object/ai_context.rs index 618b0e8e..e6910b90 100644 --- a/src/internal/object/ai_context.rs +++ b/src/internal/object/ai_context.rs @@ -1,3 +1,18 @@ +//! 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; @@ -10,7 +25,9 @@ use super::{ #[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, } @@ -18,6 +35,7 @@ pub enum SelectionStrategy { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum ContextItemKind { + /// A regular file in the repository. File, } diff --git a/src/internal/object/ai_decision.rs b/src/internal/object/ai_decision.rs index 38f58676..d4bf459c 100644 --- a/src/internal/object/ai_decision.rs +++ b/src/internal/object/ai_decision.rs @@ -1,3 +1,16 @@ +//! 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}; @@ -12,10 +25,15 @@ use super::{ #[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), diff --git a/src/internal/object/ai_evidence.rs b/src/internal/object/ai_evidence.rs index 60b1c448..98c824e8 100644 --- a/src/internal/object/ai_evidence.rs +++ b/src/internal/object/ai_evidence.rs @@ -1,3 +1,14 @@ +//! 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}; @@ -9,8 +20,11 @@ use super::ai_header::{ActorRef, AiObjectType, ArtifactRef, Header}; #[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), diff --git a/src/internal/object/ai_header.rs b/src/internal/object/ai_header.rs index 8a941467..25683cfd 100644 --- a/src/internal/object/ai_header.rs +++ b/src/internal/object/ai_header.rs @@ -1,3 +1,16 @@ +//! 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}; @@ -7,6 +20,9 @@ use uuid::Uuid; use super::ai_hash::{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 { @@ -15,17 +31,28 @@ pub enum Visibility { } /// 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, } @@ -54,6 +81,19 @@ impl fmt::Display for AiObjectType { /// 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) @@ -83,6 +123,12 @@ pub struct Header { 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, @@ -178,6 +224,8 @@ impl Header { /// 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) { diff --git a/src/internal/object/ai_patchset.rs b/src/internal/object/ai_patchset.rs index f8b88895..aa975603 100644 --- a/src/internal/object/ai_patchset.rs +++ b/src/internal/object/ai_patchset.rs @@ -1,3 +1,18 @@ +//! 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}; @@ -12,9 +27,13 @@ use super::{ #[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, } @@ -39,7 +58,9 @@ impl fmt::Display for ApplyStatus { #[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, } @@ -55,6 +76,8 @@ pub enum ChangeType { } /// 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, diff --git a/src/internal/object/ai_plan.rs b/src/internal/object/ai_plan.rs index 368dedd8..36d7830a 100644 --- a/src/internal/object/ai_plan.rs +++ b/src/internal/object/ai_plan.rs @@ -1,3 +1,17 @@ +//! 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}; @@ -9,10 +23,15 @@ use super::ai_header::{ActorRef, AiObjectType, Header}; #[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, } @@ -59,7 +78,7 @@ pub struct Plan { } impl Plan { - /// Create a new plan object + /// 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)?, @@ -69,6 +88,10 @@ impl Plan { }) } + /// 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, diff --git a/src/internal/object/ai_provenance.rs b/src/internal/object/ai_provenance.rs index 434fbbcf..16044896 100644 --- a/src/internal/object/ai_provenance.rs +++ b/src/internal/object/ai_provenance.rs @@ -1,3 +1,15 @@ +//! 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; diff --git a/src/internal/object/ai_run.rs b/src/internal/object/ai_run.rs index bcff8162..b3cc01ce 100644 --- a/src/internal/object/ai_run.rs +++ b/src/internal/object/ai_run.rs @@ -1,3 +1,21 @@ +//! 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}; @@ -12,10 +30,15 @@ use super::{ #[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, } @@ -91,6 +114,13 @@ pub struct Run { } 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, diff --git a/src/internal/object/ai_task.rs b/src/internal/object/ai_task.rs index 16968678..c51f380a 100644 --- a/src/internal/object/ai_task.rs +++ b/src/internal/object/ai_task.rs @@ -1,3 +1,37 @@ +//! 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::ai_task::{Task, GoalType}; +//! use git_internal::internal::object::ai_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}; @@ -9,10 +43,15 @@ use super::ai_header::{ActorRef, AiObjectType, Header}; #[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, } @@ -35,6 +74,8 @@ impl fmt::Display for TaskStatus { } /// 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 { @@ -96,10 +137,7 @@ impl FromStr for GoalType { /// Task object describing intent and constraints. /// Typically created first, then referenced by Run objects. /// -/// # Lifecycle -/// 1. Created with `TaskStatus::Draft` -/// 2. Moves to `Running` when an agent starts working -/// 3. Ends in `Done`, `Failed`, or `Cancelled` +/// See module documentation for lifecycle details. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Task { #[serde(flatten)] @@ -118,6 +156,13 @@ pub struct Task { } 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, diff --git a/src/internal/object/ai_tool.rs b/src/internal/object/ai_tool.rs index d2e7e711..440f6e87 100644 --- a/src/internal/object/ai_tool.rs +++ b/src/internal/object/ai_tool.rs @@ -1,3 +1,21 @@ +//! 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}; @@ -9,7 +27,9 @@ use super::ai_header::{ActorRef, AiObjectType, ArtifactRef, Header}; #[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, } From c7ad99c6b377df5976693393e76e90aba4ce665b Mon Sep 17 00:00:00 2001 From: jackie Date: Wed, 11 Feb 2026 16:48:30 +0800 Subject: [PATCH 13/13] rename objects Signed-off-by: jackie --- .../object/{ai_context.rs => context.rs} | 4 ++-- .../object/{ai_decision.rs => decision.rs} | 4 ++-- .../object/{ai_evidence.rs => evidence.rs} | 2 +- .../object/{ai_header.rs => header.rs} | 2 +- .../object/{ai_hash.rs => integrity.rs} | 0 src/internal/object/mod.rs | 22 +++++++++---------- .../object/{ai_patchset.rs => patchset.rs} | 4 ++-- src/internal/object/{ai_plan.rs => plan.rs} | 2 +- .../{ai_provenance.rs => provenance.rs} | 2 +- src/internal/object/{ai_run.rs => run.rs} | 4 ++-- src/internal/object/{ai_task.rs => task.rs} | 8 +++---- src/internal/object/{ai_tool.rs => tool.rs} | 2 +- 12 files changed, 28 insertions(+), 28 deletions(-) rename src/internal/object/{ai_context.rs => context.rs} (98%) rename src/internal/object/{ai_decision.rs => decision.rs} (98%) rename src/internal/object/{ai_evidence.rs => evidence.rs} (98%) rename src/internal/object/{ai_header.rs => header.rs} (99%) rename src/internal/object/{ai_hash.rs => integrity.rs} (100%) rename src/internal/object/{ai_patchset.rs => patchset.rs} (98%) rename src/internal/object/{ai_plan.rs => plan.rs} (98%) rename src/internal/object/{ai_provenance.rs => provenance.rs} (97%) rename src/internal/object/{ai_run.rs => run.rs} (98%) rename src/internal/object/{ai_task.rs => task.rs} (97%) rename src/internal/object/{ai_tool.rs => tool.rs} (98%) diff --git a/src/internal/object/ai_context.rs b/src/internal/object/context.rs similarity index 98% rename from src/internal/object/ai_context.rs rename to src/internal/object/context.rs index e6910b90..4b91e3bd 100644 --- a/src/internal/object/ai_context.rs +++ b/src/internal/object/context.rs @@ -17,8 +17,8 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use super::{ - ai_hash::IntegrityHash, - ai_header::{ActorRef, AiObjectType, Header}, + header::{ActorRef, AiObjectType, Header}, + integrity::IntegrityHash, }; /// Selection strategy for context snapshots. diff --git a/src/internal/object/ai_decision.rs b/src/internal/object/decision.rs similarity index 98% rename from src/internal/object/ai_decision.rs rename to src/internal/object/decision.rs index d4bf459c..6ff275be 100644 --- a/src/internal/object/ai_decision.rs +++ b/src/internal/object/decision.rs @@ -17,8 +17,8 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use super::{ - ai_hash::IntegrityHash, - ai_header::{ActorRef, AiObjectType, Header}, + header::{ActorRef, AiObjectType, Header}, + integrity::IntegrityHash, }; /// Type of decision. diff --git a/src/internal/object/ai_evidence.rs b/src/internal/object/evidence.rs similarity index 98% rename from src/internal/object/ai_evidence.rs rename to src/internal/object/evidence.rs index 98c824e8..32e2f010 100644 --- a/src/internal/object/ai_evidence.rs +++ b/src/internal/object/evidence.rs @@ -14,7 +14,7 @@ use std::fmt; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use super::ai_header::{ActorRef, AiObjectType, ArtifactRef, Header}; +use super::header::{ActorRef, AiObjectType, ArtifactRef, Header}; /// Kind of evidence. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] diff --git a/src/internal/object/ai_header.rs b/src/internal/object/header.rs similarity index 99% rename from src/internal/object/ai_header.rs rename to src/internal/object/header.rs index 25683cfd..3039b1ff 100644 --- a/src/internal/object/ai_header.rs +++ b/src/internal/object/header.rs @@ -17,7 +17,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use super::ai_hash::{IntegrityHash, compute_integrity_hash}; +use super::integrity::{IntegrityHash, compute_integrity_hash}; /// Visibility of an AI process object. /// diff --git a/src/internal/object/ai_hash.rs b/src/internal/object/integrity.rs similarity index 100% rename from src/internal/object/ai_hash.rs rename to src/internal/object/integrity.rs diff --git a/src/internal/object/mod.rs b/src/internal/object/mod.rs index bb09ab17..e7ce1a1e 100644 --- a/src/internal/object/mod.rs +++ b/src/internal/object/mod.rs @@ -1,21 +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 ai_context; -pub mod ai_decision; -pub mod ai_evidence; -pub mod ai_hash; -pub mod ai_header; -pub mod ai_patchset; -pub mod ai_plan; -pub mod ai_provenance; -pub mod ai_run; -pub mod ai_task; -pub mod ai_tool; 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/ai_patchset.rs b/src/internal/object/patchset.rs similarity index 98% rename from src/internal/object/ai_patchset.rs rename to src/internal/object/patchset.rs index aa975603..edefdc15 100644 --- a/src/internal/object/ai_patchset.rs +++ b/src/internal/object/patchset.rs @@ -19,8 +19,8 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use super::{ - ai_hash::IntegrityHash, - ai_header::{ActorRef, AiObjectType, ArtifactRef, Header}, + header::{ActorRef, AiObjectType, ArtifactRef, Header}, + integrity::IntegrityHash, }; /// Patch application status. diff --git a/src/internal/object/ai_plan.rs b/src/internal/object/plan.rs similarity index 98% rename from src/internal/object/ai_plan.rs rename to src/internal/object/plan.rs index 36d7830a..3aa6f837 100644 --- a/src/internal/object/ai_plan.rs +++ b/src/internal/object/plan.rs @@ -17,7 +17,7 @@ use std::fmt; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use super::ai_header::{ActorRef, AiObjectType, Header}; +use super::header::{ActorRef, AiObjectType, Header}; /// Plan step status. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] diff --git a/src/internal/object/ai_provenance.rs b/src/internal/object/provenance.rs similarity index 97% rename from src/internal/object/ai_provenance.rs rename to src/internal/object/provenance.rs index 16044896..1bfffef4 100644 --- a/src/internal/object/ai_provenance.rs +++ b/src/internal/object/provenance.rs @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; -use super::ai_header::{ActorRef, AiObjectType, Header}; +use super::header::{ActorRef, AiObjectType, Header}; /// Provenance object for model/provider metadata. /// Captures model/provider settings and usage. diff --git a/src/internal/object/ai_run.rs b/src/internal/object/run.rs similarity index 98% rename from src/internal/object/ai_run.rs rename to src/internal/object/run.rs index b3cc01ce..ca9d4793 100644 --- a/src/internal/object/ai_run.rs +++ b/src/internal/object/run.rs @@ -22,8 +22,8 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use super::{ - ai_hash::IntegrityHash, - ai_header::{ActorRef, AiObjectType, Header}, + header::{ActorRef, AiObjectType, Header}, + integrity::IntegrityHash, }; /// Run lifecycle status. diff --git a/src/internal/object/ai_task.rs b/src/internal/object/task.rs similarity index 97% rename from src/internal/object/ai_task.rs rename to src/internal/object/task.rs index c51f380a..66598054 100644 --- a/src/internal/object/ai_task.rs +++ b/src/internal/object/task.rs @@ -20,8 +20,8 @@ //! # Example //! //! ```rust -//! use git_internal::internal::object::ai_task::{Task, GoalType}; -//! use git_internal::internal::object::ai_header::ActorRef; +//! use git_internal::internal::object::task::{Task, GoalType}; +//! use git_internal::internal::object::header::ActorRef; //! use uuid::Uuid; //! //! let repo_id = Uuid::new_v4(); @@ -37,7 +37,7 @@ use std::{fmt, str::FromStr}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use super::ai_header::{ActorRef, AiObjectType, Header}; +use super::header::{ActorRef, AiObjectType, Header}; /// Task lifecycle status. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -246,7 +246,7 @@ impl Task { #[cfg(test)] mod tests { use super::*; - use crate::internal::object::ai_header::ActorKind; + use crate::internal::object::header::ActorKind; #[test] fn test_task_creation() { diff --git a/src/internal/object/ai_tool.rs b/src/internal/object/tool.rs similarity index 98% rename from src/internal/object/ai_tool.rs rename to src/internal/object/tool.rs index 440f6e87..6fa1ad08 100644 --- a/src/internal/object/ai_tool.rs +++ b/src/internal/object/tool.rs @@ -21,7 +21,7 @@ use std::fmt; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use super::ai_header::{ActorRef, AiObjectType, ArtifactRef, Header}; +use super::header::{ActorRef, AiObjectType, ArtifactRef, Header}; /// Tool invocation status. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]