From ec9edc294fd5e7e29977fc7b0e6fb953422bc0e2 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 19 Aug 2026 05:54:00 +0800 Subject: chore: reorganize dev tools into dev/ directory and consolidate configs Move CI, dev tools, and configs from root-level scattered locations into a unified `dev/` directory structure. Update all references across build scripts, documentation, and editor configurations. Also consolidate editor config generation into build.rs for automated synchronization of rust-analyzer settings across VS Code and Zed. --- dev/run/.gitignore | 5 + dev/run/Cargo.lock | 572 +++++++++++++++++++++ dev/run/Cargo.toml | 26 + dev/run/src/bin/ci.py | 75 +++ dev/run/src/bin/clippy-fix.ps1 | 8 + dev/run/src/bin/clippy-fix.sh | 6 + dev/run/src/bin/cov-test.rs | 571 +++++++++++++++++++++ dev/run/src/bin/deploy-api-docs.rs | 118 +++++ dev/run/src/bin/display-dependency-order.rs | 12 + dev/run/src/bin/http-page-preview.ps1 | 3 + dev/run/src/bin/http-page-preview.sh | 2 + dev/run/src/bin/install-mling.ps1 | 10 + dev/run/src/bin/install-mling.sh | 17 + dev/run/src/bin/package-all.rs | 736 ++++++++++++++++++++++++++++ dev/run/src/bin/update-version.rs | 104 ++++ dev/run/src/bin/windows-folder-hide.ps1 | 115 +++++ dev/run/src/dependency_order.rs | 196 ++++++++ dev/run/src/lib.rs | 459 +++++++++++++++++ dev/run/src/verify.rs | 506 +++++++++++++++++++ 19 files changed, 3541 insertions(+) create mode 100644 dev/run/.gitignore create mode 100644 dev/run/Cargo.lock create mode 100644 dev/run/Cargo.toml create mode 100644 dev/run/src/bin/ci.py create mode 100644 dev/run/src/bin/clippy-fix.ps1 create mode 100755 dev/run/src/bin/clippy-fix.sh create mode 100644 dev/run/src/bin/cov-test.rs create mode 100644 dev/run/src/bin/deploy-api-docs.rs create mode 100644 dev/run/src/bin/display-dependency-order.rs create mode 100644 dev/run/src/bin/http-page-preview.ps1 create mode 100755 dev/run/src/bin/http-page-preview.sh create mode 100644 dev/run/src/bin/install-mling.ps1 create mode 100755 dev/run/src/bin/install-mling.sh create mode 100644 dev/run/src/bin/package-all.rs create mode 100644 dev/run/src/bin/update-version.rs create mode 100644 dev/run/src/bin/windows-folder-hide.ps1 create mode 100644 dev/run/src/dependency_order.rs create mode 100644 dev/run/src/lib.rs create mode 100644 dev/run/src/verify.rs (limited to 'dev/run') diff --git a/dev/run/.gitignore b/dev/run/.gitignore new file mode 100644 index 0000000..40f407e --- /dev/null +++ b/dev/run/.gitignore @@ -0,0 +1,5 @@ +# All temp build artifacts will be stored in this dir +/target + +# If you want to add some Rust crates? Remove this line +# /Cargo.* diff --git a/dev/run/Cargo.lock b/dev/run/Cargo.lock new file mode 100644 index 0000000..3117d0b --- /dev/null +++ b/dev/run/Cargo.lock @@ -0,0 +1,572 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "arg-picker" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd62c395708a956e98e06b6b18200b2a65a5ae63448ed7d59cca32833aaf7265" +dependencies = [ + "arg-picker-macros", + "just_fmt 0.2.0", +] + +[[package]] +name = "arg-picker-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04fddbb5c1f26450cfc6aded0559b759b292d400dba1ff4e45128f3e0e1ffe6b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width", + "windows-sys", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[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 = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "indicatif" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +dependencies = [ + "console", + "portable-atomic", + "unicode-width", + "unit-prefix", + "web-time", +] + +[[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.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "just_fmt" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" + +[[package]] +name = "just_fmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" + +[[package]] +name = "just_template" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3edb658c34b10b69c4b3b58f7ba989cd09c82c0621dee1eef51843c2327225" +dependencies = [ + "just_fmt 0.1.2", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[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", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[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 = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tools" +version = "0.1.0" +dependencies = [ + "arg-picker", + "colored", + "flate2", + "indicatif", + "just_fmt 0.1.2", + "just_template", + "serde", + "serde_json", + "tar", + "tokio", + "toml", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[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 = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[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 = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/dev/run/Cargo.toml b/dev/run/Cargo.toml new file mode 100644 index 0000000..4935f2d --- /dev/null +++ b/dev/run/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "tools" +version = "0.1.0" +edition = "2024" +authors = ["Weicao-CatilGrass"] +description = "Development tools for mingling" +license = "MIT OR Apache-2.0" +repository = "https://github.com/catilgrass/mingling" +readme = "../README.md" +keywords = ["cli", "development", "tools"] +categories = ["command-line-interface", "development-tools"] + +[dependencies] +just_template = "0.1.3" +just_fmt = "0.1.2" +colored = "3.1.1" +toml = "0.8" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +indicatif = "0.18.4" +flate2 = "1" +tar = "0.4" +arg-picker = "0.1.0" + +[workspace] diff --git a/dev/run/src/bin/ci.py b/dev/run/src/bin/ci.py new file mode 100644 index 0000000..6234a19 --- /dev/null +++ b/dev/run/src/bin/ci.py @@ -0,0 +1,75 @@ +"""Full CI orchestration for the mingling project. + +Runs every `cargo ci` step in order: lock the workspace, run all checks, +refresh the generated artifacts, then unlock. The final `git-unlock` doubles +as the idempotency check: it fails with a non-zero exit code when the run +left the working tree dirty. + +The script locates the git repository root and runs with it as the working +directory, so it can be invoked from anywhere inside the repo. +""" + +import os +import subprocess +import sys +from pathlib import Path + +# The pipeline steps, in execution order, as (command, args) pairs. +STEPS: list[tuple[str, list[str]]] = [ + ("git-lock", []), + ("report-clean", []), + ("build-check", []), + ("clippy-check", []), + ("test-all", []), + ("example-check", []), + ("docs-check", []), + ("example-refresh", []), + ("docsify-refresh", []), + ("features-refresh", []), + # Idempotency check: exits non-zero if CI contaminated the workspace, and + # prints the diff of the contamination before restoring. + ("git-unlock", ["--show-diff"]), +] + + +def find_repo_root() -> Path: + """Return the nearest ancestor directory containing `.git`.""" + current = Path.cwd() + for directory in (current, *current.parents): + if (directory / ".git").is_dir(): + return directory + raise SystemExit("error: not inside a git repository") + + +def main() -> int: + root = find_repo_root() + os.chdir(root) + + # Signature banner: docs/res/ci_banner.txt, relative to this script + # (.run/src/bin -> four levels up is the repo root). + banner = ( + Path(__file__).resolve().parent.parent.parent.parent + / "docs" + / "res" + / "ci_banner.txt" + ) + try: + print(banner.read_text(encoding="utf-8"), end="") + except OSError: + pass + + for command, args in STEPS: + print(f"==> cargo ci {' '.join([command, *args])}") + result = subprocess.run(["cargo", "ci", command, *args], check=False) + if result.returncode != 0: + print( + f"error: step `{command}` failed with exit code {result.returncode}", + file=sys.stderr, + ) + return result.returncode + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/dev/run/src/bin/clippy-fix.ps1 b/dev/run/src/bin/clippy-fix.ps1 new file mode 100644 index 0000000..1d24f92 --- /dev/null +++ b/dev/run/src/bin/clippy-fix.ps1 @@ -0,0 +1,8 @@ +$starting_dir = Get-Location +Get-ChildItem -Recurse -Filter "Cargo.toml" | ForEach-Object { + $project_dir = $_.DirectoryName + Push-Location $project_dir + cargo clippy --fix --allow-dirty --allow-no-vcs --quiet + Pop-Location +} +Set-Location $starting_dir diff --git a/dev/run/src/bin/clippy-fix.sh b/dev/run/src/bin/clippy-fix.sh new file mode 100755 index 0000000..9771ad4 --- /dev/null +++ b/dev/run/src/bin/clippy-fix.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +find . -name "Cargo.toml" -type f | while read -r cargo_file; do + project_dir=$(dirname "$cargo_file") + (cd "$project_dir" && cargo clippy --fix --allow-dirty --allow-no-vcs --quiet) +done diff --git a/dev/run/src/bin/cov-test.rs b/dev/run/src/bin/cov-test.rs new file mode 100644 index 0000000..f62ff01 --- /dev/null +++ b/dev/run/src/bin/cov-test.rs @@ -0,0 +1,571 @@ +//! Coverage test generator for mingling. +//! +//! This script requires the **fork** of cargo-llvm-cov: +//! +//! +//! The upstream `report` command cannot include binaries of non-workspace +//! crates (examples and test crates) and unconditionally filters +//! `tests`/`examples` source files. The fork adds two flags to fix this: +//! +//! - `--object `: include arbitrary binaries in the report +//! (upstream issue taiki-e/cargo-llvm-cov#367) +//! - `--include-examples`: stop filtering source files under the +//! `examples` directory (upstream issue taiki-e/cargo-llvm-cov#503) +//! +//! The script itself does not use `--include-examples`; it passes +//! `--no-default-ignore-filename-regex` and supplies its own filter so that +//! `tests`/`benches` directories stay in the report too. +//! +//! Install it with: +//! +//! ```bash +//! cargo install --git https://github.com/Weicao-CatilGrass/cargo-llvm-cov cargo-llvm-cov +//! ``` + +use std::fs; +use std::path::{Path, PathBuf}; + +use serde::Deserialize; +use tools::{eprintln_cargo_style, println_cargo_style, run_cmd}; + +const OUTPUT_DIR: &str = "docs/cov-test"; + +/// Shared target directory for all `cargo llvm-cov` runs. +/// +/// Pointing every run at the same target dir makes all of them share the +/// instrumented build cache and, more importantly, accumulate profraw files +/// in one place so the final `report` can merge everything. +const COV_TARGET_DIR: &str = ".temp/cov-llvm"; + +/// An example's `test.toml` (`[[runs]]` entries). +#[derive(Deserialize)] +struct TestConfig { + runs: Vec, +} + +/// One `[[runs]]` entry of an example's `test.toml`. +#[derive(Deserialize)] +struct TestCase { + input: Vec, +} + +fn main() { + let repo_root = find_git_repo().expect("Failed to find git repository root"); + let output_path = repo_root.join(OUTPUT_DIR); + let cov_target = repo_root.join(COV_TARGET_DIR); + + // Read features from [package.metadata.docs.rs] + let features = tools::read_features().unwrap_or_else(|e| { + eprintln!("Error: {}", e); + std::process::exit(1); + }); + let features_arg = features.join(","); + + // Ensure output directory exists + std::fs::create_dir_all(&output_path).expect("Failed to create output directory"); + std::fs::create_dir_all(&cov_target).expect("Failed to create cov target directory"); + + // All `cargo llvm-cov` invocations below share one target dir, so profraw + // files accumulate and are merged by the final `report` command. + // SAFETY: set before any thread is spawned; this process only shells out + // to subcommands via std::process. + unsafe { + std::env::set_var("CARGO_LLVM_COV_TARGET_DIR", &cov_target); + } + + // Drop stale profraw from previous runs (keep the instrumented build cache). + clean_old_profraw(&cov_target); + + println_cargo_style!("Features: {}", features_arg); + println_cargo_style!("Target: {}", cov_target.display()); + + // 1. Workspace tests + println_cargo_style!("Running: cargo llvm-cov test --workspace"); + run_cmd!(format!( + "cargo llvm-cov test --no-report --workspace --features \"{}\" --color always", + features_arg + )) + .unwrap_or_else(|code| { + eprintln_cargo_style!("workspace tests failed with exit code {}", code); + std::process::exit(code); + }); + + // 2. Integration test crates under mingling_core/tests (excluded from the + // workspace, so they need their own `--manifest-path` runs) + for manifest in find_test_crate_manifests(&repo_root) { + println_cargo_style!( + "Running: cargo llvm-cov test {}", + manifest.file_name().unwrap_or_default().to_string_lossy() + ); + run_cmd!(format!( + "cargo llvm-cov test --no-report --manifest-path \"{}\" --color always", + manifest.display() + )) + .unwrap_or_else(|code| { + eprintln_cargo_style!( + "test crate {} failed with exit code {}", + manifest.display(), + code + ); + std::process::exit(code); + }); + } + + // 3. Examples: build each example with explicit RUSTFLAGS, then execute + // every command declared in the example's test.toml directly. + // + // NOTE: `cargo llvm-cov run` cannot be used here. Its rustc wrapper + // only instruments the crates of the *current* cargo project (with + // `--manifest-path` that is the example itself), so the mingling + // libraries — being dependencies — would not be instrumented and their + // coverage would silently be lost (once_exec.rs showed 0%). Building + // with plain RUSTFLAGS instruments the whole dependency graph. + // + // RUSTFLAGS/CARGO_TARGET_DIR are set process-wide here because only the + // `report` step (which does not compile) follows. Non-zero exit codes + // are expected for some examples (e.g. `--help` exits with 2); profraw + // is still written. + unsafe { + std::env::set_var("RUSTFLAGS", "-Cinstrument-coverage"); + std::env::set_var("CARGO_TARGET_DIR", &cov_target); + } + let examples = load_example_commands(&repo_root); + let mut built = std::collections::HashSet::new(); + for (example, input) in &examples { + if built.insert(example.clone()) { + println_cargo_style!("Building: {}", example); + run_cmd!(format!( + "cargo build --manifest-path examples/{}/Cargo.toml --color always", + example + )) + .unwrap_or_else(|code| { + eprintln_cargo_style!( + "build of example {} failed with exit code {}", + example, + code + ); + std::process::exit(code); + }); + } + let binary = cov_target.join("debug").join(get_binary_name(example)); + let profraw = format!( + "{}/example-{}.%p.profraw", + cov_target.to_string_lossy(), + example + ); + match std::process::Command::new(&binary) + .args(input) + .env("LLVM_PROFILE_FILE", &profraw) + .status() + { + Ok(status) if status.success() => {} + Ok(status) => println_cargo_style!( + "Warning: example {} exited with {:?}, profraw still recorded", + example, + status.code() + ), + Err(e) => eprintln_cargo_style!("Failed to run example {}: {}", example, e), + } + } + + // 4. Collect the binaries of non-workspace crates (examples + test crates). + // The automatic object-file detection only knows workspace members, so + // these must be passed explicitly via --object. + let member_names = workspace_member_names(&repo_root); + let object_args = collect_object_args(&cov_target, &member_names); + + // 5. Generate the merged HTML report. + // + // --no-default-ignore-filename-regex: the default regex unconditionally + // excludes `examples`/`tests` directories, which is exactly what we want + // to include here, so we take over the filter ourselves. + let ignore_re = build_ignore_regex(&cov_target); + println_cargo_style!("Running: cargo llvm-cov report --html"); + run_cmd!(format!( + "cargo llvm-cov report --html --output-dir \"{}\" --no-default-ignore-filename-regex --ignore-filename-regex \"{}\" {} --color always", + output_path.to_string_lossy(), + ignore_re, + object_args + )) + .unwrap_or_else(|code| { + eprintln_cargo_style!("cargo llvm-cov report failed with exit code {}", code); + std::process::exit(code); + }); + + // Move files from /html/ to + let html_dir = output_path.join("html"); + if html_dir.exists() && html_dir.is_dir() { + println_cargo_style!("Moving files from {}/html/ to {}/", OUTPUT_DIR, OUTPUT_DIR); + + for entry in fs::read_dir(&html_dir).expect("Failed to read html directory") { + let entry = entry.expect("Failed to read entry"); + let entry_path = entry.path(); + let file_name = entry + .file_name() + .to_str() + .expect("Invalid filename") + .to_owned(); + + let dest_path = output_path.join(&file_name); + if dest_path.exists() { + if dest_path.is_dir() { + fs::remove_dir_all(&dest_path).unwrap_or_else(|e| { + eprintln!( + "Warning: could not remove directory {}: {}", + dest_path.display(), + e + ); + }); + } else { + fs::remove_file(&dest_path).unwrap_or_else(|e| { + eprintln!( + "Warning: could not remove file {}: {}", + dest_path.display(), + e + ); + }); + } + } + fs::rename(&entry_path, &dest_path).unwrap_or_else(|e| { + eprintln!("Warning: could not move {}: {}", entry_path.display(), e); + }); + } + + fs::remove_dir(&html_dir).unwrap_or_else(|e| { + eprintln!("Warning: could not remove html directory: {}", e); + }); + + println_cargo_style!("Files moved successfully."); + } + + // 6. Recolor the per-file coverage summary with project-specific + // thresholds: 0-50% red, 51-80% yellow, 81-100% green. llvm-cov's + // built-in thresholds differ, and the color is assigned when the HTML + // is generated, so the summary table is rewritten here. + let index_path = output_path.join("index.html"); + if let Err(e) = recolor_report_index(&index_path) { + eprintln_cargo_style!("Warning: failed to recolor {}: {}", index_path.display(), e); + } + + println_cargo_style!( + "Done: coverage report generated at {}/index.html", + OUTPUT_DIR + ); +} + +/// Remove `*.profraw` from the shared target dir so stale data from previous +/// runs does not pollute the merged report. The instrumented build cache +/// (everything else) is kept. +fn clean_old_profraw(cov_target: &Path) { + if let Ok(entries) = fs::read_dir(cov_target) { + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().is_some_and(|e| e == "profraw") { + let _ = fs::remove_file(&path); + } + } + } +} + +/// All `mingling_core/tests//Cargo.toml` manifests. +fn find_test_crate_manifests(repo_root: &Path) -> Vec { + let tests_dir = repo_root.join("mingling_core/tests"); + let mut manifests = Vec::new(); + if let Ok(entries) = fs::read_dir(&tests_dir) { + for entry in entries.flatten() { + let manifest = entry.path().join("Cargo.toml"); + if manifest.is_file() { + manifests.push(manifest); + } + } + } + manifests.sort(); + manifests +} + +/// Parse every `examples//test.toml` into `(example_name, input)` pairs. +fn load_example_commands(repo_root: &Path) -> Vec<(String, Vec)> { + let examples_dir = repo_root.join("examples"); + let mut entries: Vec<_> = std::fs::read_dir(&examples_dir) + .unwrap_or_else(|e| { + eprintln_cargo_style!("Failed to read {}: {}", examples_dir.display(), e); + std::process::exit(1); + }) + .flatten() + .collect(); + entries.sort_by_key(|e| e.file_name()); + + let mut pairs = Vec::new(); + for entry in entries { + let path = entry.path(); + if !path.is_dir() { + continue; + } + let test_toml = path.join("test.toml"); + if !test_toml.is_file() { + continue; + } + let name = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default() + .to_string(); + let content = fs::read_to_string(&test_toml).unwrap_or_else(|e| { + eprintln_cargo_style!("Failed to read {}: {}", test_toml.display(), e); + std::process::exit(1); + }); + let config: TestConfig = toml::from_str(&content).unwrap_or_else(|e| { + eprintln_cargo_style!("Failed to parse {}: {}", test_toml.display(), e); + std::process::exit(1); + }); + for case in config.runs { + pairs.push((name.clone(), case.input)); + } + } + pairs +} + +/// Names of all workspace members, from `cargo metadata --no-deps`. +fn workspace_member_names(repo_root: &Path) -> Vec { + let Ok(output) = tools::run_cmd_capture_with_dir( + "cargo metadata --no-deps --format-version 1".to_string(), + repo_root, + ) else { + return Vec::new(); + }; + let Ok(json) = serde_json::from_str::(&output) else { + return Vec::new(); + }; + json["packages"] + .as_array() + .into_iter() + .flatten() + .filter_map(|p| p["name"].as_str().map(str::to_owned)) + .collect() +} + +/// Collect the binaries of non-workspace crates (examples and test crates) +/// from the shared target dir, as `--object ` arguments. +/// +/// - `debug/` root: example binaries (built via `cargo llvm-cov run`). +/// - `debug/deps/`: test crate binaries (e.g. `integration-`); their +/// names do not follow a single pattern, so anything that is not a +/// workspace-member binary and not a proc-macro `.so` is collected. +/// +/// Workspace member binaries are detected automatically by `report` and must +/// NOT be passed again (duplicate `-object` entries produce duplicated +/// output). Hard links to the same file are deduplicated by inode. +fn collect_object_args(cov_target: &Path, member_names: &[String]) -> String { + let debug_dir = cov_target.join("debug"); + let mut objects = Vec::new(); + let mut seen = std::collections::HashSet::new(); + + for dir in [debug_dir.clone(), debug_dir.join("deps")] { + let Ok(entries) = fs::read_dir(&dir) else { + continue; + }; + for entry in entries.flatten() { + let path = entry.path(); + if !path.is_file() || !is_executable(&path) { + continue; + } + if !seen.insert(file_id(&path)) { + continue; + } + let Some(name) = path.file_name().and_then(|s| s.to_str()) else { + continue; + }; + // Proc-macro shared objects are either workspace members (picked + // up automatically) or external deps (excluded from the report + // by the ignore regex), so never pass them explicitly. + if name.starts_with("lib") && name.ends_with(".so") { + continue; + } + if is_workspace_member_binary(name, member_names) { + continue; + } + objects.push(path); + } + } + + objects.sort(); + objects + .iter() + .map(|p| format!("--object \"{}\"", p.to_string_lossy())) + .collect::>() + .join(" ") +} + +/// True if the binary name (e.g. `mingling_core-fea14a01b88afcaa`) belongs to +/// a workspace member. +fn is_workspace_member_binary(name: &str, member_names: &[String]) -> bool { + let stem = strip_cargo_hash(name); + member_names.iter().any(|m| stem == m) +} + +/// Strip the cargo-generated hash suffix: `mingling_core-fea14a01b88afcaa` -> +/// `mingling_core`. Returns the input unchanged if there is no such suffix. +fn strip_cargo_hash(name: &str) -> &str { + let Some(idx) = name.rfind('-') else { + return name; + }; + let (head, tail) = name.split_at(idx); + let hash = &tail[1..]; + if hash.len() == 16 && hash.chars().all(|c| c.is_ascii_hexdigit()) { + head + } else { + name + } +} + +/// A stable identity for deduplicating hard links: device+inode on Unix, +/// canonicalized path elsewhere. +fn file_id(path: &Path) -> String { + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt as _; + if let Ok(metadata) = fs::metadata(path) { + return format!("{}:{}", metadata.dev(), metadata.ino()); + } + } + fs::canonicalize(path) + .unwrap_or_else(|_| path.to_path_buf()) + .to_string_lossy() + .into_owned() +} + +/// Resolve binary filename for the given example. +/// +/// The binary name matches the package name. On Windows, the `.exe` suffix is +/// required. +fn get_binary_name(example_name: &str) -> String { + let base = example_name; + if cfg!(target_os = "windows") { + format!("{base}.exe") + } else { + base.to_string() + } +} + +/// Rewrite the per-file coverage colors in `index.html` with project-specific +/// thresholds: 0-50% red, 51-80% yellow, 81-100% green. +fn recolor_report_index(index_path: &Path) -> std::io::Result<()> { + let content = fs::read_to_string(index_path)?; + fs::write(index_path, recolor_coverage_table(&content)) +} + +/// Recolor every `
XX% ...
` cell +/// in the coverage summary table according to the new thresholds. Cells with +/// no data (e.g. branch coverage `- (0/0)`, class `gray`) are left as-is. +fn recolor_coverage_table(input: &str) -> String { + const TD: &str = "
") else {
+            out.push_str(rest);
+            return out;
+        };
+        let color = &rest[..pre_end];
+        let tail = &rest[pre_end + "'>
".len()..];
+        let pct: String = tail
+            .trim_start()
+            .chars()
+            .take_while(|c| c.is_ascii_digit() || *c == '.')
+            .collect();
+        let new_color = match pct.parse::() {
+            Ok(v) if v <= 50.0 => "red",
+            Ok(v) if v <= 80.0 => "yellow",
+            Ok(_) => "green",
+            Err(_) => color, // no data (e.g. gray branch column)
+        };
+        out.push_str(new_color);
+        out.push_str("'>
");
+        rest = tail;
+    }
+    out.push_str(rest);
+    out
+}
+
+/// True if the file is executable: mode bits on Unix, `.exe` on Windows.
+fn is_executable(path: &Path) -> bool {
+    #[cfg(unix)]
+    {
+        use std::os::unix::fs::PermissionsExt as _;
+        let Ok(metadata) = std::fs::metadata(path) else {
+            return false;
+        };
+        metadata.permissions().mode() & 0o111 != 0
+    }
+    #[cfg(not(unix))]
+    {
+        path.extension()
+            .is_some_and(|e| e.eq_ignore_ascii_case("exe"))
+    }
+}
+
+/// Regex that keeps only the project's own sources in the report:
+/// excludes the shared llvm-cov target dir, the standard library, and
+/// external dependencies.
+fn build_ignore_regex(cov_target: &Path) -> String {
+    let target = regex_escape_path(cov_target);
+    format!(
+        "^{target}($|/)|/rustc/([0-9a-f]+|[0-9]+\\.[0-9]+\\.[0-9]+)/|/\\.cargo/(registry|git)/|/\\.rustup/toolchains($|/)"
+    )
+}
+
+/// Escape a path for use inside a regular expression (as a literal prefix).
+fn regex_escape_path(path: &Path) -> String {
+    let s = path.to_string_lossy().replace('\\', "/");
+    let mut escaped = String::with_capacity(s.len());
+    for ch in s.chars() {
+        if ch == '.' || ch == '-' {
+            escaped.push('\\');
+        }
+        escaped.push(ch);
+    }
+    escaped
+}
+
+fn find_git_repo() -> Option {
+    let mut current_dir = std::env::current_dir().ok()?;
+
+    loop {
+        let git_dir = current_dir.join(".git");
+        if git_dir.exists() && git_dir.is_dir() {
+            return Some(current_dir);
+        }
+
+        if !current_dir.pop() {
+            break;
+        }
+    }
+
+    None
+}
+
+#[cfg(test)]
+mod tests {
+    use super::recolor_coverage_table;
+
+    #[test]
+    fn recolor_thresholds() {
+        let input = concat!(
+            "
  50.00% (2/4)
", + "
  51.23% (32/52)
", + "
  80.00% (48/89)
", + "
  81.00% (1/1)
", + "
  90.00% (6/7)
", + "
- (0/0)
", + ); + let out = recolor_coverage_table(input); + assert!(out.contains("class='column-entry-red'>
  50.00%"));
+        assert!(out.contains("class='column-entry-yellow'>
  51.23%"));
+        assert!(out.contains("class='column-entry-yellow'>
  80.00%"));
+        assert!(out.contains("class='column-entry-green'>
  81.00%"));
+        assert!(out.contains("class='column-entry-green'>
  90.00%"));
+        assert!(out.contains("class='column-entry-gray'>
- (0/0)"));
+    }
+}
diff --git a/dev/run/src/bin/deploy-api-docs.rs b/dev/run/src/bin/deploy-api-docs.rs
new file mode 100644
index 0000000..961eb04
--- /dev/null
+++ b/dev/run/src/bin/deploy-api-docs.rs
@@ -0,0 +1,118 @@
+use std::path::Path;
+
+use arg_picker::{Picker, macros::arg};
+use tools::{println_cargo_style, run_cmd};
+
+const OUTPUT_DIR: &str = "docs/api-docs";
+
+fn main() {
+    let using_docsrs = Picker::from_args()
+        .pick_or_default(&arg![docsrs: bool])
+        .unwrap();
+
+    let repo_root = find_git_repo().expect("Failed to find git repository root");
+
+    // Read features from [package.metadata.docs.rs]
+    let features = tools::read_features().unwrap_or_else(|e| {
+        eprintln!("Error: {}", e);
+        std::process::exit(1);
+    });
+
+    let features_arg = features.join(",");
+
+    // Ensure output directory exists
+    let output_path = repo_root.join(OUTPUT_DIR);
+    std::fs::create_dir_all(&output_path).expect("Failed to create output directory");
+
+    // Build cargo doc command
+    let cmd = if using_docsrs {
+        format!(
+            "cargo +nightly rustdoc --features \"{}\" -p mingling --target-dir \"{}\" --color always -- --cfg docsrs",
+            features_arg,
+            output_path.join("target").to_string_lossy()
+        )
+    } else {
+        format!(
+            "cargo doc --no-deps --features \"{}\" -p mingling --target-dir \"{}\" --color always",
+            features_arg,
+            output_path.join("target").to_string_lossy()
+        )
+    };
+
+    println_cargo_style!("Features: {}", features_arg);
+    println_cargo_style!("Output: {}", output_path.display());
+
+    // Run cargo doc, then copy generated docs to output directory
+    println_cargo_style!("Building: docs (cargo doc --no-deps)");
+    run_cmd!(&cmd).unwrap_or_else(|code| {
+        eprintln!("Error: cargo doc failed with exit code {}", code);
+        std::process::exit(code);
+    });
+
+    // Copy generated docs from target/doc to OUTPUT_DIR (top level)
+    let doc_source = output_path.join("target").join("doc");
+    let doc_dest = &output_path;
+
+    if doc_source.exists() {
+        println_cargo_style!("Copying: docs to output directory");
+        // Remove old docs in destination (except target/)
+        if let Ok(entries) = std::fs::read_dir(doc_dest) {
+            for entry in entries.flatten() {
+                let path = entry.path();
+                if path.file_name().and_then(|n| n.to_str()) == Some("target") {
+                    continue;
+                }
+                if path.is_dir() {
+                    std::fs::remove_dir_all(&path).ok();
+                } else {
+                    std::fs::remove_file(&path).ok();
+                }
+            }
+        }
+        copy_dir_recursively(&doc_source, doc_dest).expect("Failed to copy documentation");
+    }
+
+    // Clean up the intermediate target directory to save space
+    std::fs::remove_dir_all(output_path.join("target")).ok();
+
+    println_cargo_style!("Done: API docs deployed to {}", output_path.display());
+}
+
+fn copy_dir_recursively(src: &Path, dst: &Path) -> std::io::Result<()> {
+    if !dst.exists() {
+        std::fs::create_dir_all(dst)?;
+    }
+
+    for entry in std::fs::read_dir(src)? {
+        let entry = entry?;
+        let file_type = entry.file_type()?;
+        let src_path = entry.path();
+        let file_name = src_path.file_name().expect("Failed to get file name");
+        let dst_path = dst.join(file_name);
+
+        if file_type.is_dir() {
+            copy_dir_recursively(&src_path, &dst_path)?;
+        } else {
+            std::fs::copy(&src_path, &dst_path)?;
+        }
+    }
+
+    Ok(())
+}
+
+fn find_git_repo() -> Option {
+    let mut current_dir = std::env::current_dir().ok()?;
+
+    loop {
+        let git_dir = current_dir.join(".git");
+        if git_dir.exists() && git_dir.is_dir() {
+            return Some(current_dir);
+        }
+
+        if !current_dir.pop() {
+            break;
+        }
+    }
+
+    None
+}
diff --git a/dev/run/src/bin/display-dependency-order.rs b/dev/run/src/bin/display-dependency-order.rs
new file mode 100644
index 0000000..a31c67a
--- /dev/null
+++ b/dev/run/src/bin/display-dependency-order.rs
@@ -0,0 +1,12 @@
+use tools::{dependency_order::display_dependency_order, eprintln_cargo_style};
+
+fn main() {
+    let order = display_dependency_order();
+    if order.is_empty() {
+        eprintln_cargo_style!("could not find workspace root or mingling crates");
+        std::process::exit(1);
+    }
+    for path in order {
+        println!("{}", path.display());
+    }
+}
diff --git a/dev/run/src/bin/http-page-preview.ps1 b/dev/run/src/bin/http-page-preview.ps1
new file mode 100644
index 0000000..8cc3579
--- /dev/null
+++ b/dev/run/src/bin/http-page-preview.ps1
@@ -0,0 +1,3 @@
+$starting_dir = Get-Location
+python -m http.server 3000
+Set-Location $starting_dir
diff --git a/dev/run/src/bin/http-page-preview.sh b/dev/run/src/bin/http-page-preview.sh
new file mode 100755
index 0000000..bed4b1c
--- /dev/null
+++ b/dev/run/src/bin/http-page-preview.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+python3 -m http.server 3000
diff --git a/dev/run/src/bin/install-mling.ps1 b/dev/run/src/bin/install-mling.ps1
new file mode 100644
index 0000000..2b55a09
--- /dev/null
+++ b/dev/run/src/bin/install-mling.ps1
@@ -0,0 +1,10 @@
+$ErrorActionPreference = "Stop"
+
+cargo build --release --manifest-path mingling_cli/Cargo.toml
+
+New-Item -ItemType Directory -Force -Path .temp/mling/bin, .temp/mling/scripts | Out-Null
+
+Copy-Item .temp/target/release/mling.exe .temp/mling/bin/
+Copy-Item .temp/target/release/mingling-cli.exe .temp/mling/bin/
+Copy-Item .temp/target/mingling/mling_comp.ps1 .temp/mling/scripts/mling_comp.ps1
+Copy-Item mingling_cli/scripts/load_mling.ps1 .temp/mling/
diff --git a/dev/run/src/bin/install-mling.sh b/dev/run/src/bin/install-mling.sh
new file mode 100755
index 0000000..e8cfa18
--- /dev/null
+++ b/dev/run/src/bin/install-mling.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+set -e
+
+cargo build --release --manifest-path mingling_cli/Cargo.toml
+
+mkdir -p .temp/mling/bin .temp/mling/scripts
+
+cp .temp/target/release/mling .temp/mling/bin/
+cp .temp/target/release/mingling-cli .temp/mling/bin/
+
+for comp in zsh sh fish; do
+    cp ".temp/target/mingling/mling_comp.$comp" ".temp/mling/scripts/mling_comp.$comp"
+done
+cp mingling_cli/scripts/load_mling.zsh .temp/mling/
+cp mingling_cli/scripts/load_mling.sh .temp/mling/
+cp mingling_cli/scripts/load_mling.fish .temp/mling/
diff --git a/dev/run/src/bin/package-all.rs b/dev/run/src/bin/package-all.rs
new file mode 100644
index 0000000..ecdd133
--- /dev/null
+++ b/dev/run/src/bin/package-all.rs
@@ -0,0 +1,736 @@
+use std::collections::HashMap;
+use std::path::{Path, PathBuf};
+
+use flate2::read::GzDecoder;
+use serde::Deserialize;
+use tar::Archive;
+use toml::Table as TomlTable;
+use tools::{
+    dependency_order::find_workspace_root, eprintln_cargo_style, println_cargo_style,
+    run_cmd_capture_with_dir, wprintln_cargo_style,
+};
+
+/// A single member from `cargo metadata` output.
+#[derive(Deserialize, Debug)]
+struct MetadataPackage {
+    name: String,
+    version: String,
+    manifest_path: String,
+}
+
+/// The top-level metadata structure.
+#[derive(Deserialize, Debug)]
+struct Metadata {
+    #[allow(dead_code)]
+    workspace_root: String,
+    packages: Vec,
+}
+
+fn main() {
+    // 1. Determine project root
+    let cwd = std::env::current_dir().expect("failed to get current working directory");
+    let workspace_root = find_workspace_root(&cwd).expect("not inside a Cargo workspace");
+    println_cargo_style!("Workspace: {}", workspace_root.display());
+
+    let pre_release_dir = workspace_root.join(".temp/pre-release");
+
+    // 2. Clean `.temp/pre-release/`
+    println_cargo_style!("Clean: .temp/pre-release/");
+    let _ = std::fs::remove_dir_all(&pre_release_dir);
+    std::fs::create_dir_all(&pre_release_dir).expect("failed to create .temp/pre-release/");
+
+    // 3. Run `cargo metadata` to get workspace members info
+    println_cargo_style!("Metadata: querying workspace members");
+    let metadata_json = run_cmd_capture_with_dir(
+        "cargo metadata --format-version 1 --no-deps",
+        &workspace_root,
+    )
+    .unwrap_or_else(|(code, _msg)| {
+        eprintln_cargo_style!(format!("cargo metadata failed (exit {code}):\n{{msg}}"));
+        std::process::exit(1);
+    });
+
+    let metadata: Metadata = serde_json::from_str(&metadata_json).unwrap_or_else(|e| {
+        eprintln_cargo_style!("failed to parse cargo metadata: {}", e);
+        std::process::exit(1);
+    });
+
+    // Filter workspace members: skip the root virtual manifest
+    let workspace_root_str = workspace_root.to_string_lossy().replace('\\', "/");
+    let members: Vec<&MetadataPackage> = metadata
+        .packages
+        .iter()
+        .filter(|p| {
+            let mp = p.manifest_path.replace('\\', "/");
+            mp.starts_with(&workspace_root_str)
+                && mp != format!("{}/Cargo.toml", workspace_root_str)
+        })
+        .collect();
+
+    if members.is_empty() {
+        eprintln_cargo_style!("No workspace members found!");
+        std::process::exit(1);
+    }
+
+    // Print member info
+    for m in &members {
+        println_cargo_style!("Member: {}@{}", m.name, m.version);
+    }
+
+    // Build version map: crate_name -> version
+    let mut version_map: HashMap = HashMap::new();
+    for m in &members {
+        version_map.insert(m.name.clone(), m.version.clone());
+    }
+
+    // Collect unique member directories that need to be copied
+    let mut member_dirs: Vec = Vec::new();
+    for m in &members {
+        let dir = Path::new(&m.manifest_path)
+            .parent()
+            .expect("manifest_path has no parent");
+        let canonical_dir = std::fs::canonicalize(dir).unwrap_or_else(|_| dir.to_path_buf());
+        let canonical_root =
+            std::fs::canonicalize(&workspace_root).unwrap_or_else(|_| workspace_root.to_path_buf());
+        let relative = canonical_dir
+            .strip_prefix(&canonical_root)
+            .map(|p| p.to_path_buf())
+            .unwrap_or_else(|_| PathBuf::from(dir.file_name().unwrap_or_default()));
+        if !member_dirs.contains(&relative) {
+            member_dirs.push(relative);
+        }
+    }
+
+    // 4. Copy files to the temp directory, preserving the workspace directory structure
+    println_cargo_style!("Copy: project structure to .temp/pre-release/");
+
+    copy_dir(
+        &workspace_root.join(".cargo"),
+        &pre_release_dir.join(".cargo"),
+    );
+
+    for dir in &member_dirs {
+        let src = workspace_root.join(dir);
+        let dst = pre_release_dir.join(dir);
+        copy_dir(&src, &dst);
+    }
+
+    copy_file(
+        &workspace_root.join("Cargo.toml"),
+        &pre_release_dir.join("Cargo.toml"),
+    );
+    copy_file(
+        &workspace_root.join("Cargo.lock"),
+        &pre_release_dir.join("Cargo.lock"),
+    );
+
+    // 5. Fully resolve ALL workspace inheritance in every member's Cargo.toml,
+    //    so each crate becomes monomorphic (no `workspace = true` references).
+    //    Then strip `[workspace.dependencies]` and `[workspace.package]` from the
+    //    root Cargo.toml, since they are no longer needed.
+    println_cargo_style!("Resolve: inline all workspace inheritance");
+
+    // Parse workspace config from the COPIED root Cargo.toml
+    let root_cargo_path = pre_release_dir.join("Cargo.toml");
+    let root_content = std::fs::read_to_string(&root_cargo_path)
+        .unwrap_or_else(|e| panic!("failed to read {}: {e}", root_cargo_path.display()));
+
+    let (ws_package, ws_deps) = parse_workspace_config(&root_content);
+
+    // Resolve each member's Cargo.toml
+    for dir in &member_dirs {
+        let member_cargo = pre_release_dir.join(dir).join("Cargo.toml");
+        if !member_cargo.exists() {
+            continue;
+        }
+        let member_content = std::fs::read_to_string(&member_cargo)
+            .unwrap_or_else(|e| panic!("failed to read {}: {e}", member_cargo.display()));
+        let resolved =
+            resolve_member_manifest(&member_content, dir, &ws_package, &ws_deps, &version_map);
+        std::fs::write(&member_cargo, &resolved)
+            .unwrap_or_else(|e| panic!("failed to write {}: {e}", member_cargo.display()));
+    }
+
+    // Strip [workspace.dependencies], [workspace.package], and root [package]
+    // from root Cargo.toml, making it a pure virtual manifest.
+    let stripped = strip_workspace_config(&root_content);
+    std::fs::write(&root_cargo_path, &stripped)
+        .unwrap_or_else(|e| panic!("failed to write {}: {e}", root_cargo_path.display()));
+
+    println_cargo_style!("Package: running cargo package --workspace --no-verify");
+
+    // 6. Run cargo package in the temp directory
+    let package_ok = run_cmd_capture_with_dir(
+        "cargo package --workspace --no-verify --color always",
+        &pre_release_dir,
+    );
+
+    match &package_ok {
+        Ok(out) => {
+            println!("{out}");
+        }
+        Err((code, msg)) => {
+            // Print output but don't fail yet
+            eprintln_cargo_style!(format!("cargo package exited with code {code}:"));
+            println!("{msg}");
+        }
+    }
+
+    // 7. Copy built packages back to .temp/target/package
+    let temp_package_dir = workspace_root.join(".temp/target/package");
+    std::fs::create_dir_all(&temp_package_dir)
+        .unwrap_or_else(|e| panic!("failed to create {}: {e}", temp_package_dir.display()));
+
+    // cargo package puts .crate files in target/package
+    let pre_release_target_package = pre_release_dir.join(".temp/target/package");
+    if pre_release_target_package.exists() {
+        println_cargo_style!("Copy: packages to .temp/target/package");
+        copy_dir_contents(&pre_release_target_package, &temp_package_dir);
+    } else {
+        wprintln_cargo_style!("No packages found in .temp/pre-release/.temp/target/package");
+    }
+
+    // 8. Export each crate as a standalone project from the .crate packages.
+    //    The .crate files contain the final publish-ready Cargo.toml with all
+    //    workspace/path deps already resolved by `cargo package`.
+    let release_dir = workspace_root.join(".temp/release");
+    println_cargo_style!("Export: standalone crates to .temp/release/");
+    let _ = std::fs::remove_dir_all(&release_dir);
+    std::fs::create_dir_all(&release_dir)
+        .unwrap_or_else(|e| panic!("failed to create {}: {e}", release_dir.display()));
+
+    for entry in std::fs::read_dir(&temp_package_dir).expect("failed to read target/package") {
+        let entry = entry.expect("failed to read entry");
+        let path = entry.path();
+        if path.extension().is_none_or(|e| e != "crate") {
+            continue;
+        }
+
+        // .crate files are gzipped tarballs. Extract to .temp/release//
+        // fname is like "mingling-0.3.0"
+        let fname = path.file_stem().unwrap().to_string_lossy().to_string();
+
+        // Derive crate directory name by stripping the version suffix
+        // mingling-0.3.0 -> mingling, arg-picker-0.1.0 -> arg-picker
+        let crate_dir_name = fname
+            .rfind('-')
+            .and_then(|dash| {
+                // Check if what follows looks like a semver
+                let ver_part = &fname[dash + 1..];
+                if ver_part.chars().next().is_some_and(|c| c.is_ascii_digit()) {
+                    Some(&fname[..dash])
+                } else {
+                    None
+                }
+            })
+            .unwrap_or(&fname)
+            .to_string();
+
+        let target_dir = release_dir.join(&crate_dir_name);
+        std::fs::create_dir_all(&target_dir)
+            .unwrap_or_else(|e| panic!("failed to create {}: {e}", target_dir.display()));
+
+        // Extract using flate2 + tar (cross-platform)
+        let file = match std::fs::File::open(&path) {
+            Ok(f) => f,
+            Err(e) => {
+                eprintln_cargo_style!("Failed to open {}: {e}", path.display());
+                continue;
+            }
+        };
+        let decoder = GzDecoder::new(file);
+        let mut archive = Archive::new(decoder);
+        if let Err(e) = archive.unpack(&target_dir) {
+            eprintln_cargo_style!("Failed to extract {}: {e}", fname);
+            continue;
+        }
+
+        // Move the contents from the inner dir up one level
+        // .crate contains a single top-level dir named after the package
+        let inner = target_dir.join(&fname);
+        if inner.exists() {
+            for inner_entry in std::fs::read_dir(&inner).expect("failed to read inner dir") {
+                let inner_entry = inner_entry.expect("failed to read entry");
+                let inner_path = inner_entry.path();
+                let dest = target_dir.join(inner_path.file_name().unwrap());
+                if dest.exists() {
+                    let _ = std::fs::remove_dir_all(&dest);
+                }
+                std::fs::rename(&inner_path, &dest).unwrap_or_else(|e| {
+                    panic!(
+                        "failed to rename {} -> {}: {e}",
+                        inner_path.display(),
+                        dest.display()
+                    )
+                });
+            }
+            let _ = std::fs::remove_dir_all(&inner);
+        }
+
+        // Clean up: remove .orig file (only the normalized Cargo.toml is needed)
+        let _ = std::fs::remove_file(target_dir.join("Cargo.toml.orig"));
+        // Also remove Cargo.lock — standalone crate doesn't need it for publish
+        let _ = std::fs::remove_file(target_dir.join("Cargo.lock"));
+
+        // Append an empty [workspace] section so each crate is a valid workspace root
+        let cargo_toml_path = target_dir.join("Cargo.toml");
+        if cargo_toml_path.exists() {
+            let mut cargo_content = std::fs::read_to_string(&cargo_toml_path)
+                .unwrap_or_else(|e| panic!("failed to read {}: {e}", cargo_toml_path.display()));
+            // Only append if there isn't already a [workspace] section
+            if !cargo_content.contains("\n[workspace]\n")
+                && !cargo_content.ends_with("\n[workspace]\n")
+            {
+                cargo_content.push_str("\n[workspace]\n");
+                std::fs::write(&cargo_toml_path, &cargo_content).unwrap_or_else(|e| {
+                    panic!("failed to write {}: {e}", cargo_toml_path.display())
+                });
+            }
+        }
+
+        println_cargo_style!("Export: {}", crate_dir_name);
+    }
+
+    println_cargo_style!("Done: .temp/release/ is ready");
+
+    // If package failed, report it
+    if package_ok.is_err() {
+        eprintln_cargo_style!("cargo package reported errors above");
+        std::process::exit(1);
+    }
+}
+
+/// Parse `[workspace.package]` and `[workspace.dependencies]` from the root Cargo.toml.
+/// Returns (package_fields, dep_values).
+fn parse_workspace_config(
+    content: &str,
+) -> (HashMap, HashMap) {
+    let table: TomlTable = content.parse().expect("failed to parse root Cargo.toml");
+
+    let mut package_fields = HashMap::new();
+    if let Some(workspace) = table.get("workspace").and_then(|w| w.as_table())
+        && let Some(pkg_table) = workspace.get("package").and_then(|p| p.as_table())
+    {
+        for (k, v) in pkg_table {
+            if let Some(s) = v.as_str() {
+                package_fields.insert(k.clone(), s.to_string());
+            }
+        }
+    }
+
+    let mut dep_values = HashMap::new();
+    if let Some(workspace) = table.get("workspace").and_then(|w| w.as_table())
+        && let Some(deps_table) = workspace.get("dependencies").and_then(|d| d.as_table())
+    {
+        for (k, v) in deps_table {
+            dep_values.insert(k.clone(), v.clone());
+        }
+    }
+
+    (package_fields, dep_values)
+}
+
+/// Serialize a `toml::Value` into Cargo-toml-compatible inline representation.
+fn toml_value_str(v: &toml::Value) -> String {
+    match v {
+        toml::Value::String(s) => format!("\"{}\"", s),
+        toml::Value::Table(t) => {
+            let items: Vec = t
+                .iter()
+                .map(|(k, val)| format!("{} = {}", k, toml_value_str(val)))
+                .collect();
+            format!("{{ {} }}", items.join(", "))
+        }
+        toml::Value::Array(a) => {
+            let items: Vec = a.iter().map(toml_value_str).collect();
+            format!("[{}]", items.join(", "))
+        }
+        toml::Value::Boolean(b) => b.to_string(),
+        toml::Value::Integer(i) => i.to_string(),
+        toml::Value::Float(f) => f.to_string(),
+        toml::Value::Datetime(dt) => format!("\"{}\"", dt),
+    }
+}
+
+/// Compute the relative path from `member_rel_dir` to `target_path`.
+/// Both are relative to workspace root.
+/// e.g. member_rel_dir="mingling", target_path="mingling_core" → "../mingling_core"
+fn make_path_relative_to_member(target_path: &str, member_rel_dir: &Path) -> String {
+    if member_rel_dir.as_os_str().is_empty() || member_rel_dir == Path::new(".") {
+        return target_path.to_string();
+    }
+    let depth = member_rel_dir.components().count();
+    let mut result = PathBuf::new();
+    for _ in 0..depth {
+        result.push("..");
+    }
+    result.push(target_path);
+    result.to_string_lossy().to_string()
+}
+
+/// Resolve a dependency definition from `[workspace.dependencies]` to an inline string.
+/// If the definition contains a path to a workspace member, add `version = "..."`
+/// and adjust the path to be relative to the member's directory.
+fn resolve_dep_def(
+    dep_name: &str,
+    dep_def: &toml::Value,
+    member_rel_dir: &Path,
+    version_map: &HashMap,
+) -> String {
+    match dep_def {
+        toml::Value::String(ver) => {
+            format!("\"{}\"", ver)
+        }
+        toml::Value::Table(t) => {
+            let mut resolved = t.clone();
+            let has_path = t.contains_key("path");
+            let is_ws_member = version_map.contains_key(dep_name);
+
+            // Fix path to be relative to member's directory
+            if has_path && let Some(path_val) = t.get("path").and_then(|v| v.as_str()) {
+                let rel = make_path_relative_to_member(path_val, member_rel_dir);
+                resolved.insert("path".to_string(), toml::Value::String(rel));
+            }
+
+            // Add version for workspace member path deps
+            if has_path
+                && is_ws_member
+                && let Some(version) = version_map.get(dep_name)
+            {
+                resolved.insert("version".to_string(), toml::Value::String(version.clone()));
+            }
+
+            let items: Vec = resolved
+                .iter()
+                .map(|(k, val)| format!("{} = {}", k, toml_value_str(val)))
+                .collect();
+            format!("{{ {} }}", items.join(", "))
+        }
+        _ => toml_value_str(dep_def),
+    }
+}
+
+/// Merge an inline `{ workspace = true, optional = true, ... }` with the workspace definition.
+/// Returns the full resolved dependency value string (without the leading `dep_name = `).
+fn merge_inline_dep(
+    inline_rest: &str,
+    dep_name: &str,
+    dep_def: &toml::Value,
+    member_rel_dir: &Path,
+    version_map: &HashMap,
+) -> String {
+    // inline_rest is the part after `=`: `{ workspace = true, optional = true }`
+    let inner = inline_rest
+        .trim()
+        .strip_prefix('{')
+        .and_then(|s| s.strip_suffix('}'))
+        .unwrap_or("");
+
+    match dep_def {
+        toml::Value::String(ver) => {
+            // Workspace def is just a version string
+            // Collect extras: everything except `workspace = true`
+            let extras: Vec<&str> = inner
+                .split(',')
+                .map(|s| s.trim())
+                .filter(|s| !s.is_empty() && *s != "workspace = true")
+                .collect();
+
+            if extras.is_empty() {
+                format!("\"{}\"", ver)
+            } else {
+                // Serialize as inline table: version + extras
+                let mut parts = vec![format!("version = \"{}\"", ver)];
+                parts.extend(extras.iter().map(|s| s.to_string()));
+                format!("{{ {} }}", parts.join(", "))
+            }
+        }
+        toml::Value::Table(t) => {
+            // Start from workspace def
+            let mut merged = t.clone();
+
+            // Fix path to be relative to member's directory
+            if let Some(path_val) = t.get("path").and_then(|v| v.as_str()) {
+                let rel = make_path_relative_to_member(path_val, member_rel_dir);
+                merged.insert("path".to_string(), toml::Value::String(rel));
+            }
+
+            // If this dep is a workspace member with a path dep, add version
+            if t.contains_key("path")
+                && version_map.contains_key(dep_name)
+                && let Some(version) = version_map.get(dep_name)
+            {
+                merged.insert("version".to_string(), toml::Value::String(version.clone()));
+            }
+
+            // Apply extra fields from the inline
+            for piece in inner.split(',').map(|s| s.trim()) {
+                let piece = piece.trim();
+                if piece.is_empty() || piece == "workspace = true" {
+                    continue;
+                }
+                // Parse `key = value` pairs
+                if let Some((raw_key, raw_val)) = piece.split_once('=') {
+                    let k = raw_key.trim();
+                    let v = raw_val.trim();
+                    if k.is_empty() {
+                        continue;
+                    }
+                    // Try to infer the value type
+                    if v == "true" {
+                        merged.insert(k.to_string(), toml::Value::Boolean(true));
+                    } else if v == "false" {
+                        merged.insert(k.to_string(), toml::Value::Boolean(false));
+                    } else if v.starts_with('"') && v.ends_with('"') {
+                        merged.insert(
+                            k.to_string(),
+                            toml::Value::String(v[1..v.len() - 1].to_string()),
+                        );
+                    } else if v.starts_with('[') && v.ends_with(']') {
+                        // Simple array parsing: strings only
+                        let arr: Vec = v[1..v.len() - 1]
+                            .split(',')
+                            .map(|s| {
+                                let s = s.trim().trim_matches('"');
+                                toml::Value::String(s.to_string())
+                            })
+                            .collect();
+                        merged.insert(k.to_string(), toml::Value::Array(arr));
+                    } else if let Ok(n) = v.parse::() {
+                        merged.insert(k.to_string(), toml::Value::Integer(n));
+                    } else if let Ok(f) = v.parse::() {
+                        merged.insert(k.to_string(), toml::Value::Float(f));
+                    } else {
+                        // Treat as string
+                        merged.insert(k.to_string(), toml::Value::String(v.to_string()));
+                    }
+                }
+            }
+
+            let items: Vec = merged
+                .iter()
+                .map(|(k, val)| format!("{} = {}", k, toml_value_str(val)))
+                .collect();
+            format!("{{ {} }}", items.join(", "))
+        }
+        _ => toml_value_str(dep_def),
+    }
+}
+
+/// Resolve ALL workspace inheritance in a single member crate's Cargo.toml:
+///   - `version.workspace = true` → `version = "0.3.0"`
+///   - `dep.workspace = true` → inline the full definition from ws_deps
+///   - `dep = { workspace = true, ... }` → merge with ws_deps definition
+fn resolve_member_manifest(
+    content: &str,
+    member_rel_dir: &Path,
+    ws_package: &HashMap,
+    ws_deps: &HashMap,
+    version_map: &HashMap,
+) -> String {
+    let mut result = String::new();
+    let mut in_package = false;
+    let mut in_section_with_deps = false;
+
+    for line in content.lines() {
+        let trimmed = line.trim();
+        let indent: String = line.chars().take_while(|c| c.is_whitespace()).collect();
+
+        // Track sections
+        if trimmed.starts_with('[') {
+            in_package = trimmed == "[package]";
+            in_section_with_deps = trimmed.starts_with("[dependencies")
+                || trimmed.starts_with("[build-dependencies")
+                || trimmed.starts_with("[dev-dependencies");
+            result.push_str(line);
+            result.push('\n');
+            continue;
+        }
+
+        // [package] section: resolve `field.workspace = true`
+        if in_package && trimmed.ends_with(".workspace = true") {
+            let key = trimmed.strip_suffix(".workspace = true").unwrap().trim();
+            if let Some(value) = ws_package.get(key) {
+                result.push_str(&format!("{indent}{key} = \"{value}\"\n"));
+                continue;
+            }
+            // Also check workspace.dependencies (for fields like `version.workspace`)
+            // when the member has its own version field inherited from workspace.package
+        }
+
+        // Dependency sections
+        if in_section_with_deps {
+            // Shorthand: `foo.workspace = true`
+            if trimmed.ends_with(".workspace = true") {
+                let key = trimmed.strip_suffix(".workspace = true").unwrap().trim();
+                if let Some(dep_def) = ws_deps.get(key) {
+                    let resolved = resolve_dep_def(key, dep_def, member_rel_dir, version_map);
+                    result.push_str(&format!("{indent}{key} = {resolved}\n"));
+                    continue;
+                }
+            }
+
+            // Inline: `foo = { workspace = true, optional = true, ... }`
+            if let Some(eq_pos) = trimmed.find("= {")
+                && trimmed.contains("workspace = true")
+            {
+                let dep_name = trimmed[..eq_pos].trim();
+                if let Some(dep_def) = ws_deps.get(dep_name) {
+                    let after_eq = trimmed[eq_pos + 1..].trim();
+                    let merged =
+                        merge_inline_dep(after_eq, dep_name, dep_def, member_rel_dir, version_map);
+                    result.push_str(&format!("{indent}{dep_name} = {merged}\n"));
+                    continue;
+                }
+            }
+        }
+
+        result.push_str(line);
+        result.push('\n');
+    }
+
+    result
+}
+
+/// Remove `[workspace.dependencies]`, `[workspace.package]`, and the root `[package]`
+/// section from root Cargo.toml, making it a pure virtual manifest.
+/// Keeps `[workspace]` with `members`, `resolver`, `exclude` so packaging still works.
+fn strip_workspace_config(content: &str) -> String {
+    let mut result = String::new();
+    let mut in_ws_deps = false;
+    let mut in_ws_package = false;
+    let mut in_root_package = false;
+
+    for line in content.lines() {
+        let trimmed = line.trim();
+
+        if trimmed == "[workspace.dependencies]" {
+            in_ws_deps = true;
+            continue;
+        }
+        if trimmed == "[workspace.package]" {
+            in_ws_package = true;
+            continue;
+        }
+        if trimmed == "[package]" && !in_ws_deps && !in_ws_package {
+            // Remove the root [package] section entirely (virtual manifest)
+            in_root_package = true;
+            continue;
+        }
+
+        if in_ws_deps {
+            if trimmed.starts_with('[') {
+                in_ws_deps = false;
+            } else {
+                continue;
+            }
+        }
+
+        if in_ws_package {
+            if trimmed.starts_with('[') {
+                in_ws_package = false;
+            } else {
+                continue;
+            }
+        }
+
+        if in_root_package {
+            if trimmed.starts_with('[') {
+                in_root_package = false;
+            } else {
+                continue;
+            }
+        }
+
+        result.push_str(line);
+        result.push('\n');
+    }
+
+    result
+}
+
+/// Recursively copy a directory.
+fn copy_dir(src: &Path, dst: &Path) {
+    copy_dir_filtered(src, dst, &|_: &Path| true)
+}
+
+/// Recursively copy a directory with a filter function.
+/// The filter receives the source path and returns `true` if the entry should be copied.
+fn copy_dir_filtered(src: &Path, dst: &Path, filter: &dyn Fn(&Path) -> bool) {
+    if !src.exists() {
+        return;
+    }
+    if !filter(src) {
+        return;
+    }
+    std::fs::create_dir_all(dst)
+        .unwrap_or_else(|e| panic!("failed to create {}: {e}", dst.display()));
+
+    for entry in std::fs::read_dir(src).expect("failed to read directory") {
+        let entry = entry.expect("failed to read entry");
+        let entry_type = entry.file_type().expect("failed to get file type");
+        let src_path = entry.path();
+        let dst_path = dst.join(entry.file_name());
+
+        if !filter(&src_path) {
+            continue;
+        }
+
+        if entry_type.is_dir() {
+            copy_dir_filtered(&src_path, &dst_path, filter);
+        } else if entry_type.is_file() || entry_type.is_symlink() {
+            copy_file(&src_path, &dst_path);
+        }
+    }
+}
+
+/// Copy a file, creating parent directories as needed.
+/// If src is a symlink, copies the target content (follow symlinks).
+fn copy_file(src: &Path, dst: &Path) {
+    if let Some(parent) = dst.parent() {
+        std::fs::create_dir_all(parent)
+            .unwrap_or_else(|e| panic!("failed to create {}: {e}", parent.display()));
+    }
+
+    let resolved = if src.is_symlink() {
+        let target = std::fs::read_link(src)
+            .unwrap_or_else(|e| panic!("failed to read symlink {}: {e}", src.display()));
+        if target.is_relative() {
+            src.parent().unwrap().join(target)
+        } else {
+            target
+        }
+    } else {
+        src.to_path_buf()
+    };
+
+    std::fs::copy(&resolved, dst).unwrap_or_else(|e| {
+        panic!(
+            "failed to copy {} -> {}: {e}",
+            resolved.display(),
+            dst.display()
+        )
+    });
+}
+
+/// Copy all files/directories from one directory into another.
+fn copy_dir_contents(src: &Path, dst: &Path) {
+    if !src.exists() {
+        return;
+    }
+    std::fs::create_dir_all(dst)
+        .unwrap_or_else(|e| panic!("failed to create {}: {e}", dst.display()));
+
+    for entry in std::fs::read_dir(src).expect("failed to read directory") {
+        let entry = entry.expect("failed to read entry");
+        let entry_type = entry.file_type().expect("failed to get file type");
+        let src_path = entry.path();
+        let dst_path = dst.join(entry.file_name());
+
+        if entry_type.is_dir() {
+            copy_dir(&src_path, &dst_path);
+        } else if entry_type.is_file() || entry_type.is_symlink() {
+            copy_file(&src_path, &dst_path);
+        }
+    }
+}
diff --git a/dev/run/src/bin/update-version.rs b/dev/run/src/bin/update-version.rs
new file mode 100644
index 0000000..679f419
--- /dev/null
+++ b/dev/run/src/bin/update-version.rs
@@ -0,0 +1,104 @@
+use std::io::Write as _;
+use std::path::Path;
+
+use serde::Deserialize;
+use tools::println_cargo_style;
+
+#[derive(Deserialize)]
+struct VersionFile {
+    file: String,
+    pattern: String,
+}
+
+#[derive(Deserialize)]
+struct Config {
+    #[serde(rename = "file")]
+    files: Vec,
+}
+
+fn main() {
+    let args: Vec = std::env::args().collect();
+
+    // Get new version
+    let new_ver = if args.len() > 1 {
+        args[1].clone()
+    } else {
+        print!("Update version to: ");
+        std::io::stdout().flush().unwrap();
+        let mut input = String::new();
+        std::io::stdin().read_line(&mut input).unwrap();
+        input.trim().to_string()
+    };
+
+    if new_ver.is_empty() {
+        eprintln!("Error: Version cannot be empty.");
+        std::process::exit(1);
+    }
+
+    // Read current version from root Cargo.toml's workspace.package.version
+    let root_cargo_path = "Cargo.toml";
+    let root_cargo_content =
+        std::fs::read_to_string(root_cargo_path).expect("Failed to read Cargo.toml");
+    let cargo_value: toml::Value = root_cargo_content
+        .parse()
+        .expect("Failed to parse Cargo.toml");
+
+    let current_ver = cargo_value["workspace"]["package"]["version"]
+        .as_str()
+        .expect("workspace.package.version not found in Cargo.toml")
+        .to_string();
+
+    if new_ver == current_ver {
+        println!("Version is already {}. Nothing to do.", current_ver);
+        return;
+    }
+
+    println_cargo_style!("Version: {} -> {}", current_ver, new_ver);
+
+    // Read version-files.toml
+    let config_path = Path::new("dev/configs").join("version-files.toml");
+    let config_str = std::fs::read_to_string(&config_path)
+        .expect("Failed to read dev/configs/version-files.toml");
+    let config: Config =
+        toml::from_str(&config_str).expect("Failed to parse dev/configs/version-files.toml");
+
+    let mut updated_count = 0;
+    let mut skipped_count = 0;
+
+    for vf in &config.files {
+        let file_path = &vf.file;
+        let old_pattern = vf.pattern.replace("{VER}", ¤t_ver);
+        let new_pattern = vf.pattern.replace("{VER}", &new_ver);
+
+        let content = match std::fs::read_to_string(file_path) {
+            Ok(c) => c,
+            Err(e) => {
+                eprintln!("Warning: Could not read {}: {}", file_path, e);
+                skipped_count += 1;
+                continue;
+            }
+        };
+
+        let new_content = content.replace(&old_pattern, &new_pattern);
+
+        if new_content == content {
+            eprintln!(
+                "Warning: Pattern '{}' not found in {}",
+                old_pattern, file_path
+            );
+            skipped_count += 1;
+            continue;
+        }
+
+        std::fs::write(file_path, &new_content)
+            .unwrap_or_else(|e| panic!("Failed to write {}: {}", file_path, e));
+        println_cargo_style!("Updated: {}", file_path);
+        updated_count += 1;
+    }
+
+    println_cargo_style!(
+        "Done: {} file(s) updated, {} file(s) skipped",
+        updated_count,
+        skipped_count
+    );
+}
diff --git a/dev/run/src/bin/windows-folder-hide.ps1 b/dev/run/src/bin/windows-folder-hide.ps1
new file mode 100644
index 0000000..ff53202
--- /dev/null
+++ b/dev/run/src/bin/windows-folder-hide.ps1
@@ -0,0 +1,115 @@
+$skipDirs = @('.git', '.temp', 'target', 'node_modules', '.pnpm')
+$selfPath = (Get-Item -LiteralPath $MyInvocation.MyCommand.Path).Directory.FullName
+
+function Test-InSkipDir {
+    param(
+        [object]$Item
+    )
+    $path = if ($Item -is [string]) {
+        $Item
+    } elseif ($Item.PSPath) {
+        $Item.PSPath -replace '^.*::', ''
+    } else {
+        $Item.FullName
+    }
+
+    $parts = $path.Split([System.IO.Path]::DirectorySeparatorChar)
+    for ($i = 0; $i -lt $parts.Length - 1; $i++) {
+        if ($parts[$i] -in $skipDirs) {
+            return $true
+        }
+    }
+    return $false
+}
+
+function Invoke-UnhideRecursive {
+    param([string]$Path)
+    Get-ChildItem -LiteralPath $Path -Force | ForEach-Object {
+        if ($_.PSIsContainer) {
+            if ($_.Name -in $skipDirs) {
+                if ($_.Attributes -band [System.IO.FileAttributes]::Hidden) {
+                    Write-Host "    -> unhiding skip directory (self only): `"$($_.FullName)`""
+                    $_.Attributes = $_.Attributes -bxor [System.IO.FileAttributes]::Hidden
+                }
+                return
+            }
+            Invoke-UnhideRecursive $_.FullName
+        } else {
+            if ($_.Attributes -band [System.IO.FileAttributes]::Hidden) {
+                Write-Host "    -> unhiding: `"$($_.FullName)`""
+                $_.Attributes = $_.Attributes -bxor [System.IO.FileAttributes]::Hidden
+            }
+        }
+    }
+}
+
+function Test-GitPathSkippable {
+    param([string]$GitPath)
+    $parts = $GitPath.Split(@('/', '\'))
+    for ($i = 0; $i -lt $parts.Length - 1; $i++) {
+        if ($parts[$i] -in $skipDirs) {
+            return $true
+        }
+    }
+    return $false
+}
+
+Write-Host "Step 1: Unhiding all files and directories (skipping $($skipDirs -join ', '))..."
+
+Invoke-UnhideRecursive -Path (Get-Location).Path
+
+Write-Host "Step 2: Hiding git-ignored items..."
+
+git ls-files --others --ignored --exclude-standard | Where-Object {
+    -not (Test-GitPathSkippable $_)
+} | ForEach-Object {
+    $itemPath = $_
+    Write-Host "... checking: `"$itemPath`""
+    $item = Get-Item $_ -Force -ErrorAction SilentlyContinue
+    if (-not $item) { return }
+
+    if ($item.FullName -eq $selfPath) { return }
+
+    if (Test-InSkipDir $item) {
+        Write-Host "    -> skipping (inside skip directory)"
+        return
+    }
+
+    if ($item.PSIsContainer) {
+        if (-not ($item.Attributes -band [System.IO.FileAttributes]::Hidden)) {
+            Write-Host "    -> hiding directory (non-recursive)"
+            $item.Attributes = $item.Attributes -bor [System.IO.FileAttributes]::Hidden
+        }
+    } else {
+        if (-not ($item.Attributes -band [System.IO.FileAttributes]::Hidden)) {
+            Write-Host "    -> hiding"
+            $item.Attributes = $item.Attributes -bor [System.IO.FileAttributes]::Hidden
+        }
+    }
+}
+
+Write-Host "Step 3: Hiding dot-prefixed items..."
+Get-ChildItem -Path . -Force -Directory | Where-Object { $_.Name -match '^\.' } | ForEach-Object {
+    Write-Host "... checking: `"$($_.FullName)`""
+    if (Test-InSkipDir $_) {
+        Write-Host "    -> skipping (inside skip directory)"
+        return
+    }
+    if (-not ($_.Attributes -band [System.IO.FileAttributes]::Hidden)) {
+        Write-Host "    -> hiding directory"
+        $_.Attributes = $_.Attributes -bor [System.IO.FileAttributes]::Hidden
+    }
+}
+
+Get-ChildItem -Path . -Force -File | Where-Object { $_.Name -match '^\.' } | ForEach-Object {
+    if ($_.FullName -eq $selfPath) { return }
+    Write-Host "... checking: `"$($_.FullName)`""
+    if (Test-InSkipDir $_) {
+        Write-Host "    -> skipping (inside skip directory)"
+        return
+    }
+    if (-not ($_.Attributes -band [System.IO.FileAttributes]::Hidden)) {
+        Write-Host "    -> hiding file"
+        $_.Attributes = $_.Attributes -bor [System.IO.FileAttributes]::Hidden
+    }
+}
diff --git a/dev/run/src/dependency_order.rs b/dev/run/src/dependency_order.rs
new file mode 100644
index 0000000..145bdbd
--- /dev/null
+++ b/dev/run/src/dependency_order.rs
@@ -0,0 +1,196 @@
+use std::collections::{HashMap, HashSet};
+use std::path::{Path, PathBuf};
+
+/// Parse Cargo.toml content and return dependency names that start with `prefix`.
+fn parse_mingling_deps(content: &str, prefix: &str) -> Vec {
+    let value: toml::Value = match content.parse() {
+        Ok(v) => v,
+        Err(_) => return Vec::new(),
+    };
+
+    let mut names = Vec::new();
+
+    // Check [dependencies]
+    if let Some(deps) = value.get("dependencies").and_then(|d| d.as_table()) {
+        for key in deps.keys() {
+            if key.starts_with(prefix) {
+                names.push(key.clone());
+            }
+        }
+    }
+
+    // Check [build-dependencies]
+    if let Some(deps) = value.get("build-dependencies").and_then(|d| d.as_table()) {
+        for key in deps.keys() {
+            if key.starts_with(prefix) {
+                names.push(key.clone());
+            }
+        }
+    }
+
+    names
+}
+
+/// Read workspace members from the root Cargo.toml.
+fn get_workspace_members(workspace_root: &std::path::Path) -> Vec {
+    let cargo_path = workspace_root.join("Cargo.toml");
+    let content = match std::fs::read_to_string(&cargo_path) {
+        Ok(c) => c,
+        Err(_) => return Vec::new(),
+    };
+
+    let value: toml::Value = match content.parse() {
+        Ok(v) => v,
+        Err(_) => return Vec::new(),
+    };
+
+    value
+        .get("workspace")
+        .and_then(|w| w.get("members"))
+        .and_then(|m| m.as_array())
+        .map(|arr| {
+            arr.iter()
+                .filter_map(|v| v.as_str().map(String::from))
+                .collect()
+        })
+        .unwrap_or_default()
+}
+
+/// Hierarchical topological sort (process layer by layer, sort siblings alphabetically).
+///
+/// `dep_map` maps each crate to the list of crates it depends on.
+/// Returns the dependency order (dependent crates come first, dependents come later),
+/// with crates at the same layer (which can be built in parallel) sorted alphabetically.
+fn topological_sort(
+    all_crates: &HashSet,
+    dep_map: &HashMap>,
+) -> Vec {
+    // in_degree[crate] = number of remaining mingling_* dependencies not yet processed
+    let mut in_degree: HashMap<&str, usize> = HashMap::new();
+    // reverse[dependency] = list of crates that depend on it
+    let mut reverse: HashMap<&str, Vec<&str>> = HashMap::new();
+
+    for name in all_crates {
+        in_degree.entry(name.as_str()).or_insert(0);
+        reverse.entry(name.as_str()).or_default();
+    }
+
+    for (crate_name, deps) in dep_map {
+        for dep in deps {
+            if all_crates.contains(dep.as_str()) {
+                reverse
+                    .get_mut(dep.as_str())
+                    .unwrap()
+                    .push(crate_name.as_str());
+                *in_degree.get_mut(crate_name.as_str()).unwrap() += 1;
+            }
+        }
+    }
+
+    let mut result: Vec = Vec::new();
+
+    // Process layer by layer: all crates with in_degree == 0 in one batch form a layer
+    loop {
+        let mut current: Vec<&str> = all_crates
+            .iter()
+            .filter(|n| in_degree.get(n.as_str()).copied().unwrap_or(0) == 0)
+            .filter(|n| !result.iter().any(|r| r.as_str() == n.as_str()))
+            .map(|s| s.as_str())
+            .collect();
+
+        if current.is_empty() {
+            break;
+        }
+
+        current.sort();
+        result.extend(current.iter().map(|s| s.to_string()));
+
+        for &node in ¤t {
+            if let Some(dependents) = reverse.get(node) {
+                for &dependent in dependents {
+                    if let Some(degree) = in_degree.get_mut(dependent) {
+                        *degree -= 1;
+                    }
+                }
+            }
+        }
+    }
+
+    result
+}
+
+/// Strip the `\\?\` prefix that `std::fs::canonicalize` may add on Windows.
+fn strip_verbatim_prefix(p: &Path) -> PathBuf {
+    let s = p.to_string_lossy();
+    let s_ref: &str = &s;
+    if let Some(rest) = s_ref.strip_prefix("\\\\?\\") {
+        PathBuf::from(rest)
+    } else {
+        p.to_path_buf()
+    }
+}
+
+/// Find the workspace root by looking for a Cargo.toml with `[workspace]` members.
+/// Starts from `start` and walks up the directory tree.
+pub fn find_workspace_root(start: &std::path::Path) -> Option {
+    let mut current = Some(strip_verbatim_prefix(
+        &std::fs::canonicalize(start).unwrap_or_else(|_| start.to_path_buf()),
+    ));
+    while let Some(dir) = current {
+        let members = get_workspace_members(&dir);
+        if !members.is_empty() {
+            return Some(dir);
+        }
+        current = dir.parent().map(|p| p.to_path_buf());
+    }
+    None
+}
+
+/// Output all crate paths in dependency order
+#[allow(unused)]
+pub fn display_dependency_order() -> Vec {
+    let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
+
+    let workspace_root = match find_workspace_root(&cwd) {
+        Some(root) => root,
+        None => return Vec::new(),
+    };
+
+    // Read workspace members from root Cargo.toml
+    let members = get_workspace_members(&workspace_root);
+
+    // Filter to crates starting with "mingling" or "arg"
+    let mingling_crates: HashSet = members
+        .into_iter()
+        .filter(|m| m.starts_with("mingling") || m.starts_with("arg"))
+        .collect();
+
+    if mingling_crates.is_empty() {
+        return Vec::new();
+    }
+
+    // Build dependency graph
+    let mut dep_map: HashMap> = HashMap::new();
+
+    for crate_name in &mingling_crates {
+        let cargo_path = workspace_root.join(crate_name).join("Cargo.toml");
+        let content = match std::fs::read_to_string(&cargo_path) {
+            Ok(c) => c,
+            Err(_) => {
+                dep_map.insert(crate_name.clone(), Vec::new());
+                continue;
+            }
+        };
+        let deps = parse_mingling_deps(&content, "mingling");
+        // Only keep deps that are actually in our set
+        let filtered: Vec = deps
+            .into_iter()
+            .filter(|d| mingling_crates.contains(d.as_str()))
+            .collect();
+        dep_map.insert(crate_name.clone(), filtered);
+    }
+
+    let sorted = topological_sort(&mingling_crates, &dep_map);
+
+    sorted.into_iter().map(PathBuf::from).collect()
+}
diff --git a/dev/run/src/lib.rs b/dev/run/src/lib.rs
new file mode 100644
index 0000000..b17a61f
--- /dev/null
+++ b/dev/run/src/lib.rs
@@ -0,0 +1,459 @@
+pub mod dependency_order;
+pub mod verify;
+
+use colored::Colorize;
+
+use std::io::IsTerminal as _;
+
+#[macro_export]
+macro_rules! run_cmd {
+    ($fmt:literal, $($arg:tt)*) => {
+        $crate::run_cmd(format!($fmt, $($arg)*))
+    };
+    ($cmd:expr) => {
+        $crate::run_cmd($cmd)
+    };
+}
+
+/// Run a shell command and capture its combined stdout+stderr output.
+/// Returns `Ok(output)` on success, `Err((exit_code, stderr))` on failure.
+#[macro_export]
+macro_rules! run_cmd_and_capture_stderr {
+    ($fmt:literal, $($arg:tt)*) => {
+        $crate::run_cmd_capture(format!($fmt, $($arg)*))
+    };
+    ($cmd:expr) => {
+        $crate::run_cmd_capture($cmd)
+    };
+}
+
+#[macro_export]
+macro_rules! println_cargo_style {
+    ($fmt:literal, $($arg:tt)*) => {
+        $crate::println_cargo_style(format!($fmt, $($arg)*))
+    };
+    ($cmd:expr) => {
+        $crate::println_cargo_style($cmd)
+    };
+}
+
+#[macro_export]
+macro_rules! eprintln_cargo_style {
+    ($fmt:literal, $($arg:tt)*) => {
+        $crate::eprintln_cargo_style(format!($fmt, $($arg)*))
+    };
+    ($cmd:expr) => {
+        $crate::eprintln_cargo_style($cmd)
+    };
+}
+
+#[macro_export]
+macro_rules! wprintln_cargo_style {
+    ($fmt:literal, $($arg:tt)*) => {
+        $crate::wprintln_cargo_style(format!($fmt, $($arg)*))
+    };
+    ($cmd:expr) => {
+        $crate::wprintln_cargo_style($cmd)
+    };
+}
+
+/// Print a message in cargo style format, with bold green prefix.
+///
+/// # Panics
+///
+/// Panics if the prefix (text before the first `:`) exceeds 12 characters.
+pub fn println_cargo_style(str: impl Into) {
+    let s = str.into();
+    let (prefix, content) = if let Some(pos) = s.find(':') {
+        (
+            s[..pos].trim().to_string(),
+            s[pos + 1..].trim_start().to_string(),
+        )
+    } else {
+        (String::new(), s.trim().to_string())
+    };
+
+    assert!(
+        prefix.len() <= 12,
+        "prefix length exceeds 12: '{}' has length {}",
+        prefix,
+        prefix.len()
+    );
+
+    let padding = " ".repeat(12 - prefix.len());
+
+    println!(
+        "{}{} {}",
+        padding,
+        prefix.bold().bright_green(),
+        content.trim()
+    );
+}
+
+pub fn eprintln_cargo_style(str: impl Into) {
+    println!("{}: {}", "error".bold().bright_red(), str.into());
+}
+
+/// Print a message in cargo style format, with bold yellow prefix (warning style).
+///
+/// # Panics
+///
+/// Panics if the prefix (text before the first `:`) exceeds 12 characters.
+pub fn wprintln_cargo_style(str: impl Into) {
+    let s = str.into();
+    let (prefix, content) = if let Some(pos) = s.find(':') {
+        (
+            s[..pos].trim().to_string(),
+            s[pos + 1..].trim_start().to_string(),
+        )
+    } else {
+        (String::new(), s.trim().to_string())
+    };
+
+    assert!(
+        prefix.len() <= 12,
+        "prefix length exceeds 12: '{}' has length {}",
+        prefix,
+        prefix.len()
+    );
+
+    let padding = " ".repeat(12 - prefix.len());
+
+    println!(
+        "{}{} {}",
+        padding,
+        prefix.bold().bright_yellow(),
+        content.trim()
+    );
+}
+
+/// Run a shell command in the current directory and return its exit status.
+///
+/// # Panics
+///
+/// Panics if the shell command cannot be spawned (e.g. the shell binary is not found).
+///
+/// # Errors
+///
+/// Returns `Err` with the exit code if the command finishes with a non-zero exit code.
+pub fn run_cmd(cmd: impl Into) -> Result<(), i32> {
+    let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
+    run_cmd_with_dir(cmd.into(), &cwd)
+}
+
+/// Run a shell command in the specified directory and return its exit status.
+///
+/// # Panics
+///
+/// Panics if the shell command cannot be spawned (e.g. the shell binary is not found).
+///
+/// # Errors
+///
+/// Returns `Err` with the exit code if the command finishes with a non-zero exit code.
+pub fn run_cmd_with_dir(cmd: impl Into, dir: &std::path::Path) -> Result<(), i32> {
+    let shell = if cfg!(target_os = "windows") {
+        "powershell"
+    } else {
+        "sh"
+    };
+    let status = std::process::Command::new(shell)
+        .arg("-c")
+        .arg(cmd.into())
+        .current_dir(dir)
+        .status()
+        .expect("failed to execute command");
+
+    let exit_code = status.code().unwrap_or(1);
+    if exit_code == 0 {
+        Ok(())
+    } else {
+        Err(exit_code)
+    }
+}
+
+/// Run a shell command and capture its combined stdout+stderr output.
+///
+/// On success returns `Ok(combined_output)`. On failure returns `Err((exit_code, stderr))`.
+/// Stderr falls back to stdout if stderr is empty.
+pub fn run_cmd_capture(cmd: impl Into) -> Result {
+    let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
+    run_cmd_capture_with_dir(cmd.into(), &cwd)
+}
+
+/// Run a shell command in the specified directory and capture its combined stdout+stderr output.
+///
+/// On success returns `Ok(combined_output)`. On failure returns `Err((exit_code, stderr))`.
+/// Stderr falls back to stdout if stderr is empty.
+pub fn run_cmd_capture_with_dir(
+    cmd: impl Into,
+    dir: &std::path::Path,
+) -> Result {
+    let shell = if cfg!(target_os = "windows") {
+        "powershell"
+    } else {
+        "sh"
+    };
+    let output = std::process::Command::new(shell)
+        .arg("-c")
+        .arg(cmd.into())
+        .current_dir(dir)
+        .output()
+        .expect("failed to execute command");
+
+    let exit_code = output.status.code().unwrap_or(1);
+    let stderr = String::from_utf8_lossy(&output.stderr).to_string();
+    let stdout = String::from_utf8_lossy(&output.stdout).to_string();
+    // Keep both streams so a failure is never hidden: when stderr carries
+    // warnings, the real failure details (e.g. the failing test name and
+    // assertion diff) usually live on stdout and must not be dropped.
+    let combined = match (stdout.trim().is_empty(), stderr.trim().is_empty()) {
+        (false, false) => format!("{stdout}\n{stderr}"),
+        (false, true) => stdout,
+        (true, false) => stderr,
+        (true, true) => stdout,
+    };
+
+    if exit_code == 0 {
+        Ok(combined)
+    } else {
+        Err((exit_code, combined))
+    }
+}
+
+/// Extract a crate-style name from a `Cargo.toml` path.
+///
+/// Examples:
+/// - `mingling_core/Cargo.toml` → `mingling_core`
+/// - `.` → `(root)`
+pub fn crate_name_from(path: &std::path::Path) -> String {
+    path.parent()
+        .and_then(|p| p.file_name())
+        .and_then(|n| n.to_str())
+        .unwrap_or("(root)")
+        .to_string()
+}
+
+/// Run a list of `(label_for_errors, crate_name_for_bar, shell_command)` tuples
+/// in parallel with a progress bar.
+///
+/// - Success: silent, the bar tracks progress:
+///   `  Building [============================] 32/32: mingling_core`
+/// - Failure: `pb.println()` prints the error immediately above the bar.
+pub fn run_parallel(phase: &str, tasks: Vec<(String, String, String)>) -> Result<(), i32> {
+    let n = tasks.len();
+    if n == 0 {
+        return Ok(());
+    }
+
+    // Cargo-style prefix: right-aligned to 12 chars, bold bright cyan
+    let padding = " ".repeat(12 - phase.len());
+    let styled_prefix = format!("{}{}", padding, phase.bold().bright_cyan());
+
+    let pb = indicatif::ProgressBar::new(n as u64);
+    pb.set_style(
+        indicatif::ProgressStyle::default_bar()
+            .template(&format!(
+                "{} [{{bar:28}}] {{pos}}/{{len}}: {{msg}}",
+                styled_prefix
+            ))
+            .unwrap()
+            .progress_chars("=> "),
+    );
+    pb.set_position(0);
+
+    // Pre-extract labels for error messages
+    let labels: Vec = tasks.iter().map(|(l, _, _)| l.clone()).collect();
+
+    let (tx, rx) = std::sync::mpsc::channel::<(usize, String, Result)>();
+
+    for (i, (_label, crate_name, cmd)) in tasks.into_iter().enumerate() {
+        let tx = tx.clone();
+        std::thread::spawn(move || {
+            let result = run_cmd_capture(&cmd);
+            let _ = tx.send((i, crate_name, result));
+        });
+    }
+    drop(tx);
+
+    let mut first_exit_code = 0;
+
+    while let Ok((i, crate_name, result)) = rx.recv() {
+        pb.inc(1);
+        pb.set_message(crate_name);
+
+        if let Err((code, output)) = result {
+            if first_exit_code == 0 {
+                first_exit_code = code;
+            }
+            let msg = format!(
+                "{}: {} failed (exit code {})",
+                "error".bright_red().bold(),
+                labels[i],
+                code,
+            );
+            let mut lines = Vec::new();
+            if !output.is_empty() {
+                lines.extend(output.lines().map(|l| format!("  {l}")));
+            }
+            if std::io::stdout().is_terminal() {
+                // On a TTY, render errors through the progress bar so they
+                // appear above it.
+                pb.println(&msg);
+                for line in &lines {
+                    pb.println(line);
+                }
+            } else {
+                // On a non-TTY (CI, piped output), `ProgressBar::println` can
+                // be swallowed, hiding the failure. Emit to plain stdout so the
+                // failure is always visible.
+                println!("{msg}");
+                for line in &lines {
+                    println!("{line}");
+                }
+            }
+        }
+    }
+
+    pb.finish_and_clear();
+
+    if first_exit_code != 0 {
+        Err(first_exit_code)
+    } else {
+        Ok(())
+    }
+}
+
+/// Run a single shell command with a progress bar, capturing its output.
+///
+/// - Success: bar clears silently.
+/// - Failure: error is printed above the bar, then the bar clears.
+pub fn run_cmd_with_progress(phase: &str, label: &str, cmd: String) -> Result<(), i32> {
+    let padding = " ".repeat(12 - phase.len());
+    let styled_prefix = format!("{}{}", padding, phase.bold().bright_cyan());
+
+    let pb = indicatif::ProgressBar::new(1);
+    pb.set_style(
+        indicatif::ProgressStyle::default_bar()
+            .template(&format!(
+                "{} [{{bar:28}}] {{pos}}/{{len}}: {{msg}}",
+                styled_prefix
+            ))
+            .unwrap()
+            .progress_chars("=> "),
+    );
+    pb.set_message(label.to_owned());
+
+    let result = run_cmd_capture(&cmd);
+    pb.inc(1);
+    pb.finish_and_clear();
+
+    match result {
+        Ok(_) => Ok(()),
+        Err((code, output)) => {
+            eprintln_cargo_style(format!("{} failed (exit code {})", label, code));
+            if !output.is_empty() {
+                println!("{}", output.trim_end());
+            }
+            Err(code)
+        }
+    }
+}
+
+/// Read `[package.metadata.docs.rs].features` from `mingling/Cargo.toml`.
+///
+/// Finds the git repository root, reads `mingling/Cargo.toml`, parses it as TOML,
+/// and extracts the feature list under `[package.metadata.docs.rs].features`.
+///
+/// # Errors
+///
+/// Returns `std::io::Error` if:
+/// - The git repository root cannot be found.
+/// - The manifest file cannot be read.
+/// - The TOML cannot be parsed.
+/// - The `[package.metadata.docs.rs].features` key is missing or empty.
+pub fn read_features() -> Result, std::io::Error> {
+    // Find git repo root
+    let mut current_dir = std::env::current_dir()?;
+    let repo_root = loop {
+        let git_dir = current_dir.join(".git");
+        if git_dir.exists() && git_dir.is_dir() {
+            break Some(current_dir);
+        }
+        if !current_dir.pop() {
+            break None;
+        }
+    };
+    let repo_root = repo_root.ok_or_else(|| {
+        std::io::Error::new(
+            std::io::ErrorKind::NotFound,
+            "Failed to find git repository root",
+        )
+    })?;
+
+    let manifest_path = repo_root.join("mingling/Cargo.toml");
+    if !manifest_path.exists() {
+        return Err(std::io::Error::new(
+            std::io::ErrorKind::NotFound,
+            format!("Manifest not found at {}", manifest_path.display()),
+        ));
+    }
+
+    let manifest_content = std::fs::read_to_string(&manifest_path)?;
+    let cargo_toml: toml::Value = manifest_content.parse().map_err(|e| {
+        std::io::Error::new(
+            std::io::ErrorKind::InvalidData,
+            format!("Failed to parse Cargo.toml: {}", e),
+        )
+    })?;
+
+    let doc_features = cargo_toml
+        .get("package")
+        .and_then(|p| p.get("metadata"))
+        .and_then(|m| m.get("docs"))
+        .and_then(|d| d.get("rs"))
+        .and_then(|rs| rs.get("features"))
+        .and_then(|f| f.as_array())
+        .ok_or_else(|| {
+            std::io::Error::new(
+                std::io::ErrorKind::NotFound,
+                "[package.metadata.docs.rs] or its 'features' key not found in mingling/Cargo.toml",
+            )
+        })?;
+
+    let features: Vec = doc_features
+        .iter()
+        .filter_map(|v| v.as_str().map(String::from))
+        .collect();
+
+    if features.is_empty() {
+        return Err(std::io::Error::new(
+            std::io::ErrorKind::InvalidData,
+            "No features defined in [package.metadata.docs.rs]",
+        ));
+    }
+
+    Ok(features)
+}
+
+#[must_use]
+pub fn cargo_tomls() -> Vec {
+    let mut cargo_tomls = Vec::new();
+    let mut dirs = vec![std::path::PathBuf::from(".")];
+    while let Some(dir) = dirs.pop() {
+        if let Ok(entries) = std::fs::read_dir(&dir) {
+            for entry in entries.flatten() {
+                let path = entry.path();
+                if path.is_dir() {
+                    // Skip the .run directory
+                    if path.file_name().and_then(|n| n.to_str()) == Some(".run") {
+                        continue;
+                    }
+                    dirs.push(path);
+                } else if path.file_name().and_then(|n| n.to_str()) == Some("Cargo.toml") {
+                    cargo_tomls.push(path);
+                }
+            }
+        }
+    }
+    cargo_tomls
+}
diff --git a/dev/run/src/verify.rs b/dev/run/src/verify.rs
new file mode 100644
index 0000000..b79bb73
--- /dev/null
+++ b/dev/run/src/verify.rs
@@ -0,0 +1,506 @@
+use std::path::Path;
+
+use crate::println_cargo_style;
+
+/// Represents a parsed code block from a markdown file
+#[derive(Debug, Clone)]
+pub struct CodeBlock {
+    /// Source file path (for reporting)
+    pub source_file: String,
+    /// The line number in source file where this block starts
+    pub line: usize,
+    /// The raw Rust source code
+    pub code: String,
+    /// Feature flags extracted from `// Features: [...]` comment
+    pub features: Vec,
+    /// Whether the block had an explicit `// Features:` header
+    pub has_features_header: bool,
+    /// Whether the block has `// NOT VERIFIED` to opt out of testing
+    pub not_verified: bool,
+    /// External dependencies extracted from `// Dependencies:` comments
+    pub external_deps: Vec<(String, String)>,
+    /// Whether this block has a `fn main` entry point
+    pub has_main: bool,
+    /// Whether this block has `gen_program!()` call
+    pub has_gen_program: bool,
+    /// Whether this block has `// BUILD TIME` annotation (write to build.rs, not main.rs)
+    pub is_build_time: bool,
+}
+
+/// Parse all ```rust code blocks from markdown content
+pub fn parse_code_blocks(content: &str, source_file: &str) -> Vec {
+    let mut blocks = Vec::new();
+    let lines: Vec<&str> = content.lines().collect();
+    let mut i = 0;
+
+    while i < lines.len() {
+        if lines[i].trim() == "```rust" {
+            if let Some(block) = parse_single_block(&lines, i, source_file) {
+                blocks.push(block);
+            }
+            i += 1;
+            while i < lines.len() && lines[i].trim() != "```" {
+                i += 1;
+            }
+        }
+        i += 1;
+    }
+
+    blocks
+}
+
+/// Parse a single code block starting at the ```rust line
+fn parse_single_block(lines: &[&str], start: usize, source_file: &str) -> Option {
+    let line_num = start + 1; // 1-based line number
+
+    let mut code_lines: Vec = Vec::new();
+    let mut features: Vec = Vec::new();
+    let mut has_features_header = false;
+    let mut not_verified = false;
+    let mut external_deps: Vec<(String, String)> = Vec::new();
+    let mut has_main = false;
+    let mut has_gen_program = false;
+    let mut is_build_time = false;
+
+    let mut idx = start + 1;
+    let mut in_header = true;
+
+    while idx < lines.len() {
+        let raw_line = lines[idx];
+        let trimmed = raw_line.trim();
+
+        if trimmed == "```" {
+            break;
+        }
+
+        // @@@ lines: strip the prefix and treat as regular Rust code
+        // These lines are hidden in the rendered docs (filtered by a docsify plugin)
+        // but must still compile.
+        if let Some(stripped) = trimmed.strip_prefix("@@@") {
+            in_header = false;
+            // Strip @@@ and optionally one following space
+            let code = stripped.trim_start();
+            if code.contains("fn main") {
+                has_main = true;
+            }
+            if code.contains("gen_program!") {
+                has_gen_program = true;
+            }
+            code_lines.push(code.to_string());
+            idx += 1;
+            continue;
+        }
+
+        // Parse header comments
+        // Check for NOT VERIFIED marker
+        if in_header && trimmed == "// NOT VERIFIED" {
+            not_verified = true;
+            idx += 1;
+            continue;
+        }
+
+        if in_header && trimmed == "// BUILD TIME" {
+            is_build_time = true;
+            idx += 1;
+            continue;
+        }
+
+        if in_header && trimmed.starts_with("// ") {
+            if trimmed.starts_with("// Features:") {
+                has_features_header = true;
+                let feat_str = trimmed.trim_start_matches("// Features:").trim();
+                if feat_str.starts_with('[') && feat_str.ends_with(']') {
+                    let inner = &feat_str[1..feat_str.len() - 1];
+                    if !inner.is_empty() {
+                        features = inner
+                            .split(',')
+                            .map(|s| s.trim().trim_matches('"').to_string())
+                            .filter(|s| !s.is_empty())
+                            .collect();
+                    }
+                }
+                idx += 1;
+                continue;
+            }
+            if trimmed == "// Dependencies:" {
+                idx += 1;
+                // Collect subsequent `// crate = "version"` lines
+                while idx < lines.len() {
+                    let next = lines[idx].trim();
+                    if next == "```" {
+                        break;
+                    }
+                    if next.starts_with("// ") {
+                        let dep_line = next.trim_start_matches("// ").trim();
+                        if let Some((name, ver)) = dep_line.split_once(" = ") {
+                            external_deps.push((
+                                name.trim().to_string(),
+                                ver.trim().trim_matches('"').to_string(),
+                            ));
+                        }
+                        idx += 1;
+                    } else {
+                        break;
+                    }
+                }
+                continue;
+            }
+        }
+
+        in_header = false;
+
+        if raw_line.contains("fn main") {
+            has_main = true;
+        }
+        if raw_line.contains("gen_program!") {
+            has_gen_program = true;
+        }
+
+        code_lines.push(raw_line.to_string());
+        idx += 1;
+    }
+
+    if code_lines.is_empty() {
+        return None;
+    }
+
+    Some(CodeBlock {
+        source_file: source_file.to_string(),
+        line: line_num,
+        code: code_lines.join("\n"),
+        features,
+        has_features_header,
+        not_verified,
+        external_deps,
+        has_main,
+        has_gen_program,
+        is_build_time,
+    })
+}
+
+/// Generate a Cargo.toml for a block
+///
+/// `manifest_path` is the full path to the Cargo.toml file being written; it is used to
+/// compute the relative path to the `mingling` crate.
+pub fn generate_cargo_toml(block: &CodeBlock, package_name: &str, manifest_path: &Path) -> String {
+    let features_str = if !block.features.is_empty() {
+        let feats: Vec = block.features.iter().map(|f| format!("\"{f}\"")).collect();
+        format!("features = [{}]", feats.join(", "))
+    } else {
+        String::new()
+    };
+
+    let mut extra_deps = String::new();
+    for (name, version) in &block.external_deps {
+        if !version.starts_with('{') {
+            if name == "serde" || name == "clap" {
+                extra_deps.push_str(&format!(
+                    "{name} = {{ version = \"{version}\", features = [\"derive\"] }}\n"
+                ));
+            } else {
+                extra_deps.push_str(&format!("{name} = \"{version}\"\n"));
+            }
+        } else {
+            extra_deps.push_str(&format!("{name} = {version}\n"));
+        }
+    }
+
+    let mingling_path = find_mingling_relative_path(manifest_path);
+
+    let deps_section = if features_str.is_empty() {
+        format!("[dependencies]\nmingling = {{ path = \"{mingling_path}\" }}\n{extra_deps}",)
+    } else {
+        format!(
+            "[dependencies]\nmingling = {{ path = \"{mingling_path}\", {features_str} }}\n{extra_deps}",
+        )
+    };
+
+    // Build-time blocks: mirror the declared features into [build-dependencies]
+    // so that build.rs can use the same feature set as the crate itself.
+    let build_deps_section = if block.is_build_time {
+        let feats_str: Vec = block.features.iter().map(|f| format!("\"{f}\"")).collect();
+        let build_feats = if feats_str.is_empty() {
+            String::new()
+        } else {
+            format!("features = [{}]", feats_str.join(", "))
+        };
+        format!(
+            "\n[build-dependencies]\nmingling = {{ path = \"{mingling_path}\", {build_feats} }}\n"
+        )
+    } else {
+        String::new()
+    };
+
+    format!(
+        r#"[package]
+	name = "{package_name}"
+	version = "0.0.0"
+	edition = "2024"
+
+{deps_section}{build_deps_section}
+[workspace]
+"#
+    )
+}
+
+/// Compute the relative path from a Cargo.toml's parent directory to the `mingling` crate.
+///
+/// The process current directory is expected to be the project root (where `mingling/` lives).
+/// Returns a forward-slash path safe for embedding in TOML strings.
+fn find_mingling_relative_path(manifest_path: &Path) -> String {
+    let manifest_dir = manifest_path
+        .parent()
+        .expect("manifest_path has no parent directory");
+    let cwd = std::env::current_dir().expect("failed to get current directory");
+
+    // Strip cwd prefix to get the relative components of the manifest directory
+    let relative_to_root = manifest_dir.strip_prefix(&cwd).unwrap_or(manifest_dir);
+    let depth = relative_to_root.components().count();
+
+    let mut result = String::new();
+    for _ in 0..depth {
+        result.push_str("../");
+    }
+    result.push_str("mingling");
+    result
+}
+
+/// Generate main.rs for a block
+///
+/// Automatically prepends `use mingling::prelude::*;` if the block doesn't already have it.
+pub fn generate_main_rs(block: &CodeBlock) -> String {
+    let mut output = String::from("#![allow(dead_code)]\n#![allow(unused)]\n");
+
+    if !block.code.contains("use mingling::prelude::*;") {
+        output.push_str("#[allow(unused_imports)]\nuse mingling::prelude::*;\n\n");
+    }
+
+    output.push_str(&block.code);
+    output.push('\n');
+
+    if !block.has_main {
+        output.push_str("\nfn main() {}\n");
+    }
+
+    if !block.has_gen_program {
+        output.push_str("\nmingling::macros::gen_program!();\n");
+    }
+
+    output
+}
+
+/// Generate build.rs for a build-time block
+///
+/// Default: code wrapped in `fn main() { }`.
+pub fn generate_build_rs(block: &CodeBlock) -> String {
+    let mut output = String::from("#![allow(dead_code)]\n#![allow(unused)]\n");
+
+    if block.has_main {
+        output.push_str(&block.code);
+    } else {
+        output.push_str("fn main() {\n");
+        for line in block.code.lines() {
+            output.push_str("    ");
+            output.push_str(line);
+            output.push('\n');
+        }
+        output.push_str("}\n");
+    }
+
+    output
+}
+
+/// Build a single code block as a Cargo project.
+///
+/// When `is_build_time` is true, `src_content` is written to `build.rs` instead of `src/main.rs`,
+/// and a minimal `src/main.rs` stub (`fn main() {}`) is created.
+pub fn build_block(
+    src_dir: &Path,
+    manifest_path: &Path,
+    cargo_toml: &str,
+    src_content: &str,
+    is_build_time: bool,
+) -> (bool, String) {
+    if let Err(e) = std::fs::create_dir_all(src_dir) {
+        return (false, format!("mkdir: {e}"));
+    }
+
+    // Write Cargo.toml
+    if let Err(e) = std::fs::write(manifest_path, cargo_toml) {
+        return (false, format!("write Cargo.toml: {e}"));
+    }
+
+    if is_build_time {
+        // Write build.rs and a stub main.rs
+        let crate_dir = manifest_path.parent().unwrap();
+        if let Err(e) = std::fs::write(crate_dir.join("build.rs"), src_content) {
+            return (false, format!("write build.rs: {e}"));
+        }
+        if let Err(e) = std::fs::write(src_dir.join("main.rs"), "fn main() {}\n") {
+            return (false, format!("write main.rs: {e}"));
+        }
+    } else {
+        // Normal: write src/main.rs
+        if let Err(e) = std::fs::write(src_dir.join("main.rs"), src_content) {
+            return (false, format!("write main.rs: {e}"));
+        }
+    }
+
+    // Check code — inherit stderr so cargo output is real-time and colored
+    let shell = if cfg!(target_os = "windows") {
+        "powershell"
+    } else {
+        "sh"
+    };
+    let cmd = format!(
+        "cargo check --color=always --manifest-path {}",
+        manifest_path.to_string_lossy()
+    );
+
+    let mut child = match std::process::Command::new(shell)
+        .arg("-c")
+        .arg(&cmd)
+        .stdout(std::process::Stdio::inherit())
+        .stderr(std::process::Stdio::piped())
+        .current_dir(std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")))
+        .spawn()
+    {
+        Ok(c) => c,
+        Err(e) => return (false, format!("spawn: {e}")),
+    };
+
+    // Read stderr (buffered, not forwarded — groups print their own output contiguously)
+    use std::io::BufRead;
+    let stderr_handle = child.stderr.take().unwrap();
+    let reader = std::io::BufReader::new(stderr_handle);
+    let mut captured = String::new();
+    for line in reader.lines() {
+        match line {
+            Ok(l) => {
+                captured.push_str(&l);
+                captured.push('\n');
+            }
+            Err(_) => break,
+        }
+    }
+
+    let status = child.wait().unwrap_or_else(|_| std::process::exit(1));
+    let exit_code = status.code().unwrap_or(1);
+
+    if exit_code == 0 {
+        (true, String::new())
+    } else {
+        let mut last_lines: Vec<&str> = captured.lines().rev().take(20).collect();
+        last_lines.reverse();
+        let detail = last_lines.join("\n");
+        (false, format!("exit code {exit_code}\n{detail}"))
+    }
+}
+
+/// Compute a stable hash for a code block based on its dependency configuration.
+///
+/// Blocks with the same features and external dependencies produce the same hash,
+/// allowing them to share a compiled crate and avoid redundant recompilation.
+///
+/// Hash input (all sorted for stability):
+/// - Sorted mingling feature strings
+/// - Sorted external dependency names
+/// - Sorted external dependency versions
+/// - Sorted external deps as `name=version` pairs
+pub fn compute_block_hash(block: &CodeBlock) -> String {
+    let mut features: Vec<&str> = block.features.iter().map(|s| s.as_str()).collect();
+    features.sort();
+    let features_str = features.join(",");
+
+    let mut dep_names: Vec<&str> = block
+        .external_deps
+        .iter()
+        .map(|(n, _)| n.as_str())
+        .collect();
+    dep_names.sort();
+    let dep_names_str = dep_names.join(",");
+
+    let mut dep_versions: Vec<&str> = block
+        .external_deps
+        .iter()
+        .map(|(_, v)| v.as_str())
+        .collect();
+    dep_versions.sort();
+    let dep_versions_str = dep_versions.join(",");
+
+    let mut deps: Vec = block
+        .external_deps
+        .iter()
+        .map(|(n, v)| format!("{n}={v}"))
+        .collect();
+    deps.sort();
+    let deps_str = deps.join(",");
+
+    let canonical = format!("{features_str}\n{dep_names_str}\n{dep_versions_str}\n{deps_str}");
+
+    // FNV-1a 64-bit hash — stable across runs (no random seed)
+    let mut hash: u64 = 0xcbf29ce484222325;
+    for &byte in canonical.as_bytes() {
+        hash ^= byte as u64;
+        hash = hash.wrapping_mul(0x100000001b3);
+    }
+
+    format!("{:016x}", hash)
+}
+
+/// Determine if a block should be treated as a test candidate.
+/// A block is NOT testable only if it has `// NOT VERIFIED` marker.
+pub fn is_block_testable(block: &CodeBlock) -> bool {
+    !block.not_verified
+}
+
+/// Write a summary report
+pub fn write_summary_report(
+    path: &Path,
+    title: &str,
+    results: &[(String, usize, bool, String)],
+    total: usize,
+    passed: usize,
+    failed: usize,
+) {
+    let mut content = String::new();
+    content.push_str(&format!("# {title}\n\n"));
+    content.push_str(&format!(
+        "Tested **{total}** code blocks: **{passed}** passed, **{failed}** failed.\n\n"
+    ));
+    content.push_str("## Results\n\n");
+    content.push_str("| Block | File | Line | Status |\n");
+    content.push_str("|-------|------|------|--------|\n");
+
+    for (i, (file, line, ok, _)) in results.iter().enumerate() {
+        let status = if *ok { "PASS" } else { "FAIL" };
+        let short_file = file.rsplit('/').next().unwrap_or(file);
+        content.push_str(&format!(
+            "| {} | {} | {} | {status} |\n",
+            i + 1,
+            short_file,
+            line
+        ));
+    }
+
+    let has_failures = results.iter().any(|(_, _, ok, _)| !ok);
+    if has_failures {
+        content.push_str("\n## Failed Blocks\n\n");
+        for (i, (file, line, ok, err)) in results.iter().enumerate() {
+            if !ok {
+                content.push_str(&format!(
+                    "### Block {} (`{}`, line {})\n\n```\n{err}\n```\n\n",
+                    i + 1,
+                    file,
+                    line
+                ));
+            }
+        }
+    }
+
+    std::fs::write(path, &content).unwrap_or_else(|e| {
+        eprintln!("Warning: failed to write {path:?}: {e}");
+    });
+
+    println_cargo_style!("Report: written to {}", path.display());
+}
-- 
cgit