From 3bf20ef5cbb7890658443c49ccfebeab86326dfb Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Mon, 20 Jul 2026 12:46:50 -0400 Subject: [PATCH 1/3] =?UTF-8?q?eoc-route-learned:=20unbreak=20the=20build?= =?UTF-8?q?=20=E2=80=94=20rand=5Fdistr=200.4=20pulled=20rand=200.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The crate declared rand 0.10 but rand_distr 0.4, which transitively depends on rand 0.8. StdRng from 0.10 therefore did not satisfy rand_distr's Rng bound and the lib failed to compile — five E0277s across classifier.rs, matrix_factorization.rs and bandit.rs. Nothing in the tree depends on eoc-route-learned, so the rot went unnoticed: the router trainer has not compiled in some time. Bump rand_distr to 0.6, which tracks rand 0.10. Normal and Beta are the only distributions used and both carry over unchanged. Tests pass. Co-Authored-By: Claude Opus 4.8 --- eoc/eoc-rs/Cargo.lock | 769 ++++++++++-------- .../crates/eoc-route-learned/Cargo.toml | 2 +- 2 files changed, 449 insertions(+), 322 deletions(-) diff --git a/eoc/eoc-rs/Cargo.lock b/eoc/eoc-rs/Cargo.lock index 928b042e3..083594668 100644 --- a/eoc/eoc-rs/Cargo.lock +++ b/eoc/eoc-rs/Cargo.lock @@ -141,7 +141,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -156,6 +156,29 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "aws-lc-rs" +version = "1.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + [[package]] name = "base64" version = "0.13.1" @@ -185,7 +208,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex 1.3.0", - "syn", + "syn 2.0.117", ] [[package]] @@ -205,16 +228,7 @@ dependencies = [ "regex", "rustc-hash 2.1.2", "shlex 1.3.0", - "syn", -] - -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec 0.6.3", + "syn 2.0.117", ] [[package]] @@ -223,15 +237,9 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec 0.8.0", + "bit-vec", ] -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - [[package]] name = "bit-vec" version = "0.8.0" @@ -258,6 +266,12 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + [[package]] name = "bumpalo" version = "3.20.3" @@ -315,7 +329,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom 7.1.3", + "nom", ] [[package]] @@ -395,7 +409,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -419,6 +433,16 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "compact_str" version = "0.9.1" @@ -451,6 +475,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -506,6 +540,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "daachorse" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f55d7153ba3b507595872a3874803f07a8a81d1e888abed8e5db7da0597d6e2" + [[package]] name = "darling" version = "0.20.11" @@ -527,7 +567,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.117", ] [[package]] @@ -538,7 +578,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -550,6 +590,12 @@ dependencies = [ "serde", ] +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + [[package]] name = "deadpool" version = "0.12.3" @@ -568,15 +614,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] - [[package]] name = "derive_builder" version = "0.20.2" @@ -595,7 +632,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -605,7 +642,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn", + "syn 2.0.117", ] [[package]] @@ -616,9 +653,15 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dyn-clone" version = "1.0.20" @@ -631,6 +674,15 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + [[package]] name = "encode_unicode" version = "1.0.0" @@ -663,7 +715,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -783,7 +835,7 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.18", - "tokenizers 0.20.4", + "tokenizers", "tokio", "tracing", "wiremock", @@ -844,7 +896,7 @@ dependencies = [ "serde_json", "tempfile", "thiserror 2.0.18", - "tokenizers 0.21.4", + "tokenizers", "tokio", "tracing", ] @@ -888,7 +940,7 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.18", - "tokenizers 0.20.4", + "tokenizers", "tokio", "tracing", "wiremock", @@ -947,7 +999,7 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.18", - "tokenizers 0.20.4", + "tokenizers", "tokio", "tracing", "unicode-normalization", @@ -1096,11 +1148,11 @@ checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" [[package]] name = "fancy-regex" -version = "0.13.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" +checksum = "e1e1dacd0d2082dfcf1351c4bdd566bbe89a2b263235a2b50058f1e130a47277" dependencies = [ - "bit-set 0.5.3", + "bit-set", "regex-automata", "regex-syntax", ] @@ -1122,9 +1174,9 @@ dependencies = [ [[package]] name = "ffmpeg-next" -version = "7.1.0" +version = "8.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da02698288e0275e442a47fc12ca26d50daf0d48b15398ba5906f20ac2e2a9f9" +checksum = "f7c4bd5ab1ac61f29c634df1175d350ded29cf74c3c6d4f7030431a5ae3c7d5d" dependencies = [ "bitflags", "ffmpeg-sys-next", @@ -1133,11 +1185,11 @@ dependencies = [ [[package]] name = "ffmpeg-sys-next" -version = "7.1.3" +version = "8.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e9c75ebd4463de9d8998fb134ba26347fe5faee62fabf0a4b4d41bd500b4ad" +checksum = "a314bc0e022a33a99567ed4bd2576bd58ffd8fcff7891c29194cfecc26a62547" dependencies = [ - "bindgen 0.70.1", + "bindgen 0.72.1", "cc", "libc", "num_cpus", @@ -1170,6 +1222,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fluent-uri" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1182,6 +1245,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1201,6 +1270,12 @@ dependencies = [ "num", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.32" @@ -1257,7 +1332,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1379,7 +1454,18 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", ] [[package]] @@ -1480,7 +1566,6 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", ] [[package]] @@ -1672,15 +1757,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - [[package]] name = "insta" version = "1.47.2" @@ -1706,33 +1782,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" -[[package]] -name = "iso8601" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1082f0c48f143442a1ac6122f67e360ceee130b967af4d50996e5154a45df46" -dependencies = [ - "nom 8.0.0", -] - -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -1757,6 +1806,55 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.117", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -1781,30 +1879,38 @@ dependencies = [ [[package]] name = "jsonschema" -version = "0.18.3" +version = "0.46.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0f4bea31643be4c6a678e9aa4ae44f0db9e5609d5ca9dc9083d06eb3e9a27a" +checksum = "f0a699d3e77675e6aa4bfffe3b907c8b5f7ed3241f9965bffb25475ad4b08d05" dependencies = [ "ahash", - "anyhow", - "base64 0.22.1", "bytecount", + "data-encoding", + "email_address", "fancy-regex", "fraction", - "getrandom 0.2.17", - "iso8601", + "getrandom 0.3.4", + "idna", "itoa", - "memchr", + "jsonschema-regex", "num-cmp", - "once_cell", - "parking_lot", + "num-traits", "percent-encoding", + "referencing", "regex", "serde", "serde_json", - "time", - "url", - "uuid", + "unicode-general-category", + "uuid-simd", +] + +[[package]] +name = "jsonschema-regex" +version = "0.46.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbd1086b01b9349fd4ef9a07433965af64c8ce8159abe633a189e4ff817bd13" +dependencies = [ + "regex-syntax", ] [[package]] @@ -1953,13 +2059,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] -name = "memoffset" -version = "0.9.1" +name = "micromap" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] +checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74" [[package]] name = "mime" @@ -2013,7 +2116,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2025,7 +2128,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2082,7 +2185,7 @@ checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2136,15 +2239,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - [[package]] name = "num" version = "0.4.3" @@ -2185,12 +2279,6 @@ dependencies = [ "serde", ] -[[package]] -name = "num-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - [[package]] name = "num-integer" version = "0.1.46" @@ -2261,7 +2349,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2321,6 +2409,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "ort" version = "2.0.0-rc.12" @@ -2339,6 +2433,12 @@ version = "2.0.0-rc.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7b497d21a8b6fbb4b5a544f8fadb77e801a09ae0add9e411d31c6f89e3c1e90" +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "parking_lot" version = "0.12.5" @@ -2423,12 +2523,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2445,7 +2539,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", ] [[package]] @@ -2472,12 +2566,12 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ - "bit-set 0.8.0", - "bit-vec 0.8.0", + "bit-set", + "bit-vec", "bitflags", "num-traits", "rand 0.9.4", - "rand_chacha 0.9.0", + "rand_chacha", "rand_xorshift", "regex-syntax", "rusty-fork", @@ -2493,37 +2587,32 @@ checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" [[package]] name = "pyo3" -version = "0.22.6" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884" +checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12" dependencies = [ - "cfg-if", - "indoc", "libc", - "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", ] [[package]] name = "pyo3-build-config" -version = "0.22.6" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38" +checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e" dependencies = [ - "once_cell", "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.22.6" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636" +checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e" dependencies = [ "libc", "pyo3-build-config", @@ -2531,27 +2620,27 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.22.6" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453" +checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813" dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn", + "syn 2.0.117", ] [[package]] name = "pyo3-macros-backend" -version = "0.22.6" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe" +checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb" dependencies = [ "heck", "proc-macro2", "pyo3-build-config", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2586,6 +2675,7 @@ version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ + "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", @@ -2636,24 +2726,13 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "rand_chacha 0.9.0", + "rand_chacha", "rand_core 0.9.5", ] @@ -2668,16 +2747,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - [[package]] name = "rand_chacha" version = "0.9.0" @@ -2688,15 +2757,6 @@ dependencies = [ "rand_core 0.9.5", ] -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - [[package]] name = "rand_core" version = "0.9.5" @@ -2714,12 +2774,12 @@ checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rand_distr" -version = "0.4.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +checksum = "4d431c2703ccf129de4d45253c03f49ebb22b97d6ad79ee3ecfc7e3f4862c1d8" dependencies = [ "num-traits", - "rand 0.8.6", + "rand 0.10.1", ] [[package]] @@ -2747,17 +2807,6 @@ dependencies = [ "rayon-core", ] -[[package]] -name = "rayon-cond" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "059f538b55efd2309c9794130bc149c6a553db90e9d99c2030785c82f0bd7df9" -dependencies = [ - "either", - "itertools 0.11.0", - "rayon", -] - [[package]] name = "rayon-cond" version = "0.4.0" @@ -2788,6 +2837,43 @@ dependencies = [ "bitflags", ] +[[package]] +name = "ref-cast" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.2", +] + +[[package]] +name = "referencing" +version = "0.46.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbf332a2f81899f6836f22c03da73dae8a664c32e3016b84692c23cddadc95d" +dependencies = [ + "ahash", + "fluent-uri", + "getrandom 0.3.4", + "hashbrown 0.16.1", + "itoa", + "micromap", + "parking_lot", + "percent-encoding", + "serde_json", +] + [[package]] name = "regex" version = "1.12.3" @@ -2819,9 +2905,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.12.28" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64 0.22.1", "bytes", @@ -2841,9 +2927,9 @@ dependencies = [ "quinn", "rustls", "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", - "serde_urlencoded", "sync_wrapper", "tokio", "tokio-rustls", @@ -2856,7 +2942,6 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots", ] [[package]] @@ -2885,6 +2970,15 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.4" @@ -2904,14 +2998,26 @@ version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ + "aws-lc-rs", "once_cell", - "ring", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.14.1" @@ -2922,12 +3028,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -2976,12 +3110,22 @@ dependencies = [ ] [[package]] -name = "schemars" -version = "0.8.22" +name = "schannel" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", + "ref-cast", "schemars_derive", "serde", "serde_json", @@ -2989,14 +3133,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.22" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 2.0.117", ] [[package]] @@ -3005,6 +3149,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.28" @@ -3059,7 +3226,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3070,7 +3237,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3086,18 +3253,6 @@ dependencies = [ "zmij", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - [[package]] name = "shlex" version = "1.3.0" @@ -3139,6 +3294,22 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "similar" version = "2.7.0" @@ -3174,7 +3345,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326" dependencies = [ "base64 0.13.1", - "nom 7.1.3", + "nom", "serde", "unicode-segmentation", ] @@ -3216,7 +3387,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.117", ] [[package]] @@ -3236,6 +3407,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -3253,14 +3435,14 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] name = "target-lexicon" -version = "0.12.16" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "tempfile" @@ -3301,7 +3483,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3312,37 +3494,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", -] - -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" -dependencies = [ - "num-conv", - "time-core", + "syn 2.0.117", ] [[package]] @@ -3372,44 +3524,13 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenizers" -version = "0.20.4" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b08cc37428a476fc9e20ac850132a513a2e1ce32b6a31addf2b74fa7033b905" -dependencies = [ - "aho-corasick", - "derive_builder", - "esaxx-rs", - "getrandom 0.2.17", - "itertools 0.12.1", - "lazy_static", - "log", - "macro_rules_attribute", - "monostate", - "onig", - "paste", - "rand 0.8.6", - "rayon", - "rayon-cond 0.3.0", - "regex", - "regex-syntax", - "serde", - "serde_json", - "spm_precompiled", - "thiserror 1.0.69", - "unicode-normalization-alignments", - "unicode-segmentation", - "unicode_categories", -] - -[[package]] -name = "tokenizers" -version = "0.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a620b996116a59e184c2fa2dfd8251ea34a36d0a514758c6f966386bd2e03476" +checksum = "44e5bea67576e04b6ff8564c5d9e09c2ef0cf476502245f2f120e497769d3112" dependencies = [ "ahash", - "aho-corasick", "compact_str", + "daachorse", "dary_heap", "derive_builder", "esaxx-rs", @@ -3422,7 +3543,7 @@ dependencies = [ "paste", "rand 0.9.4", "rayon", - "rayon-cond 0.4.0", + "rayon-cond", "regex", "regex-syntax", "serde", @@ -3458,7 +3579,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3578,7 +3699,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3615,6 +3736,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +[[package]] +name = "unicode-general-category" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -3657,12 +3784,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" - [[package]] name = "untrusted" version = "0.9.0" @@ -3694,13 +3815,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] -name = "uuid" -version = "1.23.2" +name = "uuid-simd" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" dependencies = [ - "js-sys", - "wasm-bindgen", + "outref", + "vsimd", ] [[package]] @@ -3721,6 +3842,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "wait-timeout" version = "0.2.1" @@ -3815,7 +3942,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -3852,9 +3979,9 @@ dependencies = [ [[package]] name = "wasm-streams" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" dependencies = [ "futures-util", "js-sys", @@ -3896,10 +4023,10 @@ dependencies = [ ] [[package]] -name = "webpki-roots" -version = "1.0.7" +name = "webpki-root-certs" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" dependencies = [ "rustls-pki-types", ] @@ -3944,7 +4071,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3955,7 +4082,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4206,7 +4333,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -4222,7 +4349,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -4273,7 +4400,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4301,7 +4428,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -4322,7 +4449,7 @@ checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4342,7 +4469,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -4382,7 +4509,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] diff --git a/eoc/eoc-rs/crates/eoc-route-learned/Cargo.toml b/eoc/eoc-rs/crates/eoc-route-learned/Cargo.toml index 19f2f65e4..3c81f40f3 100644 --- a/eoc/eoc-rs/crates/eoc-route-learned/Cargo.toml +++ b/eoc/eoc-rs/crates/eoc-route-learned/Cargo.toml @@ -17,7 +17,7 @@ serde_json = "1" async-trait.workspace = true thiserror.workspace = true rand = { version = "0.10", default-features = false, features = ["std", "std_rng"] } -rand_distr = { version = "0.4", default-features = false } +rand_distr = { version = "0.6", default-features = false } nalgebra = { version = "0.33", default-features = false, features = ["std", "serde-serialize"] } tracing = "0.1" -- 2.54.0 (Apple Git-157) From ea679e364caf664b43f8fba2cb78645721959d50 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Mon, 20 Jul 2026 12:46:50 -0400 Subject: [PATCH 2/3] =?UTF-8?q?jouleclaw-corpus:=20receipts=20=E2=86=92=20?= =?UTF-8?q?training=20corpus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sealed receipts already record what a cascade walk did: which tier closed the query, at what microjoule cost, at what counter honesty. That is an observed execution outcome — the same class of supervision a code model gets from "passes on head, fails on base", except it arrives signed and energy-metered rather than reconstructed after the fact. Until now receipts were terminal: written, verified, summed into cost reports, never read again. This projects them into the labelled (embedding, stage, success, cost_microjoules) rows an EOC router trainer consumes, so the ledger compounds instead of merely accumulating. Derivation is honest about its limits: - cost_microjoules is verbatim joules_uj — not modelled, not rescaled. - stage prefers the runtime's own eoc_stage label, else maps from CascadeTier only where unambiguous. L1Lawful has no EOC analogue and nothing maps to Graph, so those receipts are skipped and counted in CorpusStats rather than guessed into a bucket. - success means "this tier closed the walk". The receipt carries no failure discriminator; that is documented, not papered over. - Monotone negatives (the cheaper rungs a strict cascade must have escalated past) are opt-in, because they rest on an assumption a rung-skipping router would break. Graph is never synthesised as a negative. Two traps absorbed. The receipt's eoc_stage is lower-case ("neural") while the trainer's Stage deserialises capitalised ("Neural") and rejects anything else — passing the field through fails at ingest, so from_eoc_label converts and a test pins the emitted spelling. And the bundled StructuralEmbedder deliberately excludes tier and joules: they are the label side of every row, and a router that could read the closing tier out of its own features would score perfectly while learning nothing. Receipts carry no vector, so the embedder is a trait. The bundled one is deterministic and offline over receipt structure — it does not encode query semantics, and says so. Coupling is the wire format by design: EOC takes no dependency on JouleClaw and JouleClaw none on EOC. Verified end to end: emit_corpus → JSONL → the trainer's own loader → train_logreg and train_mf both produce routers. 14 tests. Co-Authored-By: Claude Opus 4.8 --- jouleclaw/jouleclaw-rs/Cargo.lock | 13 + jouleclaw/jouleclaw-rs/Cargo.toml | 1 + .../crates/jouleclaw-corpus/Cargo.toml | 20 + .../jouleclaw-corpus/examples/emit_corpus.rs | 70 ++ .../crates/jouleclaw-corpus/src/lib.rs | 607 ++++++++++++++++++ 5 files changed, 711 insertions(+) create mode 100644 jouleclaw/jouleclaw-rs/crates/jouleclaw-corpus/Cargo.toml create mode 100644 jouleclaw/jouleclaw-rs/crates/jouleclaw-corpus/examples/emit_corpus.rs create mode 100644 jouleclaw/jouleclaw-rs/crates/jouleclaw-corpus/src/lib.rs diff --git a/jouleclaw/jouleclaw-rs/Cargo.lock b/jouleclaw/jouleclaw-rs/Cargo.lock index 2b8292860..c2428e623 100644 --- a/jouleclaw/jouleclaw-rs/Cargo.lock +++ b/jouleclaw/jouleclaw-rs/Cargo.lock @@ -3389,6 +3389,19 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "jouleclaw-corpus" +version = "0.1.0" +dependencies = [ + "jouleclaw-energy", + "jouleclaw-intent", + "jouleclaw-prov", + "serde", + "serde_json", + "tempfile", + "thiserror 2.0.18", +] + [[package]] name = "jouleclaw-critic" version = "0.1.0" diff --git a/jouleclaw/jouleclaw-rs/Cargo.toml b/jouleclaw/jouleclaw-rs/Cargo.toml index 6a37cd19f..c129707d9 100644 --- a/jouleclaw/jouleclaw-rs/Cargo.toml +++ b/jouleclaw/jouleclaw-rs/Cargo.toml @@ -1147,6 +1147,7 @@ members = [ # top of jouleclaw-otel's SpanData. "crates/jouleclaw-otel-authz", "crates/jouleclaw-jcp", + "crates/jouleclaw-corpus", ] [workspace.package] diff --git a/jouleclaw/jouleclaw-rs/crates/jouleclaw-corpus/Cargo.toml b/jouleclaw/jouleclaw-rs/crates/jouleclaw-corpus/Cargo.toml new file mode 100644 index 000000000..d97c70097 --- /dev/null +++ b/jouleclaw/jouleclaw-rs/crates/jouleclaw-corpus/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "jouleclaw-corpus" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true +description = "Receipts → training corpus. Projects sealed JouleClaw receipts into the labelled (embedding, stage, success, cost_microjoules) rows an EOC router trainer consumes, turning the audit ledger into a supervision signal. Labels are derived from observed cascade closure — never invented; receipts whose closing tier has no EOC analogue are skipped and counted, not guessed." + +[dependencies] +jouleclaw-prov = { path = "../jouleclaw-prov" } +jouleclaw-energy = { path = "../jouleclaw-energy" } +jouleclaw-intent = { path = "../jouleclaw-intent" } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +tempfile = { workspace = true } diff --git a/jouleclaw/jouleclaw-rs/crates/jouleclaw-corpus/examples/emit_corpus.rs b/jouleclaw/jouleclaw-rs/crates/jouleclaw-corpus/examples/emit_corpus.rs new file mode 100644 index 000000000..37fbb0213 --- /dev/null +++ b/jouleclaw/jouleclaw-rs/crates/jouleclaw-corpus/examples/emit_corpus.rs @@ -0,0 +1,70 @@ +//! Emit a training corpus from a batch of sealed receipts. +//! +//! ```text +//! cargo run -p jouleclaw-corpus --example emit_corpus -- /tmp/corpus.jsonl +//! ``` +//! +//! The output file is directly loadable by an EOC router trainer's JSONL +//! loader. This example stands in a synthetic ledger for the receipts a +//! real deployment would read off disk. + +use jouleclaw_corpus::{CorpusBuilder, write_jsonl}; +use jouleclaw_energy::Provenance; +use jouleclaw_intent::{IntentVerdict, QueryIntent}; +use jouleclaw_prov::{CascadeTier, Receipt}; + +fn receipt(tier: CascadeTier, uj: u64, intent: QueryIntent, confidence: f64) -> Receipt { + Receipt { + context: None, + jc_receipt: "1".into(), + id: format!("rcpt-{}-{uj}", tier.wire_tag()), + closed_at: "2026-07-20T00:00:00Z".into(), + input_hash: "a".repeat(64), + tier, + joules_uj: uj, + energy_provenance: Provenance::HwShunt, + tools_touched: Vec::new(), + claims: Vec::new(), + eoc_stage: None, + citations: Vec::new(), + tool_receipts: Vec::new(), + token_attributions: Vec::new(), + model_id: None, + intent: Some(IntentVerdict::new(intent, confidence, "heuristic:v1")), + steward: None, + } +} + +fn main() { + let out = std::env::args() + .nth(1) + .unwrap_or_else(|| "corpus.jsonl".to_string()); + + // A ledger with the shape a real cascade produces: comprehension work + // closing cheap, generation work escalating to a model, and one lawful + // primitive that has no EOC analogue and must be skipped. + let ledger = vec![ + receipt(CascadeTier::L0Cache, 12, QueryIntent::Comprehension, 0.91), + receipt(CascadeTier::L0Cache, 9, QueryIntent::Comprehension, 0.88), + receipt(CascadeTier::L2Embed, 4_100, QueryIntent::Comprehension, 0.72), + receipt(CascadeTier::L2Embed, 3_800, QueryIntent::Review, 0.65), + receipt(CascadeTier::L3Model, 2_400_000, QueryIntent::Generation, 0.94), + receipt(CascadeTier::L3Model, 1_900_000, QueryIntent::Modification, 0.80), + receipt(CascadeTier::L4Wire, 61_000_000, QueryIntent::Generation, 0.97), + receipt(CascadeTier::L1Lawful, 40, QueryIntent::Process, 0.99), + ]; + + let (rows, stats) = CorpusBuilder::default() + .with_monotone_negatives(true) + .build(&ledger); + + write_jsonl(&out, &rows).expect("write corpus"); + + println!("receipts seen {}", stats.receipts_seen); + println!("receipts used {}", stats.receipts_used); + println!("skipped unmapped {}", stats.skipped_unmapped); + println!("rows positive {}", stats.rows_positive); + println!("rows negative {}", stats.rows_negative); + println!("rows total {}", stats.rows()); + println!("wrote {out}"); +} diff --git a/jouleclaw/jouleclaw-rs/crates/jouleclaw-corpus/src/lib.rs b/jouleclaw/jouleclaw-rs/crates/jouleclaw-corpus/src/lib.rs new file mode 100644 index 000000000..c2ecceabf --- /dev/null +++ b/jouleclaw/jouleclaw-rs/crates/jouleclaw-corpus/src/lib.rs @@ -0,0 +1,607 @@ +//! # jouleclaw-corpus +//! +//! **Receipts → training corpus.** The connector that turns the audit +//! ledger into a supervision signal. +//! +//! Every sealed [`Receipt`] already records what a cascade walk actually +//! did: which tier closed the query, how many microjoules it cost, and at +//! what counter honesty. That is an *observed execution outcome* — the same +//! class of signal a code model gets from "the test passed on head and +//! failed on base", except it arrives signed and energy-metered rather than +//! reconstructed after the fact. +//! +//! Until now those receipts were terminal: written, verified, aggregated +//! into cost reports, and never read again. This crate projects them into +//! the labelled rows an EOC router trainer consumes, so the ledger compounds +//! instead of merely accumulating. +//! +//! ## What is derived, and what is not +//! +//! - **`cost_microjoules`** — read directly from [`Receipt::joules_uj`]. Not +//! modelled, not rescaled. +//! - **`stage`** — taken from [`Receipt::eoc_stage`] when the runtime +//! reported one (authoritative), else mapped from [`CascadeTier`] but +//! **only where the mapping is unambiguous**. Receipts that cannot be +//! classified are skipped and counted in [`CorpusStats::skipped_unmapped`], +//! never guessed into a bucket. +//! - **`success`** — a sealed receipt attests the walk *closed* at +//! `tier`; that stage is labelled a success. Note honestly: the receipt +//! carries no failure discriminator, so if a runtime seals receipts for +//! failed walks they are indistinguishable here. Treat `success: true` as +//! "this tier closed the walk", which is what it is. +//! - **`embedding`** — receipts carry no vector. An [`Embedder`] supplies +//! one. The bundled [`StructuralEmbedder`] is deterministic and offline; +//! see its docs for the sharp limitation. +//! +//! ## Wire compatibility +//! +//! Rows serialise to the exact JSON an EOC router trainer's JSONL loader +//! expects. One trap this crate exists to absorb: the receipt's +//! `eoc_stage` label is **lower-case** (`"neural"`), while the trainer's +//! `Stage` enum deserialises **capitalised** (`"Neural"`) and rejects +//! anything else. Feeding the receipt field straight through fails at +//! ingest. [`CorpusStage::from_eoc_label`] absorbs the difference and +//! `stage_wire_form_is_pinned` locks the emitted spelling. +//! +//! The coupling here is the wire format, deliberately — the EOC standard +//! takes no dependency on JouleClaw, and JouleClaw takes none on EOC. + +#![forbid(unsafe_code)] +#![warn(missing_docs)] + +use std::io::Write; +use std::path::Path; + +use jouleclaw_energy::Provenance; +use jouleclaw_intent::QueryIntent; +use jouleclaw_prov::{CascadeTier, Receipt}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +/// Errors raised while building or writing a corpus. +#[derive(Debug, Error)] +pub enum Error { + /// Writing the JSONL sink failed. + #[error("corpus write failed: {0}")] + Write(String), + /// A row could not be serialised. + #[error("corpus encode failed: {0}")] + Encode(String), +} + +/// Result alias for this crate. +pub type Result = std::result::Result; + +// ───────────────────────────────────────────────────────────────────── +// Stage +// ───────────────────────────────────────────────────────────────────── + +/// The EOC cascade stage a row is labelled with. +/// +/// Variant spelling is **load-bearing**: these serialise to `"Cache"`, +/// `"Kv"`, `"Graph"`, `"Neural"` to match the consuming trainer's `Stage` +/// enum exactly. Do not add `#[serde(rename_all = ...)]` here — it would +/// silently break every downstream ingest. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum CorpusStage { + /// Content-addressed cache hit. + Cache, + /// Key-value lookup, including embedding-similarity match. + Kv, + /// Graph / triple-store retrieval. + Graph, + /// Neural inference. + Neural, +} + +impl CorpusStage { + /// Relative cost rank, cheapest first. Used to order negatives. + fn rank(self) -> u8 { + match self { + Self::Cache => 0, + Self::Kv => 1, + Self::Graph => 2, + Self::Neural => 3, + } + } + + /// Parse the label a runtime writes into [`Receipt::eoc_stage`]. + /// + /// Receipts carry the lower-case form (`"neural"`); this accepts that, + /// and is case-insensitive so a capitalised label round-trips too. + pub fn from_eoc_label(label: &str) -> Option { + match label.trim().to_ascii_lowercase().as_str() { + "cache" => Some(Self::Cache), + "kv" => Some(Self::Kv), + "graph" => Some(Self::Graph), + "neural" => Some(Self::Neural), + _ => None, + } + } + + /// Map a JouleClaw cascade tier onto an EOC stage. + /// + /// Returns `None` where no honest correspondence exists rather than + /// forcing one: + /// + /// - `L0Cache` → `Cache` + /// - `L2Embed` → `Kv` (the Kv stage is defined to include + /// embedding-similarity match) + /// - `L3Model`, `L4Wire` → `Neural` + /// - `L1Lawful` → **`None`.** A deterministic lawful primitive is not a + /// cache, a lookup, a graph walk, or an inference. There is no EOC + /// stage for it. + /// + /// Note that nothing maps *to* `Graph`: JouleClaw has no graph tier. + /// `Graph` rows only ever arise from an explicit `eoc_stage` label. + pub fn from_tier(tier: CascadeTier) -> Option { + match tier { + CascadeTier::L0Cache => Some(Self::Cache), + CascadeTier::L1Lawful => None, + CascadeTier::L2Embed => Some(Self::Kv), + CascadeTier::L3Model | CascadeTier::L4Wire => Some(Self::Neural), + } + } +} + +// ───────────────────────────────────────────────────────────────────── +// Row +// ───────────────────────────────────────────────────────────────────── + +/// One labelled training row, in the trainer's on-disk shape. +/// +/// Field names and order match the consumer's `Example` struct. See +/// [`CorpusStage`] on why the stage spelling must not change. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CorpusRow { + /// Feature vector. Dimension must be consistent across the corpus. + pub embedding: Vec, + /// Stage this row labels. + pub stage: CorpusStage, + /// Whether `stage` closed the walk. + pub success: bool, + /// Observed cost in microjoules, verbatim from the receipt. + pub cost_microjoules: u64, +} + +// ───────────────────────────────────────────────────────────────────── +// Embedding +// ───────────────────────────────────────────────────────────────────── + +/// Supplies the feature vector for a receipt. +/// +/// Receipts carry no vector, so this is the one genuinely external input. +/// Implement it over a real text embedder when the query text is available +/// alongside the receipt; use [`StructuralEmbedder`] when it is not. +pub trait Embedder { + /// Feature vector for `receipt`. Must return [`Embedder::dim`] elements. + fn embed(&self, receipt: &Receipt) -> Vec; + /// Dimension of every vector this embedder returns. + fn dim(&self) -> usize; +} + +/// Number of dimensions [`StructuralEmbedder`] emits. +pub const STRUCTURAL_DIM: usize = 15; + +/// A deterministic, offline embedder over receipt *structure*. +/// +/// ## The limitation, stated plainly +/// +/// This does **not** encode query semantics. Receipts hold a BLAKE3 of the +/// input, not the input, and a hash has no semantic locality — using its +/// bits as features would manufacture noise and dress it as signal, so this +/// embedder does not touch `input_hash`. What it captures is the *shape* of +/// a request: its intent class, how much retrieval grounded it, and how +/// trustworthy the energy counter was. That is enough to learn "requests +/// shaped like this tend to be closed by that stage at this cost", and not +/// enough to distinguish two differently-worded questions of the same shape. +/// For semantic routing, supply a real text embedder. +/// +/// ## No label leakage +/// +/// `tier`/`eoc_stage` and `joules_uj` are deliberately **excluded**. They +/// are the label side of every row — a router that could read the closing +/// tier or its cost out of the feature vector would score perfectly while +/// learning nothing. Layout: +/// +/// | dims | feature | +/// |--------|----------------------------------------------------------| +/// | 0–5 | intent one-hot (Comprehension…Other), all-zero if absent | +/// | 6 | intent confidence, `0.0` when no verdict ran | +/// | 7–9 | energy provenance one-hot (HwShunt/ModelBased/Estimator) | +/// | 10–13 | `ln(1+n)` over tools, claims, citations, attributions | +/// | 14 | 1.0 when a model id is present | +#[derive(Debug, Clone, Copy, Default)] +pub struct StructuralEmbedder; + +impl Embedder for StructuralEmbedder { + fn dim(&self) -> usize { + STRUCTURAL_DIM + } + + fn embed(&self, receipt: &Receipt) -> Vec { + let mut v = vec![0.0_f32; STRUCTURAL_DIM]; + + if let Some(verdict) = &receipt.intent { + let idx = match verdict.intent { + QueryIntent::Comprehension => 0, + QueryIntent::Generation => 1, + QueryIntent::Modification => 2, + QueryIntent::Process => 3, + QueryIntent::Review => 4, + QueryIntent::Other => 5, + // `QueryIntent` is `#[non_exhaustive]`. A variant added + // upstream lands in the unclassified slot rather than + // shifting the one-hot layout underneath a trained router. + _ => 5, + }; + v[idx] = 1.0; + v[6] = verdict.confidence as f32; + } + + let prov_idx = match receipt.energy_provenance { + Provenance::HwShunt => 7, + Provenance::ModelBased => 8, + Provenance::Estimator => 9, + }; + v[prov_idx] = 1.0; + + v[10] = ln1p(receipt.tools_touched.len()); + v[11] = ln1p(receipt.claims.len()); + v[12] = ln1p(receipt.citations.len()); + v[13] = ln1p(receipt.token_attributions.len()); + v[14] = if receipt.model_id.is_some() { 1.0 } else { 0.0 }; + + v + } +} + +fn ln1p(n: usize) -> f32 { + (n as f32).ln_1p() +} + +// ───────────────────────────────────────────────────────────────────── +// Builder +// ───────────────────────────────────────────────────────────────────── + +/// What a corpus build actually consumed and produced. +/// +/// Reported rather than swallowed: a silent skip reads as "covered +/// everything" when it did not. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct CorpusStats { + /// Receipts read. + pub receipts_seen: usize, + /// Receipts that produced at least one row. + pub receipts_used: usize, + /// Receipts skipped because their closing tier has no EOC analogue + /// (in practice: `L1Lawful`) and no `eoc_stage` label was present. + pub skipped_unmapped: usize, + /// Positive rows emitted (one per used receipt). + pub rows_positive: usize, + /// Negative rows emitted under the monotone-escalation assumption. + pub rows_negative: usize, +} + +impl CorpusStats { + /// Total rows emitted. + pub fn rows(&self) -> usize { + self.rows_positive + self.rows_negative + } +} + +/// Projects receipts into corpus rows. +pub struct CorpusBuilder { + embedder: E, + monotone_negatives: bool, +} + +impl Default for CorpusBuilder { + fn default() -> Self { + Self::new(StructuralEmbedder) + } +} + +impl CorpusBuilder { + /// Build with `embedder`, emitting positives only. + pub fn new(embedder: E) -> Self { + Self { + embedder, + monotone_negatives: false, + } + } + + /// Also emit a negative row for each cheaper stage that a monotone + /// cascade must have tried and escalated past. + /// + /// **This rests on an assumption — read before enabling.** A receipt + /// records the tier that *closed* the walk, not the tiers walked. In a + /// strict cheapest-first cascade, closing at `Neural` means `Cache` and + /// `Kv` were attempted and did not satisfy the query, which is a real + /// negative label — and the pairing of "this rung failed, that rung + /// closed it" is the whole supervision signal. A router that skips + /// rungs breaks that inference, and these rows become noise. + /// + /// `Graph` is never emitted as a negative: no JouleClaw tier maps to it, + /// so its absence from a walk is not evidence it was tried and failed. + pub fn with_monotone_negatives(mut self, yes: bool) -> Self { + self.monotone_negatives = yes; + self + } + + /// Resolve the stage a receipt closed at, preferring the runtime's own + /// label over the tier mapping. + fn stage_of(receipt: &Receipt) -> Option { + receipt + .eoc_stage + .as_deref() + .and_then(CorpusStage::from_eoc_label) + .or_else(|| CorpusStage::from_tier(receipt.tier)) + } + + /// Project one receipt. Returns an empty vec when it cannot be mapped. + pub fn rows_from_receipt(&self, receipt: &Receipt) -> Vec { + let Some(stage) = Self::stage_of(receipt) else { + return Vec::new(); + }; + let embedding = self.embedder.embed(receipt); + let mut rows = vec![CorpusRow { + embedding: embedding.clone(), + stage, + success: true, + cost_microjoules: receipt.joules_uj, + }]; + + if self.monotone_negatives { + for cheaper in [CorpusStage::Cache, CorpusStage::Kv] { + if cheaper.rank() < stage.rank() { + rows.push(CorpusRow { + embedding: embedding.clone(), + stage: cheaper, + success: false, + cost_microjoules: receipt.joules_uj, + }); + } + } + } + rows + } + + /// Project a batch of receipts, reporting what was used and skipped. + pub fn build(&self, receipts: &[Receipt]) -> (Vec, CorpusStats) { + let mut out = Vec::new(); + let mut stats = CorpusStats { + receipts_seen: receipts.len(), + ..Default::default() + }; + for r in receipts { + let rows = self.rows_from_receipt(r); + if rows.is_empty() { + stats.skipped_unmapped += 1; + continue; + } + stats.receipts_used += 1; + for row in &rows { + if row.success { + stats.rows_positive += 1; + } else { + stats.rows_negative += 1; + } + } + out.extend(rows); + } + (out, stats) + } +} + +// ───────────────────────────────────────────────────────────────────── +// Sinks +// ───────────────────────────────────────────────────────────────────── + +/// Encode rows as JSONL — one row per line, trainer-ready. +pub fn to_jsonl(rows: &[CorpusRow]) -> Result { + let mut s = String::new(); + for row in rows { + let line = serde_json::to_string(row).map_err(|e| Error::Encode(e.to_string()))?; + s.push_str(&line); + s.push('\n'); + } + Ok(s) +} + +/// Write rows to a JSONL file the trainer's loader can read directly. +pub fn write_jsonl(path: impl AsRef, rows: &[CorpusRow]) -> Result<()> { + let body = to_jsonl(rows)?; + let mut f = std::fs::File::create(path).map_err(|e| Error::Write(e.to_string()))?; + f.write_all(body.as_bytes()) + .map_err(|e| Error::Write(e.to_string())) +} + +#[cfg(test)] +mod tests { + use super::*; + use jouleclaw_intent::IntentVerdict; + + /// Struct-literal construction, as the conformance-vector generators do: + /// the builder *accounts* joules from tool/claim costs rather than taking + /// an asserted total, and these tests need an exact `joules_uj`. + fn receipt(tier: CascadeTier, uj: u64) -> Receipt { + Receipt { + context: None, + jc_receipt: "1".into(), + id: "rcpt-test".into(), + closed_at: "2026-07-20T00:00:00Z".into(), + input_hash: "a".repeat(64), + tier, + joules_uj: uj, + energy_provenance: Provenance::HwShunt, + tools_touched: Vec::new(), + claims: Vec::new(), + eoc_stage: None, + citations: Vec::new(), + tool_receipts: Vec::new(), + token_attributions: Vec::new(), + model_id: None, + intent: None, + steward: None, + } + } + + #[test] + fn stage_wire_form_is_pinned() { + // The consuming trainer rejects any other spelling. If this test + // fails, downstream ingest is already broken. + for (stage, expected) in [ + (CorpusStage::Cache, "\"Cache\""), + (CorpusStage::Kv, "\"Kv\""), + (CorpusStage::Graph, "\"Graph\""), + (CorpusStage::Neural, "\"Neural\""), + ] { + assert_eq!(serde_json::to_string(&stage).unwrap(), expected); + } + } + + #[test] + fn row_matches_trainer_field_shape() { + let row = CorpusRow { + embedding: vec![0.5, 0.25], + stage: CorpusStage::Neural, + success: true, + cost_microjoules: 1234, + }; + assert_eq!( + serde_json::to_string(&row).unwrap(), + r#"{"embedding":[0.5,0.25],"stage":"Neural","success":true,"cost_microjoules":1234}"# + ); + } + + #[test] + fn absorbs_lowercase_receipt_label() { + // The exact trap this crate exists to absorb. + assert_eq!( + CorpusStage::from_eoc_label("neural"), + Some(CorpusStage::Neural) + ); + assert_eq!(CorpusStage::from_eoc_label("kv"), Some(CorpusStage::Kv)); + assert_eq!(CorpusStage::from_eoc_label("Neural"), Some(CorpusStage::Neural)); + assert_eq!(CorpusStage::from_eoc_label("nonsense"), None); + } + + #[test] + fn lawful_tier_is_skipped_not_guessed() { + assert_eq!(CorpusStage::from_tier(CascadeTier::L1Lawful), None); + let (rows, stats) = CorpusBuilder::default().build(&[receipt(CascadeTier::L1Lawful, 10)]); + assert!(rows.is_empty()); + assert_eq!(stats.skipped_unmapped, 1); + assert_eq!(stats.receipts_used, 0); + } + + #[test] + fn eoc_label_wins_over_tier_mapping() { + let mut r = receipt(CascadeTier::L3Model, 900); + r.eoc_stage = Some("graph".into()); + let rows = CorpusBuilder::default().rows_from_receipt(&r); + // Tier alone would have said Neural; the runtime's own label wins. + assert_eq!(rows[0].stage, CorpusStage::Graph); + } + + #[test] + fn cost_is_verbatim() { + let rows = CorpusBuilder::default().rows_from_receipt(&receipt(CascadeTier::L0Cache, 7)); + assert_eq!(rows[0].cost_microjoules, 7); + assert!(rows[0].success); + assert_eq!(rows[0].stage, CorpusStage::Cache); + } + + #[test] + fn monotone_negatives_pair_failure_with_closure() { + let rows = CorpusBuilder::default() + .with_monotone_negatives(true) + .rows_from_receipt(&receipt(CascadeTier::L3Model, 5_000)); + assert_eq!(rows.len(), 3); + assert_eq!(rows[0].stage, CorpusStage::Neural); + assert!(rows[0].success); + // Cheaper rungs a monotone cascade must have escalated past. + assert!(rows[1..].iter().all(|r| !r.success)); + let neg: Vec<_> = rows[1..].iter().map(|r| r.stage).collect(); + assert_eq!(neg, vec![CorpusStage::Cache, CorpusStage::Kv]); + } + + #[test] + fn graph_is_never_a_synthesised_negative() { + // Nothing maps to Graph, so its absence is not evidence of failure. + let rows = CorpusBuilder::default() + .with_monotone_negatives(true) + .rows_from_receipt(&receipt(CascadeTier::L4Wire, 80_000)); + assert!(!rows.iter().any(|r| r.stage == CorpusStage::Graph)); + } + + #[test] + fn cheapest_tier_emits_no_negatives() { + let rows = CorpusBuilder::default() + .with_monotone_negatives(true) + .rows_from_receipt(&receipt(CascadeTier::L0Cache, 1)); + assert_eq!(rows.len(), 1); + } + + #[test] + fn embedding_never_leaks_the_label() { + // Two receipts identical but for tier and cost must embed the same; + // otherwise the router can read its own answer out of the features. + let cheap = receipt(CascadeTier::L0Cache, 1); + let dear = receipt(CascadeTier::L4Wire, 900_000); + let e = StructuralEmbedder; + assert_eq!(e.embed(&cheap), e.embed(&dear)); + } + + #[test] + fn embedding_dim_is_stable_and_declared() { + let e = StructuralEmbedder; + let mut r = receipt(CascadeTier::L3Model, 10); + assert_eq!(e.embed(&r).len(), e.dim()); + r.intent = Some(IntentVerdict::new(QueryIntent::Review, 0.75, "heuristic:v1")); + let v = e.embed(&r); + assert_eq!(v.len(), STRUCTURAL_DIM); + assert_eq!(v[4], 1.0, "Review one-hot"); + assert!((v[6] - 0.75).abs() < 1e-6, "confidence carried"); + } + + #[test] + fn stats_account_for_every_receipt() { + let batch = vec![ + receipt(CascadeTier::L0Cache, 1), + receipt(CascadeTier::L1Lawful, 2), // unmappable + receipt(CascadeTier::L3Model, 3), + ]; + let (rows, stats) = CorpusBuilder::default().build(&batch); + assert_eq!(stats.receipts_seen, 3); + assert_eq!(stats.receipts_used, 2); + assert_eq!(stats.skipped_unmapped, 1); + assert_eq!(stats.receipts_used + stats.skipped_unmapped, stats.receipts_seen); + assert_eq!(rows.len(), stats.rows()); + } + + #[test] + fn jsonl_round_trips() { + let (rows, _) = CorpusBuilder::default().build(&[receipt(CascadeTier::L3Model, 42)]); + let text = to_jsonl(&rows).unwrap(); + assert_eq!(text.lines().count(), 1); + let back: CorpusRow = serde_json::from_str(text.lines().next().unwrap()).unwrap(); + assert_eq!(back, rows[0]); + } + + #[test] + fn writes_a_file_the_loader_can_read() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("corpus.jsonl"); + let (rows, _) = CorpusBuilder::default() + .with_monotone_negatives(true) + .build(&[receipt(CascadeTier::L3Model, 42)]); + write_jsonl(&path, &rows).unwrap(); + let text = std::fs::read_to_string(&path).unwrap(); + assert_eq!(text.lines().count(), 3); + for line in text.lines() { + serde_json::from_str::(line).unwrap(); + } + } +} -- 2.54.0 (Apple Git-157) From 6e3238c8feb6ea56905bc032f775c07b5113054a Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Mon, 20 Jul 2026 13:03:09 -0400 Subject: [PATCH 3/3] =?UTF-8?q?sandbox-proof:=20prove-local=20=E2=80=94=20?= =?UTF-8?q?attested=20local=20inference,=20zero=20egress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The proof matrix showcased the cloud path and left the air-gap path undemonstrated. prove-gpu and prove-lpu both reach across a network to silicon the prover does not own; there was no counterpart proving the case the sandbox ladder was built for. Every component existed — mmap-only GGUF loading with no network fetch path, deny-by-default NetworkGrant, offline Ed25519 attestation, hardware energy counters — and nothing assembled them. prove_local runs a local engine against local weights with no network grant, meters it on this host's own counters, and seals a cell. It is the deliberate inverse of prove_inference. There the silicon belongs to someone else, so energy is an honest token Estimator. Here the silicon is ours — the one case where joules can actually be measured — so device records pass through verbatim from PowerMeter with whatever provenance the hardware honestly supports. The meter can degrade; this does not re-tag it. Three properties, each checked rather than asserted: 1. Weights are local. Opened from disk and streamed into a BLAKE3 bound into the proof, so the cell attests inference against these bytes and not merely "a model". An unresolvable path is an error. 2. No egress was granted. The NetworkGrant default — empty outbound, inbound false — is asserted before the engine is spawned. 3. Energy is this host's, measured across the engine's execution. A non-zero engine exit refuses to seal: a proof for a run that failed would attest to work that did not happen. The engine is operator-supplied and driven as a subprocess, exactly as prove_accelerator drives a GPU workload, so the sandbox standard takes no dependency on any particular inference runtime. Verified end to end on Apple Silicon against a real checkpoint: Devstral-Small-2-24B-Instruct Q4_K_M, 13.35 GiB, generating from the local GGUF at 17.4 tok/s warm — 92.68 J measured over the span via IOReport energy-model counters (HwShunt, sudo-free), sealed to receipt b3:ARwbWCm… with attestation and signature both verified. SPEC §7.2 caught the first cut: the Apple meter reports CPU, GPU and ANE together, and a hardcoded CPU-only DeviceIdentity list left the NPU energy record unnamed. Identities are now derived from the measurement rather than assumed — the standard enforcing its own invariant against an incomplete implementation. 4 guard tests: absent weights, failed engine, weight-hash binding, zero-egress posture. Co-Authored-By: Claude Opus 4.8 --- sandbox/sandbox-rs/Cargo.lock | 1 + .../crates/sandbox-proof/Cargo.toml | 1 + .../crates/sandbox-proof/src/lib.rs | 311 ++++++++++++++++++ .../crates/sandbox-proof/src/main.rs | 43 +++ 4 files changed, 356 insertions(+) diff --git a/sandbox/sandbox-rs/Cargo.lock b/sandbox/sandbox-rs/Cargo.lock index 7256b7d06..027783422 100644 --- a/sandbox/sandbox-rs/Cargo.lock +++ b/sandbox/sandbox-rs/Cargo.lock @@ -1874,6 +1874,7 @@ dependencies = [ name = "sandbox-proof" version = "0.1.0" dependencies = [ + "blake3", "ed25519-dalek", "hex", "sandbox-attest", diff --git a/sandbox/sandbox-rs/crates/sandbox-proof/Cargo.toml b/sandbox/sandbox-rs/crates/sandbox-proof/Cargo.toml index b5bcb3f56..16cfa1053 100644 --- a/sandbox/sandbox-rs/crates/sandbox-proof/Cargo.toml +++ b/sandbox/sandbox-rs/crates/sandbox-proof/Cargo.toml @@ -26,4 +26,5 @@ serde = { workspace = true } serde_json.workspace = true ureq.workspace = true hex.workspace = true +blake3.workspace = true thiserror.workspace = true diff --git a/sandbox/sandbox-rs/crates/sandbox-proof/src/lib.rs b/sandbox/sandbox-rs/crates/sandbox-proof/src/lib.rs index 7e62f644b..f24114cc6 100644 --- a/sandbox/sandbox-rs/crates/sandbox-proof/src/lib.rs +++ b/sandbox/sandbox-rs/crates/sandbox-proof/src/lib.rs @@ -981,6 +981,245 @@ pub fn prove_confidential( pub mod matrix; +// ───────────────────────────────────────────────────────────────────── +// Local (air-gapped) inference +// ───────────────────────────────────────────────────────────────────── + +/// Inputs for a **local** inference proof: an engine to run, and the weights +/// it runs against. Both live on this machine; nothing is fetched. +#[derive(Clone, Debug)] +pub struct LocalInferenceInputs { + /// The local inference engine to execute. + pub engine: String, + /// Arguments passed to the engine. + pub args: Vec, + /// Path to the model weights on local disk. Hashed into the proof, so the + /// cell attests inference against *these bytes* and not merely "a model". + pub weights: String, +} + +/// Stream a BLAKE3 over a file without loading it into memory — weights are +/// routinely gigabytes. +fn hash_file(path: &std::path::Path) -> Result { + use std::io::Read; + let mut f = std::fs::File::open(path) + .map_err(|e| ProofError::Conformance(format!("weights unreadable: {e}")))?; + let mut hasher = blake3::Hasher::new(); + let mut buf = vec![0u8; 1 << 20]; + loop { + let n = f + .read(&mut buf) + .map_err(|e| ProofError::Conformance(format!("weights read failed: {e}")))?; + if n == 0 { + break; + } + hasher.update(&buf[..n]); + } + Ok(hasher.finalize().to_hex().to_string()) +} + +/// Prove **local, offline inference**: run a local engine against local weights +/// with no network grant, meter it on this host's own energy counters, and seal +/// a cell. +/// +/// This is the deliberate inverse of [`prove_inference`]. There, the silicon +/// belongs to somebody else, so the energy is an honest token `Estimator` and +/// the call goes over the wire. Here the silicon is the prover's own, which is +/// the one case where the joules can actually be *measured* — so the device +/// records come straight off [`PowerMeter`] with whatever provenance the +/// hardware honestly supports (`HwShunt` on Apple Silicon IOReport and Linux +/// RAPL/NVML; `Estimator` where no counter exists). The meter can degrade; it +/// does not invent a reading, and this function does not re-tag one. +/// +/// Three properties the cell attests, each checked rather than asserted: +/// +/// 1. **The weights are local.** They are opened from disk and streamed into a +/// BLAKE3 that is bound into the proof. A path that does not resolve is an +/// error, not a warning. +/// 2. **No egress was granted.** The run's [`NetworkGrant`] is the default — +/// empty outbound allowlist, inbound false — and that emptiness is asserted +/// before the engine is spawned. Deny-by-default is the standard's posture; +/// this makes it a checked precondition of the proof. +/// 3. **The energy is this host's.** Measured over the engine's execution span +/// by the best counter the platform offers. +/// +/// The engine is supplied by the operator and driven as a subprocess, exactly +/// as [`prove_accelerator`] drives a GPU workload. That keeps the sandbox +/// standard free of any dependency on a particular inference runtime — this +/// proves *local inference*, not one vendor's loader. +pub fn prove_local( + inputs: &LocalInferenceInputs, + cloud: &str, + at_unix_us: u64, + proof_key: &SigningKey, +) -> Result { + use ed25519_dalek::SigningKey as Sk; + use sandbox_attest::{DeviceIdentity, Outcome}; + use sandbox_capability::{ComputeGrant, Grants}; + use sandbox_meter::PowerMeter; + use sandbox_runtime::{grants_content_id, unit_content_id, Host, RunReport}; + + let report = sandbox_conformance::run(); + let conformance: Vec = report + .results + .iter() + .map(|r| ProofClass { + class: r.class.to_string(), + passed: r.passed, + }) + .collect(); + if !report.ok() { + return Err(ProofError::Conformance("in-code pack did not pass".into())); + } + + // (1) Weights are local, and the proof is bound to their exact bytes. + let weights_path = std::path::Path::new(&inputs.weights); + if !weights_path.is_file() { + return Err(ProofError::Conformance(format!( + "weights not present locally: {}", + inputs.weights + ))); + } + let weights_bytes = std::fs::metadata(weights_path) + .map(|m| m.len()) + .unwrap_or(0); + let weights_hash = hash_file(weights_path)?; + + // (2) Deny-by-default egress, asserted rather than assumed. + let grants = Grants { + compute: ComputeGrant { + devices: vec![DeviceClass::Cpu, DeviceClass::Gpu, DeviceClass::Npu], + energy_budget_uj: u64::MAX, + cpu_time_ms: 600_000, + memory_bytes: 64u64 << 30, + }, + ..Grants::default() + }; + if !grants.network.outbound.is_empty() || grants.network.inbound { + return Err(ProofError::Conformance( + "local proof requires a zero-egress network grant".into(), + )); + } + + // (3) Meter this host's own counters across the engine's execution. + let mut meter = PowerMeter::detect(); + let sources = meter.sources().join(","); + let is_hw = meter.is_hardware(); + let (status, measurement) = meter.measure(|| { + std::process::Command::new(&inputs.engine) + .args(&inputs.args) + .status() + }); + let status = status.map_err(|e| { + ProofError::Run( + sandbox_runtime::ExecError::Unavailable(format!("spawn {}: {e}", inputs.engine)).into(), + ) + })?; + if !status.success() { + return Err(ProofError::Run( + sandbox_runtime::ExecError::Unavailable(format!( + "engine exited with {status}; refusing to seal a proof for a run that failed" + )) + .into(), + )); + } + + // Device records pass through verbatim — the meter's provenance is the + // proof's provenance. Re-tagging here is exactly the lie the ladder exists + // to prevent. + // One identity per metered class — SPEC §7.2 requires every device that + // reports energy to be named. The meter decides which classes it can see + // (on Apple Silicon that is CPU, GPU and ANE together), so the identity + // list is derived from the measurement rather than assumed. + let device_identity: Vec = measurement + .devices + .iter() + .map(|d| DeviceIdentity { + class: d.class, + model: std::env::consts::ARCH.to_string(), + driver: sources.clone(), + }) + .collect(); + let run = RunReport { + device_energy: measurement.devices.clone(), + device_identity, + outcome: Outcome::Completed, + journal: None, + storage: None, + }; + + let host = Host::new( + "sandbox-proof", + "steward.proof", + Sk::from_bytes(&[51u8; 32]), + Sk::from_bytes(&[52u8; 32]), + ); + let (receipt_vk, supervisor_vk) = host.verifying_keys(); + // The image identity *is* the weights: two runs over different weights are + // different units, and the content id says so. + let image_id = ContentId::of_bytes( + format!("sandbox-local-inference:{weights_hash}").as_bytes(), + ); + let grants_id = grants_content_id(&grants); + let unit_id = unit_content_id(&image_id, &grants_id); + let sealed = host.seal_run( + unit_id, + image_id.clone(), + &grants, + Tier::S0, + run, + 1, + [0; 8], + at_unix_us, + at_unix_us, + )?; + sealed + .receipt + .verify_sealed(&sealed.receipt_envelope, &receipt_vk, Some("steward.proof")) + .map_err(|e| ProofError::Crypto(format!("receipt: {e}")))?; + sealed + .attestation + .verify(&sealed.attestation_sig, &supervisor_vk) + .map_err(|e| ProofError::Crypto(format!("attestation: {e}")))?; + + eprintln!( + " local: {} ({:.2} GiB, b3:{}…) — {} over {:.1}s, avg {:.1} W [{}]", + weights_path + .file_name() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_default(), + weights_bytes as f64 / (1u64 << 30) as f64, + &weights_hash[..12], + if is_hw { "measured" } else { "ESTIMATED" }, + measurement.wall.as_secs_f64(), + measurement.avg_watts(), + sources, + ); + + let hardware: Vec = sealed.receipt.devices.iter().map(|d| d.class).collect(); + let attestation_id = sealed + .attestation + .content_id() + .map_err(|e| ProofError::Crypto(format!("attestation id: {e}")))?; + let cell = ProofCell { + profile: PROFILE.to_string(), + substrate: Substrate::BareMetal, + os: std::env::consts::OS.to_string(), + arch: std::env::consts::ARCH.to_string(), + cloud: cloud.to_string(), + hardware, + tier: Tier::S0, + conformance, + attestation_id, + receipt_id: sealed.receipt.content_id(), + total_estimated_uj: sealed.receipt.total.estimated_uj, + available_substrates: available_substrates(), + host_id: "sandbox-proof".to_string(), + at_unix_us, + }; + sign(cell, proof_key) +} + #[cfg(test)] mod tests { use super::*; @@ -1094,4 +1333,76 @@ mod tests { 12 * J_IN_UJ + 5 * J_OUT_UJ ); } + + // ── local (air-gapped) inference proofs ────────────────────────── + + fn local_inputs(engine: &str, weights: &str) -> LocalInferenceInputs { + LocalInferenceInputs { + engine: engine.to_string(), + args: vec![], + weights: weights.to_string(), + } + } + + #[test] + fn local_proof_refuses_absent_weights() { + // An air-gap proof whose weights are not actually on this disk is + // exactly the claim that must not be sealable. + let sk = SigningKey::from_bytes(&[9u8; 32]); + let r = prove_local( + &local_inputs("/bin/true", "/nonexistent/model.gguf"), + "local", + 0, + &sk, + ); + match r { + Err(ProofError::Conformance(m)) => assert!(m.contains("not present locally"), "{m}"), + other => panic!("expected a conformance refusal, got {other:?}"), + } + } + + #[test] + fn local_proof_refuses_a_failed_engine() { + // Sealing a proof for a run that exited non-zero would attest to work + // that did not happen. + let dir = std::env::temp_dir().join("sandbox-proof-local-test"); + std::fs::create_dir_all(&dir).unwrap(); + let w = dir.join("weights.bin"); + std::fs::write(&w, b"not a real model, but really on disk").unwrap(); + let sk = SigningKey::from_bytes(&[9u8; 32]); + let r = prove_local( + &local_inputs("/bin/false", w.to_str().unwrap()), + "local", + 0, + &sk, + ); + assert!( + matches!(r, Err(ProofError::Run(_))), + "expected a run refusal, got {r:?}" + ); + } + + #[test] + fn local_proof_binds_the_exact_weight_bytes() { + // Two different models must not produce the same unit identity. + let dir = std::env::temp_dir().join("sandbox-proof-local-hash"); + std::fs::create_dir_all(&dir).unwrap(); + let a = dir.join("a.bin"); + let b = dir.join("b.bin"); + std::fs::write(&a, b"weights-a").unwrap(); + std::fs::write(&b, b"weights-b").unwrap(); + let ha = hash_file(&a).unwrap(); + let hb = hash_file(&b).unwrap(); + assert_ne!(ha, hb); + assert_eq!(ha, hash_file(&a).unwrap(), "hash must be stable"); + assert_eq!(ha.len(), 64, "blake3 hex"); + } + + #[test] + fn local_proof_grant_is_zero_egress() { + // The posture the proof asserts before spawning anything. + let g = sandbox_capability::Grants::default(); + assert!(g.network.outbound.is_empty()); + assert!(!g.network.inbound); + } } diff --git a/sandbox/sandbox-rs/crates/sandbox-proof/src/main.rs b/sandbox/sandbox-rs/crates/sandbox-proof/src/main.rs index 2ae11a4dc..f9e7ef2fe 100644 --- a/sandbox/sandbox-rs/crates/sandbox-proof/src/main.rs +++ b/sandbox/sandbox-rs/crates/sandbox-proof/src/main.rs @@ -21,6 +21,7 @@ fn main() { match cmd { "prove" => prove(&path), "prove-s5" => prove_s5(&path), + "prove-local" => prove_local(&path), "prove-gpu" => prove_gpu(&path), "prove-lpu" => prove_lpu(&path), "prove-s6" => prove_s6(&path), @@ -131,6 +132,48 @@ fn prove_s5(path: &str) { println!("\n appended to {path}"); } +/// Prove local, offline inference: run a local engine against local weights +/// with no network grant, metered on this host's own energy counters. +fn prove_local(path: &str) { + let env = |k: &str, d: &str| std::env::var(k).unwrap_or_else(|_| d.to_string()); + let weights = env("SANDBOX_LOCAL_WEIGHTS", ""); + if weights.is_empty() { + eprintln!("set SANDBOX_LOCAL_WEIGHTS=/path/to/model.gguf"); + std::process::exit(2); + } + let engine = env("SANDBOX_LOCAL_ENGINE", ""); + if engine.is_empty() { + eprintln!("set SANDBOX_LOCAL_ENGINE=/path/to/inference-binary"); + std::process::exit(2); + } + let args: Vec = std::env::var("SANDBOX_LOCAL_ARGS") + .ok() + .map(|s| s.split_whitespace().map(String::from).collect()) + .unwrap_or_else(|| vec![weights.clone()]); + let inputs = sandbox_proof::LocalInferenceInputs { engine, args, weights }; + let cloud = env("SANDBOX_CLOUD", "local"); + let cell = match sandbox_proof::prove_local(&inputs, &cloud, now_us(), &proof_key()) { + Ok(c) => c, + Err(e) => { + eprintln!("local proof failed: {e}"); + std::process::exit(1); + } + }; + if let Err(e) = Matrix::append(path, &cell) { + eprintln!("could not append to {path}: {e}"); + std::process::exit(1); + } + let c = &cell.cell; + println!("Sandbox proof — local inference (offline, zero egress)\n"); + println!(" substrate: {}", c.substrate.label()); + println!(" os / arch: {} / {}", c.os, c.arch); + println!(" tier: {}", c.tier.wire_tag()); + println!(" attestation: {}", c.attestation_id); + println!(" receipt: {} ({} uj)", c.receipt_id, c.total_estimated_uj); + println!(" signature: {} (verified \u{2713})", &cell.signature_hex[..16]); + println!("\n appended to {path}"); +} + /// Prove the accelerator (GPU) fabric: run a GPU workload, meter it via /// nvidia-smi, seal a cell with a separate Gpu energy record + GPU identity. fn prove_gpu(path: &str) { -- 2.54.0 (Apple Git-157)