← Commit history
Cargo.lockadded+1907
@@ -0,0 +1,1907 @@+# This file is automatically @generated by Cargo.+# It is not intended for manual editing.+version = 4++[[package]]+name = "anstream"+version = "1.0.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"+dependencies = [+ "anstyle",+ "anstyle-parse",+ "anstyle-query",+ "anstyle-wincon",+ "colorchoice",+ "is_terminal_polyfill",+ "utf8parse",+]++[[package]]+name = "anstyle"+version = "1.0.14"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"++[[package]]+name = "anstyle-parse"+version = "1.0.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"+dependencies = [+ "utf8parse",+]++[[package]]+name = "anstyle-query"+version = "1.1.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"+dependencies = [+ "windows-sys 0.61.2",+]++[[package]]+name = "anstyle-wincon"+version = "3.0.11"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"+dependencies = [+ "anstyle",+ "once_cell_polyfill",+ "windows-sys 0.61.2",+]++[[package]]+name = "anyhow"+version = "1.0.104"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"++[[package]]+name = "async-trait"+version = "0.1.92"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 3.0.5",+]++[[package]]+name = "atomic-waker"+version = "1.1.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"++[[package]]+name = "axum"+version = "0.7.9"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"+dependencies = [+ "async-trait",+ "axum-core",+ "bytes",+ "futures-util",+ "http",+ "http-body",+ "http-body-util",+ "hyper",+ "hyper-util",+ "itoa",+ "matchit",+ "memchr",+ "mime",+ "percent-encoding",+ "pin-project-lite",+ "rustversion",+ "serde",+ "serde_json",+ "serde_path_to_error",+ "serde_urlencoded",+ "sync_wrapper",+ "tokio",+ "tower",+ "tower-layer",+ "tower-service",+ "tracing",+]++[[package]]+name = "axum-core"+version = "0.4.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"+dependencies = [+ "async-trait",+ "bytes",+ "futures-util",+ "http",+ "http-body",+ "http-body-util",+ "mime",+ "pin-project-lite",+ "rustversion",+ "sync_wrapper",+ "tower-layer",+ "tower-service",+ "tracing",+]++[[package]]+name = "base64"+version = "0.22.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"++[[package]]+name = "bitflags"+version = "2.13.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"++[[package]]+name = "block-buffer"+version = "0.10.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"+dependencies = [+ "generic-array",+]++[[package]]+name = "bumpalo"+version = "3.20.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"++[[package]]+name = "bytes"+version = "1.12.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"++[[package]]+name = "cc"+version = "1.4.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80"+dependencies = [+ "find-msvc-tools",+ "shlex",+]++[[package]]+name = "cfg-if"+version = "1.0.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"++[[package]]+name = "cfg_aliases"+version = "0.2.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"++[[package]]+name = "chacha20"+version = "0.10.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"+dependencies = [+ "cfg-if",+ "cpufeatures 0.3.1",+ "rand_core",+]++[[package]]+name = "clap"+version = "4.6.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"+dependencies = [+ "clap_builder",+ "clap_derive",+]++[[package]]+name = "clap_builder"+version = "4.6.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"+dependencies = [+ "anstream",+ "anstyle",+ "clap_lex",+ "strsim",+]++[[package]]+name = "clap_derive"+version = "4.6.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"+dependencies = [+ "heck",+ "proc-macro2",+ "quote",+ "syn 3.0.5",+]++[[package]]+name = "clap_lex"+version = "1.1.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"++[[package]]+name = "colorchoice"+version = "1.0.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"++[[package]]+name = "cpufeatures"+version = "0.2.17"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"+dependencies = [+ "libc",+]++[[package]]+name = "cpufeatures"+version = "0.3.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"+dependencies = [+ "libc",+]++[[package]]+name = "crypto-common"+version = "0.1.7"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"+dependencies = [+ "generic-array",+ "typenum",+]++[[package]]+name = "digest"+version = "0.10.7"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"+dependencies = [+ "block-buffer",+ "crypto-common",+]++[[package]]+name = "displaydoc"+version = "0.2.7"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 3.0.5",+]++[[package]]+name = "errno"+version = "0.3.14"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"+dependencies = [+ "libc",+ "windows-sys 0.61.2",+]++[[package]]+name = "fastrand"+version = "2.5.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"++[[package]]+name = "filetime"+version = "0.2.29"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"+dependencies = [+ "cfg-if",+ "libc",+]++[[package]]+name = "find-msvc-tools"+version = "0.1.12"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"++[[package]]+name = "form_urlencoded"+version = "1.2.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"+dependencies = [+ "percent-encoding",+]++[[package]]+name = "futures-channel"+version = "0.3.34"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"+dependencies = [+ "futures-core",+ "futures-sink",+]++[[package]]+name = "futures-core"+version = "0.3.34"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"++[[package]]+name = "futures-io"+version = "0.3.34"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"++[[package]]+name = "futures-sink"+version = "0.3.34"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"++[[package]]+name = "futures-task"+version = "0.3.34"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"++[[package]]+name = "futures-util"+version = "0.3.34"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"+dependencies = [+ "futures-core",+ "futures-io",+ "futures-sink",+ "futures-task",+ "memchr",+ "pin-project-lite",+ "slab",+]++[[package]]+name = "generic-array"+version = "0.14.7"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"+dependencies = [+ "typenum",+ "version_check",+]++[[package]]+name = "getrandom"+version = "0.2.17"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"+dependencies = [+ "cfg-if",+ "js-sys",+ "libc",+ "wasi",+ "wasm-bindgen",+]++[[package]]+name = "getrandom"+version = "0.4.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"+dependencies = [+ "cfg-if",+ "js-sys",+ "libc",+ "r-efi",+ "rand_core",+ "wasm-bindgen",+]++[[package]]+name = "heck"+version = "0.5.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"++[[package]]+name = "http"+version = "1.5.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"+dependencies = [+ "bytes",+ "itoa",+]++[[package]]+name = "http-body"+version = "1.1.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"+dependencies = [+ "bytes",+ "http",+]++[[package]]+name = "http-body-util"+version = "0.1.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"+dependencies = [+ "bytes",+ "futures-core",+ "http",+ "http-body",+ "pin-project-lite",+]++[[package]]+name = "http-range-header"+version = "0.4.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"++[[package]]+name = "httparse"+version = "1.10.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"++[[package]]+name = "httpdate"+version = "1.0.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"++[[package]]+name = "hyper"+version = "1.11.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"+dependencies = [+ "atomic-waker",+ "bytes",+ "futures-channel",+ "futures-core",+ "http",+ "http-body",+ "httparse",+ "httpdate",+ "itoa",+ "pin-project-lite",+ "smallvec",+ "tokio",+ "want",+]++[[package]]+name = "hyper-rustls"+version = "0.27.9"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"+dependencies = [+ "http",+ "hyper",+ "hyper-util",+ "rustls",+ "tokio",+ "tokio-rustls",+ "tower-service",+ "webpki-roots",+]++[[package]]+name = "hyper-util"+version = "0.1.20"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"+dependencies = [+ "base64",+ "bytes",+ "futures-channel",+ "futures-util",+ "http",+ "http-body",+ "hyper",+ "ipnet",+ "libc",+ "percent-encoding",+ "pin-project-lite",+ "socket2",+ "tokio",+ "tower-service",+ "tracing",+]++[[package]]+name = "icu_collections"+version = "2.3.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"+dependencies = [+ "displaydoc",+ "potential_utf",+ "utf8_iter",+ "yoke",+ "zerofrom",+ "zerovec",+]++[[package]]+name = "icu_locale_core"+version = "2.3.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"+dependencies = [+ "displaydoc",+ "litemap",+ "tinystr",+ "writeable",+ "zerovec",+]++[[package]]+name = "icu_normalizer"+version = "2.3.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"+dependencies = [+ "icu_collections",+ "icu_normalizer_data",+ "icu_properties",+ "icu_provider",+ "smallvec",+ "zerovec",+]++[[package]]+name = "icu_normalizer_data"+version = "2.3.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"++[[package]]+name = "icu_properties"+version = "2.3.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"+dependencies = [+ "displaydoc",+ "icu_collections",+ "icu_locale_core",+ "icu_properties_data",+ "icu_provider",+ "zerotrie",+ "zerovec",+]++[[package]]+name = "icu_properties_data"+version = "2.3.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"++[[package]]+name = "icu_provider"+version = "2.3.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"+dependencies = [+ "displaydoc",+ "icu_locale_core",+ "writeable",+ "yoke",+ "zerofrom",+ "zerotrie",+ "zerovec",+]++[[package]]+name = "idna"+version = "1.1.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"+dependencies = [+ "idna_adapter",+ "smallvec",+ "utf8_iter",+]++[[package]]+name = "idna_adapter"+version = "1.2.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"+dependencies = [+ "icu_normalizer",+ "icu_properties",+]++[[package]]+name = "ipnet"+version = "2.12.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0"++[[package]]+name = "is_terminal_polyfill"+version = "1.70.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"++[[package]]+name = "itoa"+version = "1.0.18"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"++[[package]]+name = "js-sys"+version = "0.3.105"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"+dependencies = [+ "cfg-if",+ "futures-util",+ "wasm-bindgen",+]++[[package]]+name = "lazy_static"+version = "1.5.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"++[[package]]+name = "libc"+version = "0.2.189"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"++[[package]]+name = "linux-raw-sys"+version = "0.12.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"++[[package]]+name = "litemap"+version = "0.8.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"++[[package]]+name = "lock_api"+version = "0.4.14"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"+dependencies = [+ "scopeguard",+]++[[package]]+name = "log"+version = "0.4.34"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"++[[package]]+name = "lru-slab"+version = "0.1.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"++[[package]]+name = "matchit"+version = "0.7.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"++[[package]]+name = "memchr"+version = "2.8.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"++[[package]]+name = "mime"+version = "0.3.17"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"++[[package]]+name = "mime_guess"+version = "2.0.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"+dependencies = [+ "mime",+ "unicase",+]++[[package]]+name = "mio"+version = "1.2.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"+dependencies = [+ "libc",+ "wasi",+ "windows-sys 0.61.2",+]++[[package]]+name = "nu-ansi-term"+version = "0.50.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"+dependencies = [+ "windows-sys 0.61.2",+]++[[package]]+name = "once_cell"+version = "1.21.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"++[[package]]+name = "once_cell_polyfill"+version = "1.70.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"++[[package]]+name = "parking_lot"+version = "0.12.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"+dependencies = [+ "lock_api",+ "parking_lot_core",+]++[[package]]+name = "parking_lot_core"+version = "0.9.12"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"+dependencies = [+ "cfg-if",+ "libc",+ "redox_syscall",+ "smallvec",+ "windows-link",+]++[[package]]+name = "percent-encoding"+version = "2.3.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"++[[package]]+name = "pin-project-lite"+version = "0.2.17"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"++[[package]]+name = "potential_utf"+version = "0.1.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"+dependencies = [+ "zerovec",+]++[[package]]+name = "proc-macro2"+version = "1.0.107"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"+dependencies = [+ "unicode-ident",+]++[[package]]+name = "quinn"+version = "0.11.11"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"+dependencies = [+ "bytes",+ "cfg_aliases",+ "pin-project-lite",+ "quinn-proto",+ "quinn-udp",+ "rustc-hash",+ "rustls",+ "socket2",+ "thiserror",+ "tokio",+ "tracing",+ "web-time",+]++[[package]]+name = "quinn-proto"+version = "0.11.17"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83"+dependencies = [+ "bytes",+ "getrandom 0.4.3",+ "lru-slab",+ "rand",+ "rand_pcg",+ "ring",+ "rustc-hash",+ "rustls",+ "rustls-pki-types",+ "slab",+ "thiserror",+ "tinyvec",+ "tracing",+ "web-time",+]++[[package]]+name = "quinn-udp"+version = "0.5.15"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"+dependencies = [+ "cfg_aliases",+ "libc",+ "once_cell",+ "socket2",+ "tracing",+ "windows-sys 0.61.2",+]++[[package]]+name = "quote"+version = "1.0.47"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"+dependencies = [+ "proc-macro2",+]++[[package]]+name = "r-efi"+version = "6.0.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"++[[package]]+name = "rand"+version = "0.10.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"+dependencies = [+ "chacha20",+ "getrandom 0.4.3",+ "rand_core",+]++[[package]]+name = "rand_core"+version = "0.10.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"++[[package]]+name = "rand_pcg"+version = "0.10.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"+dependencies = [+ "rand_core",+]++[[package]]+name = "redox_syscall"+version = "0.5.18"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"+dependencies = [+ "bitflags",+]++[[package]]+name = "reqwest"+version = "0.12.28"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"+dependencies = [+ "base64",+ "bytes",+ "futures-channel",+ "futures-core",+ "futures-util",+ "http",+ "http-body",+ "http-body-util",+ "hyper",+ "hyper-rustls",+ "hyper-util",+ "js-sys",+ "log",+ "percent-encoding",+ "pin-project-lite",+ "quinn",+ "rustls",+ "rustls-pki-types",+ "serde",+ "serde_json",+ "serde_urlencoded",+ "sync_wrapper",+ "tokio",+ "tokio-rustls",+ "tower",+ "tower-http 0.6.11",+ "tower-service",+ "url",+ "wasm-bindgen",+ "wasm-bindgen-futures",+ "web-sys",+ "webpki-roots",+]++[[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.17",+ "libc",+ "untrusted",+ "windows-sys 0.52.0",+]++[[package]]+name = "rustc-hash"+version = "2.1.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"++[[package]]+name = "rustix"+version = "1.1.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"+dependencies = [+ "bitflags",+ "errno",+ "libc",+ "linux-raw-sys",+ "windows-sys 0.61.2",+]++[[package]]+name = "rustls"+version = "0.23.44"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba"+dependencies = [+ "once_cell",+ "ring",+ "rustls-pki-types",+ "rustls-webpki",+ "subtle",+ "zeroize",+]++[[package]]+name = "rustls-pki-types"+version = "1.15.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"+dependencies = [+ "web-time",+ "zeroize",+]++[[package]]+name = "rustls-webpki"+version = "0.103.15"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"+dependencies = [+ "ring",+ "rustls-pki-types",+ "untrusted",+]++[[package]]+name = "rustversion"+version = "1.0.23"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"++[[package]]+name = "ryu"+version = "1.0.23"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"++[[package]]+name = "scopeguard"+version = "1.2.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"++[[package]]+name = "serde"+version = "1.0.229"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"+dependencies = [+ "serde_core",+ "serde_derive",+]++[[package]]+name = "serde_core"+version = "1.0.229"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"+dependencies = [+ "serde_derive",+]++[[package]]+name = "serde_derive"+version = "1.0.229"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 3.0.5",+]++[[package]]+name = "serde_json"+version = "1.0.151"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"+dependencies = [+ "itoa",+ "memchr",+ "serde",+ "serde_core",+ "zmij",+]++[[package]]+name = "serde_path_to_error"+version = "0.1.20"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"+dependencies = [+ "itoa",+ "serde",+ "serde_core",+]++[[package]]+name = "serde_urlencoded"+version = "0.7.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"+dependencies = [+ "form_urlencoded",+ "itoa",+ "ryu",+ "serde",+]++[[package]]+name = "service-kicad"+version = "1.1.1-dev.0"+dependencies = [+ "anyhow",+ "clap",+ "reqwest",+ "serde",+ "serde_json",+ "service-kicad-project",+ "tempfile",+]++[[package]]+name = "service-kicad-api"+version = "1.1.1-dev.0"+dependencies = [+ "anyhow",+ "axum",+ "serde",+ "serde_json",+ "service-kicad-project",+ "tempfile",+ "tokio",+ "tower-http 0.5.2",+ "tracing",+ "tracing-subscriber",+ "urlencoding",+]++[[package]]+name = "service-kicad-project"+version = "1.1.1-dev.0"+dependencies = [+ "anyhow",+ "serde",+ "serde_json",+ "sha2",+ "tar",+ "tempfile",+]++[[package]]+name = "sha2"+version = "0.10.9"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"+dependencies = [+ "cfg-if",+ "cpufeatures 0.2.17",+ "digest",+]++[[package]]+name = "sharded-slab"+version = "0.1.7"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"+dependencies = [+ "lazy_static",+]++[[package]]+name = "shlex"+version = "2.0.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"++[[package]]+name = "signal-hook-registry"+version = "1.4.8"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"+dependencies = [+ "errno",+ "libc",+]++[[package]]+name = "slab"+version = "0.4.12"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"++[[package]]+name = "smallvec"+version = "1.16.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f"++[[package]]+name = "socket2"+version = "0.6.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"+dependencies = [+ "libc",+ "windows-sys 0.61.2",+]++[[package]]+name = "stable_deref_trait"+version = "1.2.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"++[[package]]+name = "strsim"+version = "0.11.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"++[[package]]+name = "subtle"+version = "2.6.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"++[[package]]+name = "syn"+version = "2.0.119"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"+dependencies = [+ "proc-macro2",+ "quote",+ "unicode-ident",+]++[[package]]+name = "syn"+version = "3.0.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"+dependencies = [+ "proc-macro2",+ "quote",+ "unicode-ident",+]++[[package]]+name = "sync_wrapper"+version = "1.0.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"+dependencies = [+ "futures-core",+]++[[package]]+name = "synstructure"+version = "0.13.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 2.0.119",+]++[[package]]+name = "tar"+version = "0.4.46"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"+dependencies = [+ "filetime",+ "libc",+ "xattr",+]++[[package]]+name = "tempfile"+version = "3.27.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"+dependencies = [+ "fastrand",+ "getrandom 0.4.3",+ "once_cell",+ "rustix",+ "windows-sys 0.61.2",+]++[[package]]+name = "thiserror"+version = "2.0.20"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"+dependencies = [+ "thiserror-impl",+]++[[package]]+name = "thiserror-impl"+version = "2.0.20"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 3.0.5",+]++[[package]]+name = "thread_local"+version = "1.1.10"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"+dependencies = [+ "cfg-if",+]++[[package]]+name = "tinystr"+version = "0.8.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"+dependencies = [+ "displaydoc",+ "zerovec",+]++[[package]]+name = "tinyvec"+version = "1.13.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "4cf0ded5c4e56918d8f8a339e1bb67d038d3bc6d144ac407904015ba2e4cde9b"+dependencies = [+ "tinyvec_macros",+]++[[package]]+name = "tinyvec_macros"+version = "0.1.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"++[[package]]+name = "tokio"+version = "1.53.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"+dependencies = [+ "bytes",+ "libc",+ "mio",+ "parking_lot",+ "pin-project-lite",+ "signal-hook-registry",+ "socket2",+ "tokio-macros",+ "windows-sys 0.61.2",+]++[[package]]+name = "tokio-macros"+version = "2.7.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 3.0.5",+]++[[package]]+name = "tokio-rustls"+version = "0.26.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67"+dependencies = [+ "rustls",+ "tokio",+]++[[package]]+name = "tokio-util"+version = "0.7.19"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"+dependencies = [+ "bytes",+ "futures-core",+ "futures-sink",+ "pin-project-lite",+ "tokio",+]++[[package]]+name = "tower"+version = "0.5.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"+dependencies = [+ "futures-core",+ "futures-util",+ "pin-project-lite",+ "sync_wrapper",+ "tokio",+ "tower-layer",+ "tower-service",+ "tracing",+]++[[package]]+name = "tower-http"+version = "0.5.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"+dependencies = [+ "bitflags",+ "bytes",+ "futures-util",+ "http",+ "http-body",+ "http-body-util",+ "http-range-header",+ "httpdate",+ "mime",+ "mime_guess",+ "percent-encoding",+ "pin-project-lite",+ "tokio",+ "tokio-util",+ "tower-layer",+ "tower-service",+ "tracing",+]++[[package]]+name = "tower-http"+version = "0.6.11"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"+dependencies = [+ "bitflags",+ "bytes",+ "futures-util",+ "http",+ "http-body",+ "pin-project-lite",+ "tower",+ "tower-layer",+ "tower-service",+ "url",+]++[[package]]+name = "tower-layer"+version = "0.3.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"++[[package]]+name = "tower-service"+version = "0.3.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"++[[package]]+name = "tracing"+version = "0.1.44"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"+dependencies = [+ "log",+ "pin-project-lite",+ "tracing-attributes",+ "tracing-core",+]++[[package]]+name = "tracing-attributes"+version = "0.1.31"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 2.0.119",+]++[[package]]+name = "tracing-core"+version = "0.1.36"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"+dependencies = [+ "once_cell",+ "valuable",+]++[[package]]+name = "tracing-log"+version = "0.2.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"+dependencies = [+ "log",+ "once_cell",+ "tracing-core",+]++[[package]]+name = "tracing-subscriber"+version = "0.3.23"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"+dependencies = [+ "nu-ansi-term",+ "sharded-slab",+ "smallvec",+ "thread_local",+ "tracing-core",+ "tracing-log",+]++[[package]]+name = "try-lock"+version = "0.2.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"++[[package]]+name = "typenum"+version = "1.20.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"++[[package]]+name = "unicase"+version = "2.9.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"++[[package]]+name = "unicode-ident"+version = "1.0.24"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"++[[package]]+name = "untrusted"+version = "0.9.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"++[[package]]+name = "url"+version = "2.5.8"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"+dependencies = [+ "form_urlencoded",+ "idna",+ "percent-encoding",+ "serde",+]++[[package]]+name = "urlencoding"+version = "2.1.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"++[[package]]+name = "utf8_iter"+version = "1.0.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"++[[package]]+name = "utf8parse"+version = "0.2.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"++[[package]]+name = "valuable"+version = "0.1.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"++[[package]]+name = "version_check"+version = "0.9.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"++[[package]]+name = "want"+version = "0.3.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"+dependencies = [+ "try-lock",+]++[[package]]+name = "wasi"+version = "0.11.1+wasi-snapshot-preview1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"++[[package]]+name = "wasm-bindgen"+version = "0.2.128"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"+dependencies = [+ "cfg-if",+ "once_cell",+ "rustversion",+ "wasm-bindgen-macro",+ "wasm-bindgen-shared",+]++[[package]]+name = "wasm-bindgen-futures"+version = "0.4.78"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928"+dependencies = [+ "js-sys",+ "wasm-bindgen",+]++[[package]]+name = "wasm-bindgen-macro"+version = "0.2.128"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"+dependencies = [+ "quote",+ "wasm-bindgen-macro-support",+]++[[package]]+name = "wasm-bindgen-macro-support"+version = "0.2.128"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"+dependencies = [+ "bumpalo",+ "proc-macro2",+ "quote",+ "syn 3.0.5",+ "wasm-bindgen-shared",+]++[[package]]+name = "wasm-bindgen-shared"+version = "0.2.128"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"+dependencies = [+ "unicode-ident",+]++[[package]]+name = "web-sys"+version = "0.3.105"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8"+dependencies = [+ "js-sys",+ "wasm-bindgen",+]++[[package]]+name = "web-time"+version = "1.1.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"+dependencies = [+ "js-sys",+ "wasm-bindgen",+]++[[package]]+name = "webpki-roots"+version = "1.0.9"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"+dependencies = [+ "rustls-pki-types",+]++[[package]]+name = "windows-link"+version = "0.2.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"++[[package]]+name = "windows-sys"+version = "0.52.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"+dependencies = [+ "windows-targets",+]++[[package]]+name = "windows-sys"+version = "0.61.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"+dependencies = [+ "windows-link",+]++[[package]]+name = "windows-targets"+version = "0.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"+dependencies = [+ "windows_aarch64_gnullvm",+ "windows_aarch64_msvc",+ "windows_i686_gnu",+ "windows_i686_gnullvm",+ "windows_i686_msvc",+ "windows_x86_64_gnu",+ "windows_x86_64_gnullvm",+ "windows_x86_64_msvc",+]++[[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_msvc"+version = "0.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"++[[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_gnullvm"+version = "0.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"++[[package]]+name = "windows_i686_msvc"+version = "0.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"++[[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_gnullvm"+version = "0.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"++[[package]]+name = "windows_x86_64_msvc"+version = "0.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"++[[package]]+name = "writeable"+version = "0.6.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"++[[package]]+name = "xattr"+version = "1.6.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"+dependencies = [+ "libc",+ "rustix",+]++[[package]]+name = "yoke"+version = "0.8.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"+dependencies = [+ "stable_deref_trait",+ "yoke-derive",+ "zerofrom",+]++[[package]]+name = "yoke-derive"+version = "0.8.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 2.0.119",+ "synstructure",+]++[[package]]+name = "zerofrom"+version = "0.1.8"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"+dependencies = [+ "zerofrom-derive",+]++[[package]]+name = "zerofrom-derive"+version = "0.1.7"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 2.0.119",+ "synstructure",+]++[[package]]+name = "zeroize"+version = "1.9.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"++[[package]]+name = "zerotrie"+version = "0.2.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"+dependencies = [+ "displaydoc",+ "yoke",+ "zerofrom",+]++[[package]]+name = "zerovec"+version = "0.11.8"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"+dependencies = [+ "yoke",+ "zerofrom",+ "zerovec-derive",+]++[[package]]+name = "zerovec-derive"+version = "0.11.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 3.0.5",+]++[[package]]+name = "zmij"+version = "1.0.23"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
Cargo.toml+2−2
@@ -1,9 +1,9 @@ [workspace] resolver = "2"-members = ["api", "cli"]+members = ["api", "cli", "project"]  [workspace.package]-version = "1.1.0"+version = "1.1.1-dev.0" edition = "2021" license = "AGPL-3.0-or-later" repository = "https://github.com/adom-inc/service-kicad"
README.md+46
@@ -223,3 +223,49 @@ cycle the container pulls, rebuilds, and relaunches — `/health`'s `api_version` in `service.json` + `docs/service-kicad-api.md`. Open PRs against the wiki page (`adom-wiki pr`) so review happens where users look; the GitHub repo (`adom-inc/service-kicad`) is the working origin.++## Candidate: project-aware PCB DRC++The `feature/project-aware-drc` candidate adds a complete-project path for fab-rule+qualification. This source has protocol/transport tests, but is not deployed on the+shared service and has not yet passed the native KiCad negative-control fixtures.+Check the installed CLI's help and service capabilities before using it.++```bash+service-kicad pcb drc-project ./project --board board.kicad_pcb \+  --include Parts.pretty --out project-drc.json+# Separate intentionally failing fixture: prove a named custom rule ran.+service-kicad pcb drc-project ./rule-fixture --board rule-probe.kicad_pcb \+  --expect-rule depth-lab-proof-custom-rules-loaded --out probe-drc.json+```++The board, matching `.kicad_pro` and `.kicad_dru` are required. Use+`--allow-no-custom-rules` only when intentionally relying on project settings+without custom rules. Adjacent library tables are included; include their local+libraries explicitly. Unrelated boards, credentials and arbitrary project-directory+files are not scanned/uploaded. The package supports regular KiCad text inputs;+links, duplicate/case-aliased paths, unsafe paths and special archive entries are+rejected. Select a board relative to the project root, including subdirectories.++The response contains the native report, service/native versions, supplied input+hashes, diagnostics and optional expected-rule observation. `ok` means the operation+and requested probe completed, not that the board passed DRC. A negative-control+probe normally produces a native violation. Removing that probe in a separate copy+is required before evaluating the real design. Input hashes do not establish rule+enforcement by themselves. HTTP 422 reports a requested rule absent from native+violation descriptions; the CLI saves this full response to `--out` before exiting+nonzero. Native failures never retry with default rules.++The legacy single-file `pcb drc` command now emits `PROJECT_RULES_OMITTED` on stderr+when matching project or rule files are present locally. It retains its existing+response format. The new operation uses a separate endpoint and does not alter+shared production settings or require desktop focus.++Validation: `cargo test --workspace`; tests using a subprocess fixture explicitly+report `test-double-not-native`. A real KiCad project run must still verify the+named probe, malformed-rule rejection and the intended fabrication constraints.++A self-contained local CLI/HTTP check is also available after `cargo build --workspace`:+`python3 verification/test_project_drc.py`. It starts only its own temporary API+process, uses a clearly labeled protocol test double, verifies five transport cases,+and stops its process. It does not install software or call the shared service.
SKILL.md+21
@@ -107,3 +107,24 @@ one-script deploy — see the README on the wiki page | **2** | service unreachable | `service-kicad health` to confirm; the container may be restarting | | **3** | auth failed (401/403) | `adom-cli auth refresh` | | **4** | service-side error | CLI prints stderr; usually input-specific |++## Project rule qualification (candidate source; discover before use)++Before calling DRC to qualify a fabrication profile, inspect the installed+`service-kicad pcb --help`. The project-aware command is introduced by the+1.1.1-dev.0 candidate and is not yet a demonstrated shared-service capability.+When available, use `pcb drc-project <directory> --board <relative.kicad_pcb>` with+matching .kicad_pro/.kicad_dru and explicit `--include` local library inputs.+Missing project/rule files must fail; intentionally absent custom rules require+`--allow-no-custom-rules`. Do not infer that a raw-board check uploaded sibling files.+The candidate single-file command warns with `PROJECT_RULES_OMITTED` when it sees+those siblings.++Use a separate negative-control project and `--expect-rule <name>` to check that a+known failing rule appears in native violation descriptions. Retain the complete+response with `--out`, including failed probe/native errors. Inspect detailed+violations, file hashes and native/service versions. `ok` and matching input hashes+do not mean DRC passed or all constraints were enforced. Remove the probe only in a+new validation copy, and run the actual profile checks there. If the service lacks+project transport, report that gap rather than replacing it with default-rule evidence.+Never bypass a rejected desktop transfer to manufacture a native-validation claim.
api/Cargo.toml+2
@@ -22,3 +22,5 @@ tracing.workspace = true tracing-subscriber.workspace = true tempfile.workspace = true urlencoding = "2.1"++service-kicad-project = { path = "../project" }
api/src/main.rs+3
@@ -15,6 +15,8 @@ //! //! Contract: `docs/service-kicad-api.md`. +mod project_drc;+ use axum::{     body::Bytes,     extract::{DefaultBodyLimit, Path as AxumPath, Query, RawQuery},@@ -44,6 +46,7 @@ async fn main() -> anyhow::Result<()> {         .route("/version", get(version))         // kicad-cli pcb operations         .route("/kicad/pcb/drc",             post(pcb_drc))+        .route("/kicad/pcb/drc-project",     post(project_drc::run))         .route("/kicad/pcb/export/svg",      post(pcb_export_svg))         .route("/kicad/pcb/export/gerbers",  post(pcb_export_gerbers))         .route("/kicad/pcb/export/step",     post(pcb_export_step))
api/src/project_drc.rsadded+245
@@ -0,0 +1,245 @@+//! Project-aware DRC; kept separate from the legacy single-board endpoint.+use axum::{+    body::Bytes,+    extract::Query,+    http::StatusCode,+    response::{IntoResponse, Response},+    Json,+};+use serde::Deserialize;+use serde_json::{json, Value};+use std::{path::Path, sync::OnceLock, time::Duration};+use tokio::{process::Command, sync::Semaphore};++#[derive(Deserialize)]+pub struct Options {+    pub board: String,+    #[serde(default = "require_rules_default")]+    pub require_rules: bool,+    pub expected_rule: Option<String>,+}+fn require_rules_default() -> bool {+    true+}+fn failure(status: StatusCode, code: &str, message: impl ToString) -> Response {+    (status, Json(json!({"ok":false,"errorCode":code,"error":message.to_string(),"hints":["No fallback to single-file/default-rule DRC occurred. Inspect the selected project and custom rules."]}))).into_response()+}+pub async fn run(Query(options): Query<Options>, body: Bytes) -> Response {+    static JOBS: OnceLock<Semaphore> = OnceLock::new();+    let Ok(_permit) = JOBS.get_or_init(|| Semaphore::new(2)).try_acquire() else {+        return failure(+            StatusCode::TOO_MANY_REQUESTS,+            "project_drc_busy",+            "Two project jobs are already running; retry this request later.",+        );+    };+    run_with_executable(options, body, &super::kicad_cli()).await+}+async fn run_with_executable(options: Options, body: Bytes, executable: &str) -> Response {+    let project = match service_kicad_project::unpack(&body, &options.board, options.require_rules)+    {+        Ok(p) => p,+        Err(e) => return failure(StatusCode::BAD_REQUEST, "invalid_project", e),+    };+    let output_file = match tempfile::NamedTempFile::with_suffix(".json") {+        Ok(f) => f,+        Err(e) => return failure(StatusCode::INTERNAL_SERVER_ERROR, "output_create_failed", e),+    };+    let native = match native_command(+        executable,+        project.directory.path(),+        &project.board,+        output_file.path(),+    )+    .await+    {+        Ok(v) => v,+        Err(e) => return failure(StatusCode::BAD_REQUEST, "native_drc_failed", e),+    };+    let report: Value = match std::fs::read(output_file.path())+        .ok()+        .and_then(|b| serde_json::from_slice(&b).ok())+    {+        Some(v) => v,+        None => {+            return failure(+                StatusCode::BAD_GATEWAY,+                "invalid_native_report",+                "KiCad did not produce valid JSON",+            )+        }+    };+    let Some(violations) = report.get("violations").and_then(Value::as_array) else {+        return failure(+            StatusCode::BAD_GATEWAY,+            "invalid_native_report",+            "Native JSON lacks the violations array",+        );+    };+    let expected_observed = options.expected_rule.as_ref().map(|name| {+        !name.trim().is_empty()+            && violations.iter().any(|v| {+                v.get("description")+                    .and_then(Value::as_str)+                    .is_some_and(|d| d.contains(name))+            })+    });+    let hints = if project.rules.is_some() {+        vec!["Project and custom-rule files were supplied at the matching board stem. Input hashes alone do not prove each constraint was enforced; use named negative-control cases."]+    } else {+        vec!["Explicit no-custom-rules mode: project settings were supplied; custom-rule enforcement is not claimed."]+    };+    let result = json!({+        "ok":expected_observed!=Some(false),+        "operation":"pcb.drc-project",+        "serviceVersion":env!("CARGO_PKG_VERSION"),"serviceGitSha":super::GIT_SHA,+        "nativeKicadVersion":report.get("kicad_version"),+        "board":options.board,"project":project.project.to_string_lossy(),+        "customRules":project.rules.as_ref().map(|p|p.to_string_lossy()),+        "inputManifest":project.files,"nativeStdout":native.0,"nativeStderr":native.1,+        "expectedRule":options.expected_rule,"expectedRuleObservedInViolation":expected_observed,+        "report":report,"hints":hints,+        "errorCode":if expected_observed==Some(false){Some("expected_rule_not_observed")}else{None},+    });+    (+        if expected_observed == Some(false) {+            StatusCode::UNPROCESSABLE_ENTITY+        } else {+            StatusCode::OK+        },+        Json(result),+    )+        .into_response()+}+/// Argument vectors, selected project cwd and a bounded child lifetime; no shell.+async fn native_command(+    executable: &str,+    directory: &Path,+    board: &Path,+    out: &Path,+) -> anyhow::Result<(String, String)> {+    let model_dir = super::dir_or_default("KICAD_PACKAGES3D_DIR", "/usr/share/kicad/3dmodels");+    let mut cmd = Command::new(executable);+    cmd.current_dir(directory)+        .arg("pcb")+        .arg("drc")+        .arg(board)+        .arg("-o")+        .arg(out)+        .args(["--format", "json"])+        .kill_on_drop(true);+    for version in 6..=10 {+        cmd.env(format!("KICAD{version}_3DMODEL_DIR"), &model_dir);+    }+    let result = tokio::time::timeout(Duration::from_secs(110), cmd.output())+        .await+        .map_err(|_| anyhow::anyhow!("native_project_drc_timeout: 110 seconds"))??;+    let stdout = String::from_utf8_lossy(&result.stdout).into_owned();+    let stderr = String::from_utf8_lossy(&result.stderr).into_owned();+    if !result.status.success() {+        anyhow::bail!(+            "native exit {:?}; stderr: {}; stdout: {}",+            result.status.code(),+            stderr,+            stdout+        );+    }+    Ok((stdout, stderr))+}++#[cfg(all(test, unix))]+mod tests {+    use super::*;+    use std::{fs, os::unix::fs::PermissionsExt};+    fn fixture() -> (tempfile::TempDir, Vec<u8>) {+        let dir = tempfile::tempdir().unwrap();+        fs::write(dir.path().join("board with spaces.kicad_pcb"), "(board)").unwrap();+        fs::write(dir.path().join("board with spaces.kicad_pro"), "{}").unwrap();+        fs::write(+            dir.path().join("board with spaces.kicad_dru"),+            "(version 1)\n(rule named-negative-control (constraint disallow via))",+        )+        .unwrap();+        let bytes =+            service_kicad_project::pack(dir.path(), "board with spaces.kicad_pcb", &[], true)+                .unwrap();+        (dir, bytes)+    }+    fn stub(dir: &Path, name: &str, script: &str) -> String {+        let path = dir.join(name);+        fs::write(&path, script).unwrap();+        fs::set_permissions(&path, fs::Permissions::from_mode(0o700)).unwrap();+        path.to_str().unwrap().into()+    }+    fn options(expected: Option<&str>) -> Options {+        Options {+            board: "board with spaces.kicad_pcb".into(),+            require_rules: true,+            expected_rule: expected.map(Into::into),+        }+    }+    async fn value(response: Response) -> Value {+        serde_json::from_slice(+            &axum::body::to_bytes(response.into_body(), 1024 * 1024)+                .await+                .unwrap(),+        )+        .unwrap()+    }+    #[tokio::test]+    async fn subprocess_receives_same_stem_project_and_reports_exact_inputs() {+        let (dir, bytes) = fixture();+        let exe = stub(+            dir.path(),+            "fake-kicad",+            r#"#!/bin/sh+set -eu+[ "$1" = pcb ] && [ "$2" = drc ]+[ -f "${3%.kicad_pcb}.kicad_pro" ]+[ -f "${3%.kicad_pcb}.kicad_dru" ]+cat > "$5" <<'JSON'+{"kicad_version":"test-double-not-native","violations":[{"severity":"error","type":"disallowed","description":"named-negative-control"}]}+JSON+"#,+        );+        let response =+            run_with_executable(options(Some("named-negative-control")), bytes.into(), &exe).await;+        assert_eq!(response.status(), StatusCode::OK);+        let v = value(response).await;+        assert_eq!(v["inputManifest"].as_array().unwrap().len(), 3);+        assert_eq!(v["expectedRuleObservedInViolation"], true);+        assert_eq!(v["nativeKicadVersion"], "test-double-not-native");+    }+    #[tokio::test]+    async fn malformed_native_rules_are_errors_without_default_fallback() {+        let (dir, bytes) = fixture();+        let exe = stub(+            dir.path(),+            "error-kicad",+            "#!/bin/sh\nprintf 'Error parsing custom rules' >&2\nexit 2\n",+        );+        let response = run_with_executable(options(None), bytes.into(), &exe).await;+        assert_eq!(response.status(), StatusCode::BAD_REQUEST);+        let v = value(response).await;+        assert_eq!(v["errorCode"], "native_drc_failed");+        assert!(v["error"]+            .as_str()+            .unwrap()+            .contains("Error parsing custom rules"));+    }+    #[tokio::test]+    async fn missing_expected_rule_is_not_a_successful_validation() {+        let (dir, bytes) = fixture();+        let exe = stub(+            dir.path(),+            "empty-kicad",+            "#!/bin/sh\necho '{\"kicad_version\":\"test-double\",\"violations\":[]}' > \"$5\"\n",+        );+        let response =+            run_with_executable(options(Some("named-negative-control")), bytes.into(), &exe).await;+        assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);+        let v = value(response).await;+        assert_eq!(v["errorCode"], "expected_rule_not_observed");+        assert!(v["report"]["violations"].is_array());+    }+}
cli/Cargo.toml+2
@@ -18,3 +18,5 @@ reqwest = { version = "0.12", default-features = false, features = ["blocking", serde.workspace = true serde_json.workspace = true tempfile.workspace = true++service-kicad-project = { path = "../project" }
cli/src/main.rs+52
@@ -71,6 +71,18 @@ enum PcbCmd {         #[arg(long, default_value = "text")] format: String,         #[arg(long)] out: Option<PathBuf>,     },+    /// DRC with the matching native project and custom rules. Select the board explicitly.+    DrcProject {+        directory: PathBuf,+        #[arg(long)] board: String,+        /// Additional project-relative KiCad files/directories, e.g. a local .pretty library.+        #[arg(long)] include: Vec<PathBuf>,+        /// Explicitly permit an absent .kicad_dru; project settings still required.+        #[arg(long)] allow_no_custom_rules: bool,+        /// Negative-control check: require this rule name in a native violation description.+        #[arg(long)] expect_rule: Option<String>,+        #[arg(long)] out: Option<PathBuf>,+    },     /// POST /kicad/pcb/export/{svg,gerbers,step}.     #[command(subcommand)]     Export(PcbExportCmd),@@ -314,9 +326,30 @@ fn main() -> Result<()> {             Ok(())         }         Cmd::Pcb(PcbCmd::Drc { file, format, out }) => {+            let omitted: Vec<_> = ["kicad_pro", "kicad_dru"].iter().map(|ext|file.with_extension(ext)).filter(|p|p.exists()).collect();+            if !omitted.is_empty() {+                eprintln!("{}",serde_json::json!({"level":"warning","code":"PROJECT_RULES_OMITTED","files":omitted,"hint":"This command uploads only the board. Use pcb drc-project <directory> --board <relative-board.kicad_pcb> to include project settings and custom rules."}));+            }             let bytes = post_file_bytes("/kicad/pcb/drc", &file)?;             print_json_pretty_or_write(&bytes, &format, out.as_ref())         }+        Cmd::Pcb(PcbCmd::DrcProject { directory, board, include, allow_no_custom_rules, expect_rule, out }) => {+            if expect_rule.as_ref().is_some_and(|s|s.trim().is_empty()) { anyhow::bail!("expected rule name must not be empty"); }+            let bytes=service_kicad_project::pack(&directory,&board,&include,!allow_no_custom_rules)?;+            let path=project_drc_path(&board,!allow_no_custom_rules,expect_rule.as_deref())?;+            let response=http_client().post(format!("{}{}",base_url(),path)).header("Content-Type","application/x-tar").body(bytes).send().unwrap_or_else(|e| {+                eprintln!("service-kicad: unreachable ({e})");std::process::exit(2);+            });+            let status=response.status();+            let result=response.bytes()?.to_vec();+            // Preserve the full native report even for a failed expected-rule check.+            write_or_stdout(&result,out.as_ref())?;+            if !status.is_success() {+                eprintln!("service-kicad: project DRC returned {status}; detailed response preserved in output");+                std::process::exit(http_error_exit_code(status.as_u16()));+            }+            Ok(())+        }         Cmd::Pcb(PcbCmd::Export(PcbExportCmd::Svg { file, side, layers, out })) => {             let mut path = format!("/kicad/pcb/export/svg?side={}", urlencode(&side));             if let Some(l) = layers {@@ -437,6 +470,25 @@ fn glb_gate(acknowledged: bool) {     std::process::exit(1); } +fn project_drc_path(board: &str, require_rules: bool, expected_rule: Option<&str>) -> Result<String> {+    // Use a real query encoder: ampersands, hashes and plus signs can be filenames.+    let mut url=reqwest::Url::parse("http://project.invalid/kicad/pcb/drc-project")?;+    url.query_pairs_mut().append_pair("board",board).append_pair("require_rules",if require_rules {"true"} else {"false"});+    if let Some(name)=expected_rule {url.query_pairs_mut().append_pair("expected_rule",name);}+    Ok(format!("{}?{}",url.path(),url.query().unwrap_or_default()))+}+#[cfg(test)]+mod project_cli_tests {+    #[test]+    fn project_names_cannot_inject_query_options() {+        let board="a&require_rules=false+#?.kicad_pcb";+        let path=super::project_drc_path(board,true,Some("rule &+?#")).unwrap();+        let u=reqwest::Url::parse(&format!("http://test.invalid{path}")).unwrap();+        let values:Vec<_>=u.query_pairs().collect();assert_eq!(values.len(),3);+        assert_eq!(values[0].1,board);assert_eq!(values[1].1,"true");assert_eq!(values[2].1,"rule &+?#");+    }+}+ /// Absolute-minimum URL encoder — just the three characters we actually /// see in library/name path segments. fn urlencode(s: &str) -> String {
docs/service-kicad-api.md+42
@@ -139,3 +139,45 @@ migrate cleanly). - **Patch bump**: bug fixes, dependency updates, no contract change.  Every bump edits `service.json::api_version` AND this doc's header.++## Candidate additive endpoint: project-aware PCB DRC++`POST /kicad/pcb/drc-project?board=<relative.kicad_pcb>&require_rules=true`+accepts an **uncompressed tar containing regular KiCad input files only**, maximum+100 MiB and 4096 entries. Directory entries, links, device files, duplicate paths+(including case aliases), absolute paths and traversal are rejected. Accepted files:+`.kicad_pcb`, `.kicad_pro`, `.kicad_dru`, `.kicad_mod`, `.kicad_sym`, `.kicad_wks`,+`fp-lib-table`, `sym-lib-table`. Each job extracts to a fresh temporary directory and+keeps the selected board's path and matching stem. Selection is explicit even if the+archive contains several boards. A matching native project JSON object is required.++`require_rules` defaults to true. A missing matching custom-rule file fails before+KiCad runs unless false is explicitly supplied. This switch does not override a+malformed rules file that is present. `expected_rule=<name>` optionally requires the+name to appear in a native violation description, for negative-control fixtures.+It is an observation check, not a general proof that all rules worked.++The new route admits at most two concurrent jobs and gives each KiCad child a+110-second deadline (shorter than the current 120-second CLI HTTP timeout). Commands+use argument vectors and the job directory, never a shell. This is file/job isolation,+not a claim that KiCad itself is an OS sandbox; normal supported library references+still need native validation.++Response JSON includes `ok`, `operation`, `serviceVersion`, `serviceGitSha`,+`nativeKicadVersion`, `board`, `project`, `customRules`, `inputManifest[]` with+path/bytes/SHA256, `nativeStdout`, `nativeStderr`, `report`, `hints`, and optional+`expectedRuleObservedInViolation`. Hashes identify files supplied to the native+process; do not call them proof that every rule was enforced. `ok` is not a DRC pass.++- 200: native JSON obtained and any requested named-rule observation succeeded;+  `report.violations` may still contain errors.+- 400: invalid archive/project or failed native command, with `errorCode` and no+  default-rule retry.+- 422: expected rule was absent; full native report is retained in the response.+- 429: two project jobs are already active; retry later.+- 502: native output was missing/malformed or lacked its violations array.++The CLI preserves response JSON to `--out` even for 422/other HTTP errors and exits+nonzero. A successful request, archived source or passing test-double suite is not+native fabrication qualification. Deployment and real KiCad checks remain pending+for this candidate.
project/Cargo.tomladded+12
@@ -0,0 +1,12 @@+[package]+name = "service-kicad-project"+version.workspace = true+edition.workspace = true+license.workspace = true+[dependencies]+anyhow.workspace = true+serde.workspace = true+serde_json.workspace = true+tempfile.workspace = true+tar = "0.4"+sha2 = "0.10"
project/src/lib.rsadded+402
@@ -0,0 +1,402 @@+//! Exact project transport. Does not claim native rules were enforced.+use anyhow::{bail, Context, Result};+use serde::Serialize;+use sha2::{Digest, Sha256};+use std::{+    collections::{BTreeMap, BTreeSet},+    fs,+    io::{Cursor, Read},+    path::{Component, Path, PathBuf},+};++pub const MAX_BYTES: u64 = 100 * 1024 * 1024;+pub const MAX_FILES: usize = 4096;+#[derive(Debug, Clone, Serialize)]+pub struct InputFile {+    pub path: String,+    pub bytes: u64,+    pub sha256: String,+}+pub struct Project {+    pub directory: tempfile::TempDir,+    pub board: PathBuf,+    pub project: PathBuf,+    pub rules: Option<PathBuf>,+    pub files: Vec<InputFile>,+}+fn relative_name(path: &Path) -> Result<String> {+    let s = path.to_str().context("project path must be UTF-8")?;+    if s.is_empty()+        || s.contains(['\\', ':'])+        || s.chars().any(char::is_control)+        || s.starts_with('/')+        || s.split('/').any(|p| p.is_empty() || p == "." || p == "..")+    {+        bail!("unsafe_project_path: {s:?}");+    }+    if path+        .components()+        .any(|c| !matches!(c, Component::Normal(_)))+    {+        bail!("unsafe_project_path: {s:?}");+    }+    Ok(s.to_owned())+}+fn allowed(path: &Path) -> bool {+    matches!(+        path.file_name().and_then(|x| x.to_str()),+        Some("fp-lib-table" | "sym-lib-table")+    ) || matches!(+        path.extension().and_then(|x| x.to_str()),+        Some("kicad_pcb" | "kicad_pro" | "kicad_dru" | "kicad_mod" | "kicad_sym" | "kicad_wks")+    )+}+fn file_record(path: String, bytes: &[u8]) -> InputFile {+    InputFile {+        path,+        bytes: bytes.len() as u64,+        sha256: format!("{:x}", Sha256::digest(bytes)),+    }+}+/// Extract only regular KiCad input files, never links or special entries.+/// The fresh temp directory is retained by Project through the native command.+pub fn unpack(bytes: &[u8], selected: &str, require_rules: bool) -> Result<Project> {+    if bytes.len() as u64 > MAX_BYTES {+        bail!("project_too_large");+    }+    let board = PathBuf::from(relative_name(Path::new(selected))?);+    if board.extension().and_then(|s| s.to_str()) != Some("kicad_pcb") {+        bail!("selected_board_must_be_kicad_pcb");+    }+    let directory = tempfile::tempdir()?;+    let mut archive = tar::Archive::new(Cursor::new(bytes));+    let mut names = BTreeSet::new();+    let mut files = Vec::new();+    let mut total = 0u64;+    for (count, entry) in archive.entries()?.enumerate() {+        if count >= MAX_FILES {+            bail!("too_many_project_entries");+        }+        let mut entry = entry?;+        let kind = entry.header().entry_type();+        if !kind.is_file() {+            bail!("project_entry_not_regular_file");+        }+        let path = entry.path()?.into_owned();+        let name = relative_name(&path)?;+        if !allowed(&path) {+            bail!("unsupported_project_file: {name}");+        }+        if !names.insert(name.to_lowercase()) {+            bail!("duplicate_project_path: {name}");+        }+        let size = entry.size();+        total = total.checked_add(size).context("project_size_overflow")?;+        if total > MAX_BYTES {+            bail!("project_too_large");+        }+        let mut data = Vec::new();+        entry.by_ref().take(size + 1).read_to_end(&mut data)?;+        if data.len() as u64 != size {+            bail!("truncated_project_entry: {name}");+        }+        let dest = directory.path().join(&path);+        fs::create_dir_all(dest.parent().context("missing parent")?)?;+        fs::write(dest, &data)?;+        files.push(file_record(name, &data));+    }+    if !directory.path().join(&board).is_file() {+        bail!("selected_board_missing: {selected}");+    }+    let project = board.with_extension("kicad_pro");+    if !directory.path().join(&project).is_file() {+        bail!("matching_project_missing: {}", project.display());+    }+    // Reject malformed project JSON before invoking native KiCad.+    let config: serde_json::Value =+        serde_json::from_slice(&fs::read(directory.path().join(&project))?)+            .context("invalid_kicad_project_json")?;+    if !config.is_object() {+        bail!("kicad_project_must_be_object");+    }+    let rule_path = board.with_extension("kicad_dru");+    let rules = directory+        .path()+        .join(&rule_path)+        .is_file()+        .then_some(rule_path);+    if require_rules && rules.is_none() {+        bail!(+            "matching_custom_rules_missing: use an explicit no-custom-rules mode only if intended"+        );+    }+    files.sort_by(|a, b| a.path.cmp(&b.path));+    Ok(Project {+        directory,+        board,+        project,+        rules,+        files,+    })+}+/// Only requested inputs are uploaded. No implicit recursive scan of the workspace.+pub fn pack(+    root: &Path,+    selected: &str,+    includes: &[PathBuf],+    require_rules: bool,+) -> Result<Vec<u8>> {+    let root = root.canonicalize().context("project directory")?;+    let board = PathBuf::from(relative_name(Path::new(selected))?);+    let mut paths = BTreeSet::new();+    let mut count = 0usize;+    fn collect(+        root: &Path,+        rel: &Path,+        paths: &mut BTreeSet<PathBuf>,+        count: &mut usize,+    ) -> Result<()> {+        relative_name(rel)?;+        // Check every path ancestor; canonical containment alone could accept links.+        let mut at = root.to_owned();+        for part in rel.components() {+            at.push(part);+            if fs::symlink_metadata(&at)?.file_type().is_symlink() {+                bail!("project_symlink_rejected: {}", rel.display());+            }+        }+        let full = root.join(rel);+        let metadata = fs::symlink_metadata(&full)?;+        *count += 1;+        if *count > MAX_FILES {+            bail!("too_many_project_entries");+        }+        if metadata.is_dir() {+            for child in fs::read_dir(full)? {+                collect(root, &rel.join(child?.file_name()), paths, count)?;+            }+        } else if metadata.is_file() && allowed(rel) {+            paths.insert(rel.to_owned());+        } else {+            bail!("unsupported_project_file: {}", rel.display());+        }+        Ok(())+    }+    collect(&root, &board, &mut paths, &mut count)?;+    collect(+        &root,+        &board.with_extension("kicad_pro"),+        &mut paths,+        &mut count,+    )?;+    let rules = board.with_extension("kicad_dru");+    if require_rules || root.join(&rules).exists() {+        collect(&root, &rules, &mut paths, &mut count)?;+    }+    for name in ["fp-lib-table", "sym-lib-table"] {+        let path = board.parent().unwrap_or(Path::new("")).join(name);+        if root.join(&path).exists() {+            collect(&root, &path, &mut paths, &mut count)?;+        }+    }+    for path in includes {+        collect(&root, path, &mut paths, &mut count)?;+    }+    let mut tar = tar::Builder::new(Vec::new());+    let mut total = 0u64;+    let mut names = BTreeMap::new();+    for path in paths {+        let name = relative_name(&path)?;+        if names.insert(name.to_lowercase(), name.clone()).is_some() {+            bail!("duplicate_project_path: {name}");+        }+        if fs::metadata(root.join(&path))?.len() > MAX_BYTES - total {+            bail!("project_too_large");+        }+        let data = fs::read(root.join(&path))?;+        total += data.len() as u64;+        if total > MAX_BYTES {+            bail!("project_too_large");+        }+        let mut header = tar::Header::new_gnu();+        header.set_mode(0o600);+        header.set_size(data.len() as u64);+        header.set_cksum();+        tar.append_data(&mut header, path, Cursor::new(data))?;+    }+    let bytes = tar.into_inner()?;+    // Same validation and size limits as the API, before making a network call.+    unpack(&bytes, selected, require_rules)?;+    Ok(bytes)+}++#[cfg(test)]+mod tests {+    use super::*;+    fn archive(entries: &[(&str, &[u8], tar::EntryType)]) -> Vec<u8> {+        let mut builder = tar::Builder::new(Vec::new());+        for (name, content, kind) in entries {+            let mut header = tar::Header::new_gnu();+            // Raw header allows deliberately invalid path fixtures that append_data rejects.+            header.as_mut_bytes()[..100].fill(0);+            header.as_mut_bytes()[..name.len()].copy_from_slice(name.as_bytes());+            header.set_size(content.len() as u64);+            header.set_mode(0o600);+            header.set_entry_type(*kind);+            header.set_cksum();+            builder.append(&header, *content).unwrap();+        }+        builder.into_inner().unwrap()+    }+    fn valid() -> Vec<u8> {+        archive(&[+            (+                "nested/board.kicad_pcb",+                b"(board)",+                tar::EntryType::Regular,+            ),+            ("nested/board.kicad_pro", b"{}", tar::EntryType::Regular),+            (+                "nested/board.kicad_dru",+                b"(version 1)",+                tar::EntryType::Regular,+            ),+        ])+    }+    #[test]+    fn exact_stem_nested_inputs_and_hashes_survive() {+        let p = unpack(&valid(), "nested/board.kicad_pcb", true).unwrap();+        assert_eq!(+            p.rules.as_deref(),+            Some(Path::new("nested/board.kicad_dru"))+        );+        assert_eq!(+            fs::read(p.directory.path().join(&p.project)).unwrap(),+            b"{}"+        );+        assert_eq!(+            p.files+                .iter()+                .find(|f| f.path.ends_with(".kicad_pro"))+                .unwrap()+                .sha256,+            "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"+        );+    }+    #[test]+    fn traversal_absolute_and_windows_paths_are_rejected() {+        for path in [+            "../board.kicad_pcb",+            "/board.kicad_pcb",+            "C:/board.kicad_pcb",+            "nested/../board.kicad_pcb",+            "nested\\board.kicad_pcb",+        ] {+            let bytes = archive(&[(path, b"x", tar::EntryType::Regular)]);+            assert!(unpack(&bytes, "board.kicad_pcb", true).is_err(), "{path}");+        }+    }+    #[test]+    fn links_and_special_files_are_rejected() {+        for kind in [+            tar::EntryType::Symlink,+            tar::EntryType::Link,+            tar::EntryType::Fifo,+            tar::EntryType::Directory,+        ] {+            assert!(unpack(+                &archive(&[("board.kicad_pcb", b"", kind)]),+                "board.kicad_pcb",+                true+            )+            .is_err());+        }+    }+    #[test]+    fn duplicate_paths_including_case_aliases_are_rejected() {+        let bytes = archive(&[+            ("board.kicad_pcb", b"a", tar::EntryType::Regular),+            ("BOARD.kicad_pcb", b"b", tar::EntryType::Regular),+        ]);+        assert!(unpack(&bytes, "board.kicad_pcb", true)+            .err()+            .unwrap()+            .to_string()+            .contains("duplicate"));+    }+    #[test]+    fn missing_selected_board_project_and_rules_fail_without_fallback() {+        assert!(unpack(&valid(), "other.kicad_pcb", true).is_err());+        assert!(unpack(+            &archive(&[("b.kicad_pcb", b"x", tar::EntryType::Regular)]),+            "b.kicad_pcb",+            true+        )+        .is_err());+        let bytes = archive(&[+            ("b.kicad_pcb", b"x", tar::EntryType::Regular),+            ("b.kicad_pro", b"{}", tar::EntryType::Regular),+        ]);+        assert!(unpack(&bytes, "b.kicad_pcb", true).is_err());+        assert!(unpack(&bytes, "b.kicad_pcb", false)+            .unwrap()+            .rules+            .is_none());+    }+    #[test]+    fn project_json_must_be_an_object() {+        let bytes = archive(&[+            ("b.kicad_pcb", b"x", tar::EntryType::Regular),+            ("b.kicad_pro", b"[]", tar::EntryType::Regular),+        ]);+        assert!(unpack(&bytes, "b.kicad_pcb", false).is_err());+    }+    #[test]+    fn pack_preserves_only_selected_files_and_explicit_libraries() {+        let temp = tempfile::tempdir().unwrap();+        for (name, content) in [+            ("a.kicad_pcb", "a"),+            ("a.kicad_pro", "{}"),+            ("a.kicad_dru", "(version 1)"),+            ("other.kicad_pcb", "private unrelated board"),+            ("credentials.txt", "not an upload"),+        ] {+            fs::write(temp.path().join(name), content).unwrap();+        }+        fs::create_dir(temp.path().join("Parts.pretty")).unwrap();+        fs::write(+            temp.path().join("Parts.pretty/A.kicad_mod"),+            "(footprint A)",+        )+        .unwrap();+        let bytes = pack(temp.path(), "a.kicad_pcb", &["Parts.pretty".into()], true).unwrap();+        let p = unpack(&bytes, "a.kicad_pcb", true).unwrap();+        assert_eq!(p.files.len(), 4);+        assert!(!p+            .files+            .iter()+            .any(|f| f.path.contains("other") || f.path.contains("credentials")));+    }+    #[test]+    #[cfg(unix)]+    fn cli_rejects_linked_files_and_linked_ancestors() {+        let temp = tempfile::tempdir().unwrap();+        let real = tempfile::tempdir().unwrap();+        fs::write(real.path().join("a.kicad_pcb"), "a").unwrap();+        std::os::unix::fs::symlink(real.path(), temp.path().join("linked")).unwrap();+        assert!(pack(temp.path(), "linked/a.kicad_pcb", &[], false).is_err());+    }+    #[test]+    fn explicit_board_selection_does_not_pick_first_file() {+        let bytes = archive(&[+            ("a.kicad_pcb", b"wrong", tar::EntryType::Regular),+            ("z.kicad_pcb", b"right", tar::EntryType::Regular),+            ("z.kicad_pro", b"{}", tar::EntryType::Regular),+        ]);+        let p = unpack(&bytes, "z.kicad_pcb", false).unwrap();+        assert_eq!(+            fs::read(p.directory.path().join(p.board)).unwrap(),+            b"right"+        );+    }+}
verification/test_project_drc.pyadded+57
@@ -0,0 +1,57 @@+#!/usr/bin/env python3+"""Reproducible HTTP/CLI tests with a protocol double. Never native KiCad DRC."""+import argparse,hashlib,json,os,socket,subprocess,tempfile,time,urllib.request+from pathlib import Path+ap=argparse.ArgumentParser();ap.add_argument('--bin-dir',type=Path,default=Path('target/debug'));args=ap.parse_args();bin_dir=args.bin_dir.resolve()+with tempfile.TemporaryDirectory(prefix='service-kicad-protocol-test-') as temp:+ root=Path(temp);fixtures=root/'inputs';fixtures.mkdir();stub=root/'fake-kicad'+ stub.write_text('''#!/usr/bin/env python3+import json,sys+from pathlib import Path+if sys.argv[1:]==['version']:+ print('TEST-DOUBLE-NOT-NATIVE-KICAD');raise SystemExit(0)+a=sys.argv[1:];assert a[:2]==['pcb','drc'];b=Path(a[2]);assert b.exists() and b.with_suffix('.kicad_pro').exists();r=b.with_suffix('.kicad_dru').read_text()+if 'MALFORMED_TEST_MARKER' in r:+ print('TEST DOUBLE: error parsing custom rules',file=sys.stderr);raise SystemExit(2)+v=[{'severity':'error','type':'disallowed','description':'TEST DOUBLE: named-negative-control'}] if 'named-negative-control' in r else []+Path(a[a.index('-o')+1]).write_text(json.dumps({'kicad_version':'TEST-DOUBLE-NOT-NATIVE-KICAD','violations':v}))+''');stub.chmod(0o700)+ sock=socket.socket();sock.bind(('127.0.0.1',0));port=sock.getsockname()[1];sock.close()+ url=f'http://127.0.0.1:{port}';env=dict(os.environ,PORT=str(port),KICAD_CLI=str(stub),SERVICE_KICAD_URL=url)+ log=(root/'server.log').open('w');server=subprocess.Popen([str(bin_dir/'service-kicad-api')],env=env,stdout=log,stderr=log)+ results=[]+ try:+  for _ in range(50):+   if server.poll() is not None:raise RuntimeError('Test API terminated')+   try:+    with urllib.request.urlopen(url+'/health',timeout=.2) as response:health=json.load(response)+    assert health['kicad_version']=='TEST-DOUBLE-NOT-NATIVE-KICAD';break+   except OSError:time.sleep(.1)+  else:raise RuntimeError('Test API did not become ready')+  def run(case,stem,rule,success,error=None):+   for ext,text in [('kicad_pcb','(board)'),('kicad_pro','{}'),('kicad_dru',rule)]:+    (fixtures/f'{stem}.{ext}').write_text(text)+   out=root/f'{case}.json'+   command=[str(bin_dir/'service-kicad'),'pcb','drc-project',str(fixtures),'--board',stem+'.kicad_pcb','--expect-rule','named-negative-control','--out',str(out)]+   p=subprocess.run(command,env=env,capture_output=True,text=True,timeout=15)+   assert (p.returncode==0)==success,(case,p.returncode,p.stderr)+   v=json.loads(out.read_text());assert v['ok']==success+   if success:+    assert v['nativeKicadVersion']=='TEST-DOUBLE-NOT-NATIVE-KICAD'+    assert len(v['inputManifest'])==3+    for f in v['inputManifest']:assert f['sha256']==hashlib.sha256((fixtures/f['path']).read_bytes()).hexdigest()+   else:assert v['errorCode']==error+   results.append({'case':case,'exitCode':p.returncode,'errorCode':v.get('errorCode'),'nativeKicadExecuted':False})+  run('exact-inputs','board','named-negative-control',True)+  run('encoded-name','board & plus+#','named-negative-control',True)+  run('missing-rule','missing-probe','(version 1)',False,'expected_rule_not_observed')+  run('error-forwarding','malformed','MALFORMED_TEST_MARKER',False,'native_drc_failed')+  p=subprocess.run([str(bin_dir/'service-kicad'),'pcb','drc',str(fixtures/'board.kicad_pcb'),'--format','json'],env=env,capture_output=True,text=True,timeout=15)+  assert 'PROJECT_RULES_OMITTED' in p.stderr+  results.append({'case':'single-file-omission-hint','hintSeen':True,'nativeKicadExecuted':False})+  print(json.dumps({'mode':'Protocol test double, not native DRC','serviceVersion':health['version'],'serviceGitSha':health['git_sha'],'results':results},indent=2))+ finally:+  server.terminate()+  try:server.wait(timeout=3)+  except subprocess.TimeoutExpired:server.kill();server.wait(timeout=3)+  log.close()