From 511dbc6db2e06339e382be5f81161abbd37e90b9 Mon Sep 17 00:00:00 2001 From: MuMDIA Date: Sun, 30 Aug 2026 10:59:36 +0200 Subject: [PATCH 01/14] feat(desktop): milestone 1 of the console, a window that runs a search First milestone of the desktop application plan: pick an mzML and a FASTA or a library, start a search, watch it, read the results. Windows and Linux. No engine changes. Progress comes from the `.report.json` files the engine already writes, results from the report `rescore` already emits, and the run itself from the documented CLI. The whole milestone is an application, which is what makes it cheap to throw away if the shape turns out to be wrong. The engine is a CHILD PROCESS, not a linked crate. The engine installs no signal handler anywhere, so stopping a run is a kill, and a Rust thread cannot be killed: linked in-process there would be no Stop button at all, only a window that ignores you for an hour. Supporting reasons: a stage panic ends one run rather than the session, and rayon's global pool can only be built once per process, so --threads could not otherwise change between runs. The application also shows the exact command it ran, which a linked engine could not offer. Process control is three deep -- application, engine, Python workers -- so cancelling uses a process group on Linux and `taskkill /T` on Windows. A hard kill skips destructors, so the atomic-write layer never removes its `.tmp-` files; cancelling therefore sweeps them, or the next run would start in a dirty folder. Closing the window cancels every live run for the same reason. Two bugs found by writing the tests rather than by reading the code: - The terminal state was published before the watcher had scanned, so a caller polling for completion could read a finished run with no stages and no results. The interface would have flashed an empty results screen. The final scan now happens in the waiter, before the status stops being `running`. - The cancel test passed while never cancelling anything: the fixture search finishes in about 700 ms, faster than a stop can be issued. It now says so instead, and `kill_tree` is covered directly by a test that spawns a process and checks the kill lands. Verified: 10 unit tests including stage folding and results parsing against real artifact reports from a recorded run, plus 2 end-to-end tests that drive a real search through the supervisor. fmt and clippy -D warnings clean. The frontend is plain ES modules with no build step, so the release pipeline still needs no Node. Co-Authored-By: Claude Opus 5 (1M context) --- desktop/.gitignore | 9 + desktop/Cargo.lock | 4670 +++++++++++++++++ desktop/Cargo.toml | 21 + desktop/README.md | 88 + desktop/src-tauri/Cargo.toml | 26 + desktop/src-tauri/build.rs | 3 + desktop/src-tauri/capabilities/default.json | 6 + desktop/src-tauri/icons/128x128.png | Bin 0 -> 2613 bytes desktop/src-tauri/icons/128x128@2x.png | Bin 0 -> 5947 bytes desktop/src-tauri/icons/256x256.png | Bin 0 -> 5947 bytes desktop/src-tauri/icons/32x32.png | Bin 0 -> 644 bytes desktop/src-tauri/icons/icon.ico | Bin 0 -> 6629 bytes desktop/src-tauri/icons/icon.png | Bin 0 -> 5947 bytes desktop/src-tauri/src/engine.rs | 144 + desktop/src-tauri/src/lib.rs | 8 + desktop/src-tauri/src/main.rs | 161 + desktop/src-tauri/src/run.rs | 701 +++ desktop/src-tauri/tauri.conf.json | 39 + desktop/src-tauri/tests/end_to_end.rs | 182 + .../src-tauri/tests/fixtures/run_out/cal.json | 19 + .../run_out/chromatograms.parquet.report.json | 25 + .../run_out/features.parquet.report.json | 19 + .../run_out/features.parquet.schema.json | 392 ++ ...ment_library_fragments.parquet.report.json | 21 + ...ent_library_precursors.parquet.report.json | 21 + .../fragment_quant.parquet.report.json | 40 + .../tests/fixtures/run_out/manifest.json | 239 + .../run_out/peptide_quant.parquet.report.json | 40 + .../run_out/peptides.parquet.report.json | 25 + .../tests/fixtures/run_out/peptides.tsv | 152 + .../run_out/peptidoforms.parquet.report.json | 21 + .../protein_group_quant.parquet.report.json | 40 + .../tests/fixtures/run_out/proteins.tsv | 1 + .../run_out/psms_competed.parquet.report.json | 19 + .../run_out/psms_competed.parquet.schema.json | 392 ++ .../psms_extracted.parquet.report.json | 25 + .../run_out/psms_scored.parquet.report.json | 31 + .../run_out/run_windows.parquet.report.json | 20 + .../run_out/seed_psms.parquet.masscal.json | 11 + .../run_out/seed_psms.parquet.report.json | 21 + .../isolation_windows.parquet.report.json | 18 + .../spectra/ms2_to_ms1.parquet.report.json | 18 + .../spectra/spectra_ms1.parquet.report.json | 18 + .../spectra/spectra_ms2.parquet.report.json | 18 + desktop/ui/app.css | 180 + desktop/ui/app.js | 370 ++ desktop/ui/index.html | 163 + 47 files changed, 8417 insertions(+) create mode 100644 desktop/.gitignore create mode 100644 desktop/Cargo.lock create mode 100644 desktop/Cargo.toml create mode 100644 desktop/README.md create mode 100644 desktop/src-tauri/Cargo.toml create mode 100644 desktop/src-tauri/build.rs create mode 100644 desktop/src-tauri/capabilities/default.json create mode 100644 desktop/src-tauri/icons/128x128.png create mode 100644 desktop/src-tauri/icons/128x128@2x.png create mode 100644 desktop/src-tauri/icons/256x256.png create mode 100644 desktop/src-tauri/icons/32x32.png create mode 100644 desktop/src-tauri/icons/icon.ico create mode 100644 desktop/src-tauri/icons/icon.png create mode 100644 desktop/src-tauri/src/engine.rs create mode 100644 desktop/src-tauri/src/lib.rs create mode 100644 desktop/src-tauri/src/main.rs create mode 100644 desktop/src-tauri/src/run.rs create mode 100644 desktop/src-tauri/tauri.conf.json create mode 100644 desktop/src-tauri/tests/end_to_end.rs create mode 100644 desktop/src-tauri/tests/fixtures/run_out/cal.json create mode 100644 desktop/src-tauri/tests/fixtures/run_out/chromatograms.parquet.report.json create mode 100644 desktop/src-tauri/tests/fixtures/run_out/features.parquet.report.json create mode 100644 desktop/src-tauri/tests/fixtures/run_out/features.parquet.schema.json create mode 100644 desktop/src-tauri/tests/fixtures/run_out/fragment_library_fragments.parquet.report.json create mode 100644 desktop/src-tauri/tests/fixtures/run_out/fragment_library_precursors.parquet.report.json create mode 100644 desktop/src-tauri/tests/fixtures/run_out/fragment_quant.parquet.report.json create mode 100644 desktop/src-tauri/tests/fixtures/run_out/manifest.json create mode 100644 desktop/src-tauri/tests/fixtures/run_out/peptide_quant.parquet.report.json create mode 100644 desktop/src-tauri/tests/fixtures/run_out/peptides.parquet.report.json create mode 100644 desktop/src-tauri/tests/fixtures/run_out/peptides.tsv create mode 100644 desktop/src-tauri/tests/fixtures/run_out/peptidoforms.parquet.report.json create mode 100644 desktop/src-tauri/tests/fixtures/run_out/protein_group_quant.parquet.report.json create mode 100644 desktop/src-tauri/tests/fixtures/run_out/proteins.tsv create mode 100644 desktop/src-tauri/tests/fixtures/run_out/psms_competed.parquet.report.json create mode 100644 desktop/src-tauri/tests/fixtures/run_out/psms_competed.parquet.schema.json create mode 100644 desktop/src-tauri/tests/fixtures/run_out/psms_extracted.parquet.report.json create mode 100644 desktop/src-tauri/tests/fixtures/run_out/psms_scored.parquet.report.json create mode 100644 desktop/src-tauri/tests/fixtures/run_out/run_windows.parquet.report.json create mode 100644 desktop/src-tauri/tests/fixtures/run_out/seed_psms.parquet.masscal.json create mode 100644 desktop/src-tauri/tests/fixtures/run_out/seed_psms.parquet.report.json create mode 100644 desktop/src-tauri/tests/fixtures/run_out/spectra/isolation_windows.parquet.report.json create mode 100644 desktop/src-tauri/tests/fixtures/run_out/spectra/ms2_to_ms1.parquet.report.json create mode 100644 desktop/src-tauri/tests/fixtures/run_out/spectra/spectra_ms1.parquet.report.json create mode 100644 desktop/src-tauri/tests/fixtures/run_out/spectra/spectra_ms2.parquet.report.json create mode 100644 desktop/ui/app.css create mode 100644 desktop/ui/app.js create mode 100644 desktop/ui/index.html diff --git a/desktop/.gitignore b/desktop/.gitignore new file mode 100644 index 00000000..a4b036ab --- /dev/null +++ b/desktop/.gitignore @@ -0,0 +1,9 @@ +# Build output for the desktop workspace. +target/ + +# Engine and installer binaries staged into the bundle by CI, never committed: +# they are build artifacts of another workspace and are megabytes each. +src-tauri/binaries/ + +# Tauri writes generated permission schemas here. +src-tauri/gen/ diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock new file mode 100644 index 00000000..79b334b1 --- /dev/null +++ b/desktop/Cargo.lock @@ -0,0 +1,4670 @@ +# 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 = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.20", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.20", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.4+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link 0.2.1", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7955dfc218a8afb29dfeffd540e3a6e96baeb94fe7138228dd7cc6937fbbf96" +dependencies = [ + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[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 = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.20", + "windows-sys 0.61.2", +] + +[[package]] +name = "mumdia-console" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-dialog", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plist" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.1", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.13+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.20", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.1", + "jiff", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.1", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.20", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.119", + "tauri-utils", + "thiserror 2.0.20", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2d3c1dbe38037e7f590cdf2492594d5ceebe031e7bc7e827509b22a999d2940" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.20", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.20", + "toml 1.1.4+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.20", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.20", + "toml 1.1.4+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.4+spec-1.1.0", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.1", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap 2.14.1", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.1", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.1", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap 2.14.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "045979e3f037cd18ad1cb2a419dfda133c5c29c9f3453370079f2255d46c257e" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.20", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.20", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.20", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml new file mode 100644 index 00000000..c60e299d --- /dev/null +++ b/desktop/Cargo.toml @@ -0,0 +1,21 @@ +# Separate workspace, deliberately. +# +# The desktop application is NOT a member of `rust/mumdia`'s workspace and does not +# depend on the `mumdia` crate. Two reasons: +# +# 1. It spawns the engine as a child process rather than linking it, so there is +# nothing to depend on. See src-tauri/src/run.rs for why (the engine has no +# signal handling, and a linked engine could not be stopped). +# 2. Keeping Tauri out of the engine workspace means `cargo test --workspace` and +# the CI lint job never compile a webview. +[workspace] +members = ["src-tauri"] +resolver = "2" + +# Optimise the shipped application for size rather than speed: it is a window and a +# process supervisor, and every millisecond of real work happens in the engine. +[profile.release] +strip = true +opt-level = "s" +lto = true +codegen-units = 1 diff --git a/desktop/README.md b/desktop/README.md new file mode 100644 index 00000000..9f15814b --- /dev/null +++ b/desktop/README.md @@ -0,0 +1,88 @@ +# MuMDIA Console + +Desktop interface for the MuMDIA search engine, for people who should not have to use +a terminal. Windows and Linux. + +This directory is a **separate Cargo workspace**. It is not a member of +`rust/mumdia`'s workspace and does not depend on the `mumdia` crate, so +`cargo test --workspace` in the engine never compiles a webview. + +## Running it during development + +```bash +cd desktop/src-tauri +cargo run +``` + +The application needs an engine binary. It looks in this order: + +1. `$MUMDIA_BIN`, if set; +2. beside its own executable, and in a `binaries/` subdirectory beside it (this is + where a release bundle puts it); +3. `rust/mumdia/target/release/mumdia` relative to a `cargo run` build, which is the + convenient case while developing; +4. anything named `mumdia` on `PATH`. + +It runs `--version` on whatever it finds at startup, so a binary that exists but +cannot execute fails immediately rather than an hour into a search. + +To point it at a specific build: + +```bash +MUMDIA_BIN=/path/to/mumdia cargo run # Linux +$env:MUMDIA_BIN = "C:\path\to\mumdia.exe"; cargo run # Windows PowerShell +``` + +## Why the engine is a subprocess and not a linked library + +The engine is a library crate, so linking it looks attractive. It is the wrong +choice, for one decisive reason: **the engine installs no signal handler anywhere**, +so stopping a run is a kill, and a Rust thread cannot be killed. Linked in-process +there would be no Stop button at all. + +Two supporting reasons. A stage panic would take the whole application down rather +than ending one run — and the engine does still panic on some malformed input. And +rayon's global pool can only be built once per process, so `--threads` could not +change between runs. + +The cost is that the application must resolve a path and manage a process tree. That +is `src/engine.rs` and `src/run.rs`. + +## Process control + +The tree is three deep: application, engine, and the Python workers the engine +spawns. Killing only the engine orphans a worker that may hold tens of gigabytes. + +- **Linux**: the engine is spawned into a new process group, and cancelling signals + the group (`TERM`, then `KILL`). +- **Windows**: `taskkill /T /F` walks the tree at kill time. + +A hard kill skips destructors, so the engine's atomic-write layer never removes its +`.tmp-` files. Cancelling therefore sweeps them from the output directory, or the +next run would start in a dirty folder. + +Closing the window cancels every running search, for the same reason. + +## How progress works + +No log parsing. Every engine stage writes `.report.json` beside its output, +carrying the producing stage, row count, elapsed time and per-stage statistics. The +application polls the output directory and folds those into one row per stage. + +The results panel is read from `psms_scored.parquet.report.json`, which records the +classifier that **actually** ran alongside the one requested. Those differ when a +sidecar fails and `rescore.strict` is false, and the interface says so rather than +echoing the request. + +## Frontend + +Plain ES modules, no framework and no build step, so the release pipeline needs no +Node. `ui/` is served as static files. If this grows to include the generated +settings editor, revisit that decision then — it is much easier to add a bundler +later than to remove one. + +## What is not here yet + +This is milestone 1 of the plan: it runs a search and shows the result. Component +installation (bundled `uv`), the generated settings editor, pre-flight disk and +peak-cap checks, and the release bundles are milestones 2 to 4. diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml new file mode 100644 index 00000000..a559b74e --- /dev/null +++ b/desktop/src-tauri/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "mumdia-console" +version = "0.1.0" +edition = "2021" +description = "Desktop interface for the MuMDIA DIA search engine" +license = "Apache-2.0" +publish = false + +[lib] +name = "mumdia_console" +path = "src/lib.rs" + +# The Tauri shell. Named with a hyphen because that is the executable users see; +# the library keeps the underscore Rust requires. +[[bin]] +name = "mumdia-console" +path = "src/main.rs" + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +tauri-plugin-dialog = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/desktop/src-tauri/build.rs b/desktop/src-tauri/build.rs new file mode 100644 index 00000000..d860e1e6 --- /dev/null +++ b/desktop/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/desktop/src-tauri/capabilities/default.json b/desktop/src-tauri/capabilities/default.json new file mode 100644 index 00000000..f8bfb6be --- /dev/null +++ b/desktop/src-tauri/capabilities/default.json @@ -0,0 +1,6 @@ +{ + "identifier": "default", + "description": "Native file and folder pickers for the main window.", + "windows": ["main"], + "permissions": ["core:default", "dialog:default"] +} diff --git a/desktop/src-tauri/icons/128x128.png b/desktop/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..cf6c3703bd776664a460b1462bab6edce285d78a GIT binary patch literal 2613 zcmZuzYdD+P7XI={;uBP;+u&3TW@?P0E=7w(OG2tH9ceL|Oj~WpsOeDS5|M=IWk#uL z={T*p#5L789jZ)8RI3yfQI~3q4#K1y+DJo6oV35skG=Qvti9HH*Sp@mpJ!bPJ%raq znV|px&>^Cri?f)^3=lZn&{Ej1D)PRMW4PLKawgCDzawzk_wXd>L4{SUv^A)7rfm3RL)NUHa25zp&JR&&x!DMVZI|iiuk+nOf5QzgD-z?w<#xNc~ zqeP>;soh(B=EY)+M>-S7fTqKL=#Y*y+E2@vW)xb5vef{u#wqT3R*Mm^gag< zZD;`v?};<5Hc;M8u&GAck~@YKVF-YG_-4ua)TE~g2n%7sDMD#We{pULcBhrcag>O} z6O7Wgv95z!!t?b4SH)j{-9I(0F zuhgg^e2zarf){{bmUk1(%vRMPb*!{>-s;{S3BjqT#aL5r7?0!lu}O;0RZvzi<4Gt{ zEzy^&qz&M$8lyUooyVCatPLXt7uWZ2q<&V|!<4rP%z%qoJZ0a)yacZnU>a>xOTgS+Nf)6MMmYT&ZdM&tM*H#U|N%rGiP6TU*&8AL0XG`Zvurpg2 z#M+TSX`n5}ExpleP}Q!ta5y|9$TwOsTsZqieibdcI5Mc|`<&jJ91OvUT&8?1I7JiN z^cmdRXbQvyqn_(zPXx~saaikbH$@A0p6Z!)j4^b~e&H4;ewz(l5$?s;czXm$%y)= zFmOWCY3>)aEj+0Pa0uI+%QC*qctr0NX(rOs)GayMpw?$Nv7#Y|%cF4($Ke1$PNS)! z)St*3y-&@HQiY9dkHt~(liUzd^?d&jlG^LNMOdwrjD7CR=lZJI(*jN3#iv#`N?q{g zWv%t(-@R;a*V-Li_0w^}Jtz%F-_&!uQ-@54Ao%WbDeFVd;|5-w}IrC zn~ayGU6=v+b;o=Lt@mr)%&Y6|xe6@SN+f|fJZz6`{E+4q!cx05c*+&-TfLg!>!Z`L zl&bQSys#kFq}uK*kKWHQS#RjZ_3iDdI{>{H2y_lSIFgxAvAz%VQ<^H$JXD zA^PKLf5q-j3}XGWXJ|4#io*>aEC{EB0r4Mw$jih&?*TgZZ{nX}{@6y0=)7Mk7>hf~ zX$wSC?L=)h+}3`DvLlwkmC|za*&=O_XB2u*$K zJt!ZcCuBcy5nT93-xQ4wh1MbiXnil^?Y9%e*rOliPX1fe!^snT=P90YM|GvsDw~-| zk6hmC0a<`F%*`km3Rd2YCog>|A#Qezu9*lPjiCGt!MBRWj*#iWcgTl54Y9MEzy3E$ z*8DFYoA>%NG#x(6ssve-cFZ9Cul(Vp+oVuGW|Ky|b4T%d)JTiz*88!fW?^U{^3}d2 z#dKUP%gOr*4PgP=>-H$$elO0Y63{Ut>~J!g0o(t#i#`;tX$YPf@ zBPQQkJKkMOvVqn@cV_01Ntr#W#tPHg$?t7YR|Z)vYvb2)vB zBuo@;B^(XIm1s9PocQOAwd#nN(3TXkw`8R4woLgks(K6v)9^c4)x~*i)hHHv4fGH0 zxbh2pZ(G(><%Ir;$&H~S?UY8H1KOP#+a?USdZbp!lWp{(ljp{W>xwR~4g7VYT}NfX zx4niT%$pIzR_$Ne9^|_%E^+QlkbW-CUeiH?q6v1hQN!y?2MQd|rt2g-RO5BdL};Un zE)~oUmu5F1;9OJ`I|mcit_CP_K)1W+FmrWf2?VRtJ9a7!s=HS_`x%NOTwVV^gndqjvs~nKlAC= A`2YX_ literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/128x128@2x.png b/desktop/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..cb5002d452c81fafe0721c0775db65138f2f9191 GIT binary patch literal 5947 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 aGT`n*r%Qj+n!SU-Cp36XP<3EZ&i?_=JKH`0 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/256x256.png b/desktop/src-tauri/icons/256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..cb5002d452c81fafe0721c0775db65138f2f9191 GIT binary patch literal 5947 zcmch5`Ck*)_x@ySx^F5TE$dC}-zPG(a*yV7FDY4c1V;`eLpJ~a zFR$qw?HD&TS3(Ma<|5WKG6JauJUWE2U<3y-sB{9PE`S1{RYwfr@{u`2GLp7h%~FI1 zWAb0=JQ9&f^8dW%XIn+;8Zu_2NDbvld;%b=Ttk~2^-OsHa6dYKt zfRzbS>re83U2xJIW%)Po{L~f*Z89&iCgs7(iplzE~Q#=m~Tkxz z$`q{l7|(t7yY=W?tsRAJk4day`tfqqTpLB~#M2?qKQ%Qxi39E?gkaWNHmkZ@4Gq?( z=6!y^TpEkcRWG0@rW0kTrFOyR7}vxa?rr($*~cK4T1~ot*Ch692TNzY-G)#bS`FDn`%_QsL(p<$2l0t5cgCyrQLParC`^dU+p4)8R%))Ys zrGAMzo)#B^LwJxN3Zf`er2k_sR3?9_$&hyBLogHC7N`;)<=FANoMvILnJNw*U5osP zBk)|y^pGE>oxdNK=}yK<5ych#0`M?^SD5E4(U2OIOC zCC)s-u!z_a16M`z+(CB)tK??-5~LDP$%NLUELCHxVRY)%gEPE(l44&DwB&}i?Wkl< zbFlGm|NUU~OwCT69RN#T2WM^~Lo!aB&g+eJ9_IC#>vG98O-+t`87>^)1(=G^=3#k? zj+e>&*nRvTB&zYOtG{}R@QG2O35WD!YiAyu zne7F!=3u$RBHLrql5)acJPOAd+wxd$hDsU7NP^EiN$_lfiueQtrA=b0wgbJ(T!u^e z4|ornI!hf3E{WuQ3E^I!*NLKa4NP?Wh+%0U;B&UI$|bcL7G}svEF=)q|BKYc0ts6a zCB~UmraJ&`2KTtKa{cM61?73>1Drs z_%p@?H-oQ?LDi3=FND$X)6djL=;Y@}2EySA(-=_jUNUwa?p%+K{=1eWu>h%@x5qrm zINu+pi}9$rnnJBTfIERBC}Kt>2R^LYrQ-r&2snz&wuOunp|iu#fl`A*AYAmn*p5;Z z*;JTJOuzY=k*GGtTGe=iK3?4b`iX4AkFmn$^C0(bJvYW+p$rd7I?KCEN-||auNnkA4Kdz-LtLXQgQ%WZkP zi_t>RkP-x>RZs17M;ur=0CrMEzvwckFX{$*z}r@k&r?b1pu)A^&B%Q;r- z7u9Q6(o}w8p(%0OXP%Op`b8Hs;Rz^7YpHCojkB3Os7m#)$gCvuYr^t znsm=yVX7G!u)bH|6DIc64SLjRSgjnRY;z&Ol`0s9V_c0=6rv+gnf}?EMqX+GKL)P5 z$)k0KDkK$%iHsAY^Q$t10qXA;GHXc5eoK5SNXb%zX8PWqx2LF1Yf&XPSjEA$tOw+P z0Z`?h^sYcb)7z;Bz%*K{+a8U&XzMUxK68iF@{dySKGnDsAT?hyBa1bhRT|V**Mh>6 zLadh0uxsw57ls{#zGjcL=|EBc-;hDo-wauaP^81@Rk4T{Zh+i0f$jWG5cf9A4> z!LwX~=xK_SU_}9sYaaIm|GXTX3p9+3-4*IxWt<*O%9{>|V+U6u)i3b%n@loVtR^yr z0V6;4xrB=&bOsL8b60d=jAI6bkwSfOgVc%1snW!oyg-$xTf@pR^m@o2e@jfBZ5J$I zPlik~2OW(m^~v0SBSN=dhFWO76%HQ+Ogp|HGU@L=DOJs3%=}V>RQKY~I5O0Ipl*uB zz*Gx<5Zo0h-tYQV6DfJWz)zh({cE*V#F$ngysSyFMB~-moEnROGsuDW@L>+gTFS{N z7+O4wqBz<>Q-Uukpent&;s%nSQqQ^%HkqF{n`Jq=V?^SmrBb$qCPiD(Z1}XCjP^H?Nr2I^mNmpKXhySIe{Gu~t z>|{B*QX7+oPK+Fm3B#Jm?St?NnZ&py=<`1FDkP1n@kctEvN5Unj!TX!v3(ehhdWz2 z^8!W>210YI^##fd@zi~0k6E&Vcro7gCNENzDn)O-RA>5LIaK;39Jf*DE!6I)*EjKM zEcn52RTlQpotdCZQ8Wgn6}yXHU)6+LYYJTTJEROFpq;LL%tB+nj$yeeHIAV zA(dpIHii;(60XV>UneC8fP?C$7FzvYVs-kdqh*b!@tGz+71!_6Ggtk2MR$(onSAit zX#`-gODlC(sJ|}A42l1L+g;SKJoE>698%^{Q(Jp7(7#joghXbAM(Cb%HB2nS(|Km) zXvj!vE>HqXT6;e8PGD^}eiB)pV@R)gGd-0bHnc2}dE-@vo+Xp_Xr|ujoz!*GZUH?U zrjexaR?Exv-s%*^6p$t=%+BuSyj{)c>SDn9mx_#FvIGA~9j;_+z;0DB=8<(}=ay>e9Rdhks_*h4a zmdt5dL9rDS@whflLvRR~qG&i$8mUmt(0RFYb?Na2Q_$3@XQkg0>qBsBtvad9Dkl3) zam)R;VI2q{Y_fENHn>3_spJNvQh;G9%;N1g?_5OMRgR7>5yXd3llS9%gt*+T5|y$8 zVFV&E#Pm;r(Ir(QCUF5)G5vD`oWf_wzzaY)%ynLjivGC= z(%Z`TZbaguwbsXo*uaL8Z_HJ?BP^K&Z}{M2GQjrmZGP%m`+DaGtCyOoqUNfhLR?u0t!7DFeFX z7Z)GAH@-!!Jq0&&jV@FfXz%-oPri;KKEKIJEavy;arIqYXpBsl)9Z5NoCEz(M#OBJ zWi1Zw`Sk?wI!T*H81wXt#f``4VQ|%HT$?}B(}&Dp zqt^_Q-a^63!|eF98QxY^aVy#~b5#}Q70TV4yFKaU&N1N&P0G9Mw!b+h1`DkFSkh03 z?I&E-ut)RE<}*deu0(9C4QqYPu*xR=Vr}RxNv5z_Il!^{6(v*vj#K&s{bkpd^3N6V z`yWnef;)6R(s90j7@_wEdLR%_X1p%rp#P^I7)8Lnn{IUGgHRr~v;)^0iH?9-OuPXz~((DB~evrZQq z#glPBVy0THH>eIm9G4s?Vtolk@OsYkZ-Pd%gv|?uyJp>!3uvP|H&6%dCN^XG_E#=J zVBQ?CjxefT%8yiaW2w{B(-%l_S@v<&h~&UnV=HROGuJ089cojk&hXAv`QX&~C#~19 zlv88=XV^*4@$dKA$HhoR`+MaHn$a)N70O7@IA>f(6qxOUMclUigRvvMe4N49rvx;z_fI058CQ9e9$~SyG;QNzs(?Q&JV{KJm zOLVxzLYc5x`_fvs!40lKT*Cv*v{YpS^{+6i2+zmU3Ci)mJ_vaXs+OQA%}om?K95I7 z@HJG*BGhp-Fl{?nv}EE+gRUg@@Hm9-kcn_vM@{Q(V!xn15^{YzK5tTF1u#Y)vwy{R}p^ zJ1|D=7yG9wg#Yn&fKTSS+HLJK1+UE&IW>)RpQ5FY4|t2NFR^w@=glkt{-F$HXe1b1Z)ERYiHDvBg^1 z_-;+cfEI*}RvpCoM-eP3m1lULzWQs*BJB#Yq7?grB&EKb=q}27VwFAy0he7No_a_> zzwkb&(7yA{@_>(zZpqKwKEDL? zcBNXP-nTI(kG{pe*56Ug=j=UF*MH}{w@F+HIg?0_g8T7VW|=9nj$40unPiXO65+D# zl^dc2<^h+GN)e@k5HDHV!AY_$-Mm(%c=pC#aGTud1o`Ai9sNt#z4=EaodPB54qy6%RPlqb9!HDeSw%If- z1g{0gdO_1XKWe@|{l_9^)xxqovE$LFIh*1?I7Fk}+vQ&XqvF4s0Oq(5aU*WSYAl{b zf}w*AT5NG6P6l;STYo=eIk`kR-ZjArl#rAKp7+i9`#r?E6YSoPbV@)tT$IK94jwqi zT#K-1^%o)@cXsz^De9+9-F~Uc2j5TENhL7g zh<{F&H{-Qs;omg7NiGa?bI|RB&7N`pnAQ6`(x$vlWs3wC6&v7&0`bzEI@4WIPEmuU zSciHkXT3~L1b?)6J9=#=+rZ;i$IiEmZ$4D{^qvifAh;0{w7jOe|&Xf zk51NU(4^`sivlhY4pVF2=bR5FyYYLh1dnaMYE!r#-5N$xvj-3Ep0dN__zBtG7vCXO z9eGnTi@h8oGUCc)W|P|@=C!)qHEA}_48JR1tgUGEUu{B3`f+}j~w8Ann(1!Bh|`%VQqabH|KcuCk><=;p0aglcLhE}1X-(`Wd51G%7bsY9N z9gJ)uo~RD?5l#)?j9*fCvrb!GDsR1%s(=RGl-+>~>8?BA3=-Pgex!G#0gj9S!XSx&A!;ulW% z)l)D=@RJBHFPxtzo{D+fB`}0t#m1U9aDTQ$4=NZs)|zT}WVc1hhZol;4}Lg>Z*H^> ztzKSpk=-k-K7_{Kxjo;9iF()8(DPn;#(luMRS{OOekwX>d;N|wtf0jvIwSGZ3D&-Y z)f2H$R>Pw6tX!IiA{5&O(+@3t$~H}nS~1Q(p82yrv0R%Nz_Iy>CUmSxr$1Zs_{`lIhPeiS&HYPkitsou2g*^RO@{i0sg3+unZp15bA&suNpP zVO0O{LEZA_x7K9YA1gdG?{WLS=?YB$3LO(wus7d3{l{Q6)7EIH(Kf}oZd0xfc~JvR zoeg?}PaELcReILm+SpJ`orc>_1h>138D?XN_^W-+j~Y{PVav16;Q2^|$RnQXZy-}!D&vfK8l z%W`Py=AXwO6d##tvg!}L$6@9%$9yN^jve@O`_i{d0_)hN%DJoW@58UlHjH=p+a2}w aGT`n*r%Qj+n!SU-Cp36XP<3EZ&i?_=JKH`0 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/32x32.png b/desktop/src-tauri/icons/32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..693d922a6186aabbe1acf55ead899a08969afc5d GIT binary patch literal 644 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 DgQ)=h literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/icon.png b/desktop/src-tauri/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..cb5002d452c81fafe0721c0775db65138f2f9191 GIT binary patch literal 5947 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 aGT`n*r%Qj+n!SU-Cp36XP<3EZ&i?_=JKH`0 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/src/engine.rs b/desktop/src-tauri/src/engine.rs new file mode 100644 index 00000000..839cb694 --- /dev/null +++ b/desktop/src-tauri/src/engine.rs @@ -0,0 +1,144 @@ +//! Locating the engine binary and asking it what it is. +//! +//! The application ships the engine beside itself rather than linking it, so the +//! first question at startup is "where is it, and does it run". Resolution order +//! mirrors `ci/smoke.sh`'s `find_bin` in spirit: an explicit override wins, then the +//! bundled copy, then whatever is on PATH. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +use serde::Serialize; + +/// Name of the engine executable on this platform. +pub const EXE: &str = if cfg!(windows) { + "mumdia.exe" +} else { + "mumdia" +}; + +/// What the application knows about the engine it will run. +#[derive(Serialize, Clone, Debug)] +pub struct Info { + /// Absolute path to the binary that will be spawned. + pub path: String, + /// The `--version` line, verbatim. + pub version: String, + /// How the path was found, for the "why is it using that one" question. + pub source: &'static str, +} + +/// Every place the engine may live, in the order they are tried. +/// +/// `MUMDIA_BIN` first so a developer can point the application at a freshly built +/// engine without reinstalling it. Then the two bundled locations: Tauri puts +/// declared resources next to the executable, and the `binaries/` subdirectory is +/// where the release workflow stages the engine. PATH last, because a copy the user +/// installed separately is the least likely to be the one we were tested against. +fn candidates() -> Vec<(PathBuf, &'static str)> { + let mut out = Vec::new(); + + if let Some(p) = std::env::var_os("MUMDIA_BIN") { + out.push((PathBuf::from(p), "MUMDIA_BIN")); + } + if let Ok(exe) = std::env::current_exe() { + if let Some(dir) = exe.parent() { + out.push((dir.join(EXE), "bundled")); + out.push((dir.join("binaries").join(EXE), "bundled")); + // `cargo run` from src-tauri puts the app in target/debug, with the + // repository three levels up. Convenient during development and + // harmless in a release, where the path simply does not exist. + out.push(( + dir.join("../../../../rust/mumdia/target/release").join(EXE), + "repository build", + )); + } + } + out +} + +/// Resolve the engine, or explain every place that was tried. +pub fn resolve() -> Result<(PathBuf, &'static str), String> { + let cands = candidates(); + for (path, source) in &cands { + if path.is_file() { + let abs = path.canonicalize().unwrap_or_else(|_| path.clone()); + return Ok((strip_unc(abs), source)); + } + } + if let Ok(path) = which_on_path() { + return Ok((path, "PATH")); + } + let tried: Vec = cands + .iter() + .map(|(p, s)| format!(" {} ({s})", p.display())) + .collect(); + Err(format!( + "could not find the {EXE} engine. Tried:\n{}\n and every directory on PATH.\n\ + Set MUMDIA_BIN to the engine binary, or reinstall the application.", + tried.join("\n") + )) +} + +/// `Path::canonicalize` returns a `\\?\C:\...` extended-length path on Windows, which +/// many programs render but few accept back. Strip the prefix so the path we display +/// is the path a user could paste into a terminal. +fn strip_unc(p: PathBuf) -> PathBuf { + let s = p.to_string_lossy().to_string(); + match s.strip_prefix(r"\\?\") { + Some(rest) => PathBuf::from(rest), + None => p, + } +} + +fn which_on_path() -> Result { + let path = std::env::var_os("PATH").ok_or(())?; + for dir in std::env::split_paths(&path) { + let cand = dir.join(EXE); + if cand.is_file() { + return Ok(cand); + } + } + Err(()) +} + +/// Resolve the engine and ask it for its version. +/// +/// Running it is the point: a binary that exists but cannot execute (wrong +/// architecture, missing library, quarantined by antivirus) fails here, at startup, +/// rather than at the moment someone starts an hour-long search. +pub fn info() -> Result { + let (path, source) = resolve()?; + let out = command(&path) + .arg("--version") + .output() + .map_err(|e| format!("found {} but could not run it: {e}", path.display()))?; + if !out.status.success() { + return Err(format!( + "{} --version exited with {}", + path.display(), + out.status + )); + } + Ok(Info { + path: path.display().to_string(), + version: String::from_utf8_lossy(&out.stdout).trim().to_string(), + source, + }) +} + +/// A `Command` for the engine with the console window suppressed on Windows. +/// +/// Without this every engine invocation flashes a console window, including the +/// version probe at startup. +pub fn command(path: &Path) -> Command { + #[allow(unused_mut)] + let mut cmd = Command::new(path); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + cmd.creation_flags(CREATE_NO_WINDOW); + } + cmd +} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs new file mode 100644 index 00000000..464e2716 --- /dev/null +++ b/desktop/src-tauri/src/lib.rs @@ -0,0 +1,8 @@ +//! The console's logic, as a library so integration tests can drive a real search +//! without a window. +//! +//! `main.rs` is the Tauri shell over this: it owns the window, the command surface +//! and the run registry, and nothing else. + +pub mod engine; +pub mod run; diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs new file mode 100644 index 00000000..3e0af8df --- /dev/null +++ b/desktop/src-tauri/src/main.rs @@ -0,0 +1,161 @@ +// A release build must not open a console window behind the application. Debug +// builds keep it, because that is where the developer's own `println!` goes. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use mumdia_console::{engine, run}; + +use std::collections::HashMap; +use std::path::Path; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; + +use tauri::Manager; + +#[derive(Default)] +struct AppState { + runs: Mutex>>, + next_id: AtomicU64, +} + +/// Which engine will be used, and does it execute. +#[tauri::command] +fn engine_info() -> Result { + engine::info() +} + +/// Start a search. Returns the run id used by every subsequent call. +#[tauri::command] +fn start_run(state: tauri::State<'_, AppState>, req: run::Request) -> Result { + let id = format!("run-{}", state.next_id.fetch_add(1, Ordering::SeqCst) + 1); + let handle = run::start(id.clone(), req)?; + state + .runs + .lock() + .map_err(|_| "internal state is poisoned".to_string())? + .insert(id.clone(), handle); + Ok(id) +} + +/// Poll one run. The interface calls this on a timer; everything it displays is here. +#[tauri::command] +fn run_state(state: tauri::State<'_, AppState>, id: String) -> Option { + let runs = state.runs.lock().ok()?; + runs.get(&id).map(|r| r.snapshot()) +} + +/// Stop a run: kill the process tree, then sweep the temporary files it left. +#[tauri::command] +fn cancel_run(state: tauri::State<'_, AppState>, id: String) -> Result<(), String> { + let handle = { + let runs = state + .runs + .lock() + .map_err(|_| "internal state is poisoned".to_string())?; + runs.get(&id).cloned() + }; + match handle { + Some(r) => { + // Killing waits for SIGTERM to be given a chance on Unix, so do it off + // the command thread and let the interface keep polling meanwhile. + std::thread::spawn(move || r.cancel()); + Ok(()) + } + None => Err(format!("no such run: {id}")), + } +} + +/// Open a folder in the platform file manager. +#[tauri::command] +fn reveal(path: String) -> Result<(), String> { + let p = Path::new(&path); + if !p.exists() { + return Err(format!("{path} does not exist")); + } + #[cfg(windows)] + let r = std::process::Command::new("explorer").arg(p).spawn(); + #[cfg(target_os = "macos")] + let r = std::process::Command::new("open").arg(p).spawn(); + #[cfg(all(unix, not(target_os = "macos")))] + let r = std::process::Command::new("xdg-open").arg(p).spawn(); + // `explorer` returns a non-zero exit code even on success, so only the spawn + // itself is checked. + r.map(|_| ()) + .map_err(|e| format!("could not open {path}: {e}")) +} + +/// Configuration presets shipped beside the engine, for the input screen. +/// +/// Found rather than hard-coded: the release archive carries `configs/examples/`, and +/// listing what is actually there means a preset cannot be offered that does not +/// exist. An empty list is a valid answer and the interface says so. +#[tauri::command] +fn presets() -> Vec { + let mut out = Vec::new(); + let mut dirs = Vec::new(); + if let Ok(exe) = std::env::current_exe() { + if let Some(d) = exe.parent() { + dirs.push(d.join("configs").join("examples")); + dirs.push(d.join("../../../../configs/examples")); + } + } + for dir in dirs { + let Ok(entries) = std::fs::read_dir(&dir) else { + continue; + }; + let mut found: Vec<_> = entries + .flatten() + .map(|e| e.path()) + .filter(|p| p.extension().is_some_and(|e| e == "json")) + .collect(); + found.sort(); + for p in found { + let name = p + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("config") + .to_string(); + out.push(serde_json::json!({ + "name": name, + "path": p.display().to_string(), + })); + } + if !out.is_empty() { + break; + } + } + out +} + +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) + .manage(AppState::default()) + .invoke_handler(tauri::generate_handler![ + engine_info, + start_run, + run_state, + cancel_run, + reveal, + presets + ]) + .on_window_event(|window, event| { + // Closing the window must not leave an engine, or a Python worker, running + // with no way to reach it. Every live run is stopped first. + if let tauri::WindowEvent::Destroyed = event { + if let Some(state) = window.app_handle().try_state::() { + let handles: Vec<_> = state + .runs + .lock() + .map(|r| r.values().cloned().collect()) + .unwrap_or_default(); + for h in handles { + if h.snapshot().status == "running" { + h.cancel(); + } + } + } + } + }) + .run(tauri::generate_context!()) + .expect("failed to start the MuMDIA console"); +} diff --git a/desktop/src-tauri/src/run.rs b/desktop/src-tauri/src/run.rs new file mode 100644 index 00000000..73c1e84b --- /dev/null +++ b/desktop/src-tauri/src/run.rs @@ -0,0 +1,701 @@ +//! Starting, watching and stopping one search. +//! +//! # Why the engine is a child process and not a linked library +//! +//! The engine is a library crate, so linking it and calling stages in-process looks +//! attractive: one binary, no path resolution, no version skew. It is the wrong +//! choice, for one decisive reason and two supporting ones. +//! +//! The engine installs no signal handler anywhere, so stopping a run is a kill, and +//! a Rust thread cannot be killed. Linked in-process there would be no Stop button +//! at all, only a window that ignores you for an hour. Supporting reasons: a stage +//! panic would take the whole application down with it rather than ending one run, +//! and rayon's global pool can only be built once per process, so `--threads` could +//! not change between runs. +//! +//! # How progress is observed +//! +//! Not by parsing the log. Every stage writes `.report.json` beside its +//! output, carrying the producing stage, row count, elapsed time and per-stage +//! statistics. Polling the output directory for those files is a structured progress +//! feed that costs the engine nothing and works identically for a run this +//! application started and one it is merely looking at. + +use std::collections::BTreeMap; +use std::io::{BufRead, BufReader}; +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use serde::{Deserialize, Serialize}; + +use crate::engine; + +/// How often the output directory is rescanned for stage reports. +const POLL: Duration = Duration::from_millis(700); +/// Log lines kept in memory. The pane shows the tail; the full log is on disk. +const LOG_TAIL: usize = 4000; + +/// What the interface asks for when it starts a search. +#[derive(Deserialize, Debug, Clone)] +pub struct Request { + pub mzml: String, + pub out_dir: String, + /// FASTA mode. Mutually exclusive with the library pair. + pub fasta: Option, + pub lib_precursors: Option, + pub lib_fragments: Option, + pub config: Option, + pub threads: Option, +} + +/// One stage, as observed from the artifact reports it produced. +/// +/// A stage can write several artifacts (`convert` writes four), so rows and elapsed +/// time are summed and the artifact count is kept, which is more honest than +/// reporting whichever file happened to be read last. +#[derive(Serialize, Clone, Debug, Default)] +pub struct Stage { + pub name: String, + pub rows: u64, + pub elapsed_ms: u64, + pub artifacts: usize, +} + +/// Everything the results panel shows, taken from the scored table's own report. +/// +/// Read from disk rather than recomputed: `psms_scored.parquet.report.json` records +/// the classifier that ACTUALLY ran alongside the one that was requested, and those +/// differ when a sidecar fails and `rescore.strict` is false. +#[derive(Serialize, Clone, Debug, Default)] +pub struct Results { + pub classifier: String, + pub classifier_requested: String, + pub config_hash: String, + pub peptides_1pct: u64, + pub precursors_1pct: u64, + pub protein_groups_1pct: u64, + pub psms: u64, + pub has_peptides_tsv: bool, + pub has_proteins_tsv: bool, +} + +/// The whole observable state of a run. Serialised to the interface on every poll. +#[derive(Serialize, Clone, Debug)] +pub struct Snapshot { + pub id: String, + /// `starting` | `running` | `done` | `failed` | `cancelled` + pub status: String, + pub exit_code: Option, + pub error: Option, + pub stages: Vec, + pub log: Vec, + pub out_dir: String, + /// The exact command line, so it can be shown, copied and reproduced. + pub command: String, + pub started_unix_ms: u64, + pub elapsed_ms: u64, + pub results: Option, + /// True in library-input mode, which skips digest, peptidoforms and predict-frag. + pub library_mode: bool, +} + +pub struct Run { + pub snapshot: Mutex, + /// Process id of the engine. On Unix this is also its process-group id, because + /// it is spawned into a new group. + pid: Mutex>, + cancelled: AtomicBool, +} + +impl Run { + fn set(&self, f: F) { + if let Ok(mut s) = self.snapshot.lock() { + f(&mut s); + } + } + + pub fn snapshot(&self) -> Snapshot { + self.snapshot + .lock() + .map(|s| s.clone()) + .unwrap_or_else(|e| e.into_inner().clone()) + } + + /// Stop the run: kill the process tree, then remove the rubble. + /// + /// Both halves matter. The engine spawns Python workers, so killing only the + /// engine would orphan a process that may hold tens of gigabytes. And a hard + /// kill skips destructors, so the atomic-write layer never removes its + /// `.tmp-` files; without a sweep the next run starts in a dirty directory. + pub fn cancel(&self) { + self.cancelled.store(true, Ordering::SeqCst); + let pid = self.pid.lock().ok().and_then(|p| *p); + if let Some(pid) = pid { + kill_tree(pid); + } + let out_dir = self.snapshot().out_dir; + sweep_temp_files(Path::new(&out_dir)); + self.set(|s| { + if s.status == "running" || s.status == "starting" { + s.status = "cancelled".into(); + } + }); + } +} + +/// Kill a process and everything it spawned. +/// +/// Deliberately shelling out rather than calling the platform APIs directly: both +/// would need `unsafe`, and this is not on any hot path. `taskkill /T` walks the +/// tree at kill time and `kill` on a negative pid signals the whole group, so a +/// Python worker the application never knew about is included either way. +fn kill_tree(pid: u32) { + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + let _ = std::process::Command::new("taskkill") + .args(["/PID", &pid.to_string(), "/T", "/F"]) + .creation_flags(CREATE_NO_WINDOW) + .status(); + } + #[cfg(unix)] + { + // Negative pid addresses the process group. TERM first so the engine can + // unwind and clean up its own temp files, KILL shortly after for anything + // that ignored it. + let group = format!("-{pid}"); + let _ = std::process::Command::new("kill") + .args(["-TERM", &group]) + .status(); + std::thread::sleep(Duration::from_millis(1500)); + let _ = std::process::Command::new("kill") + .args(["-KILL", &group]) + .status(); + } +} + +/// Remove `*.tmp-` files left by a killed run, recursively. +fn sweep_temp_files(dir: &Path) { + for f in walk(dir) { + if f.file_name() + .and_then(|n| n.to_str()) + .is_some_and(|n| n.contains(".tmp-")) + { + let _ = std::fs::remove_file(&f); + } + } +} + +/// Every file under `dir`, recursively. Small hand-rolled walk to avoid a dependency +/// for one function; output directories are shallow. +fn walk(dir: &Path) -> Vec { + let mut out = Vec::new(); + let mut stack = vec![dir.to_path_buf()]; + while let Some(d) = stack.pop() { + let Ok(entries) = std::fs::read_dir(&d) else { + continue; + }; + for e in entries.flatten() { + let p = e.path(); + match e.file_type() { + Ok(t) if t.is_dir() => stack.push(p), + Ok(t) if t.is_file() => out.push(p), + _ => {} + } + } + } + out +} + +fn now_ms() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0) +} + +/// Build the argument list, and reject the input combinations the engine would +/// reject anyway — here, where the message can point at a field. +fn argv(req: &Request) -> Result, String> { + let lib = req.lib_precursors.is_some() || req.lib_fragments.is_some(); + if lib && req.fasta.is_some() { + return Err("choose either a FASTA or a spectral library, not both".into()); + } + if !lib && req.fasta.is_none() { + return Err("select a FASTA file or a spectral library".into()); + } + if lib && (req.lib_precursors.is_none() || req.lib_fragments.is_none()) { + return Err("a spectral library needs both the precursor and the fragment table".into()); + } + if req.mzml.trim().is_empty() { + return Err("select an mzML file".into()); + } + if req.out_dir.trim().is_empty() { + return Err("choose a folder for the results".into()); + } + + let mut a: Vec = vec!["run".into()]; + a.push("--mzml".into()); + a.push(req.mzml.clone()); + a.push("--out-dir".into()); + a.push(req.out_dir.clone()); + if let Some(f) = &req.fasta { + a.push("--fasta".into()); + a.push(f.clone()); + } + if let (Some(p), Some(g)) = (&req.lib_precursors, &req.lib_fragments) { + a.push("--lib-precursors".into()); + a.push(p.clone()); + a.push("--lib-fragments".into()); + a.push(g.clone()); + } + if let Some(c) = &req.config { + if !c.trim().is_empty() { + a.push("--config".into()); + a.push(c.clone()); + } + } + if let Some(t) = req.threads { + if t > 0 { + a.push("--threads".into()); + a.push(t.to_string()); + } + } + Ok(a) +} + +/// Quote an argument for display only. This string is shown and copied, never +/// executed, so it just has to be pasteable. +fn quote(s: &str) -> String { + if s.contains(' ') { + format!("\"{s}\"") + } else { + s.to_string() + } +} + +/// Start a search. Returns immediately with a handle; progress arrives by polling. +pub fn start(id: String, req: Request) -> Result, String> { + let args = argv(&req)?; + let (exe, _source) = engine::resolve()?; + + std::fs::create_dir_all(&req.out_dir) + .map_err(|e| format!("cannot create the results folder {}: {e}", req.out_dir))?; + + let display = format!( + "{} {}", + quote(&exe.display().to_string()), + args.iter().map(|a| quote(a)).collect::>().join(" ") + ); + + let library_mode = req.lib_precursors.is_some(); + let run = Arc::new(Run { + snapshot: Mutex::new(Snapshot { + id: id.clone(), + status: "starting".into(), + exit_code: None, + error: None, + stages: Vec::new(), + log: Vec::new(), + out_dir: req.out_dir.clone(), + command: display, + started_unix_ms: now_ms(), + elapsed_ms: 0, + results: None, + library_mode, + }), + pid: Mutex::new(None), + cancelled: AtomicBool::new(false), + }); + + let mut cmd = engine::command(&exe); + cmd.args(&args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .stdin(Stdio::null()); + + // A new process group, so cancelling can signal the engine AND the Python + // workers it spawns. Windows gets the same effect from `taskkill /T`. + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + cmd.process_group(0); + } + + let mut child = cmd + .spawn() + .map_err(|e| format!("could not start {}: {e}", exe.display()))?; + + let pid = child.id(); + if let Ok(mut p) = run.pid.lock() { + *p = Some(pid); + } + run.set(|s| s.status = "running".into()); + + // stderr carries the log; stdout carries result summaries. Both are shown. + for (stream, tag) in [ + ( + child + .stderr + .take() + .map(|s| Box::new(s) as Box), + "", + ), + ( + child + .stdout + .take() + .map(|s| Box::new(s) as Box), + "", + ), + ] { + let Some(stream) = stream else { continue }; + let run = Arc::clone(&run); + let _ = tag; + std::thread::spawn(move || { + let reader = BufReader::new(stream); + for line in reader.lines().map_while(Result::ok) { + run.set(|s| { + s.log.push(line); + if s.log.len() > LOG_TAIL { + let drop = s.log.len() - LOG_TAIL; + s.log.drain(0..drop); + } + }); + } + }); + } + + // Progress: rescan the output directory until the run stops. + { + let run = Arc::clone(&run); + let out_dir = PathBuf::from(&req.out_dir); + std::thread::spawn(move || { + let started = Instant::now(); + loop { + let stages = scan_stages(&out_dir); + let running = { + let s = run.snapshot(); + s.status == "running" || s.status == "starting" + }; + run.set(|s| { + s.stages = stages; + s.elapsed_ms = started.elapsed().as_millis() as u64; + }); + if !running { + // The final scan belongs to the waiter, not here: it has to happen + // BEFORE the status becomes terminal, or a caller that polls until + // the run is finished can read a snapshot whose stages and results + // have not been filled in yet. + break; + } + std::thread::sleep(POLL); + } + }); + } + + // Reap the child, then publish the terminal state in one step. + // + // The order matters. Everything a finished run displays -- its stages and its + // results -- is read from disk here, BEFORE the status stops being `running`. + // Doing it the other way round leaves a window in which the run says it is + // finished but has no stages, which an interface polling for completion will + // reliably catch: the results screen renders empty and then fills in. + { + let run = Arc::clone(&run); + let out_dir = PathBuf::from(&req.out_dir); + std::thread::spawn(move || { + let outcome = child.wait(); + let stages = scan_stages(&out_dir); + let results = read_results(&out_dir); + match outcome { + Ok(status) => run.set(|s| { + s.stages = stages; + s.results = results; + s.exit_code = status.code(); + if s.status == "cancelled" { + return; + } + if status.success() { + s.status = "done".into(); + } else { + s.status = "failed".into(); + // The last stderr line is almost always the anyhow error + // chain, which is the sentence worth showing. + s.error = s.log.iter().rev().find(|l| !l.trim().is_empty()).cloned(); + } + }), + Err(e) => run.set(|s| { + s.stages = stages; + s.status = "failed".into(); + s.error = Some(format!("could not wait for the engine: {e}")); + }), + } + }); + } + + Ok(run) +} + +/// Fold every `*.report.json` under `dir` into one row per producing stage. +fn scan_stages(dir: &Path) -> Vec { + let mut by_stage: BTreeMap = BTreeMap::new(); + for f in walk(dir) { + if !f + .file_name() + .and_then(|n| n.to_str()) + .is_some_and(|n| n.ends_with(".report.json")) + { + continue; + } + let Ok(text) = std::fs::read_to_string(&f) else { + continue; + }; + let Ok(v) = serde_json::from_str::(&text) else { + continue; + }; + let Some(stage) = v.get("stage").and_then(|s| s.as_str()) else { + continue; + }; + let e = by_stage.entry(stage.to_string()).or_insert_with(|| Stage { + name: stage.to_string(), + ..Default::default() + }); + e.rows += v.get("rows").and_then(|r| r.as_u64()).unwrap_or(0); + e.elapsed_ms += v.get("elapsed_ms").and_then(|r| r.as_u64()).unwrap_or(0); + e.artifacts += 1; + } + by_stage.into_values().collect() +} + +/// Read the results panel out of the scored table's report. +fn read_results(dir: &Path) -> Option { + let path = dir.join("psms_scored.parquet.report.json"); + let text = std::fs::read_to_string(path).ok()?; + let v: serde_json::Value = serde_json::from_str(&text).ok()?; + let params = v.get("params"); + let stats = v.get("stats"); + let s = |o: Option<&serde_json::Value>, k: &str| -> String { + o.and_then(|p| p.get(k)) + .and_then(|x| x.as_str()) + .unwrap_or("") + .to_string() + }; + let n = |o: Option<&serde_json::Value>, k: &str| -> u64 { + o.and_then(|p| p.get(k)) + .and_then(|x| x.as_u64()) + .unwrap_or(0) + }; + Some(Results { + classifier: s(stats, "classifier"), + classifier_requested: s(params, "classifier_requested"), + config_hash: s(params, "config_hash"), + peptides_1pct: n(stats, "target_peptides_at_1pct"), + precursors_1pct: n(stats, "target_precursors_at_1pct"), + protein_groups_1pct: n(stats, "target_protein_groups_at_1pct"), + psms: n(stats, "psms"), + has_peptides_tsv: dir.join("peptides.tsv").is_file(), + has_proteins_tsv: dir.join("proteins.tsv").is_file(), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn req() -> Request { + Request { + mzml: "a.mzML".into(), + out_dir: "out".into(), + fasta: None, + lib_precursors: None, + lib_fragments: None, + config: None, + threads: None, + } + } + + #[test] + fn fasta_and_library_together_is_rejected() { + let mut r = req(); + r.fasta = Some("p.fasta".into()); + r.lib_precursors = Some("p.parquet".into()); + r.lib_fragments = Some("f.parquet".into()); + let e = argv(&r).unwrap_err(); + assert!(e.contains("not both"), "{e}"); + } + + #[test] + fn a_search_space_is_required() { + let e = argv(&req()).unwrap_err(); + assert!(e.contains("FASTA") && e.contains("library"), "{e}"); + } + + #[test] + fn half_a_library_is_rejected() { + // The engine would reject this too, but only after starting up. Catching it + // here lets the message name the missing field. + let mut r = req(); + r.lib_precursors = Some("p.parquet".into()); + let e = argv(&r).unwrap_err(); + assert!(e.contains("both"), "{e}"); + } + + #[test] + fn fasta_mode_builds_the_documented_invocation() { + let mut r = req(); + r.fasta = Some("p.fasta".into()); + r.threads = Some(8); + r.config = Some("c.json".into()); + assert_eq!( + argv(&r).unwrap(), + vec![ + "run", + "--mzml", + "a.mzML", + "--out-dir", + "out", + "--fasta", + "p.fasta", + "--config", + "c.json", + "--threads", + "8", + ] + ); + } + + #[test] + fn library_mode_passes_both_tables_and_no_fasta() { + let mut r = req(); + r.lib_precursors = Some("p.parquet".into()); + r.lib_fragments = Some("f.parquet".into()); + let a = argv(&r).unwrap(); + assert!(a.contains(&"--lib-precursors".to_string())); + assert!(a.contains(&"--lib-fragments".to_string())); + assert!(!a.contains(&"--fasta".to_string())); + } + + #[test] + fn an_empty_config_is_omitted_rather_than_passed_as_an_empty_path() { + let mut r = req(); + r.fasta = Some("p.fasta".into()); + r.config = Some(" ".into()); + assert!(!argv(&r).unwrap().contains(&"--config".to_string())); + } + + /// Fold real artifact reports, written by a real run, into stage rows. + #[test] + fn stages_are_folded_from_real_artifact_reports() { + let dir = + std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/run_out"); + if !dir.is_dir() { + eprintln!("fixture missing, skipping"); + return; + } + let stages = scan_stages(&dir); + let names: Vec<&str> = stages.iter().map(|s| s.name.as_str()).collect(); + for expected in [ + "convert", + "digest", + "peptidoforms", + "predict-frag", + "search-seed", + "rt-im-train", + "extract", + "features", + "compete", + "rescore", + "quant", + ] { + assert!(names.contains(&expected), "missing {expected} in {names:?}"); + } + // `convert` writes four artifacts under spectra/; the walk must recurse and + // the four must fold into one row. + let convert = stages.iter().find(|s| s.name == "convert").unwrap(); + assert_eq!(convert.artifacts, 4); + assert_eq!(convert.rows, 8 + 480 + 60 + 480); + } + + #[test] + fn results_come_from_the_scored_report_not_from_the_request() { + let dir = + std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/run_out"); + if !dir.is_dir() { + eprintln!("fixture missing, skipping"); + return; + } + let r = read_results(&dir).expect("scored report should parse"); + assert_eq!(r.classifier, "native_tda"); + assert_eq!(r.peptides_1pct, 151); + assert_eq!(r.psms, 152); + assert!(r.has_peptides_tsv && r.has_proteins_tsv); + } + + /// The engine spawns Python workers, so a kill that reaches only the process we + /// launched would leave one behind holding tens of gigabytes. This spawns a + /// parent that spawns its own child and checks the kill lands. + /// + /// The end-to-end cancel test cannot cover this: the fixture search finishes in + /// under a second, faster than a stop can be issued. + #[test] + fn kill_tree_terminates_the_process_it_is_given() { + let mut cmd = if cfg!(windows) { + let mut c = std::process::Command::new("cmd"); + c.args(["/C", "ping -n 30 127.0.0.1"]); + c + } else { + let mut c = std::process::Command::new("sh"); + c.args(["-c", "sleep 30 & wait"]); + c + }; + cmd.stdout(Stdio::null()).stderr(Stdio::null()); + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + cmd.process_group(0); + } + let mut child = cmd.spawn().expect("could not spawn the test process"); + std::thread::sleep(Duration::from_millis(400)); + assert!( + matches!(child.try_wait(), Ok(None)), + "the test process exited on its own; the test proves nothing" + ); + + kill_tree(child.id()); + + let start = Instant::now(); + loop { + match child.try_wait() { + Ok(Some(_)) => break, + _ if start.elapsed() > Duration::from_secs(15) => { + let _ = child.kill(); + panic!("the process survived kill_tree"); + } + _ => std::thread::sleep(Duration::from_millis(100)), + } + } + } + + #[test] + fn the_temp_sweep_removes_only_temp_files() { + let dir = std::env::temp_dir().join(format!("mumdia_sweep_{}", std::process::id())); + let nested = dir.join("spectra"); + std::fs::create_dir_all(&nested).unwrap(); + let keep = dir.join("peptides.tsv"); + let kill = dir.join("psms_scored.parquet.tmp-12345"); + let kill_nested = nested.join("spectra_ms2.parquet.tmp-9"); + for f in [&keep, &kill, &kill_nested] { + std::fs::write(f, b"x").unwrap(); + } + sweep_temp_files(&dir); + assert!(keep.is_file(), "a real output must survive the sweep"); + assert!(!kill.is_file(), "the temp file must go"); + assert!(!kill_nested.is_file(), "the sweep must recurse"); + let _ = std::fs::remove_dir_all(&dir); + } +} diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json new file mode 100644 index 00000000..179880cd --- /dev/null +++ b/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "MuMDIA", + "version": "0.1.0", + "identifier": "be.ugent.compomics.mumdia.console", + "build": { + "frontendDist": "../ui" + }, + "app": { + "windows": [ + { + "label": "main", + "title": "MuMDIA", + "width": 1120, + "height": 800, + "minWidth": 900, + "minHeight": 620 + } + ], + "security": { + "csp": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:" + }, + "withGlobalTauri": true + }, + "bundle": { + "active": true, + "targets": [ + "msi", + "appimage" + ], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/icon.ico" + ], + "shortDescription": "MuMDIA DIA search engine", + "longDescription": "Desktop interface for the MuMDIA data-independent acquisition proteomics search engine." + } +} diff --git a/desktop/src-tauri/tests/end_to_end.rs b/desktop/src-tauri/tests/end_to_end.rs new file mode 100644 index 00000000..a46582da --- /dev/null +++ b/desktop/src-tauri/tests/end_to_end.rs @@ -0,0 +1,182 @@ +//! Drive a real search through the supervisor, with no window involved. +//! +//! This is the milestone-1 acceptance criterion expressed as a test: spawn the +//! engine, watch the artifact reports appear, and read the results back. It covers +//! the parts a person clicking buttons would exercise, minus the buttons. +//! +//! Skipped unless both are set, so it never fails a machine that has no engine: +//! +//! MUMDIA_BIN the engine binary +//! MUMDIA_TEST_MZML an mzML to search +//! MUMDIA_TEST_FASTA a FASTA to digest +//! +//! `ci/smoke.sh` generates a suitable fixture pair in its work directory. + +use std::time::{Duration, Instant}; + +/// Poll a run until it leaves the running state, or give up. +fn wait_for_finish( + run: &mumdia_console::run::Run, + limit: Duration, +) -> mumdia_console::run::Snapshot { + let start = Instant::now(); + loop { + let s = run.snapshot(); + if s.status != "running" && s.status != "starting" { + return s; + } + if start.elapsed() > limit { + panic!( + "run did not finish within {limit:?}; last status {}", + s.status + ); + } + std::thread::sleep(Duration::from_millis(200)); + } +} + +fn env(name: &str) -> Option { + std::env::var(name).ok().filter(|v| !v.trim().is_empty()) +} + +#[test] +fn a_fasta_search_runs_to_completion_and_reports_itself() { + let (Some(mzml), Some(fasta)) = (env("MUMDIA_TEST_MZML"), env("MUMDIA_TEST_FASTA")) else { + eprintln!("MUMDIA_TEST_MZML / MUMDIA_TEST_FASTA not set; skipping"); + return; + }; + if env("MUMDIA_BIN").is_none() { + eprintln!("MUMDIA_BIN not set; skipping"); + return; + } + + let out = std::env::temp_dir().join(format!("mumdia_console_e2e_{}", std::process::id())); + let _ = std::fs::remove_dir_all(&out); + + let req = mumdia_console::run::Request { + mzml, + out_dir: out.display().to_string(), + fasta: Some(fasta), + lib_precursors: None, + lib_fragments: None, + config: None, + threads: Some(2), + }; + + let run = mumdia_console::run::start("e2e".into(), req).expect("the engine should start"); + + // The displayed command must be the real invocation, because a user is invited + // to copy it into a terminal. + let cmd = run.snapshot().command; + assert!(cmd.contains(" run "), "{cmd}"); + assert!(cmd.contains("--fasta"), "{cmd}"); + assert!(cmd.contains("--threads 2"), "{cmd}"); + + let s = wait_for_finish(&run, Duration::from_secs(600)); + assert_eq!( + s.status, + "done", + "run failed: {:?}\n{}", + s.error, + s.log.join("\n") + ); + assert_eq!(s.exit_code, Some(0)); + + // Progress was actually observed while it ran, not reconstructed at the end. + let names: Vec<&str> = s.stages.iter().map(|x| x.name.as_str()).collect(); + for expected in ["convert", "search-seed", "extract", "rescore", "quant"] { + assert!( + names.contains(&expected), + "missing stage {expected}: {names:?}" + ); + } + + // The log is captured from the engine's stderr. + assert!(!s.log.is_empty(), "no log lines were captured"); + + // Results come from the scored table's own report. + let r = s + .results + .expect("results should be present after a successful run"); + assert!(!r.classifier.is_empty(), "the classifier should be named"); + assert!(r.psms > 0, "a successful fixture run scores some PSMs"); + assert!( + r.has_peptides_tsv, + "the report stage should have written peptides.tsv" + ); + + let _ = std::fs::remove_dir_all(&out); +} + +#[test] +fn stopping_a_run_kills_it_and_leaves_no_temp_files() { + let (Some(mzml), Some(fasta)) = (env("MUMDIA_TEST_MZML"), env("MUMDIA_TEST_FASTA")) else { + eprintln!("MUMDIA_TEST_MZML / MUMDIA_TEST_FASTA not set; skipping"); + return; + }; + if env("MUMDIA_BIN").is_none() { + eprintln!("MUMDIA_BIN not set; skipping"); + return; + } + + let out = std::env::temp_dir().join(format!("mumdia_console_cancel_{}", std::process::id())); + let _ = std::fs::remove_dir_all(&out); + + let req = mumdia_console::run::Request { + mzml, + out_dir: out.display().to_string(), + fasta: Some(fasta), + lib_precursors: None, + lib_fragments: None, + config: None, + threads: Some(1), + }; + let run = mumdia_console::run::start("cancel".into(), req).expect("the engine should start"); + + // Long enough to be doing real work and holding files open, short enough that a + // small fixture has not finished. + std::thread::sleep(Duration::from_millis(400)); + run.cancel(); + + let s = wait_for_finish(&run, Duration::from_secs(60)); + + // The fixture search takes about two seconds, so on a fast machine it can finish + // before the cancel lands. Say so rather than asserting something that did not + // happen: a test that quietly passes without exercising the thing it names is + // worse than no test. Point it at real data and the cancellation path runs. + if s.status == "done" { + eprintln!( + "the run completed in {} ms, before the stop could land; cancellation was NOT exercised (use a larger input to cover it)", + s.elapsed_ms + ); + } else { + assert_eq!( + s.status, "cancelled", + "a stopped run reports itself as cancelled" + ); + assert_ne!(s.exit_code, Some(0), "a killed engine did not exit cleanly"); + } + + // A hard kill skips destructors, so the sweep is what keeps the folder clean. + let mut leftovers = Vec::new(); + let mut stack = vec![out.clone()]; + while let Some(d) = stack.pop() { + let Ok(entries) = std::fs::read_dir(&d) else { + continue; + }; + for e in entries.flatten() { + let p = e.path(); + if p.is_dir() { + stack.push(p); + } else if p.to_string_lossy().contains(".tmp-") { + leftovers.push(p); + } + } + } + assert!( + leftovers.is_empty(), + "temp files survived the sweep: {leftovers:?}" + ); + + let _ = std::fs::remove_dir_all(&out); +} diff --git a/desktop/src-tauri/tests/fixtures/run_out/cal.json b/desktop/src-tauri/tests/fixtures/run_out/cal.json new file mode 100644 index 00000000..a9411bc9 --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/cal.json @@ -0,0 +1,19 @@ +{ + "calibration_status": "loess", + "holdout_resid_abs_median_s": null, + "holdout_resid_p_rt_s": null, + "intercept": 21.540443852217876, + "method": "loess", + "multiplier": 1.0, + "n_holdout": null, + "n_sizing_train": null, + "n_train": 110, + "p_rt": 0.95, + "rt_residual_abs_median_s": 1.29002288639483, + "rt_residual_mad_s": 1.2889020987967257, + "rt_residual_median_s": 0.011864247283511986, + "slope": 0.9453850141345133, + "w_rt": 11.826482451982308, + "w_rt_sizing": "in_sample", + "window_holdout_frac": 0.0 +} \ No newline at end of file diff --git a/desktop/src-tauri/tests/fixtures/run_out/chromatograms.parquet.report.json b/desktop/src-tauri/tests/fixtures/run_out/chromatograms.parquet.report.json new file mode 100644 index 00000000..961bbf4c --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/chromatograms.parquet.report.json @@ -0,0 +1,25 @@ +{ + "logical_name": "chromatograms", + "schema_name": "chromatograms", + "schema_version": 1, + "stage": "extract", + "rows": 2556, + "content_hash": "884d4dba966c1d4168180150ec9c09579d2cd883fac7b854a266a8c76dd63346", + "params": { + "effective_frag_tol_ppm": 5.0, + "frag_ppm_offset": -0.00026103398249893126, + "frag_tol_ppm": 20.0, + "gate_coelution_min": 0.5, + "gate_min_score": 0.2, + "gate_mode": "apex_pearson", + "presence_min_coelution": 2, + "presence_min_fragments": 3, + "scan_window": 3 + }, + "stats": { + "accepted": 284, + "scan_window": 3 + }, + "model_identity": null, + "elapsed_ms": 11 +} \ No newline at end of file diff --git a/desktop/src-tauri/tests/fixtures/run_out/features.parquet.report.json b/desktop/src-tauri/tests/fixtures/run_out/features.parquet.report.json new file mode 100644 index 00000000..f494283e --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/features.parquet.report.json @@ -0,0 +1,19 @@ +{ + "logical_name": "features", + "schema_name": "features", + "schema_version": 1, + "stage": "features", + "rows": 284, + "content_hash": "686820b41516510286c9c94f753940f26fb9a748435e7c9864f281a231267bf1", + "params": { + "coelution_corr_threshold": 0.9, + "set": "Extended" + }, + "stats": { + "feature_schema_id": "e5b4eff0133e411dbf406947c89466619ec557ab37874bc3948792d9890afe8e", + "n_features": 387, + "set": "Extended" + }, + "model_identity": null, + "elapsed_ms": 18 +} \ No newline at end of file diff --git a/desktop/src-tauri/tests/fixtures/run_out/features.parquet.schema.json b/desktop/src-tauri/tests/fixtures/run_out/features.parquet.schema.json new file mode 100644 index 00000000..6b9230bc --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/features.parquet.schema.json @@ -0,0 +1,392 @@ +{ + "feature_columns": [ + "rt_error_abs", + "rt_error_rel", + "n_matched_fragments", + "coelution_run", + "log_apex_intensity", + "frag_corr", + "frag_cosine", + "spectral_angle", + "coelution_mean", + "coelution_best", + "n_coelution_above", + "charge", + "peptide_length", + "n_proteins", + "library_norm_manhattan", + "library_rmsd", + "xcorr_coelution", + "xcorr_shape", + "sum_b_intensity", + "sum_y_intensity", + "diff_by_intensity", + "n_b_ions", + "n_y_ions", + "weighted_mass_error", + "mean_mass_error", + "isotope_corr", + "ms1_isom1_ratio", + "log_mono_ms1", + "has_ms1", + "log_sn", + "n_observations", + "base_width_rt", + "seed_score", + "seed_identified", + "matched_fraction", + "profile_cos", + "ref_corr", + "best_ref_corr", + "low_frag_coel", + "evidence", + "contrast_min", + "resid_corr", + "coel_clean", + "shadow_frac", + "spectrum_cosine_matched", + "spectrum_cosine_sqrt", + "spectrum_cosine_log", + "spectral_angle_sqrt", + "spectral_angle_matched", + "pearson_intensity_matched", + "pearson_intensity_log", + "spearman_intensity", + "spearman_intensity_matched", + "kendall_tau_intensity", + "dot_product_raw", + "dot_product_norm", + "library_recall_intensity", + "manhattan_sim", + "manhattan_sqrt", + "rmsd_norm", + "mae_norm", + "mse_log", + "mae_weighted_pred", + "abs_diff_q3", + "max_positive_residual", + "chebyshev_dist", + "minkowski_p3", + "bray_curtis", + "bray_curtis_sqrt", + "canberra", + "canberra_matched", + "wave_hedges", + "chi_square_pearson", + "chi_square_symmetric", + "divergence_distance", + "bhattacharyya_coef", + "hellinger", + "squared_chord", + "harmonic_mean_sim", + "jaccard_presence", + "dice_presence", + "intensity_weighted_pearson", + "regression_slope", + "gini_diff", + "wasserstein_mz", + "footrule_norm", + "rank_overlap_top3", + "top1_frag_match", + "top1_predicted_observed", + "frac_top3_predicted_observed", + "count_strong_predicted_absent", + "frac_predicted_absent", + "cosine_area", + "pearson_area", + "spectral_angle_area", + "cosine_fullwindow", + "stein_scott_weighted_dot", + "log_dot_product", + "spectral_log_evidence", + "scribe_score", + "log_dot_product_area", + "spectral_log_evidence_area", + "scribe_score_area", + "cosine_high_ordinal", + "cosine_robust_trim1", + "cosine_robust_trim2", + "cosine_robust_trim3", + "spectral_entropy_similarity", + "weighted_spectral_entropy_similarity", + "spectral_entropy_similarity_sqrt", + "spectral_entropy_similarity_topk", + "spectral_entropy_similarity_area", + "jensen_shannon_divergence", + "jeffreys_divergence", + "kl_obs_pred", + "kl_pred_obs", + "cross_entropy_obs_pred", + "obs_spectrum_entropy", + "pred_spectrum_entropy", + "entropy_diff", + "entropy_ratio", + "obs_normalized_entropy", + "normalized_entropy_diff", + "residual_spectrum_entropy", + "entropy_weight_obs", + "frag_ref_corr_mean", + "frag_ref_corr_obsweighted", + "frag_ref_corr_min", + "frag_ref_corr_std", + "frag_ref_corr_sq_mean", + "frag_ref_corr_topk_weighted", + "n_frag_ref_corr_above_0_9", + "frac_frag_ref_corr_above_0_8", + "frag_ref_corr_mean_full", + "full_vs_peak_corr_gain", + "pairwise_coelution_weighted", + "pairwise_coelution_min", + "pairwise_coelution_median", + "pairwise_coelution_std", + "pairwise_coelution_frac_negative", + "pairwise_coelution_hi", + "pairwise_coelution_lo", + "coelution_hi_lo_contrast", + "coelution_corr_entropy", + "xcorr_shape_mean", + "xcorr_shape_min", + "xcorr_shape_std", + "xcorr_lag_mean_abs", + "xcorr_lag_std", + "xcorr_lag_iqr", + "xcorr_lag_frac_zero", + "xcorr_lag_max_abs", + "xcorr_lag_entropy", + "ref_xcorr_lag_mean", + "ref_xcorr_shape_mean", + "observed_sum_vs_template_corr", + "frag_loo_ref_corr_mean", + "frag_loo_ref_corr_min", + "frac_frags_apex_aligned", + "top3_frag_ref_corr", + "by_cross_coelution", + "by_cross_lag_mean", + "charge_cross_coelution", + "explained_variance_ref", + "profile_residual_fraction", + "n_interfered_fragments", + "corrected_vs_raw_cos", + "corrected_vs_raw_ratio", + "ifs_removed_count", + "ifs_removed_intensity_frac", + "ifs_corr_gain", + "ifs_retained_frac", + "matched_frac_after_ifs", + "peak_to_full_area_ratio_profile", + "peak_to_full_area_ratio_frag_mean", + "peak_to_full_area_ratio_weighted", + "out_of_peak_intensity_frac", + "profile_corr_full_vs_peak_delta", + "frac_frag_ref_corr_below_0_5", + "explained_apex_intensity_frac", + "apex_purity", + "interference_apex_residual_fraction", + "dominant_frag_ref_corr", + "explained_variance_ratio", + "second_component_fraction", + "profile_second_peak_ratio", + "n_competing_peaks_in_window", + "matched_pred_intensity_fraction", + "top_pred_frag_matched", + "gaussian_fit_r2", + "gaussian_cosine", + "emg_fit_improvement", + "apex_prominence", + "profile_peak_snr", + "fwhm_seconds", + "fwhm_to_window_ratio", + "width_at_10pct", + "width_ratio_10_50", + "hwhm_asymmetry", + "tailing_factor_usp", + "asymmetry_factor_10pct", + "apex_sharpness", + "apex_curvature", + "apex_to_boundary_ratio", + "apex_dominance", + "zigzag_index", + "jaggedness", + "roughness_2nd_deriv", + "n_local_maxima", + "modality", + "rt_skewness", + "rt_excess_kurtosis", + "rt_std_seconds", + "mean_mode_offset", + "fraction_area_within_fwhm", + "triangle_area_similarity", + "baseline_fraction", + "peak_completeness", + "apex_centering_offset", + "intensity_score", + "total_xic_log", + "frag_fwhm_cv", + "frag_fwhm_mean", + "frag_apex_rt_dispersion", + "frag_apex_rt_dispersion_weighted", + "frag_apex_offset_from_profile_mean", + "frag_gaussianity_mean", + "frag_gaussianity_weighted", + "frag_zigzag_mean", + "sumtrace_unweighted_gaussian_r2", + "reference_profile_rt_entropy_peak", + "reference_profile_rt_entropy_ratio", + "median_abs_frag_ppm", + "signed_mean_frag_ppm", + "ppm_std", + "ppm_iqr", + "ppm_range", + "max_abs_frag_ppm", + "intensity_weighted_abs_ppm", + "intensity_weighted_signed_ppm", + "intensity_weighted_ppm_std", + "lib_weighted_abs_ppm", + "frac_frag_within_half_tol", + "high_ppm_intensity_frac", + "ppm_intensity_anticorr", + "mass_error_mz_trend", + "mean_abs_mz_error_da", + "mass_evidence_gauss", + "mass_log_evidence", + "n_matched_b", + "n_matched_y", + "frac_matched_b", + "frac_matched_y", + "by_count_balance", + "by_intensity_ratio", + "by_ratio_agreement", + "by_ratio_consistency", + "longest_b_run", + "longest_y_run", + "longest_run_max", + "longest_run_frac_length", + "series_coverage_b", + "series_coverage_y", + "sequence_coverage", + "series_gap_fraction", + "by_complement_count", + "by_complement_mz_consistency", + "by_complement_coelution", + "ordinal_intensity_concordance_y", + "ordinal_intensity_concordance_b", + "series_coelution_y", + "series_coelution_b", + "spectral_angle_b", + "spectral_angle_y", + "pearson_b", + "pearson_y", + "cosine_charge1", + "cosine_charge2", + "charge_corr_balance", + "mean_matched_ordinal_norm", + "by_ion_contiguous_intensity", + "by_ion_contiguous_lib_frac", + "both_series_present", + "ms1_isotope_cosine_apex", + "ms1_isotope_spectral_angle_apex", + "ms1_isotope_chi2_apex", + "ms1_isotope_manhattan_apex", + "iso_ratio_1_0", + "iso_ratio_2_0", + "iso_plus1_ratio_dev", + "iso_plus2_ratio_dev", + "iso_minus_one_fraction", + "iso_overlap_flag", + "log_ms1_mono", + "ms1_total_isotope_log", + "has_ms1_signal", + "ms1_isotope_apex_entropy_3", + "ms1_m1_entropy_contribution", + "ms1_ms2_time_corr", + "ms1_ms2_envelope_time_corr", + "ms1_iso_coelution", + "ms1_ms2_apex_rt_delta", + "ms1_iso_ratio_stability", + "ms1_mono_gaussianity", + "ms1_ms2_fwhm_ratio", + "ms1_isotope_corr_xic", + "ms1_envelope_over_time_corr", + "ms1_isotope_xic_shape_consistency", + "ms1_isotope_height_corr", + "rt_error_signed", + "rt_error_squared", + "rt_error_signed_norm_gradient", + "rt_error_abs_norm_gradient", + "observed_rt_raw", + "predicted_rt_raw", + "observed_rt_fraction", + "predicted_rt_fraction", + "rt_error_over_peak_width", + "rt_error_over_fwhm", + "rt_diff_profile_apex", + "predicted_rt_in_gradient", + "log_seed_hyperscore", + "seed_hyperscore_per_matched", + "precursor_charge", + "charge_is_2", + "charge_is_3", + "charge_is_4plus", + "precursor_mass", + "log_total_matched_intensity", + "n_matched_frags", + "n_predicted_frags", + "frag_corr_peakmax", + "frag_cosine_peakmax", + "spectral_angle_peakmax", + "frag_corr_matched_nz", + "frag_cosine_matched_nz", + "peakmax_apex_gain", + "n_frag_present_inpeak", + "frac_frag_present_inpeak", + "coelution_mean_bothpos", + "coelution_mean_summpos", + "ref_corr_nz", + "profile_cos_nz", + "rank_corr_vs_apex_mean", + "rank_corr_vs_apex_std", + "rank_corr_adjacent_mean", + "kendall_vs_apex_mean", + "top1_frag_persistence", + "top2_order_persistence", + "argmax_frag_entropy", + "self_cosine_vs_apex_mean", + "n_peak_scans", + "peak_window_degenerate", + "frag_apex_rt_std", + "frag_apex_rt_mad", + "frag_apex_max_dev", + "frag_apex_mean_dev", + "frag_apex_agree_frac", + "precursor_frag_apex_delta", + "peak_symmetry", + "peak_tailing", + "peak_n_local_maxima", + "peak_shoulder_score", + "peak_fwhm_scans", + "peak_truncation", + "apex_frac_of_window", + "frag_mass_err_median", + "frag_mass_err_abs_median", + "frag_mass_err_std", + "frag_mass_err_iqr", + "frag_mass_err_max_abs", + "frag_mass_err_range", + "effective_frag_count", + "evidence_concentration", + "frac_top3_pred_observed", + "frac_top5_pred_observed", + "deconv_explained_frac", + "deconv_active", + "deconv_share", + "deconv_max_collinearity", + "shadow_kept_frac", + "peak_contested_frac", + "peak_contested_count_frac", + "peak_apportioned_frac", + "n_charge_states", + "charge_multi_flag", + "cross_charge_intensity_log" + ], + "schema_id": "e5b4eff0133e411dbf406947c89466619ec557ab37874bc3948792d9890afe8e" +} \ No newline at end of file diff --git a/desktop/src-tauri/tests/fixtures/run_out/fragment_library_fragments.parquet.report.json b/desktop/src-tauri/tests/fixtures/run_out/fragment_library_fragments.parquet.report.json new file mode 100644 index 00000000..4fbeecc6 --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/fragment_library_fragments.parquet.report.json @@ -0,0 +1,21 @@ +{ + "logical_name": "fragment_library_fragments", + "schema_name": "fragment_library_fragments", + "schema_version": 1, + "stage": "predict-frag", + "rows": 22920, + "content_hash": "b1e6fd23482df40ddd81f13c133eab8d44c28016004a56eb8f0b6efd1c03291b", + "params": { + "fragment_predictor": "Native", + "ms2pip_model": "HCD", + "rt_predictor": "Native", + "top_n": 6 + }, + "stats": { + "candidates": 3820, + "fragments": 22920, + "parse_errors": 0 + }, + "model_identity": "native-rt-v1; native-frag-v1", + "elapsed_ms": 15 +} \ No newline at end of file diff --git a/desktop/src-tauri/tests/fixtures/run_out/fragment_library_precursors.parquet.report.json b/desktop/src-tauri/tests/fixtures/run_out/fragment_library_precursors.parquet.report.json new file mode 100644 index 00000000..b6ce34f3 --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/fragment_library_precursors.parquet.report.json @@ -0,0 +1,21 @@ +{ + "logical_name": "fragment_library_precursors", + "schema_name": "fragment_library_precursors", + "schema_version": 1, + "stage": "predict-frag", + "rows": 3820, + "content_hash": "9516998ec891009f8131080baac32c108d58dbff11f382556601d54f084d9394", + "params": { + "fragment_predictor": "Native", + "ms2pip_model": "HCD", + "rt_predictor": "Native", + "top_n": 6 + }, + "stats": { + "candidates": 3820, + "fragments": 22920, + "parse_errors": 0 + }, + "model_identity": "native-rt-v1; native-frag-v1", + "elapsed_ms": 15 +} \ No newline at end of file diff --git a/desktop/src-tauri/tests/fixtures/run_out/fragment_quant.parquet.report.json b/desktop/src-tauri/tests/fixtures/run_out/fragment_quant.parquet.report.json new file mode 100644 index 00000000..1eb5cc38 --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/fragment_quant.parquet.report.json @@ -0,0 +1,40 @@ +{ + "logical_name": "fragment_quant", + "schema_name": "fragment_quant", + "schema_version": 1, + "stage": "quant", + "rows": 906, + "content_hash": "0f5ef625af43bf8a88cf09fb24c185951674666d42a427eed2ab2aea39b5c5e7", + "params": { + "apex_rt_column_present": true, + "baseline_flank_scans": 12, + "baseline_quantile": 0.25, + "baseline_subtract": false, + "bound_peak": true, + "candidates_with_scored_apex": 152, + "chromatograms": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/chromatograms.parquet", + "config_hash": "7dfde09c5047373b95038420ca4248fd4aadf46942bdd8630c30ce1b13403ef2", + "fixed_scan_halfwidth": 0, + "fixed_window_s": 0.0, + "fragment_selection": "ObservedArea", + "peak_fraction": 0.16666666666666666, + "peak_grace": 1, + "peak_window_mode": "PerCandidate", + "psms_scored": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/psms_scored.parquet", + "q_filter": "PeptideQ", + "q_threshold": 0.01, + "reliable_q": 0.001, + "rollup": "TopNSum", + "top_n_fragments": 3, + "top_n_peptides": 3 + }, + "stats": { + "nonquantifiable_peptides": 0, + "peptide_rows": 151, + "protein_group_rows": 16, + "quantified_peptides": 151, + "quantified_protein_groups": 16 + }, + "model_identity": null, + "elapsed_ms": 4 +} \ No newline at end of file diff --git a/desktop/src-tauri/tests/fixtures/run_out/manifest.json b/desktop/src-tauri/tests/fixtures/run_out/manifest.json new file mode 100644 index 00000000..dc9ba80d --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/manifest.json @@ -0,0 +1,239 @@ +{ + "mumdia_version": "0.1.0", + "git_sha": "8ea79be03399-dirty", + "commit_date": "2026-08-29T07:25:51+02:00", + "cli_args": [ + "C:\\Users\\robbi\\mumdia_build\\release\\mumdia.exe", + "run", + "--fasta", + "test_data/fixture.fasta", + "--mzml", + "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/fixture.mzML", + "--out-dir", + "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out", + "--config", + "configs/examples/native.json", + "--threads", + "2" + ], + "config_json": "{\"prescan\":{\"tol_da\":0.005,\"rt_slack_s\":150.0,\"rt_bin_s\":25.0,\"top_peaks\":150,\"mods\":[\"C:Carbamidomethyl\",\"M:Oxidation\"],\"anchor_mods\":[]},\"rng_seed\":0,\"digest\":{\"enzyme\":\"trypsin_p\",\"missed_cleavages\":2,\"min_len\":5,\"max_len\":50,\"decoy\":{\"strategy\":\"reverse\"},\"n_term_met_excision\":true},\"peptidoforms\":{\"fixed_mods\":[{\"residue\":\"C\",\"name\":\"Carbamidomethyl\"}],\"variable_mods\":[{\"residue\":\"M\",\"name\":\"Oxidation\"}],\"max_variable_mods\":1,\"charge_min\":2,\"charge_max\":3,\"charge_by_basic_residues\":false,\"unknown_modification\":\"error\"},\"predict_frag\":{\"predictor\":\"native\",\"rt_predictor\":\"native\",\"charge2_from_precursor_charge\":2,\"charge_by_basic_residues\":false,\"top_n_fragments\":6,\"ms2pip_model\":\"HCD\",\"ms2pip_python\":null,\"deeplc_python\":null,\"sidecar_script_dir\":\"scripts\"},\"search_seed\":{\"fdr_seed\":0.01,\"fragment_tol_ppm\":20.0,\"report_psms\":5,\"min_matched_peaks\":4,\"top_n_peaks\":300,\"matcher\":\"fragindex\",\"two_pass_mass_cal\":false,\"mass_cal_loess\":false},\"rt_im_train\":{\"calibration_method\":\"loess\",\"q_train\":0.01,\"p_rt\":0.95,\"rt_window_multiplier\":1.0,\"min_seed_for_calibration\":50,\"loess_span\":0.3,\"fallback_rt_window_s\":120.0,\"finetune_deeplc\":false,\"finetune_epochs\":25,\"finetune_patience\":10,\"finetune_batch\":0,\"adaptive_rt_window\":false,\"adaptive_rt_bins\":12,\"rt_window_min_s\":1.0,\"window_holdout_frac\":0.0},\"extract\":{\"fixed_scan_window\":3,\"frag_tol_ppm\":20.0,\"prec_tol_ppm\":20.0,\"presence_min_matched\":3,\"presence_min_fragments\":3,\"presence_min_coelution\":2,\"gate_min_score\":0.2,\"min_matched_fraction\":0.0,\"apex_top_fragments\":0,\"apex_rt_prior_s\":120.0,\"apex_count_tol\":1,\"apex_count_window\":5,\"apex_gaussian_sigma_scans\":0.0,\"emit_window_grid\":true,\"bucket_size\":8192,\"peak_claim\":\"none\",\"claim_cues\":{\"mz_close\":false,\"mz_close_sigma_ppm\":5.0,\"rt_prior\":false,\"rt_prior_tau_s\":30.0,\"ms1_support\":false,\"reassign\":false,\"apportion_em_iters\":0},\"emit_demix_features\":false,\"demix_lambda\":1.0,\"demix_max_candidates\":64,\"demix_scan_stride\":1,\"emit_contested_features\":false,\"peak_claim_margin\":2.0,\"matcher\":\"fragindex\",\"min_coelution_run\":0,\"ms1_rescue\":false,\"retain_top_peaks\":1,\"promote_top_peaks\":1,\"alt_peak_min_area_frac\":0.1,\"alt_peak_min_separation_s\":5.0,\"emit_candidate_audit\":false,\"apex_evidence_rank\":true,\"emit_gate_diagnostics\":false,\"gate_mode\":\"apex_pearson\",\"gate_coelution_min\":0.5},\"features\":{\"set\":\"extended\",\"emit_pin\":false,\"coelution_corr_threshold\":0.9,\"prec_tol_ppm\":20.0,\"bound_features\":true,\"bound_peak_fraction\":0.3333333333333333,\"bound_peak_grace\":0,\"bound_from_confident\":true,\"bound_confident_pct\":50.0,\"ms1_precursor_features\":false},\"compete\":{\"group_by\":\"base_peptide\",\"apex_rt_tolerance_s\":5.0,\"mode\":\"winner_take_all\",\"margin\":0.0,\"unique_evidence_min_fragments\":2,\"emit_competition_audit\":false},\"rescore\":{\"classifier\":\"native_tda\",\"folds\":3,\"train_fdr\":0.01,\"num_iter\":10,\"max_feature_matrix_gib\":0.0,\"python\":null,\"percolator_bin\":null,\"entrapment_marker\":null,\"entrapment_exclude\":null,\"entrapment_contaminant_markers\":[],\"entrapment_ratio\":1.0,\"strict\":true,\"handoff\":\"tsv\"},\"quant\":{\"q_threshold\":0.01,\"top_n_fragments\":3,\"top_n_peptides\":3,\"rollup\":\"top_n_sum\",\"bound_peak\":true,\"peak_fraction\":0.16666666666666666,\"peak_grace\":1,\"peak_window_mode\":\"per_candidate\",\"reliable_q\":0.001,\"q_filter\":\"peptide_q\",\"interference_envelope\":false,\"fragment_selection\":\"observed_area\",\"fixed_scan_halfwidth\":0,\"baseline_subtract\":false,\"baseline_flank_scans\":12,\"baseline_quantile\":0.25,\"fixed_window_s\":0.0},\"mbr\":{\"strategy\":\"none\",\"q_anchor\":0.01,\"min_anchor_runs\":2,\"q_transfer\":0.01,\"rt_window_s\":20.0,\"decoy_transfer\":\"permuted_rt\",\"consensus_corr_min\":0.0,\"requant_all\":false,\"python\":null},\"experiment\":{\"parallel_runs\":1,\"finetune_scope\":\"first_run_only\"}}", + "config_hash": "7dfde09c5047373b95038420ca4248fd4aadf46942bdd8630c30ce1b13403ef2", + "model_identities": { + "feature_schema_id": "e5b4eff0133e411dbf406947c89466619ec557ab37874bc3948792d9890afe8e", + "fragment_predictor": "Native", + "rescorer": "native-percolator-lite-v1", + "rt_predictor": "Native" + }, + "inputs": { + "fasta": { + "path": "test_data/fixture.fasta", + "bytes": 2670, + "content_hash": "ff9ba5b1e9367f8641bec0a9e7eb438c62991ef935acf44c2c48618c73d06f2a" + }, + "mzml": { + "path": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/fixture.mzML", + "bytes": 1761106, + "content_hash": "f5187d0c1d35be705377d9db90ce0fae5d22ae062c02d42cd1a63dd291907cf9" + } + }, + "artifacts": { + "chromatograms": { + "logical_name": "chromatograms", + "path": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/chromatograms.parquet", + "format": "parquet", + "schema_name": "chromatograms", + "schema_version": 1, + "rows": 2556, + "content_hash": "884d4dba966c1d4168180150ec9c09579d2cd883fac7b854a266a8c76dd63346", + "producing_stage": "extract", + "config_hash": "7dfde09c5047373b95038420ca4248fd4aadf46942bdd8630c30ce1b13403ef2" + }, + "features": { + "logical_name": "features", + "path": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/features.parquet", + "format": "parquet", + "schema_name": "features", + "schema_version": 1, + "rows": 284, + "content_hash": "686820b41516510286c9c94f753940f26fb9a748435e7c9864f281a231267bf1", + "producing_stage": "features", + "config_hash": "7dfde09c5047373b95038420ca4248fd4aadf46942bdd8630c30ce1b13403ef2" + }, + "fragment_library_fragments": { + "logical_name": "fragment_library_fragments", + "path": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/fragment_library_fragments.parquet", + "format": "parquet", + "schema_name": "fragment_library_fragments", + "schema_version": 1, + "rows": 22920, + "content_hash": "b1e6fd23482df40ddd81f13c133eab8d44c28016004a56eb8f0b6efd1c03291b", + "producing_stage": "predict-frag", + "config_hash": "7dfde09c5047373b95038420ca4248fd4aadf46942bdd8630c30ce1b13403ef2" + }, + "fragment_library_precursors": { + "logical_name": "fragment_library_precursors", + "path": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/fragment_library_precursors.parquet", + "format": "parquet", + "schema_name": "fragment_library_precursors", + "schema_version": 1, + "rows": 3820, + "content_hash": "9516998ec891009f8131080baac32c108d58dbff11f382556601d54f084d9394", + "producing_stage": "predict-frag", + "config_hash": "7dfde09c5047373b95038420ca4248fd4aadf46942bdd8630c30ce1b13403ef2" + }, + "fragment_quant": { + "logical_name": "fragment_quant", + "path": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/fragment_quant.parquet", + "format": "parquet", + "schema_name": "fragment_quant", + "schema_version": 1, + "rows": 906, + "content_hash": "0f5ef625af43bf8a88cf09fb24c185951674666d42a427eed2ab2aea39b5c5e7", + "producing_stage": "quant", + "config_hash": "7dfde09c5047373b95038420ca4248fd4aadf46942bdd8630c30ce1b13403ef2" + }, + "isolation_windows": { + "logical_name": "isolation_windows", + "path": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/spectra/isolation_windows.parquet", + "format": "parquet", + "schema_name": "isolation_windows", + "schema_version": 1, + "rows": 8, + "content_hash": "3ceab06d7b4d1ed3d814722235920d2677bb6877535fa02767a67519de0ea32f", + "producing_stage": "convert", + "config_hash": "e11217cb603b8854a93c176b421cd926fb371ab35ee4647c4301b3f1c7631c3b" + }, + "ms2_to_ms1": { + "logical_name": "ms2_to_ms1", + "path": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/spectra/ms2_to_ms1.parquet", + "format": "parquet", + "schema_name": "ms2_to_ms1", + "schema_version": 1, + "rows": 480, + "content_hash": "c60988ab4d075d9655fb7406169ee92e804c7dff05b42124e818ccc9f249346f", + "producing_stage": "convert", + "config_hash": "e11217cb603b8854a93c176b421cd926fb371ab35ee4647c4301b3f1c7631c3b" + }, + "peptide_quant": { + "logical_name": "peptide_quant", + "path": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/peptide_quant.parquet", + "format": "parquet", + "schema_name": "peptide_quant", + "schema_version": 2, + "rows": 151, + "content_hash": "db6dee23c4e5f1b40fad156b10a16f3a3748b3c83016770370933282ee36af88", + "producing_stage": "quant", + "config_hash": "7dfde09c5047373b95038420ca4248fd4aadf46942bdd8630c30ce1b13403ef2" + }, + "peptides": { + "logical_name": "peptides", + "path": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/peptides.parquet", + "format": "parquet", + "schema_name": "peptides", + "schema_version": 1, + "rows": 876, + "content_hash": "b88380d3934a4f87e4c65b900f1a8e2edfc156eb599f967859a4f70d91ef7106", + "producing_stage": "digest", + "config_hash": "7dfde09c5047373b95038420ca4248fd4aadf46942bdd8630c30ce1b13403ef2" + }, + "peptidoforms": { + "logical_name": "peptidoforms", + "path": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/peptidoforms.parquet", + "format": "parquet", + "schema_name": "peptidoforms", + "schema_version": 1, + "rows": 3820, + "content_hash": "b8c0df35a3ba1faa0ac44736aa95e31563735a93b6b5f99c9a0749b2445b04f3", + "producing_stage": "peptidoforms", + "config_hash": "7dfde09c5047373b95038420ca4248fd4aadf46942bdd8630c30ce1b13403ef2" + }, + "protein_group_quant": { + "logical_name": "protein_group_quant", + "path": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/protein_group_quant.parquet", + "format": "parquet", + "schema_name": "protein_group_quant", + "schema_version": 2, + "rows": 16, + "content_hash": "1eb8ddb19566390bc59e17ede88b0d7264d48b3831079346b74c73aeaa8b5920", + "producing_stage": "quant", + "config_hash": "7dfde09c5047373b95038420ca4248fd4aadf46942bdd8630c30ce1b13403ef2" + }, + "psms_competed": { + "logical_name": "psms_competed", + "path": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/psms_competed.parquet", + "format": "parquet", + "schema_name": "psms_competed", + "schema_version": 3, + "rows": 152, + "content_hash": "f5d11d59f94a46211bcddcf5b2ce49de95ed4192d5ee361aa61e7b1d5ef9d518", + "producing_stage": "compete", + "config_hash": "7dfde09c5047373b95038420ca4248fd4aadf46942bdd8630c30ce1b13403ef2" + }, + "psms_extracted": { + "logical_name": "psms_extracted", + "path": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/psms_extracted.parquet", + "format": "parquet", + "schema_name": "psms_extracted", + "schema_version": 2, + "rows": 284, + "content_hash": "7f2eae351f0d936467458d3ef2942b020f93fc9b72d7484e2c3a08942f44d033", + "producing_stage": "extract", + "config_hash": "7dfde09c5047373b95038420ca4248fd4aadf46942bdd8630c30ce1b13403ef2" + }, + "psms_scored": { + "logical_name": "psms_scored", + "path": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/psms_scored.parquet", + "format": "parquet", + "schema_name": "psms_scored", + "schema_version": 4, + "rows": 152, + "content_hash": "9c92be4ad83b794db312831fb8797877e0230156cd297624f9120401b5fb82a7", + "producing_stage": "rescore", + "config_hash": "7dfde09c5047373b95038420ca4248fd4aadf46942bdd8630c30ce1b13403ef2" + }, + "run_windows": { + "logical_name": "run_windows", + "path": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/run_windows.parquet", + "format": "parquet", + "schema_name": "run_windows", + "schema_version": 1, + "rows": 3820, + "content_hash": "cec9473ba70a0955e310cdc9ceb1a1962bbf3fcce67d354f6dfe09ca00885c73", + "producing_stage": "rt-im-train", + "config_hash": "7dfde09c5047373b95038420ca4248fd4aadf46942bdd8630c30ce1b13403ef2" + }, + "seed_psms": { + "logical_name": "seed_psms", + "path": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/seed_psms.parquet", + "format": "parquet", + "schema_name": "seed_psms", + "schema_version": 1, + "rows": 179, + "content_hash": "a12ee0d4627587e8c2dfb7fda55a0258ec4563c3e0067a3c93effa7edbf64686", + "producing_stage": "search-seed", + "config_hash": "7dfde09c5047373b95038420ca4248fd4aadf46942bdd8630c30ce1b13403ef2" + }, + "spectra_ms1": { + "logical_name": "spectra_ms1", + "path": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/spectra/spectra_ms1.parquet", + "format": "parquet", + "schema_name": "spectra_ms1", + "schema_version": 1, + "rows": 60, + "content_hash": "c70bba2b31203c20d8108d47c3f802c0ac5b79bb797701cf1b29041d5cd0fa6d", + "producing_stage": "convert", + "config_hash": "e11217cb603b8854a93c176b421cd926fb371ab35ee4647c4301b3f1c7631c3b" + }, + "spectra_ms2": { + "logical_name": "spectra_ms2", + "path": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/spectra/spectra_ms2.parquet", + "format": "parquet", + "schema_name": "spectra_ms2", + "schema_version": 1, + "rows": 480, + "content_hash": "6228d8b36e3931ed6271122e735494957c8217dbd33d8a81d1fef568b775cf26", + "producing_stage": "convert", + "config_hash": "e11217cb603b8854a93c176b421cd926fb371ab35ee4647c4301b3f1c7631c3b" + } + } +} \ No newline at end of file diff --git a/desktop/src-tauri/tests/fixtures/run_out/peptide_quant.parquet.report.json b/desktop/src-tauri/tests/fixtures/run_out/peptide_quant.parquet.report.json new file mode 100644 index 00000000..f23eaad8 --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/peptide_quant.parquet.report.json @@ -0,0 +1,40 @@ +{ + "logical_name": "peptide_quant", + "schema_name": "peptide_quant", + "schema_version": 2, + "stage": "quant", + "rows": 151, + "content_hash": "db6dee23c4e5f1b40fad156b10a16f3a3748b3c83016770370933282ee36af88", + "params": { + "apex_rt_column_present": true, + "baseline_flank_scans": 12, + "baseline_quantile": 0.25, + "baseline_subtract": false, + "bound_peak": true, + "candidates_with_scored_apex": 152, + "chromatograms": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/chromatograms.parquet", + "config_hash": "7dfde09c5047373b95038420ca4248fd4aadf46942bdd8630c30ce1b13403ef2", + "fixed_scan_halfwidth": 0, + "fixed_window_s": 0.0, + "fragment_selection": "ObservedArea", + "peak_fraction": 0.16666666666666666, + "peak_grace": 1, + "peak_window_mode": "PerCandidate", + "psms_scored": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/psms_scored.parquet", + "q_filter": "PeptideQ", + "q_threshold": 0.01, + "reliable_q": 0.001, + "rollup": "TopNSum", + "top_n_fragments": 3, + "top_n_peptides": 3 + }, + "stats": { + "nonquantifiable_peptides": 0, + "peptide_rows": 151, + "protein_group_rows": 16, + "quantified_peptides": 151, + "quantified_protein_groups": 16 + }, + "model_identity": null, + "elapsed_ms": 4 +} \ No newline at end of file diff --git a/desktop/src-tauri/tests/fixtures/run_out/peptides.parquet.report.json b/desktop/src-tauri/tests/fixtures/run_out/peptides.parquet.report.json new file mode 100644 index 00000000..0a474311 --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/peptides.parquet.report.json @@ -0,0 +1,25 @@ +{ + "logical_name": "peptides", + "schema_name": "peptides", + "schema_version": 1, + "stage": "digest", + "rows": 876, + "content_hash": "b88380d3934a4f87e4c65b900f1a8e2edfc156eb599f967859a4f70d91ef7106", + "params": { + "decoy_strategy": "Reverse", + "enzyme": "TrypsinP", + "max_decoy_attempts": 64, + "max_len": 50, + "min_len": 5, + "missed_cleavages": 2, + "rng_seed": 0 + }, + "stats": { + "decoy_collision_retries": 0, + "dropped_target_decoy_pairs": 0, + "n_decoys": 438, + "n_targets": 438 + }, + "model_identity": null, + "elapsed_ms": 2 +} \ No newline at end of file diff --git a/desktop/src-tauri/tests/fixtures/run_out/peptides.tsv b/desktop/src-tauri/tests/fixtures/run_out/peptides.tsv new file mode 100644 index 00000000..7004413b --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/peptides.tsv @@ -0,0 +1,152 @@ +precursor stripped_sequence charge protein q_value score quantity +HHALPAR HHALPAR 3 sp|FIXT05|FIX05_TEST 0.006623 5.1179 48580307.1 +VM[Oxidation]LLYDK VMLLYDK 3 sp|FIXT08|FIX08_TEST 0.006623 5.0160 25206727.8 +DQDFMMK DQDFMMK 3 sp|FIXT01|FIX01_TEST 0.006623 4.8114 19589306.7 +HGGELTVDK HGGELTVDK 3 sp|FIXT11|FIX11_TEST 0.006623 5.0495 23309212.7 +FNQEFM[Oxidation]K FNQEFMK 3 sp|FIXT15|FIX15_TEST 0.006623 5.0803 39505347.8 +FNSIALAFK FNSIALAFK 3 sp|FIXT13|FIX13_TEST 0.006623 5.0196 17612255.0 +DPSQDWFK DPSQDWFK 3 sp|FIXT02|FIX02_TEST 0.006623 5.2486 49942119.3 +FGMASGK FGMASGK 2 sp|FIXT15|FIX15_TEST 0.006623 1.9968 32594288.5 +IVFFGGDYR IVFFGGDYR 3 sp|FIXT03|FIX03_TEST 0.006623 5.1964 29162611.5 +EEMAFPGVSK EEMAFPGVSK 3 sp|FIXT12|FIX12_TEST 0.006623 5.2598 20842822.0 +FTAISSIPWK FTAISSIPWK 3 sp|FIXT13|FIX13_TEST 0.006623 5.0984 13268190.8 +LVELLPTFDR LVELLPTFDR 3 sp|FIXT14|FIX14_TEST 0.006623 5.0073 27343912.4 +STHIHTDWPR STHIHTDWPR 3 sp|FIXT02|FIX02_TEST 0.006623 5.1712 12560409.1 +TTSESHHESHK TTSESHHESHK 3 sp|FIXT08|FIX08_TEST 0.006623 5.2781 23140475.0 +VASQMSLQSSDGR VASQMSLQSSDGR 3 sp|FIXT15|FIX15_TEST 0.006623 3.2939 19193106.2 +NLPNGAWPPYIR NLPNGAWPPYIR 3 sp|FIXT16|FIX16_TEST 0.006623 1.9634 26899500.0 +LHNPAFVIQYDK LHNPAFVIQYDK 3 sp|FIXT08|FIX08_TEST 0.006623 5.1281 37712583.5 +SSFLMTGIPWQAR SSFLMTGIPWQAR 3 sp|FIXT04|FIX04_TEST 0.006623 5.1989 11816956.6 +APM[Oxidation]VVGFGWVINFK APMVVGFGWVINFK 3 sp|FIXT06|FIX06_TEST 0.006623 5.2251 22501906.5 +THFEQVPM[Oxidation]FWPR THFEQVPMFWPR 3 sp|FIXT14|FIX14_TEST 0.006623 6.3208 47349097.0 +LGMQIGPISFSAYPR LGMQIGPISFSAYPR 3 sp|FIXT15|FIX15_TEST 0.006623 5.2012 14346644.8 +SWVTGDLQWHMYK SWVTGDLQWHMYK 3 sp|FIXT07|FIX07_TEST 0.006623 5.2450 44406585.4 +WNYSM[Oxidation]QER WNYSMQER 2 sp|FIXT02|FIX02_TEST 0.006623 5.3086 35624475.1 +TM[Oxidation]IQHVMNEYTDLK TMIQHVMNEYTDLK 3 sp|FIXT07|FIX07_TEST 0.006623 5.1907 16724874.9 +TFYPATEMNNSWMR TFYPATEMNNSWMR 3 sp|FIXT08|FIX08_TEST 0.006623 5.1925 43552720.9 +QITDNM[Oxidation]WFK QITDNMWFK 2 sp|FIXT06|FIX06_TEST 0.006623 4.9791 28344402.7 +DYWGFPMEHLVDYK DYWGFPMEHLVDYK 3 sp|FIXT14|FIX14_TEST 0.006623 5.1523 12691033.0 +IPFNDAELM[Oxidation]IIYMTK IPFNDAELMIIYMTK 3 sp|FIXT05|FIX05_TEST 0.006623 5.2300 34610339.6 +DMWWYQTTNPQGFR DMWWYQTTNPQGFR 3 sp|FIXT02|FIX02_TEST 0.006623 5.5184 47496065.9 +IM[Oxidation]MYGWDWK IMMYGWDWK 2 sp|FIXT14|FIX14_TEST 0.006623 6.8611 47911820.3 +M[Oxidation]HDIGDGEDLR MHDIGDGEDLR 2 sp|FIXT12|FIX12_TEST 0.006623 5.3150 29344114.7 +MHLVQWFWQLQNHR MHLVQWFWQLQNHR 3 sp|FIXT16|FIX16_TEST 0.006623 5.1609 17428896.2 +IYSPYTVKEPEQIEM[Oxidation]K IYSPYTVKEPEQIEMK 3 sp|FIXT01|FIX01_TEST 0.006623 7.3188 31298494.7 +DTWVQTHDVTR DTWVQTHDVTR 2 sp|FIXT08|FIX08_TEST 0.006623 5.3049 28003241.3 +AFPEHAFTSPYK AFPEHAFTSPYK 2 sp|FIXT03|FIX03_TEST 0.006623 4.7113 5619232.3 +YAIPTVQRHPVNHTDSFK YAIPTVQRHPVNHTDSFK 3 sp|FIXT04|FIX04_TEST 0.006623 5.8861 36928863.7 +LWGANWEWMLK LWGANWEWMLK 2 sp|FIXT10|FIX10_TEST 0.006623 5.1563 10142656.5 +AWNLVWKDEHMVYM[Oxidation]SSR AWNLVWKDEHMVYMSSR 3 sp|FIXT14|FIX14_TEST 0.006623 -0.0794 22719712.5 +EDM[Oxidation]TQTVFGQAGAK EDMTQTVFGQAGAK 2 sp|FIXT16|FIX16_TEST 0.006623 5.1634 29547387.4 +DQDFMMKHPIIIYLYEVK DQDFMMKHPIIIYLYEVK 3 sp|FIXT01|FIX01_TEST 0.006623 -1.0713 31904186.7 +M[Oxidation]HDIGDGEDLRDNDPVMWR MHDIGDGEDLRDNDPVMWR 3 sp|FIXT12|FIX12_TEST 0.006623 2.1324 22243397.8 +LQMVSGELLLDHKNLINQLR LQMVSGELLLDHKNLINQLR 3 sp|FIXT05|FIX05_TEST 0.006623 -3.0267 12388366.2 +VFIFLYTGEPVSGK VFIFLYTGEPVSGK 2 sp|FIXT13|FIX13_TEST 0.006623 5.1966 37150167.0 +LDPTSGPMDVFPPGR LDPTSGPMDVFPPGR 2 sp|FIXT16|FIX16_TEST 0.006623 5.2685 27792345.4 +LVELLPTFDRIMMYGWDWK LVELLPTFDRIMMYGWDWK 3 sp|FIXT14|FIX14_TEST 0.006623 3.4382 46066713.5 +FNQEFMKFGMASGK FNQEFMKFGMASGK 2 sp|FIXT15|FIX15_TEST 0.006623 -4.2878 13016898.1 +EQNDEHVNSVLPLR EQNDEHVNSVLPLR 2 sp|FIXT11|FIX11_TEST 0.006623 6.8677 30590418.4 +SVMLNIWHATPNLR SVMLNIWHATPNLR 2 sp|FIXT10|FIX10_TEST 0.006623 5.2424 28783564.7 +QITDNMWFKWINIDWASISR QITDNMWFKWINIDWASISR 3 sp|FIXT06|FIX06_TEST 0.006623 3.8383 23377945.5 +SLAQGWRTFYPATEM[Oxidation]NNSWMR SLAQGWRTFYPATEMNNSWMR 3 sp|FIXT08|FIX08_TEST 0.006623 2.1700 27386507.8 +LWYSWGVM[Oxidation]WEVNK LWYSWGVMWEVNK 2 sp|FIXT09|FIX09_TEST 0.006623 5.3120 37640941.9 +FPQADM[Oxidation]EIEPFFEK FPQADMEIEPFFEK 2 sp|FIXT01|FIX01_TEST 0.006623 5.1701 13370764.7 +HEPHVTKALLMQWYQATPNDTR HEPHVTKALLMQWYQATPNDTR 3 sp|FIXT11|FIX11_TEST 0.006623 7.8683 37794364.6 +WIYALDHAQLYDGYKHTFPHAK WIYALDHAQLYDGYKHTFPHAK 3 sp|FIXT11|FIX11_TEST 0.006623 2.2265 31879304.6 +DEMNVWSNQQFSWK DEMNVWSNQQFSWK 2 sp|FIXT13|FIX13_TEST 0.006623 5.1838 25322221.7 +LWYSWGVM[Oxidation]WEVNKTTDYHEFR LWYSWGVMWEVNKTTDYHEFR 3 sp|FIXT09|FIX09_TEST 0.006623 7.5927 49465021.8 +AVEGAAALRETSGIWWKYAIPTVQR AVEGAAALRETSGIWWKYAIPTVQR 3 sp|FIXT04|FIX04_TEST 0.006623 -1.8925 24421953.7 +M[Oxidation]QDNPNDIPYDILHR MQDNPNDIPYDILHR 2 sp|FIXT03|FIX03_TEST 0.006623 5.2762 38600157.9 +IM[Oxidation]MYGWDWKTHFEQVPMFWPR IMMYGWDWKTHFEQVPMFWPR 3 sp|FIXT14|FIX14_TEST 0.006623 6.3988 37652902.6 +EIINSTYHHPYSPYR EIINSTYHHPYSPYR 2 sp|FIXT08|FIX08_TEST 0.006623 5.1824 17421658.2 +DNDPVMWRHNYYENM[Oxidation]QSSGTFLR DNDPVMWRHNYYENMQSSGTFLR 3 sp|FIXT12|FIX12_TEST 0.006623 -2.5247 15442846.8 +IDYIVYHPTIYQKWYMVWVELR IDYIVYHPTIYQKWYMVWVELR 3 sp|FIXT02|FIX02_TEST 0.006623 0.0969 17404898.5 +FQFMLLAPDWDYLYK FQFMLLAPDWDYLYK 2 sp|FIXT06|FIX06_TEST 0.006623 5.2171 27852727.8 +LDPTSGPMDVFPPGRDLANPVQEESFK LDPTSGPMDVFPPGRDLANPVQEESFK 3 sp|FIXT16|FIX16_TEST 0.006623 5.8220 49092154.1 +MFHEEWYKAVEGAAALRETSGIWWK MFHEEWYKAVEGAAALRETSGIWWK 3 sp|FIXT04|FIX04_TEST 0.006623 6.0846 41924498.7 +M[Oxidation]FHEEWYKAVEGAAALR MFHEEWYKAVEGAAALR 2 sp|FIXT04|FIX04_TEST 0.006623 -5.1824 6333597.2 +WMWSENNNHWNIPKVVEGDQELGTR WMWSENNNHWNIPKVVEGDQELGTR 3 sp|FIXT15|FIX15_TEST 0.006623 2.8305 39674702.4 +YWFWQPM[Oxidation]DPWSPPSKFTAISSIPWK YWFWQPMDPWSPPSKFTAISSIPWK 3 sp|FIXT13|FIX13_TEST 0.006623 7.9455 37358434.9 +HPIIIYLYEVKFPQADM[Oxidation]EIEPFFEK HPIIIYLYEVKFPQADMEIEPFFEK 3 sp|FIXT01|FIX01_TEST 0.006623 0.5280 16865554.7 +FYPGMQRMHDIGDGEDLRDNDPVMWR FYPGMQRMHDIGDGEDLRDNDPVMWR 3 sp|FIXT12|FIX12_TEST 0.006623 -6.2701 10573083.7 +WYGVSMWWKIMSWVWLYMYIAMEK WYGVSMWWKIMSWVWLYMYIAMEK 3 sp|FIXT09|FIX09_TEST 0.006623 -6.3099 5781413.2 +IVFFGGDYREYFQNADGR IVFFGGDYREYFQNADGR 2 sp|FIXT03|FIX03_TEST 0.006623 -1.4635 16896069.7 +HNYYENMQSSGTFLRMQPHIIAFAANSR HNYYENMQSSGTFLRMQPHIIAFAANSR 3 sp|FIXT12|FIX12_TEST 0.006623 3.8245 33574482.7 +WSWVYMNEVTIRHHALPARSFVQSPHR WSWVYMNEVTIRHHALPARSFVQSPHR 3 sp|FIXT05|FIX05_TEST 0.006623 6.1679 30425685.9 +DELLGIYMDEFLKMQQYYSSWIDFQGR DELLGIYMDEFLKMQQYYSSWIDFQGR 3 sp|FIXT16|FIX16_TEST 0.006623 6.6290 38977889.3 +EDMTQTVFGQAGAKMHLVQWFWQLQNHR EDMTQTVFGQAGAKMHLVQWFWQLQNHR 3 sp|FIXT16|FIX16_TEST 0.006623 3.9737 27464994.0 +LWGANWEWM[Oxidation]LKDVTTTTAK LWGANWEWMLKDVTTTTAK 2 sp|FIXT10|FIX10_TEST 0.006623 -10.4039 5865711.9 +YMSEGWTVMGHPIERSVM[Oxidation]LNIWHATPNLR YMSEGWTVMGHPIERSVMLNIWHATPNLR 3 sp|FIXT10|FIX10_TEST 0.006623 8.5180 41926718.7 +DLTLAIGISQVDRAWNLVWK DLTLAIGISQVDRAWNLVWK 2 sp|FIXT14|FIX14_TEST 0.006623 -0.5090 19627528.3 +QFVTHIM[Oxidation]PDRLWGANWEWMLKDVTTTTAK QFVTHIMPDRLWGANWEWMLKDVTTTTAK 3 sp|FIXT10|FIX10_TEST 0.006623 7.8488 45178131.6 +TMIQHVMNEYTDLKIFIYDYNFLHNWR TMIQHVMNEYTDLKIFIYDYNFLHNWR 3 sp|FIXT07|FIX07_TEST 0.006623 7.9974 41045807.7 +TQWAFAERVQEHELWWAR TQWAFAERVQEHELWWAR 2 sp|FIXT10|FIX10_TEST 0.006623 -1.9068 14719401.3 +DLTLAIGISQVDRAWNLVWKDEHM[Oxidation]VYMSSR DLTLAIGISQVDRAWNLVWKDEHMVYMSSR 3 sp|FIXT14|FIX14_TEST 0.006623 9.2179 42179845.4 +WINIDWASISRMYIAIINPK WINIDWASISRMYIAIINPK 2 sp|FIXT06|FIX06_TEST 0.006623 6.4217 39142628.9 +VGNNYM[Oxidation]QGFNHDVGRYWFWQPMDPWSPPSK VGNNYMQGFNHDVGRYWFWQPMDPWSPPSK 3 sp|FIXT13|FIX13_TEST 0.006623 9.9152 47960620.9 +YSQIGIIHSWDRMVLQNGAQNKQIYLGTADLK YSQIGIIHSWDRMVLQNGAQNKQIYLGTADLK 3 sp|FIXT11|FIX11_TEST 0.006623 -4.5352 7800532.2 +TYGQQATTIKMNLDGWAFFM[Oxidation]R TYGQQATTIKMNLDGWAFFMR 2 sp|FIXT09|FIX09_TEST 0.006623 0.2023 24448191.9 +YSYDM[Oxidation]MGTASNRLWYQWHSRSTHIHTDWPR YSYDMMGTASNRLWYQWHSRSTHIHTDWPR 3 sp|FIXT02|FIX02_TEST 0.006623 2.4320 20194841.6 +VPEGAIHWVGKHGDLWWEDISK VPEGAIHWVGKHGDLWWEDISK 2 sp|FIXT09|FIX09_TEST 0.006623 7.5256 48991215.0 +TESFWQGDYHVGRDQDFM[Oxidation]MKHPIIIYLYEVK TESFWQGDYHVGRDQDFMMKHPIIIYLYEVK 3 sp|FIXT01|FIX01_TEST 0.006623 3.1509 44919593.4 +SSFLM[Oxidation]TGIPWQARVVYYGSYFK SSFLMTGIPWQARVVYYGSYFK 2 sp|FIXT04|FIX04_TEST 0.006623 -1.4234 13851431.5 +MNLDGWAFFMRVPEGAIHWVGKHGDLWWEDISK MNLDGWAFFMRVPEGAIHWVGKHGDLWWEDISK 3 sp|FIXT09|FIX09_TEST 0.006623 -4.8434 6631299.0 +LVELLPTFDRIMMYGWDWKTHFEQVPMFWPR LVELLPTFDRIMMYGWDWKTHFEQVPMFWPR 3 sp|FIXT14|FIX14_TEST 0.006623 -2.6037 15703723.1 +DQDFM[Oxidation]MKHPIIIYLYEVKFPQADMEIEPFFEK DQDFMMKHPIIIYLYEVKFPQADMEIEPFFEK 3 sp|FIXT01|FIX01_TEST 0.006623 -348.3624 15256653.8 +QFVTHIMPDRLWGANWEWM[Oxidation]LK QFVTHIMPDRLWGANWEWMLK 2 sp|FIXT10|FIX10_TEST 0.006623 7.9608 36285056.6 +DPSQDWFKNPHTHIFGFQLTIKYSYDM[Oxidation]MGTASNR DPSQDWFKNPHTHIFGFQLTIKYSYDMMGTASNR 3 sp|FIXT02|FIX02_TEST 0.006623 1.0531 17408992.1 +APM[Oxidation]VVGFGWVINFKQITDNMWFK APMVVGFGWVINFKQITDNMWFK 2 sp|FIXT06|FIX06_TEST 0.006623 11.9058 50444533.9 +MQPHIIAFAANSRLSMNWFQHNR MQPHIIAFAANSRLSMNWFQHNR 2 sp|FIXT12|FIX12_TEST 0.006623 7.2723 48499826.8 +EIINSTYHHPYSPYRVM[Oxidation]LLYDKLHNPAFVIQYDK EIINSTYHHPYSPYRVMLLYDKLHNPAFVIQYDK 3 sp|FIXT08|FIX08_TEST 0.006623 7.0999 47531581.5 +DSDTAPWRWM[Oxidation]WSENNNHWNIPK DSDTAPWRWMWSENNNHWNIPK 2 sp|FIXT15|FIX15_TEST 0.006623 11.2223 52795786.8 +WINIDWASISRM[Oxidation]YIAIINPKNYVAVAYNNEYIDMK WINIDWASISRMYIAIINPKNYVAVAYNNEYIDMK 3 sp|FIXT06|FIX06_TEST 0.006623 2.5190 30203337.9 +HEPHVTKALLMQWYQATPNDTREQNDEHVNSVLPLR HEPHVTKALLMQWYQATPNDTREQNDEHVNSVLPLR 3 sp|FIXT11|FIX11_TEST 0.006623 -1.6157 24068721.0 +DNDPVMWRHNYYENMQSSGTFLRMQPHIIAFAANSR DNDPVMWRHNYYENMQSSGTFLRMQPHIIAFAANSR 3 sp|FIXT12|FIX12_TEST 0.006623 -5.8746 8423723.9 +M[Oxidation]YIAIINPKNYVAVAYNNEYIDMK MYIAIINPKNYVAVAYNNEYIDMK 2 sp|FIXT06|FIX06_TEST 0.006623 10.7453 53136438.3 +STHIHTDWPRDMWWYQTTNPQGFREQM[Oxidation]VTSLYYR STHIHTDWPRDMWWYQTTNPQGFREQMVTSLYYR 3 sp|FIXT02|FIX02_TEST 0.006623 5.4764 36513928.1 +TFGSTHHREEMAFPGVSKFYPGM[Oxidation]QR TFGSTHHREEMAFPGVSKFYPGMQR 2 sp|FIXT12|FIX12_TEST 0.006623 8.4409 42183337.5 +YHHVDFSQFQM[Oxidation]TKEIINSTYHHPYSPYRVMLLYDK YHHVDFSQFQMTKEIINSTYHHPYSPYRVMLLYDK 3 sp|FIXT08|FIX08_TEST 0.006623 -7.0396 9468244.0 +DLANPVQEESFKLDDSAALSWMWMPK DLANPVQEESFKLDDSAALSWMWMPK 2 sp|FIXT16|FIX16_TEST 0.006623 1.3770 25812463.5 +VSLHVIWQM[Oxidation]AGHRFFMYLIQISEVK VSLHVIWQMAGHRFFMYLIQISEVK 2 sp|FIXT06|FIX06_TEST 0.006623 3.0918 23566640.6 +HIMQTHQTLALFAKWTEQIMGVPEQK HIMQTHQTLALFAKWTEQIMGVPEQK 2 sp|FIXT15|FIX15_TEST 0.006623 -5.3525 18175491.1 +IM[Oxidation]SWVWLYMYIAMEKTYGQQATTIK IMSWVWLYMYIAMEKTYGQQATTIK 2 sp|FIXT09|FIX09_TEST 0.006623 10.8936 44761620.5 +FYNPGVNDTISRTMIQHVM[Oxidation]NEYTDLK FYNPGVNDTISRTMIQHVMNEYTDLK 2 sp|FIXT07|FIX07_TEST 0.006623 2.8819 29195194.3 +FPQADM[Oxidation]EIEPFFEKNNDGAAMGNNEALR FPQADMEIEPFFEKNNDGAAMGNNEALR 2 sp|FIXT01|FIX01_TEST 0.006623 9.3125 47990901.8 +FYHGPQSHLAPRIPFNDAELMIIYMTK FYHGPQSHLAPRIPFNDAELMIIYMTK 2 sp|FIXT05|FIX05_TEST 0.006623 0.8099 21303169.3 +EEM[Oxidation]AFPGVSKFYPGMQRMHDIGDGEDLR EEMAFPGVSKFYPGMQRMHDIGDGEDLR 2 sp|FIXT12|FIX12_TEST 0.006623 11.0251 48809250.2 +LDDSAALSWMWMPKDELLGIYM[Oxidation]DEFLK LDDSAALSWMWMPKDELLGIYMDEFLK 2 sp|FIXT16|FIX16_TEST 0.006623 11.1121 48234125.1 +LGM[Oxidation]QIGPISFSAYPRHIMQTHQTLALFAK LGMQIGPISFSAYPRHIMQTHQTLALFAK 2 sp|FIXT15|FIX15_TEST 0.006623 1.1847 18457790.9 +WWIPTSYEENM[Oxidation]KYSHAPMLAYHIYHK WWIPTSYEENMKYSHAPMLAYHIYHK 2 sp|FIXT10|FIX10_TEST 0.006623 6.8843 35346508.0 +THFEQVPMFWPRDYWGFPMEHLVDYK THFEQVPMFWPRDYWGFPMEHLVDYK 2 sp|FIXT14|FIX14_TEST 0.006623 -1.3766 10949982.7 +NNDGAAMGNNEALRIYSPYTVKEPEQIEM[Oxidation]K NNDGAAMGNNEALRIYSPYTVKEPEQIEMK 2 sp|FIXT01|FIX01_TEST 0.006623 2.1208 30862976.4 +IFIYDYNFLHNWRSWVTGDLQWHMYK IFIYDYNFLHNWRSWVTGDLQWHMYK 2 sp|FIXT07|FIX07_TEST 0.006623 6.3690 35478127.9 +EHFFLDVRLVELLPTFDRIM[Oxidation]MYGWDWK EHFFLDVRLVELLPTFDRIMMYGWDWK 2 sp|FIXT14|FIX14_TEST 0.006623 5.1690 37769896.3 +MMDDAWSFWDEHGKYMSEGWTVM[Oxidation]GHPIER MMDDAWSFWDEHGKYMSEGWTVMGHPIER 2 sp|FIXT10|FIX10_TEST 0.006623 4.8275 22654250.5 +WGGDMDEVVRTESFWQGDYHVGRDQDFM[Oxidation]MK WGGDMDEVVRTESFWQGDYHVGRDQDFMMK 2 sp|FIXT01|FIX01_TEST 0.006623 2.6433 18969720.8 +TYGQQATTIKMNLDGWAFFM[Oxidation]RVPEGAIHWVGK TYGQQATTIKMNLDGWAFFMRVPEGAIHWVGK 2 sp|FIXT09|FIX09_TEST 0.006623 3.6672 28435758.7 +SSFLMTGIPWQARVVYYGSYFKM[Oxidation]FHEEWYK SSFLMTGIPWQARVVYYGSYFKMFHEEWYK 2 sp|FIXT04|FIX04_TEST 0.006623 -3.7186 43349765.8 +HPVNHTDSFKALHVMGIFRTASEEDIHAVEIVTK HPVNHTDSFKALHVMGIFRTASEEDIHAVEIVTK 2 sp|FIXT04|FIX04_TEST 0.006623 1.1944 18096630.4 +WYM[Oxidation]VWVELRDPSQDWFKNPHTHIFGFQLTIK WYMVWVELRDPSQDWFKNPHTHIFGFQLTIK 2 sp|FIXT02|FIX02_TEST 0.006623 5.5601 48689602.3 +M[Oxidation]QDNPNDIPYDILHRIVFFGGDYREYFQNADGR MQDNPNDIPYDILHRIVFFGGDYREYFQNADGR 2 sp|FIXT03|FIX03_TEST 0.006623 -4.5743 7474406.4 +HIMQTHQTLALFAKWTEQIM[Oxidation]GVPEQKFNQEFMK HIMQTHQTLALFAKWTEQIMGVPEQKFNQEFMK 2 sp|FIXT15|FIX15_TEST 0.006623 4.8413 24950940.3 +LQM[Oxidation]VSGELLLDHKNLINQLRDDGDYVHQHETFIK LQMVSGELLLDHKNLINQLRDDGDYVHQHETFIK 2 sp|FIXT05|FIX05_TEST 0.006623 3.5352 20199602.9 +MHDIGDGEDLRDNDPVMWRHNYYENM[Oxidation]QSSGTFLR MHDIGDGEDLRDNDPVMWRHNYYENMQSSGTFLR 2 sp|FIXT12|FIX12_TEST 0.006623 10.0501 51118722.7 +DYWGFPMEHLVDYKNDWELTATEHSKADLM[Oxidation]DQDR DYWGFPMEHLVDYKNDWELTATEHSKADLMDQDR 2 sp|FIXT14|FIX14_TEST 0.006623 -4.0430 18137698.2 +VPEGAIHWVGKHGDLWWEDISKFYDLASGFHYWMK VPEGAIHWVGKHGDLWWEDISKFYDLASGFHYWMK 2 sp|FIXT09|FIX09_TEST 0.006623 12.0050 52662259.9 +SFVQSPHRLFWGWHQVQDHFEKLQMVSGELLLDHK SFVQSPHRLFWGWHQVQDHFEKLQMVSGELLLDHK 2 sp|FIXT05|FIX05_TEST 0.006623 8.2503 40658837.3 +WYGVSMWWKIMSWVWLYM[Oxidation]YIAMEKTYGQQATTIK WYGVSMWWKIMSWVWLYMYIAMEKTYGQQATTIK 2 sp|FIXT09|FIX09_TEST 0.006623 -1082.1209 26904853.1 +TFYPATEMNNSWMRTTSESHHESHKDTWVQTHDVTR TFYPATEMNNSWMRTTSESHHESHKDTWVQTHDVTR 2 sp|FIXT08|FIX08_TEST 0.006623 -2.8155 11319221.0 +WM[Oxidation]WSENNNHWNIPKVVEGDQELGTRVASQMSLQSSDGR WMWSENNNHWNIPKVVEGDQELGTRVASQMSLQSSDGR 2 sp|FIXT15|FIX15_TEST 0.006623 -6.1943 6186360.9 +IMSWVWLYMYIAMEKTYGQQATTIKMNLDGWAFFMR IMSWVWLYMYIAMEKTYGQQATTIKMNLDGWAFFMR 2 sp|FIXT09|FIX09_TEST 0.006623 9.6590 43928065.8 +QDAPQISATSFTKAFPEHAFTSPYKWMNLHDVYDMVAFR QDAPQISATSFTKAFPEHAFTSPYKWMNLHDVYDMVAFR 2 sp|FIXT03|FIX03_TEST 0.006623 9.5181 52830984.9 +IM[Oxidation]MYGWDWKTHFEQVPMFWPRDYWGFPMEHLVDYK IMMYGWDWKTHFEQVPMFWPRDYWGFPMEHLVDYK 2 sp|FIXT14|FIX14_TEST 0.006623 4.5983 28299806.0 +VASQMSLQSSDGRLGMQIGPISFSAYPRHIMQTHQTLALFAK VASQMSLQSSDGRLGMQIGPISFSAYPRHIMQTHQTLALFAK 2 sp|FIXT15|FIX15_TEST 0.006623 -6.1551 15343926.6 +VQEHELWWARWWIPTSYEENM[Oxidation]KYSHAPMLAYHIYHK VQEHELWWARWWIPTSYEENMKYSHAPMLAYHIYHK 2 sp|FIXT10|FIX10_TEST 0.006623 5.8658 34793069.8 +LGMQIGPISFSAYPRHIMQTHQTLALFAKWTEQIMGVPEQK LGMQIGPISFSAYPRHIMQTHQTLALFAKWTEQIMGVPEQK 2 sp|FIXT15|FIX15_TEST 0.006623 11.4455 52265711.4 +SIPIAIDPHEPMHMKVSLHVIWQM[Oxidation]AGHRFFMYLIQISEVK SIPIAIDPHEPMHMKVSLHVIWQMAGHRFFMYLIQISEVK 2 sp|FIXT06|FIX06_TEST 0.006623 6.5839 35567088.6 +THFEQVPMFWPRDYWGFPM[Oxidation]EHLVDYKNDWELTATEHSK THFEQVPMFWPRDYWGFPMEHLVDYKNDWELTATEHSK 2 sp|FIXT14|FIX14_TEST 0.006623 5.6838 34802511.1 +AFPEHAFTSPYKWMNLHDVYDMVAFRNDNWDQVNFAHIGR AFPEHAFTSPYKWMNLHDVYDMVAFRNDNWDQVNFAHIGR 2 sp|FIXT03|FIX03_TEST 0.006623 -4.7874 14225761.8 +DDGDYVHQHETFIKFYHGPQSHLAPRIPFNDAELMIIYMTK DDGDYVHQHETFIKFYHGPQSHLAPRIPFNDAELMIIYMTK 2 sp|FIXT05|FIX05_TEST 0.006623 7.6208 47117389.7 +MDDAWSFWDEHGKYM[Oxidation]SEGWTVMGHPIERSVMLNIWHATPNLR MDDAWSFWDEHGKYMSEGWTVMGHPIERSVMLNIWHATPNLR 2 sp|FIXT10|FIX10_TEST 0.006623 -39.4688 43729525.5 +M[Oxidation]MDDAWSFWDEHGKYMSEGWTVMGHPIERSVMLNIWHATPNLR MMDDAWSFWDEHGKYMSEGWTVMGHPIERSVMLNIWHATPNLR 2 sp|FIXT10|FIX10_TEST 0.006623 -27.7492 46468485.6 +FQFMLLAPDWDYLYKSIPIAIDPHEPM[Oxidation]HMKVSLHVIWQMAGHR FQFMLLAPDWDYLYKSIPIAIDPHEPMHMKVSLHVIWQMAGHR 2 sp|FIXT06|FIX06_TEST 0.006623 -5.5552 15158008.8 diff --git a/desktop/src-tauri/tests/fixtures/run_out/peptidoforms.parquet.report.json b/desktop/src-tauri/tests/fixtures/run_out/peptidoforms.parquet.report.json new file mode 100644 index 00000000..4c3c4b4c --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/peptidoforms.parquet.report.json @@ -0,0 +1,21 @@ +{ + "logical_name": "peptidoforms", + "schema_name": "peptidoforms", + "schema_version": 1, + "stage": "peptidoforms", + "rows": 3820, + "content_hash": "b8c0df35a3ba1faa0ac44736aa95e31563735a93b6b5f99c9a0749b2445b04f3", + "params": { + "charges": "2..3", + "fixed_mods": [ + "C:Carbamidomethyl" + ], + "max_variable_mods": 1, + "variable_mods": [ + "M:Oxidation" + ] + }, + "stats": {}, + "model_identity": null, + "elapsed_ms": 3 +} \ No newline at end of file diff --git a/desktop/src-tauri/tests/fixtures/run_out/protein_group_quant.parquet.report.json b/desktop/src-tauri/tests/fixtures/run_out/protein_group_quant.parquet.report.json new file mode 100644 index 00000000..18d9fba4 --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/protein_group_quant.parquet.report.json @@ -0,0 +1,40 @@ +{ + "logical_name": "protein_group_quant", + "schema_name": "protein_group_quant", + "schema_version": 2, + "stage": "quant", + "rows": 16, + "content_hash": "1eb8ddb19566390bc59e17ede88b0d7264d48b3831079346b74c73aeaa8b5920", + "params": { + "apex_rt_column_present": true, + "baseline_flank_scans": 12, + "baseline_quantile": 0.25, + "baseline_subtract": false, + "bound_peak": true, + "candidates_with_scored_apex": 152, + "chromatograms": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/chromatograms.parquet", + "config_hash": "7dfde09c5047373b95038420ca4248fd4aadf46942bdd8630c30ce1b13403ef2", + "fixed_scan_halfwidth": 0, + "fixed_window_s": 0.0, + "fragment_selection": "ObservedArea", + "peak_fraction": 0.16666666666666666, + "peak_grace": 1, + "peak_window_mode": "PerCandidate", + "psms_scored": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/psms_scored.parquet", + "q_filter": "PeptideQ", + "q_threshold": 0.01, + "reliable_q": 0.001, + "rollup": "TopNSum", + "top_n_fragments": 3, + "top_n_peptides": 3 + }, + "stats": { + "nonquantifiable_peptides": 0, + "peptide_rows": 151, + "protein_group_rows": 16, + "quantified_peptides": 151, + "quantified_protein_groups": 16 + }, + "model_identity": null, + "elapsed_ms": 4 +} \ No newline at end of file diff --git a/desktop/src-tauri/tests/fixtures/run_out/proteins.tsv b/desktop/src-tauri/tests/fixtures/run_out/proteins.tsv new file mode 100644 index 00000000..fcb8a3b5 --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/proteins.tsv @@ -0,0 +1 @@ +protein_group q_value quantity diff --git a/desktop/src-tauri/tests/fixtures/run_out/psms_competed.parquet.report.json b/desktop/src-tauri/tests/fixtures/run_out/psms_competed.parquet.report.json new file mode 100644 index 00000000..b8f70ad8 --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/psms_competed.parquet.report.json @@ -0,0 +1,19 @@ +{ + "logical_name": "psms_competed", + "schema_name": "psms_competed", + "schema_version": 3, + "stage": "compete", + "rows": 152, + "content_hash": "f5d11d59f94a46211bcddcf5b2ce49de95ed4192d5ee361aa61e7b1d5ef9d518", + "params": { + "group_by": "BasePeptide", + "mode": "WinnerTakeAll" + }, + "stats": { + "input_rows": 284, + "kept": 152, + "removed": 132 + }, + "model_identity": null, + "elapsed_ms": 10 +} \ No newline at end of file diff --git a/desktop/src-tauri/tests/fixtures/run_out/psms_competed.parquet.schema.json b/desktop/src-tauri/tests/fixtures/run_out/psms_competed.parquet.schema.json new file mode 100644 index 00000000..6b9230bc --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/psms_competed.parquet.schema.json @@ -0,0 +1,392 @@ +{ + "feature_columns": [ + "rt_error_abs", + "rt_error_rel", + "n_matched_fragments", + "coelution_run", + "log_apex_intensity", + "frag_corr", + "frag_cosine", + "spectral_angle", + "coelution_mean", + "coelution_best", + "n_coelution_above", + "charge", + "peptide_length", + "n_proteins", + "library_norm_manhattan", + "library_rmsd", + "xcorr_coelution", + "xcorr_shape", + "sum_b_intensity", + "sum_y_intensity", + "diff_by_intensity", + "n_b_ions", + "n_y_ions", + "weighted_mass_error", + "mean_mass_error", + "isotope_corr", + "ms1_isom1_ratio", + "log_mono_ms1", + "has_ms1", + "log_sn", + "n_observations", + "base_width_rt", + "seed_score", + "seed_identified", + "matched_fraction", + "profile_cos", + "ref_corr", + "best_ref_corr", + "low_frag_coel", + "evidence", + "contrast_min", + "resid_corr", + "coel_clean", + "shadow_frac", + "spectrum_cosine_matched", + "spectrum_cosine_sqrt", + "spectrum_cosine_log", + "spectral_angle_sqrt", + "spectral_angle_matched", + "pearson_intensity_matched", + "pearson_intensity_log", + "spearman_intensity", + "spearman_intensity_matched", + "kendall_tau_intensity", + "dot_product_raw", + "dot_product_norm", + "library_recall_intensity", + "manhattan_sim", + "manhattan_sqrt", + "rmsd_norm", + "mae_norm", + "mse_log", + "mae_weighted_pred", + "abs_diff_q3", + "max_positive_residual", + "chebyshev_dist", + "minkowski_p3", + "bray_curtis", + "bray_curtis_sqrt", + "canberra", + "canberra_matched", + "wave_hedges", + "chi_square_pearson", + "chi_square_symmetric", + "divergence_distance", + "bhattacharyya_coef", + "hellinger", + "squared_chord", + "harmonic_mean_sim", + "jaccard_presence", + "dice_presence", + "intensity_weighted_pearson", + "regression_slope", + "gini_diff", + "wasserstein_mz", + "footrule_norm", + "rank_overlap_top3", + "top1_frag_match", + "top1_predicted_observed", + "frac_top3_predicted_observed", + "count_strong_predicted_absent", + "frac_predicted_absent", + "cosine_area", + "pearson_area", + "spectral_angle_area", + "cosine_fullwindow", + "stein_scott_weighted_dot", + "log_dot_product", + "spectral_log_evidence", + "scribe_score", + "log_dot_product_area", + "spectral_log_evidence_area", + "scribe_score_area", + "cosine_high_ordinal", + "cosine_robust_trim1", + "cosine_robust_trim2", + "cosine_robust_trim3", + "spectral_entropy_similarity", + "weighted_spectral_entropy_similarity", + "spectral_entropy_similarity_sqrt", + "spectral_entropy_similarity_topk", + "spectral_entropy_similarity_area", + "jensen_shannon_divergence", + "jeffreys_divergence", + "kl_obs_pred", + "kl_pred_obs", + "cross_entropy_obs_pred", + "obs_spectrum_entropy", + "pred_spectrum_entropy", + "entropy_diff", + "entropy_ratio", + "obs_normalized_entropy", + "normalized_entropy_diff", + "residual_spectrum_entropy", + "entropy_weight_obs", + "frag_ref_corr_mean", + "frag_ref_corr_obsweighted", + "frag_ref_corr_min", + "frag_ref_corr_std", + "frag_ref_corr_sq_mean", + "frag_ref_corr_topk_weighted", + "n_frag_ref_corr_above_0_9", + "frac_frag_ref_corr_above_0_8", + "frag_ref_corr_mean_full", + "full_vs_peak_corr_gain", + "pairwise_coelution_weighted", + "pairwise_coelution_min", + "pairwise_coelution_median", + "pairwise_coelution_std", + "pairwise_coelution_frac_negative", + "pairwise_coelution_hi", + "pairwise_coelution_lo", + "coelution_hi_lo_contrast", + "coelution_corr_entropy", + "xcorr_shape_mean", + "xcorr_shape_min", + "xcorr_shape_std", + "xcorr_lag_mean_abs", + "xcorr_lag_std", + "xcorr_lag_iqr", + "xcorr_lag_frac_zero", + "xcorr_lag_max_abs", + "xcorr_lag_entropy", + "ref_xcorr_lag_mean", + "ref_xcorr_shape_mean", + "observed_sum_vs_template_corr", + "frag_loo_ref_corr_mean", + "frag_loo_ref_corr_min", + "frac_frags_apex_aligned", + "top3_frag_ref_corr", + "by_cross_coelution", + "by_cross_lag_mean", + "charge_cross_coelution", + "explained_variance_ref", + "profile_residual_fraction", + "n_interfered_fragments", + "corrected_vs_raw_cos", + "corrected_vs_raw_ratio", + "ifs_removed_count", + "ifs_removed_intensity_frac", + "ifs_corr_gain", + "ifs_retained_frac", + "matched_frac_after_ifs", + "peak_to_full_area_ratio_profile", + "peak_to_full_area_ratio_frag_mean", + "peak_to_full_area_ratio_weighted", + "out_of_peak_intensity_frac", + "profile_corr_full_vs_peak_delta", + "frac_frag_ref_corr_below_0_5", + "explained_apex_intensity_frac", + "apex_purity", + "interference_apex_residual_fraction", + "dominant_frag_ref_corr", + "explained_variance_ratio", + "second_component_fraction", + "profile_second_peak_ratio", + "n_competing_peaks_in_window", + "matched_pred_intensity_fraction", + "top_pred_frag_matched", + "gaussian_fit_r2", + "gaussian_cosine", + "emg_fit_improvement", + "apex_prominence", + "profile_peak_snr", + "fwhm_seconds", + "fwhm_to_window_ratio", + "width_at_10pct", + "width_ratio_10_50", + "hwhm_asymmetry", + "tailing_factor_usp", + "asymmetry_factor_10pct", + "apex_sharpness", + "apex_curvature", + "apex_to_boundary_ratio", + "apex_dominance", + "zigzag_index", + "jaggedness", + "roughness_2nd_deriv", + "n_local_maxima", + "modality", + "rt_skewness", + "rt_excess_kurtosis", + "rt_std_seconds", + "mean_mode_offset", + "fraction_area_within_fwhm", + "triangle_area_similarity", + "baseline_fraction", + "peak_completeness", + "apex_centering_offset", + "intensity_score", + "total_xic_log", + "frag_fwhm_cv", + "frag_fwhm_mean", + "frag_apex_rt_dispersion", + "frag_apex_rt_dispersion_weighted", + "frag_apex_offset_from_profile_mean", + "frag_gaussianity_mean", + "frag_gaussianity_weighted", + "frag_zigzag_mean", + "sumtrace_unweighted_gaussian_r2", + "reference_profile_rt_entropy_peak", + "reference_profile_rt_entropy_ratio", + "median_abs_frag_ppm", + "signed_mean_frag_ppm", + "ppm_std", + "ppm_iqr", + "ppm_range", + "max_abs_frag_ppm", + "intensity_weighted_abs_ppm", + "intensity_weighted_signed_ppm", + "intensity_weighted_ppm_std", + "lib_weighted_abs_ppm", + "frac_frag_within_half_tol", + "high_ppm_intensity_frac", + "ppm_intensity_anticorr", + "mass_error_mz_trend", + "mean_abs_mz_error_da", + "mass_evidence_gauss", + "mass_log_evidence", + "n_matched_b", + "n_matched_y", + "frac_matched_b", + "frac_matched_y", + "by_count_balance", + "by_intensity_ratio", + "by_ratio_agreement", + "by_ratio_consistency", + "longest_b_run", + "longest_y_run", + "longest_run_max", + "longest_run_frac_length", + "series_coverage_b", + "series_coverage_y", + "sequence_coverage", + "series_gap_fraction", + "by_complement_count", + "by_complement_mz_consistency", + "by_complement_coelution", + "ordinal_intensity_concordance_y", + "ordinal_intensity_concordance_b", + "series_coelution_y", + "series_coelution_b", + "spectral_angle_b", + "spectral_angle_y", + "pearson_b", + "pearson_y", + "cosine_charge1", + "cosine_charge2", + "charge_corr_balance", + "mean_matched_ordinal_norm", + "by_ion_contiguous_intensity", + "by_ion_contiguous_lib_frac", + "both_series_present", + "ms1_isotope_cosine_apex", + "ms1_isotope_spectral_angle_apex", + "ms1_isotope_chi2_apex", + "ms1_isotope_manhattan_apex", + "iso_ratio_1_0", + "iso_ratio_2_0", + "iso_plus1_ratio_dev", + "iso_plus2_ratio_dev", + "iso_minus_one_fraction", + "iso_overlap_flag", + "log_ms1_mono", + "ms1_total_isotope_log", + "has_ms1_signal", + "ms1_isotope_apex_entropy_3", + "ms1_m1_entropy_contribution", + "ms1_ms2_time_corr", + "ms1_ms2_envelope_time_corr", + "ms1_iso_coelution", + "ms1_ms2_apex_rt_delta", + "ms1_iso_ratio_stability", + "ms1_mono_gaussianity", + "ms1_ms2_fwhm_ratio", + "ms1_isotope_corr_xic", + "ms1_envelope_over_time_corr", + "ms1_isotope_xic_shape_consistency", + "ms1_isotope_height_corr", + "rt_error_signed", + "rt_error_squared", + "rt_error_signed_norm_gradient", + "rt_error_abs_norm_gradient", + "observed_rt_raw", + "predicted_rt_raw", + "observed_rt_fraction", + "predicted_rt_fraction", + "rt_error_over_peak_width", + "rt_error_over_fwhm", + "rt_diff_profile_apex", + "predicted_rt_in_gradient", + "log_seed_hyperscore", + "seed_hyperscore_per_matched", + "precursor_charge", + "charge_is_2", + "charge_is_3", + "charge_is_4plus", + "precursor_mass", + "log_total_matched_intensity", + "n_matched_frags", + "n_predicted_frags", + "frag_corr_peakmax", + "frag_cosine_peakmax", + "spectral_angle_peakmax", + "frag_corr_matched_nz", + "frag_cosine_matched_nz", + "peakmax_apex_gain", + "n_frag_present_inpeak", + "frac_frag_present_inpeak", + "coelution_mean_bothpos", + "coelution_mean_summpos", + "ref_corr_nz", + "profile_cos_nz", + "rank_corr_vs_apex_mean", + "rank_corr_vs_apex_std", + "rank_corr_adjacent_mean", + "kendall_vs_apex_mean", + "top1_frag_persistence", + "top2_order_persistence", + "argmax_frag_entropy", + "self_cosine_vs_apex_mean", + "n_peak_scans", + "peak_window_degenerate", + "frag_apex_rt_std", + "frag_apex_rt_mad", + "frag_apex_max_dev", + "frag_apex_mean_dev", + "frag_apex_agree_frac", + "precursor_frag_apex_delta", + "peak_symmetry", + "peak_tailing", + "peak_n_local_maxima", + "peak_shoulder_score", + "peak_fwhm_scans", + "peak_truncation", + "apex_frac_of_window", + "frag_mass_err_median", + "frag_mass_err_abs_median", + "frag_mass_err_std", + "frag_mass_err_iqr", + "frag_mass_err_max_abs", + "frag_mass_err_range", + "effective_frag_count", + "evidence_concentration", + "frac_top3_pred_observed", + "frac_top5_pred_observed", + "deconv_explained_frac", + "deconv_active", + "deconv_share", + "deconv_max_collinearity", + "shadow_kept_frac", + "peak_contested_frac", + "peak_contested_count_frac", + "peak_apportioned_frac", + "n_charge_states", + "charge_multi_flag", + "cross_charge_intensity_log" + ], + "schema_id": "e5b4eff0133e411dbf406947c89466619ec557ab37874bc3948792d9890afe8e" +} \ No newline at end of file diff --git a/desktop/src-tauri/tests/fixtures/run_out/psms_extracted.parquet.report.json b/desktop/src-tauri/tests/fixtures/run_out/psms_extracted.parquet.report.json new file mode 100644 index 00000000..1fc03f57 --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/psms_extracted.parquet.report.json @@ -0,0 +1,25 @@ +{ + "logical_name": "psms_extracted", + "schema_name": "psms_extracted", + "schema_version": 2, + "stage": "extract", + "rows": 284, + "content_hash": "7f2eae351f0d936467458d3ef2942b020f93fc9b72d7484e2c3a08942f44d033", + "params": { + "effective_frag_tol_ppm": 5.0, + "frag_ppm_offset": -0.00026103398249893126, + "frag_tol_ppm": 20.0, + "gate_coelution_min": 0.5, + "gate_min_score": 0.2, + "gate_mode": "apex_pearson", + "presence_min_coelution": 2, + "presence_min_fragments": 3, + "scan_window": 3 + }, + "stats": { + "accepted": 284, + "scan_window": 3 + }, + "model_identity": null, + "elapsed_ms": 11 +} \ No newline at end of file diff --git a/desktop/src-tauri/tests/fixtures/run_out/psms_scored.parquet.report.json b/desktop/src-tauri/tests/fixtures/run_out/psms_scored.parquet.report.json new file mode 100644 index 00000000..d1723c20 --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/psms_scored.parquet.report.json @@ -0,0 +1,31 @@ +{ + "logical_name": "psms_scored", + "schema_name": "psms_scored", + "schema_version": 4, + "stage": "rescore", + "rows": 152, + "content_hash": "9c92be4ad83b794db312831fb8797877e0230156cd297624f9120401b5fb82a7", + "params": { + "classifier": "native_tda", + "classifier_requested": "NativeTda", + "competed_inputs": [ + "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/out/psms_competed.parquet" + ], + "config_hash": "7dfde09c5047373b95038420ca4248fd4aadf46942bdd8630c30ce1b13403ef2", + "feature_schema_id": "e5b4eff0133e411dbf406947c89466619ec557ab37874bc3948792d9890afe8e", + "folds": 3, + "num_iter": 10, + "strict": true, + "train_fdr": 0.01 + }, + "stats": { + "classifier": "native_tda", + "psms": 152, + "target_peptides_at_1pct": 151, + "target_precursors_at_1pct": 151, + "target_protein_groups_at_1pct": 0, + "target_psms_at_1pct": 151 + }, + "model_identity": "native-percolator-lite-v1", + "elapsed_ms": 66 +} \ No newline at end of file diff --git a/desktop/src-tauri/tests/fixtures/run_out/run_windows.parquet.report.json b/desktop/src-tauri/tests/fixtures/run_out/run_windows.parquet.report.json new file mode 100644 index 00000000..01542294 --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/run_windows.parquet.report.json @@ -0,0 +1,20 @@ +{ + "logical_name": "run_windows", + "schema_name": "run_windows", + "schema_version": 1, + "stage": "rt-im-train", + "rows": 3820, + "content_hash": "cec9473ba70a0955e310cdc9ceb1a1962bbf3fcce67d354f6dfe09ca00885c73", + "params": { + "method": "Loess", + "p_rt": 0.95, + "q_train": 0.01 + }, + "stats": { + "calibration_status": "loess", + "n_train": 110, + "w_rt": 11.826482451982308 + }, + "model_identity": null, + "elapsed_ms": 2 +} \ No newline at end of file diff --git a/desktop/src-tauri/tests/fixtures/run_out/seed_psms.parquet.masscal.json b/desktop/src-tauri/tests/fixtures/run_out/seed_psms.parquet.masscal.json new file mode 100644 index 00000000..76391422 --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/seed_psms.parquet.masscal.json @@ -0,0 +1,11 @@ +{ + "cal_passes": 1, + "frag_ppm_offset": -0.00026103398249893126, + "frag_ppm_sigma": 5.0, + "frag_tol_ppm": 5.0, + "mz_cal_grid_mz": [], + "mz_cal_grid_ppm": [], + "n_dev": 1068, + "ppm_residual_mad": 0.019232855968052062, + "ppm_residual_median": 0.0 +} \ No newline at end of file diff --git a/desktop/src-tauri/tests/fixtures/run_out/seed_psms.parquet.report.json b/desktop/src-tauri/tests/fixtures/run_out/seed_psms.parquet.report.json new file mode 100644 index 00000000..ec474ae6 --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/seed_psms.parquet.report.json @@ -0,0 +1,21 @@ +{ + "logical_name": "seed_psms", + "schema_name": "seed_psms", + "schema_version": 1, + "stage": "search-seed", + "rows": 179, + "content_hash": "a12ee0d4627587e8c2dfb7fda55a0258ec4563c3e0067a3c93effa7edbf64686", + "params": { + "fdr_seed": 0.01, + "fragment_tol_ppm": 20.0, + "min_matched_peaks": 4, + "report_psms": 5, + "top_n_peaks": 300 + }, + "stats": { + "psms": 179, + "targets_at_q0.01": 179 + }, + "model_identity": "native-seed-hyperscore-v1", + "elapsed_ms": 6 +} \ No newline at end of file diff --git a/desktop/src-tauri/tests/fixtures/run_out/spectra/isolation_windows.parquet.report.json b/desktop/src-tauri/tests/fixtures/run_out/spectra/isolation_windows.parquet.report.json new file mode 100644 index 00000000..a15de6c8 --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/spectra/isolation_windows.parquet.report.json @@ -0,0 +1,18 @@ +{ + "logical_name": "isolation_windows", + "schema_name": "isolation_windows", + "schema_version": 1, + "stage": "convert", + "rows": 8, + "content_hash": "3ceab06d7b4d1ed3d814722235920d2677bb6877535fa02767a67519de0ea32f", + "params": { + "config_hash": "e11217cb603b8854a93c176b421cd926fb371ab35ee4647c4301b3f1c7631c3b", + "max_spectra": 0, + "mzml": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/fixture.mzML", + "top_peaks_ms1": 0, + "top_peaks_ms2": 0 + }, + "stats": {}, + "model_identity": null, + "elapsed_ms": 12 +} \ No newline at end of file diff --git a/desktop/src-tauri/tests/fixtures/run_out/spectra/ms2_to_ms1.parquet.report.json b/desktop/src-tauri/tests/fixtures/run_out/spectra/ms2_to_ms1.parquet.report.json new file mode 100644 index 00000000..e20fa2c4 --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/spectra/ms2_to_ms1.parquet.report.json @@ -0,0 +1,18 @@ +{ + "logical_name": "ms2_to_ms1", + "schema_name": "ms2_to_ms1", + "schema_version": 1, + "stage": "convert", + "rows": 480, + "content_hash": "c60988ab4d075d9655fb7406169ee92e804c7dff05b42124e818ccc9f249346f", + "params": { + "config_hash": "e11217cb603b8854a93c176b421cd926fb371ab35ee4647c4301b3f1c7631c3b", + "max_spectra": 0, + "mzml": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/fixture.mzML", + "top_peaks_ms1": 0, + "top_peaks_ms2": 0 + }, + "stats": {}, + "model_identity": null, + "elapsed_ms": 12 +} \ No newline at end of file diff --git a/desktop/src-tauri/tests/fixtures/run_out/spectra/spectra_ms1.parquet.report.json b/desktop/src-tauri/tests/fixtures/run_out/spectra/spectra_ms1.parquet.report.json new file mode 100644 index 00000000..2f07841c --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/spectra/spectra_ms1.parquet.report.json @@ -0,0 +1,18 @@ +{ + "logical_name": "spectra_ms1", + "schema_name": "spectra_ms1", + "schema_version": 1, + "stage": "convert", + "rows": 60, + "content_hash": "c70bba2b31203c20d8108d47c3f802c0ac5b79bb797701cf1b29041d5cd0fa6d", + "params": { + "config_hash": "e11217cb603b8854a93c176b421cd926fb371ab35ee4647c4301b3f1c7631c3b", + "max_spectra": 0, + "mzml": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/fixture.mzML", + "top_peaks_ms1": 0, + "top_peaks_ms2": 0 + }, + "stats": {}, + "model_identity": null, + "elapsed_ms": 12 +} \ No newline at end of file diff --git a/desktop/src-tauri/tests/fixtures/run_out/spectra/spectra_ms2.parquet.report.json b/desktop/src-tauri/tests/fixtures/run_out/spectra/spectra_ms2.parquet.report.json new file mode 100644 index 00000000..48f97a05 --- /dev/null +++ b/desktop/src-tauri/tests/fixtures/run_out/spectra/spectra_ms2.parquet.report.json @@ -0,0 +1,18 @@ +{ + "logical_name": "spectra_ms2", + "schema_name": "spectra_ms2", + "schema_version": 1, + "stage": "convert", + "rows": 480, + "content_hash": "6228d8b36e3931ed6271122e735494957c8217dbd33d8a81d1fef568b775cf26", + "params": { + "config_hash": "e11217cb603b8854a93c176b421cd926fb371ab35ee4647c4301b3f1c7631c3b", + "max_spectra": 0, + "mzml": "C:/Users/robbi/AppData/Local/Temp/claude/H--OneDrive---UGent-MuMDIA-NG/ed948dad-9d5a-423e-9f52-cf197cbee207/scratchpad/smoke_final/fixture.mzML", + "top_peaks_ms1": 0, + "top_peaks_ms2": 0 + }, + "stats": {}, + "model_identity": null, + "elapsed_ms": 12 +} \ No newline at end of file diff --git a/desktop/ui/app.css b/desktop/ui/app.css new file mode 100644 index 00000000..10d99e9c --- /dev/null +++ b/desktop/ui/app.css @@ -0,0 +1,180 @@ +/* MuMDIA console. + One palette, defined light-first and redefined for dark, so the application + follows the operating system without a second stylesheet. */ + +:root { + --ground:#F6F7F8; --panel:#FFFFFF; --sunk:#EDEFF2; + --ink:#14181D; --ink-soft:#4A535E; --ink-faint:#77818D; + --rule:#DDE1E7; --rule-strong:#C3C9D2; + --accent:#1E4D8C; --accent-soft:#E8EEF7; --on-accent:#FFFFFF; + --ok:#2F7D4F; --ok-soft:#E4F1E9; + --warn:#9A6510; --warn-soft:#FBF0DA; + --danger:#A93826; --danger-soft:#FAE7E3; + --radius:7px; +} +@media (prefers-color-scheme: dark) { + :root { + --ground:#0E1216; --panel:#171D24; --sunk:#1C222A; + --ink:#E4E8ED; --ink-soft:#A6AFBA; --ink-faint:#79838F; + --rule:#262E37; --rule-strong:#38424E; + --accent:#7FA9E0; --accent-soft:#17263A; --on-accent:#0E1216; + --ok:#6FBF8F; --ok-soft:#14251B; + --warn:#D9A544; --warn-soft:#2A2113; + --danger:#E08472; --danger-soft:#2C1814; + } +} + +* { box-sizing: border-box; } +html, body { height: 100%; } +body { + margin: 0; + display: grid; + grid-template-columns: 190px 1fr; + background: var(--ground); + color: var(--ink); + font: 14px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + -webkit-font-smoothing: antialiased; + overflow: hidden; +} + +/* ── rail ─────────────────────────────────────────────────────────────── */ +.rail { + background: var(--panel); + border-right: 1px solid var(--rule); + display: flex; flex-direction: column; + padding: 18px 0 12px; + min-width: 0; +} +.brand { + font-size: 15px; font-weight: 700; letter-spacing: .02em; + padding: 0 18px 16px; +} +.rail nav { display: flex; flex-direction: column; gap: 2px; } +.nav { + appearance: none; border: 0; background: none; text-align: left; + font: inherit; color: var(--ink); padding: 8px 18px; cursor: pointer; + border-left: 2px solid transparent; +} +.nav:hover:not(:disabled) { background: var(--sunk); } +.nav.on { background: var(--accent-soft); color: var(--accent); font-weight: 600; border-left-color: var(--accent); } +.nav:disabled { color: var(--ink-faint); cursor: default; opacity: .55; } +.nav:focus-visible, .btn:focus-visible, .tab:focus-visible, +select:focus-visible, input:focus-visible, summary:focus-visible { + outline: 2px solid var(--accent); outline-offset: 2px; +} +.rail-foot { margin-top: auto; padding: 12px 18px 0; border-top: 1px solid var(--rule); } +.engine-line { font-size: 11px; color: var(--ink-faint); word-break: break-word; line-height: 1.4; } + +/* ── screens ──────────────────────────────────────────────────────────── */ +main { overflow-y: auto; padding: 28px 30px 40px; min-width: 0; } +.screen { display: none; max-width: 780px; } +.screen.on { display: block; } +h1 { font-size: 21px; font-weight: 650; margin: 0 0 3px; letter-spacing: -.01em; } +.sub { color: var(--ink-soft); font-size: 13px; margin: 0 0 20px; } +.head-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; } +.head-row .sub { margin-bottom: 20px; } + +.card { + background: var(--panel); border: 1px solid var(--rule); + border-radius: var(--radius); padding: 14px 16px; margin-bottom: 12px; +} +.card-title { font-size: 13px; font-weight: 600; margin-bottom: 10px; } +.hint { font-size: 12px; color: var(--ink-faint); margin: 8px 0 0; line-height: 1.5; } +.hint.inline { margin: 0; } + +.picker { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; } +.picker:last-of-type { margin-bottom: 0; } +.picked { + flex: 1; min-width: 0; font-size: 12.5px; color: var(--ink-soft); + background: var(--sunk); border: 1px solid var(--rule); border-radius: 5px; + padding: 6px 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + direction: rtl; text-align: left; /* keep the filename visible when the path is long */ +} +.picked.set { color: var(--ink); } + +.tabs { display: flex; gap: 6px; margin-bottom: 12px; } +.tab { + appearance: none; font: inherit; font-size: 12.5px; cursor: pointer; + border: 1px solid var(--rule); background: var(--panel); color: var(--ink-soft); + padding: 5px 12px; border-radius: 20px; +} +.tab.on { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); font-weight: 600; } + +.row { display: flex; align-items: center; gap: 10px; margin-bottom: 9px; } +.row:last-child { margin-bottom: 0; } +.row label { font-size: 12.5px; color: var(--ink-soft); width: 62px; flex: none; } +select, input[type="number"] { + font: inherit; font-size: 12.5px; padding: 5px 9px; + border: 1px solid var(--rule); border-radius: 5px; + background: var(--panel); color: var(--ink); +} +input[type="number"] { width: 110px; } +select { min-width: 220px; } + +.btn { + appearance: none; font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; + padding: 7px 14px; border-radius: 5px; + border: 1px solid var(--accent); background: var(--accent); color: var(--on-accent); +} +.btn:hover { filter: brightness(1.06); } +.btn:disabled { opacity: .5; cursor: default; filter: none; } +.btn.quiet { background: transparent; color: var(--ink); border-color: var(--rule-strong); } +.btn.big { font-size: 14px; padding: 9px 20px; } +.btn.small { font-size: 12px; padding: 4px 10px; margin-top: 8px; } + +.actions { display: flex; align-items: center; gap: 14px; margin-top: 18px; } + +.banner { + border-radius: 6px; padding: 11px 14px; margin-bottom: 14px; font-size: 13px; + border: 1px solid transparent; white-space: pre-wrap; line-height: 1.5; +} +.banner.bad { background: var(--danger-soft); border-color: var(--danger); color: var(--ink); } +.banner.warn { background: var(--warn-soft); border-color: var(--warn); color: var(--ink); } + +/* ── stages ───────────────────────────────────────────────────────────── */ +.stages { display: flex; flex-direction: column; } +.stage { + display: grid; grid-template-columns: 20px 1fr auto auto; gap: 10px; + align-items: center; padding: 7px 0; border-bottom: 1px solid var(--rule); + font-size: 13px; +} +.stage:last-child { border-bottom: none; } +.stage .mark { text-align: center; font-size: 12px; color: var(--ink-faint); } +.stage.done .mark { color: var(--ok); } +.stage.now { background: var(--accent-soft); border-radius: 5px; padding-left: 8px; padding-right: 8px; margin: 0 -8px; } +.stage.now .mark { color: var(--accent); } +.stage.todo .name, .stage.todo .mark { color: var(--ink-faint); } +.stage .name { min-width: 0; } +.stage .stat, .stage .time { + font-size: 11.5px; color: var(--ink-faint); font-variant-numeric: tabular-nums; text-align: right; +} +.stage .time { min-width: 58px; } + +/* ── log and command ──────────────────────────────────────────────────── */ +.details summary { font-size: 13px; font-weight: 600; cursor: pointer; } +.cmd, .log { + font-family: ui-monospace, "Cascadia Mono", "Consolas", monospace; + font-size: 11.5px; line-height: 1.6; background: var(--sunk); + border: 1px solid var(--rule); border-radius: 6px; padding: 10px 12px; + margin: 10px 0 0; white-space: pre-wrap; word-break: break-word; +} +.logcard { display: flex; flex-direction: column; } +.log { max-height: 300px; overflow-y: auto; white-space: pre; word-break: normal; margin: 0; } + +/* ── results ──────────────────────────────────────────────────────────── */ +.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 12px; } +.kpi { background: var(--panel); border: 1px solid var(--rule); border-radius: var(--radius); padding: 12px 14px; } +.kpi .v { font-size: 23px; font-weight: 650; font-variant-numeric: tabular-nums; letter-spacing: -.02em; } +.kpi .k { font-size: 11.5px; color: var(--ink-soft); margin-top: 1px; } +.kpi .u { + font-family: ui-monospace, "Cascadia Mono", monospace; + font-size: 10.5px; color: var(--ink-faint); margin-top: 5px; +} +.files { display: flex; flex-direction: column; gap: 5px; font-size: 12.5px; } +.files code { + font-family: ui-monospace, "Cascadia Mono", monospace; font-size: 11.5px; + background: var(--sunk); border: 1px solid var(--rule); + border-radius: 4px; padding: 1px 6px; +} + +@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } } diff --git a/desktop/ui/app.js b/desktop/ui/app.js new file mode 100644 index 00000000..111c855d --- /dev/null +++ b/desktop/ui/app.js @@ -0,0 +1,370 @@ +// MuMDIA console frontend. +// +// No framework and no build step, deliberately: the heaviest screen here is a list +// of stages and a handful of fields, and adding Node to the release pipeline for +// that would be a poor trade. If this grows into the generated 86-field settings +// editor, revisit it then. +// +// State lives in the backend. This polls `run_state` and renders what it returns, +// so a reload or a reopened window shows the truth rather than a stale copy. + +const { invoke } = window.__TAURI__.core; +const dialog = window.__TAURI__.dialog; + +const $ = (id) => document.getElementById(id); + +// ── pipeline order ────────────────────────────────────────────────────────── +// The engine reports the stage that produced each artifact, but not what is still +// to come, so the expected sequence lives here. Library-input mode skips the three +// library-building stages, exactly as the engine does. +const STAGES_FASTA = [ + ["convert", "Reading spectra"], + ["digest", "Digesting the FASTA"], + ["peptidoforms", "Expanding peptidoforms"], + ["predict-frag", "Predicting the library"], + ["search-seed", "First-pass search"], + ["rt-im-train", "Retention-time model"], + ["extract", "Extracting chromatograms"], + ["features", "Computing features"], + ["compete", "Competition"], + ["rescore", "Rescoring"], + ["quant", "Quantification"], + ["report", "Writing the report"], +]; +const SKIP_IN_LIBRARY_MODE = new Set(["digest", "peptidoforms", "predict-frag"]); + +const state = { + mode: "fasta", + picks: { mzml: "", fasta: "", lib_precursors: "", lib_fragments: "", out_dir: "" }, + runId: null, + timer: null, + lastStatus: null, + outDir: "", +}; + +// ── small helpers ─────────────────────────────────────────────────────────── +const fmtInt = (n) => (n ?? 0).toLocaleString("en-GB"); + +function fmtDuration(ms) { + if (!ms || ms < 0) return ""; + const s = Math.round(ms / 1000); + if (s < 60) return `${s}s`; + const m = Math.floor(s / 60); + if (m < 60) return `${m}m ${String(s % 60).padStart(2, "0")}s`; + return `${Math.floor(m / 60)}h ${String(m % 60).padStart(2, "0")}m`; +} + +function baseName(p) { + if (!p) return ""; + const parts = p.split(/[\\/]/); + return parts[parts.length - 1] || p; +} + +function show(el, on) { + if (on) el.removeAttribute("hidden"); + else el.setAttribute("hidden", ""); +} + +function banner(el, message) { + if (!message) { + show(el, false); + return; + } + el.textContent = message; + show(el, true); +} + +function screen(name) { + for (const s of document.querySelectorAll(".screen")) { + s.classList.toggle("on", s.id === `screen-${name}`); + } + for (const b of document.querySelectorAll(".nav")) { + b.classList.toggle("on", b.dataset.screen === name); + } +} + +// ── startup ───────────────────────────────────────────────────────────────── +async function init() { + const cores = navigator.hardwareConcurrency; + if (cores) $("cores").textContent = `${cores} cores available`; + + try { + const info = await invoke("engine_info"); + $("engine-line").textContent = `${info.version}\n${info.path}`; + $("engine-line").title = `${info.version} — found via ${info.source}\n${info.path}`; + } catch (e) { + $("engine-line").textContent = "engine not found"; + banner($("engine-error"), String(e)); + $("start").disabled = true; + } + + try { + const list = await invoke("presets"); + const sel = $("preset"); + for (const p of list) { + const o = document.createElement("option"); + o.value = p.path; + o.textContent = p.name; + sel.appendChild(o); + } + } catch { + /* Presets are a convenience; engine defaults remain available without them. */ + } + + for (const b of document.querySelectorAll(".nav")) { + b.addEventListener("click", () => !b.disabled && screen(b.dataset.screen)); + } + for (const t of document.querySelectorAll(".tab")) { + t.addEventListener("click", () => setMode(t.dataset.mode)); + } + for (const b of document.querySelectorAll("[data-pick]")) { + b.addEventListener("click", () => pick(b.dataset.pick)); + } + $("start").addEventListener("click", start); + $("stop").addEventListener("click", stop); + $("another").addEventListener("click", () => screen("input")); + $("open-folder").addEventListener("click", () => invoke("reveal", { path: state.outDir })); + $("copy-cmd").addEventListener("click", async () => { + await navigator.clipboard.writeText($("cmd").textContent); + $("copy-cmd").textContent = "Copied"; + setTimeout(() => ($("copy-cmd").textContent = "Copy"), 1200); + }); +} + +function setMode(mode) { + state.mode = mode; + for (const t of document.querySelectorAll(".tab")) t.classList.toggle("on", t.dataset.mode === mode); + show($("mode-fasta"), mode === "fasta"); + show($("mode-library"), mode === "library"); +} + +// ── file pickers ──────────────────────────────────────────────────────────── +const FILTERS = { + mzml: [{ name: "mzML", extensions: ["mzML", "mzml"] }], + fasta: [{ name: "FASTA", extensions: ["fasta", "fa", "fas"] }], + lib_precursors: [{ name: "Parquet", extensions: ["parquet"] }], + lib_fragments: [{ name: "Parquet", extensions: ["parquet"] }], +}; +const LABEL = { + mzml: "p-mzml", + fasta: "p-fasta", + lib_precursors: "p-libp", + lib_fragments: "p-libf", + out_dir: "p-out", +}; + +async function pick(what) { + const chosen = + what === "out_dir" + ? await dialog.open({ directory: true, multiple: false }) + : await dialog.open({ multiple: false, filters: FILTERS[what] }); + if (!chosen) return; + const path = Array.isArray(chosen) ? chosen[0] : chosen; + state.picks[what] = path; + const el = $(LABEL[what]); + // Shown right-to-left so the filename stays visible on a long path; the full + // path is the tooltip. + el.textContent = path; + el.title = path; + el.classList.add("set"); + banner($("start-error"), ""); +} + +// ── starting and polling ──────────────────────────────────────────────────── +async function start() { + banner($("start-error"), ""); + const p = state.picks; + const threads = parseInt($("threads").value, 10); + const req = { + mzml: p.mzml, + out_dir: p.out_dir, + fasta: state.mode === "fasta" ? p.fasta || null : null, + lib_precursors: state.mode === "library" ? p.lib_precursors || null : null, + lib_fragments: state.mode === "library" ? p.lib_fragments || null : null, + config: $("preset").value || null, + threads: Number.isFinite(threads) && threads > 0 ? threads : null, + }; + + try { + state.runId = await invoke("start_run", { req }); + } catch (e) { + banner($("start-error"), String(e)); + return; + } + + state.outDir = p.out_dir; + state.lastStatus = null; + $("nav-progress").disabled = false; + $("nav-results").disabled = true; + $("stop").disabled = false; + $("log").textContent = ""; + banner($("run-error"), ""); + screen("progress"); + + clearInterval(state.timer); + state.timer = setInterval(poll, 700); + poll(); +} + +async function stop() { + if (!state.runId) return; + $("stop").disabled = true; + $("stop").textContent = "Stopping…"; + try { + await invoke("cancel_run", { id: state.runId }); + } catch (e) { + banner($("run-error"), String(e)); + } +} + +async function poll() { + if (!state.runId) return; + let s; + try { + s = await invoke("run_state", { id: state.runId }); + } catch { + return; + } + if (!s) return; + render(s); + + if (s.status !== "running" && s.status !== "starting") { + clearInterval(state.timer); + state.timer = null; + $("stop").disabled = true; + $("stop").textContent = "Stop"; + if (s.status === "done") { + $("nav-results").disabled = false; + renderResults(s); + screen("results"); + } + } +} + +// ── rendering ─────────────────────────────────────────────────────────────── +function render(s) { + const seen = new Map(s.stages.map((x) => [x.name, x])); + const expected = STAGES_FASTA.filter( + ([key]) => !(s.library_mode && SKIP_IN_LIBRARY_MODE.has(key)) + ); + + // The furthest stage with a report is the one in progress; everything before it + // is done. `report` writes TSVs rather than an artifact report, so it is only + // ever complete once the process itself has finished successfully. + let lastSeen = -1; + expected.forEach(([key], i) => { + if (seen.has(key)) lastSeen = i; + }); + const finished = s.status === "done"; + + const rows = expected.map(([key, label], i) => { + const st = seen.get(key); + let cls, mark; + if (finished) { + cls = "done"; + mark = "✓"; + } else if (st && i < lastSeen) { + cls = "done"; + mark = "✓"; + } else if (st && i === lastSeen) { + cls = s.status === "running" ? "now" : "done"; + mark = s.status === "running" ? "●" : "✓"; + } else if (i === lastSeen + 1 && s.status === "running") { + cls = "now"; + mark = "●"; + } else { + cls = "todo"; + mark = "·"; + } + const stat = st && st.rows ? `${fmtInt(st.rows)} rows` : ""; + const time = st && st.elapsed_ms ? fmtDuration(st.elapsed_ms) : ""; + return `
+ ${mark} + ${label} + ${stat} + ${time} +
`; + }); + $("stages").innerHTML = rows.join(""); + + const titles = { + starting: "Starting", + running: "Searching", + done: "Finished", + failed: "Search failed", + cancelled: "Search stopped", + }; + $("prog-title").textContent = titles[s.status] || s.status; + + const done = expected.filter(([k]) => seen.has(k)).length; + const parts = []; + if (s.status === "running") parts.push(`stage ${Math.min(done + 1, expected.length)} of ${expected.length}`); + if (s.elapsed_ms) parts.push(fmtDuration(s.elapsed_ms)); + if (s.status === "cancelled") parts.push("partial results were discarded"); + $("prog-sub").textContent = parts.join(" · "); + + $("cmd").textContent = s.command; + + if (s.error && (s.status === "failed" || s.status === "cancelled")) { + banner($("run-error"), s.error); + } + + // Only touch the log when it changed, so a user scrolled up to read something + // is not yanked back to the bottom on every poll. + const log = $("log"); + const text = s.log.join("\n"); + if (log.textContent !== text) { + const atBottom = log.scrollHeight - log.scrollTop - log.clientHeight < 40; + log.textContent = text; + if (atBottom) log.scrollTop = log.scrollHeight; + } +} + +function renderResults(s) { + const r = s.results; + $("res-title").textContent = "Finished"; + const bits = [fmtDuration(s.elapsed_ms)]; + if (r && r.classifier) bits.push(`rescoring: ${r.classifier}`); + if (r && r.config_hash) bits.push(`settings ${r.config_hash.slice(0, 8)}`); + $("res-sub").textContent = bits.filter(Boolean).join(" · "); + + // The requested classifier and the one that ran can differ, when a sidecar fails + // and strict mode is off. Saying so is the whole point of reading the artifact + // report rather than echoing the request. + const requested = (r?.classifier_requested || "").toLowerCase().replace(/_/g, ""); + const actual = (r?.classifier || "").toLowerCase().replace(/_/g, ""); + if (r && requested && actual && requested !== actual) { + banner( + $("res-warn"), + `Rescoring fell back to ${r.classifier}; ${r.classifier_requested} was requested. ` + + `The counts below come from ${r.classifier}.` + ); + } else { + banner($("res-warn"), ""); + } + + // Every count names its row unit and its q-value column. A number without both + // is not interpretable, and this is where a screenshot gets taken. + const kpis = r + ? [ + [fmtInt(r.peptides_1pct), "peptides", "peptide_q_value ≤ 0.01"], + [fmtInt(r.precursors_1pct), "precursors", "precursor_q ≤ 0.01"], + [fmtInt(r.protein_groups_1pct), "protein groups", "pg_q_value ≤ 0.01"], + [fmtInt(r.psms), "PSMs scored", "all, before thresholding"], + ] + : []; + $("kpis").innerHTML = kpis + .map(([v, k, u]) => `
${v}
${k}
${u}
`) + .join(""); + + const files = []; + if (r?.has_peptides_tsv) files.push(["peptides.tsv", "one row per (peptidoform, charge), selected by peptide q"]); + if (r?.has_proteins_tsv) files.push(["proteins.tsv", "protein groups"]); + files.push(["psms_scored.parquet", "every scored match, with all features"]); + files.push(["manifest.json", "engine version, commit, and a hash of every input"]); + $("files").innerHTML = files + .map(([f, d]) => `
${f}  ${d}
`) + .join(""); +} + +init(); diff --git a/desktop/ui/index.html b/desktop/ui/index.html new file mode 100644 index 00000000..d780a3f1 --- /dev/null +++ b/desktop/ui/index.html @@ -0,0 +1,163 @@ + + + + + +MuMDIA + + + + + + +
+ + +
+

New search

+

Select the data, choose where results go, and start.

+ + + +
+
Spectra
+
+
No file selected
+ +
+
+ +
+
Search space
+
+ + +
+ +
+
+
No file selected
+ +
+

+ The engine digests the FASTA and predicts the library itself. Simplest to + start from, and considerably less sensitive than an imported library. +

+
+ + +
+ +
+
Results folder
+
+
No folder selected
+ +
+

+ Use an empty folder. A search overwrites its own outputs and cannot resume, so + starting a second search here replaces the first. +

+
+ +
+
Settings
+
+ + +
+
+ + + +
+
+ + + +
+ + Runs take from minutes to hours. +
+
+ + +
+
+
+

Searching

+

Starting…

+
+ +
+ + + +
+
+
+ +
+ Command being run +

+      
+    
+ +
+
Log
+

+    
+
+ + +
+
+
+

Results

+

+
+ +
+ + + +
+ +
+
Files
+
+
+ +
+ +
+
+ +
+ + + + From 0c22017daa66e68e77f386b9d61227a398640120 Mon Sep 17 00:00:00 2001 From: MuMDIA Date: Sun, 30 Aug 2026 11:10:28 +0200 Subject: [PATCH 02/14] feat(engine): machine-readable doctor and config schema, for the console Two additions the desktop application needs, and nothing else changes. `mumdia doctor --json` emits the same check as data. The application renders one row per sidecar role and offers to install what is missing, which needs the modules, versions and provenance as fields rather than a paragraph to regex. The function is split into a builder and a renderer, so the prose form is produced from the same report and cannot describe a different outcome. Text output is unchanged and the exit status still carries the verdict. `configs/config-schema.json` is the settings form as data: 150 settings with type, typed default, doc comment, enum choices and the gate markers already used in the reference document. Generated by `ci/gen_config_reference.py`, which already parses `config.rs` for exactly this and already runs in CI, so one parse now produces both artifacts and `--check` covers both. The alternative was for the application to carry its own copy of the field list, which is a second thing to keep in step and the one that drifts is the one a user reads. Defaults are rendered by the same function the Markdown table uses, then given their JSON type: `true` not `"true"`, `0.2` not `"0.2"`, `base_peptide` not `` `base_peptide` ``. Computed defaults resolve to their value, so `features.bound_peak_fraction` is 0.333333 rather than the prose `1.0 / 3.0 (0.333333)`. The schema ships without any packaging change: the release archive already copies `configs/` wholesale. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 5 +- ci/gen_config_reference.py | 157 +- configs/config-schema.json | 2184 +++++++++++++++++++++++++ rust/mumdia/crates/mumdia/src/main.rs | 304 +++- 4 files changed, 2562 insertions(+), 88 deletions(-) create mode 100644 configs/config-schema.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a93434b..c61cf6b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -434,7 +434,10 @@ jobs: # docs/24 is generated from config.rs, so a new field or a changed default # must land with its documentation or CI fails. A reference nobody # regenerates is worse than none, because it reads as current. - - name: Config reference is current + # One parse of config.rs produces two artifacts: the reference document a + # person reads, and configs/config-schema.json, which the desktop settings + # editor renders its form from. They go stale together, so --check covers both. + - name: Config reference and schema are current run: python ci/gen_config_reference.py --check # The release binary is statically linked, so it carries 173 third-party crates diff --git a/ci/gen_config_reference.py b/ci/gen_config_reference.py index cda053eb..9d002acd 100644 --- a/ci/gen_config_reference.py +++ b/ci/gen_config_reference.py @@ -38,6 +38,7 @@ import argparse import ast import difflib +import json import re import sys from pathlib import Path @@ -47,6 +48,7 @@ CRATES_DIR = REPO_ROOT / "rust" / "mumdia" / "crates" SCRIPTS_DIR = REPO_ROOT / "scripts" DEFAULT_OUT = REPO_ROOT / "docs" / "24_config_reference.md" +DEFAULT_SCHEMA_OUT = REPO_ROOT / "configs" / "config-schema.json" GENERATOR = "ci/gen_config_reference.py" CONFIG_RS_REL = "rust/mumdia/crates/mumdia-core/src/config.rs" @@ -1533,9 +1535,137 @@ def check(out_path: Path, generated: str) -> int: return 1 + +# --------------------------------------------------------------------------- +# Machine-readable schema, for the desktop application's settings editor +# --------------------------------------------------------------------------- + + +def build_schema() -> dict: + """The same parse the reference document uses, as data instead of prose. + + The desktop application renders one form control per field, and it must not + carry its own copy of the field list, the types, the defaults or the help text: + a second copy is a second thing to keep in step, and the one that drifts is the + one a user reads. So the form is generated from this, and this is generated from + `config.rs`, checked for staleness in CI exactly as the Markdown is. + + Every field carries the doc comment verbatim as `help`, and `gates` carries the + markers already used in the reference ("benchmark-gated", "experimental", ...), + so the interface can mark a parameter that must not be changed casually. + """ + text = CONFIG_RS.read_text(encoding="utf-8") + structs, enums, _warnings = parse_config_rs(text) + sections, items = walk_sections(structs) + + def field_entry(path: str, field: Field) -> dict: + base = base_type(field.rtype) + # `default_rendered` is populated by the Markdown row builder, which the + # schema does not run. Render it here with the same function, so the value + # the form shows and the value the table shows are produced by one code + # path and cannot disagree. + rendered = ( + render_default(field.default_expr, field.rtype, enums, structs) + if field.default_expr is not None + else None + ) + # `render_default` produces a Markdown cell, so an enum arrives as + # `` `base_peptide` `` and a number as text. The schema is data: strip the + # formatting and give the value its JSON type, so a form control can be + # populated without the interface having to unpick Markdown. + def typed_default(text_value: str | None, kind: str): + if text_value is None: + return None + v = text_value.strip().strip("`").strip() + # A computed default is rendered for prose as `1.0 / 3.0 (0.333333)`. + # The parenthesised value is the number a form needs. + m = re.fullmatch(r".*\(([-+0-9.eE]+)\)", v) + if m: + v = m.group(1) + if kind == "bool": + return {"true": True, "false": False}.get(v, v) + if kind in ("integer", "float"): + try: + return int(v) if kind == "integer" else float(v) + except ValueError: + return v + return v + + kind = ( + "enum" + if base in enums + else "bool" + if base == "bool" + else "float" + if base in ("f32", "f64") + else "integer" + if base in ("u8", "u16", "u32", "u64", "usize", "i8", "i16", "i32", "i64") + else "string" + if base in ("String", "str") + else "other" + ) + entry: dict = { + "path": f"{path}.{field.name}" if path else field.name, + "name": field.name, + "section": path, + "rust_type": field.rtype, + "kind": kind, + "optional": field.rtype.startswith("Option<"), + "default": typed_default(rendered, kind), + "default_text": rendered, + "help": field.doc.strip(), + "gates": gate_markers(field.doc), + "source_line": field.line, + } + if base in enums: + entry["choices"] = [snake_case(v.name) for v in enums[base].variants] + return entry + + fields: list[dict] = [] + for path, _kind, struct in sections: + for field in struct.fields: + if base_type(field.rtype) in structs: + continue # a nested section, not a leaf setting + fields.append(field_entry(path, field)) + + # Vec item structs are reachable settings too, but they are edited as lists + # rather than as single controls; name them so the interface can say so instead + # of silently omitting them. + list_sections = [] + for paths, struct in items: + list_sections.append( + { + "paths": paths, + "item": struct.name, + "fields": [field_entry("", f) for f in struct.fields], + } + ) + + return { + "generated_by": GENERATOR, + "source": CONFIG_RS_REL, + "sections": [p for p, _k, _s in sections], + "fields": fields, + "list_sections": list_sections, + "profiles": { + name: [{"path": p, "value": v} for p, v in changes] + for name, changes in parse_profiles(text).items() + }, + } + + +def schema_text() -> str: + return json.dumps(build_schema(), indent=2, ensure_ascii=False) + "\n" + + def main(argv: list[str] | None = None) -> int: ap = argparse.ArgumentParser(description=__doc__.split("\n")[0]) ap.add_argument("--out", default=str(DEFAULT_OUT), help="output Markdown path") + ap.add_argument( + "--schema-out", + default=str(DEFAULT_SCHEMA_OUT), + help="output JSON schema path, read by the desktop settings editor", + ) ap.add_argument( "--check", action="store_true", @@ -1548,13 +1678,38 @@ def main(argv: list[str] | None = None) -> int: generated, stats = build_document() out_path = Path(args.out) + schema = schema_text() + schema_path = Path(args.schema_out) if args.check: - return check(out_path, generated) + rc = check(out_path, generated) + # Both artifacts come from one parse of one file, so they go stale together + # and must be checked together. + current = ( + schema_path.read_text(encoding="utf-8") if schema_path.is_file() else "" + ) + if current.replace("\r\n", "\n") != schema: + print( + f"{schema_path.as_posix()} is stale. Regenerate with:\n" + f" python {GENERATOR}", + file=sys.stderr, + ) + rc = 1 + else: + n = len(json.loads(schema)["fields"]) + print(f"{schema_path.as_posix()} is up to date ({n} settings).") + return rc out_path.parent.mkdir(parents=True, exist_ok=True) with open(out_path, "w", encoding="utf-8", newline="\n") as handle: handle.write(generated) + schema_path.parent.mkdir(parents=True, exist_ok=True) + with open(schema_path, "w", encoding="utf-8", newline="\n") as handle: + handle.write(schema) + print( + f"wrote {schema_path.as_posix()}: " + f"{len(json.loads(schema)['fields'])} settings." + ) print( f"wrote {out_path.as_posix()}: {len(generated.splitlines())} lines, " f"{generated.count(chr(10) + '## ')} sections, " diff --git a/configs/config-schema.json b/configs/config-schema.json new file mode 100644 index 00000000..23f5d60b --- /dev/null +++ b/configs/config-schema.json @@ -0,0 +1,2184 @@ +{ + "generated_by": "ci/gen_config_reference.py", + "source": "rust/mumdia/crates/mumdia-core/src/config.rs", + "sections": [ + "", + "prescan", + "digest", + "digest.decoy", + "peptidoforms", + "predict_frag", + "search_seed", + "rt_im_train", + "extract", + "extract.claim_cues", + "features", + "compete", + "rescore", + "quant", + "mbr", + "experiment" + ], + "fields": [ + { + "path": "rng_seed", + "name": "rng_seed", + "section": "", + "rust_type": "u64", + "kind": "integer", + "optional": false, + "default": 0, + "default_text": "0", + "help": "", + "gates": [], + "source_line": 1459 + }, + { + "path": "prescan.tol_da", + "name": "tol_da", + "section": "prescan", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.005, + "default_text": "0.005", + "help": "Peak-delta match tolerance in Da. Permissive on purpose: a false tag only fails to prune, while a missed tag discards a real candidate with no way to recover it downstream.", + "gates": [], + "source_line": 284 + }, + { + "path": "prescan.rt_slack_s", + "name": "rt_slack_s", + "section": "prescan", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 150.0, + "default_text": "150.0", + "help": "Widen each candidate's RT window by this many seconds before binning. The window comes from a calibration fitted on a different run, and `cal.json` residuals are in-sample and roughly 3x optimistic, so size this from out-of-sample RT error, not from the reported fit.", + "gates": [], + "source_line": 288 + }, + { + "path": "prescan.rt_bin_s", + "name": "rt_bin_s", + "section": "prescan", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 25.0, + "default_text": "25.0", + "help": "RT bin width for the observed-tag index.", + "gates": [], + "source_line": 290 + }, + { + "path": "prescan.top_peaks", + "name": "top_peaks", + "section": "prescan", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 150, + "default_text": "150", + "help": "Most intense peaks per MS2 used to build tags (0 = all). This bounds the O(peaks^2) delta search and is NOT destructive: it only affects tag construction, never the spectra artifact that extraction later reads.", + "gates": [], + "source_line": 294 + }, + { + "path": "prescan.mods", + "name": "mods", + "section": "prescan", + "rust_type": "Vec", + "kind": "other", + "optional": false, + "default": "[\"C:Carbamidomethyl\", \"M:Oxidation\"]", + "default_text": "[\"C:Carbamidomethyl\", \"M:Oxidation\"]", + "help": "Residue:UniModName entries that may appear in a screened peptidoform, e.g. `C:Carbamidomethyl`. A peptidoform carrying anything outside this set plus `anchor_mods` is dropped rather than screened on a partially understood sequence.", + "gates": [], + "source_line": 298 + }, + { + "path": "prescan.anchor_mods", + "name": "anchor_mods", + "section": "prescan", + "rust_type": "Vec", + "kind": "other", + "optional": false, + "default": "[]", + "default_text": "[]", + "help": "Residue:UniModName entries the screen anchors ON. Only trimers covering one of these positions count as evidence, so backbone signal cannot keep a modified hypothesis alive.", + "gates": [], + "source_line": 301 + }, + { + "path": "digest.enzyme", + "name": "enzyme", + "section": "digest", + "rust_type": "Enzyme", + "kind": "enum", + "optional": false, + "default": "trypsin_p", + "default_text": "`trypsin_p`", + "help": "", + "gates": [], + "source_line": 319, + "choices": [ + "trypsin_p", + "trypsin" + ] + }, + { + "path": "digest.missed_cleavages", + "name": "missed_cleavages", + "section": "digest", + "rust_type": "u32", + "kind": "integer", + "optional": false, + "default": 2, + "default_text": "2", + "help": "", + "gates": [], + "source_line": 320 + }, + { + "path": "digest.min_len", + "name": "min_len", + "section": "digest", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 5, + "default_text": "5", + "help": "", + "gates": [], + "source_line": 321 + }, + { + "path": "digest.max_len", + "name": "max_len", + "section": "digest", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 50, + "default_text": "50", + "help": "", + "gates": [], + "source_line": 322 + }, + { + "path": "digest.n_term_met_excision", + "name": "n_term_met_excision", + "section": "digest", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": true, + "default_text": "true", + "help": "N-terminal methionine excision: when a protein begins with `M`, also emit the initiator-Met-removed form of its N-terminal peptides. The initiator methionine is cleaved in vivo for most proteins, so search engines (including DIA-NN via `--met-excision`) enumerate both forms. Omitting it makes the search database structurally miss those excised peptides.", + "gates": [], + "source_line": 329 + }, + { + "path": "digest.decoy.strategy", + "name": "strategy", + "section": "digest.decoy", + "rust_type": "DecoyStrategy", + "kind": "enum", + "optional": false, + "default": "reverse", + "default_text": "`reverse`", + "help": "", + "gates": [], + "source_line": 265, + "choices": [ + "reverse", + "scramble", + "diann_shift", + "none" + ] + }, + { + "path": "peptidoforms.fixed_mods", + "name": "fixed_mods", + "section": "peptidoforms", + "rust_type": "Vec", + "kind": "other", + "optional": false, + "default": "[{\"residue\": \"C\", \"name\": \"Carbamidomethyl\"}]", + "default_text": "[{\"residue\": \"C\", \"name\": \"Carbamidomethyl\"}]", + "help": "UniMod names applied to every matching residue (residue -> mod name).", + "gates": [], + "source_line": 348 + }, + { + "path": "peptidoforms.variable_mods", + "name": "variable_mods", + "section": "peptidoforms", + "rust_type": "Vec", + "kind": "other", + "optional": false, + "default": "[{\"residue\": \"M\", \"name\": \"Oxidation\"}]", + "default_text": "[{\"residue\": \"M\", \"name\": \"Oxidation\"}]", + "help": "", + "gates": [], + "source_line": 349 + }, + { + "path": "peptidoforms.max_variable_mods", + "name": "max_variable_mods", + "section": "peptidoforms", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 1, + "default_text": "1", + "help": "", + "gates": [], + "source_line": 350 + }, + { + "path": "peptidoforms.charge_min", + "name": "charge_min", + "section": "peptidoforms", + "rust_type": "i32", + "kind": "integer", + "optional": false, + "default": 2, + "default_text": "2", + "help": "", + "gates": [], + "source_line": 351 + }, + { + "path": "peptidoforms.charge_max", + "name": "charge_max", + "section": "peptidoforms", + "rust_type": "i32", + "kind": "integer", + "optional": false, + "default": 3, + "default_text": "3", + "help": "", + "gates": [], + "source_line": 352 + }, + { + "path": "peptidoforms.charge_by_basic_residues", + "name": "charge_by_basic_residues", + "section": "peptidoforms", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": false, + "default_text": "false", + "help": "Composition-based precursor charge range. When true, ignore `charge_min`/`charge_max` and emit every charge from 1 up to `1 (N-terminus) + (#R + #H + #K)`, the proton-carrying capacity of the peptide. Peptides therefore never receive a charge state they cannot physically hold, and each peptide's range depends on its own basic-residue count. Default false (fixed `charge_min..=charge_max` for every peptide). Pairs with `predict_frag.charge_by_basic_residues` for fragments. Changing the enumerated charge states changes the search/training/FDR population, so this remains benchmark-gated.", + "gates": [ + "benchmark-gated" + ], + "source_line": 362 + }, + { + "path": "peptidoforms.unknown_modification", + "name": "unknown_modification", + "section": "peptidoforms", + "rust_type": "UnknownModPolicy", + "kind": "enum", + "optional": false, + "default": "error", + "default_text": "`error`", + "help": "`error` (default) or `skip` for unknown modifications.", + "gates": [], + "source_line": 364, + "choices": [ + "error", + "skip" + ] + }, + { + "path": "predict_frag.predictor", + "name": "predictor", + "section": "predict_frag", + "rust_type": "FragPredictorKind", + "kind": "enum", + "optional": false, + "default": "native", + "default_text": "`native`", + "help": "", + "gates": [], + "source_line": 406, + "choices": [ + "native", + "ms2pip" + ] + }, + { + "path": "predict_frag.rt_predictor", + "name": "rt_predictor", + "section": "predict_frag", + "rust_type": "RtPredictorKind", + "kind": "enum", + "optional": false, + "default": "native", + "default_text": "`native`", + "help": "", + "gates": [], + "source_line": 407, + "choices": [ + "native", + "deeplc" + ] + }, + { + "path": "predict_frag.charge2_from_precursor_charge", + "name": "charge2_from_precursor_charge", + "section": "predict_frag", + "rust_type": "i32", + "kind": "integer", + "optional": false, + "default": 2, + "default_text": "2", + "help": "Fragment charges rule: charge 1 always; charge 2 added for precursor charge >= this threshold (docs/18_findings_and_decisions.md). Default 2: DIA-NN uses doubly-charged fragments for ~16% of charge-2 precursors' transitions, so blocking them (the old default of 3) discarded real signal.", + "gates": [], + "source_line": 413 + }, + { + "path": "predict_frag.charge_by_basic_residues", + "name": "charge_by_basic_residues", + "section": "predict_frag", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": false, + "default_text": "false", + "help": "Composition-based fragment charge cap. When true, a b/y fragment is kept at charge z only if `z <= 1 (its N-terminal amine) + (#R + #H + #K within that fragment)`, and never above the precursor charge. This supersedes the `charge2_from_precursor_charge` rule when set. Default false. Pairs with `peptidoforms.charge_by_basic_residues` for precursors; benchmark-gated because it changes the scored transition set.", + "gates": [ + "benchmark-gated" + ], + "source_line": 420 + }, + { + "path": "predict_frag.top_n_fragments", + "name": "top_n_fragments", + "section": "predict_frag", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 6, + "default_text": "6", + "help": "", + "gates": [], + "source_line": 421 + }, + { + "path": "predict_frag.ms2pip_model", + "name": "ms2pip_model", + "section": "predict_frag", + "rust_type": "String", + "kind": "string", + "optional": false, + "default": "\"HCD\"", + "default_text": "\"HCD\"", + "help": "", + "gates": [], + "source_line": 422 + }, + { + "path": "predict_frag.ms2pip_python", + "name": "ms2pip_python", + "section": "predict_frag", + "rust_type": "Option", + "kind": "other", + "optional": true, + "default": "null", + "default_text": "null", + "help": "Python executable for the MS2PIP sidecar (env with ms2pip + pyarrow).", + "gates": [], + "source_line": 424 + }, + { + "path": "predict_frag.deeplc_python", + "name": "deeplc_python", + "section": "predict_frag", + "rust_type": "Option", + "kind": "other", + "optional": true, + "default": "null", + "default_text": "null", + "help": "Python executable for the DeepLC sidecar (env with deeplc + pyarrow).", + "gates": [], + "source_line": 426 + }, + { + "path": "predict_frag.sidecar_script_dir", + "name": "sidecar_script_dir", + "section": "predict_frag", + "rust_type": "String", + "kind": "string", + "optional": false, + "default": "\"scripts\"", + "default_text": "\"scripts\"", + "help": "Directory holding the sidecar worker scripts.", + "gates": [], + "source_line": 428 + }, + { + "path": "search_seed.fdr_seed", + "name": "fdr_seed", + "section": "search_seed", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.01, + "default_text": "0.01", + "help": "", + "gates": [], + "source_line": 449 + }, + { + "path": "search_seed.fragment_tol_ppm", + "name": "fragment_tol_ppm", + "section": "search_seed", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 20.0, + "default_text": "20.0", + "help": "", + "gates": [], + "source_line": 450 + }, + { + "path": "search_seed.report_psms", + "name": "report_psms", + "section": "search_seed", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 5, + "default_text": "5", + "help": "Max reported PSMs per spectrum (wide-window DIA, docs/07_search_seed.md).", + "gates": [], + "source_line": 452 + }, + { + "path": "search_seed.min_matched_peaks", + "name": "min_matched_peaks", + "section": "search_seed", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 4, + "default_text": "4", + "help": "Minimum matched fragments for a seed PSM.", + "gates": [], + "source_line": 454 + }, + { + "path": "search_seed.top_n_peaks", + "name": "top_n_peaks", + "section": "search_seed", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 300, + "default_text": "300", + "help": "If > 0, probe only the `top_n_peaks` most intense peaks per MS2 scan (0 = all peaks). The seed only produces calibration anchors (RT/mass/IM), which come from abundant peptides, so this cuts the dominant per-peak index probing cost without discarding peaks from the downstream extraction artifact. Default 300; set to 0 to probe every converted peak.", + "gates": [], + "source_line": 460 + }, + { + "path": "search_seed.matcher", + "name": "matcher", + "section": "search_seed", + "rust_type": "MatcherKind", + "kind": "enum", + "optional": false, + "default": "fragindex", + "default_text": "`fragindex`", + "help": "Fragment-matcher backend (docs/06_predict_frag_index_matchers.md). Default `Fragindex`.", + "gates": [], + "source_line": 463, + "choices": [ + "bucketed", + "fragindex" + ] + }, + { + "path": "search_seed.two_pass_mass_cal", + "name": "two_pass_mass_cal", + "section": "search_seed", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": false, + "default_text": "false", + "help": "Robust two-pass fragment mass calibration (sensitivity_plan P3.1). After the first median-offset + tolerance fit, re-fit on only the deviations inside the first-pass tolerance window (rejecting outliers), giving a tighter, more robust offset + local uncertainty. Falls back to the single-pass result when too few in-window calibrants remain. Default false (single pass unchanged).", + "gates": [], + "source_line": 469 + }, + { + "path": "search_seed.mass_cal_loess", + "name": "mass_cal_loess", + "section": "search_seed", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": false, + "default_text": "false", + "help": "m/z-dependent fragment mass calibration. When true, fit a LOESS of the calibrant ppm deviation versus fragment m/z and emit a sampled correction grid to `.masscal.json`; extract then applies an m/z-interpolated offset per peak instead of the single scalar `frag_ppm_offset`. This removes any m/z-correlated curvature the flat offset leaves. Default false (scalar offset unchanged), opt-in and benchmark-gated.", + "gates": [ + "benchmark-gated" + ], + "source_line": 476 + }, + { + "path": "rt_im_train.calibration_method", + "name": "calibration_method", + "section": "rt_im_train", + "rust_type": "CalibrationMethod", + "kind": "enum", + "optional": false, + "default": "loess", + "default_text": "`loess`", + "help": "", + "gates": [], + "source_line": 496, + "choices": [ + "loess", + "linear", + "none" + ] + }, + { + "path": "rt_im_train.q_train", + "name": "q_train", + "section": "rt_im_train", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.01, + "default_text": "0.01", + "help": "", + "gates": [], + "source_line": 497 + }, + { + "path": "rt_im_train.p_rt", + "name": "p_rt", + "section": "rt_im_train", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.95, + "default_text": "0.95", + "help": "Percentile of |obs - calibrated_pred| residuals for the RT window.", + "gates": [], + "source_line": 499 + }, + { + "path": "rt_im_train.rt_window_multiplier", + "name": "rt_window_multiplier", + "section": "rt_im_train", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 1.0, + "default_text": "1.0", + "help": "", + "gates": [], + "source_line": 500 + }, + { + "path": "rt_im_train.min_seed_for_calibration", + "name": "min_seed_for_calibration", + "section": "rt_im_train", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 50, + "default_text": "50", + "help": "", + "gates": [], + "source_line": 501 + }, + { + "path": "rt_im_train.loess_span", + "name": "loess_span", + "section": "rt_im_train", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.3, + "default_text": "0.3", + "help": "LOESS span (fraction of points in each local fit).", + "gates": [], + "source_line": 503 + }, + { + "path": "rt_im_train.fallback_rt_window_s", + "name": "fallback_rt_window_s", + "section": "rt_im_train", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 120.0, + "default_text": "120.0", + "help": "Fallback fixed RT window in seconds when calibration cannot be fit.", + "gates": [], + "source_line": 505 + }, + { + "path": "rt_im_train.finetune_deeplc", + "name": "finetune_deeplc", + "section": "rt_im_train", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": false, + "default_text": "false", + "help": "Fine-tune the DeepLC multitask model on this run's confident seed PSMs and rewrite the library's `predicted_irt` before RT calibration. Requires `predict_frag.deeplc_python` (the DeepLC interpreter). Off by default; the main use is library-input mode, where the base iRT comes from the imported library rather than a DeepLC prediction.", + "gates": [], + "source_line": 511 + }, + { + "path": "rt_im_train.finetune_epochs", + "name": "finetune_epochs", + "section": "rt_im_train", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 25, + "default_text": "25", + "help": "DeepLC fine-tune training epochs (passed to `deeplc_finetune.py --epochs`). Early stopping with `finetune_patience` usually halts before this cap, so it is an upper bound rather than a fixed count. Only used when `finetune_deeplc`.", + "gates": [], + "source_line": 515 + }, + { + "path": "rt_im_train.finetune_patience", + "name": "finetune_patience", + "section": "rt_im_train", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 10, + "default_text": "10", + "help": "DeepLC fine-tune early-stopping patience (`--patience`): epochs without validation-loss improvement before stopping. Only used when `finetune_deeplc`.", + "gates": [], + "source_line": 518 + }, + { + "path": "rt_im_train.finetune_batch", + "name": "finetune_batch", + "section": "rt_im_train", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 0, + "default_text": "0", + "help": "DeepLC fine-tune batch size (`--batch`). 0 (default) auto-scales to the confident seed size so each epoch has >= ~30 gradient steps; a fixed large batch underfits small seeds (a ~4k-peptide reference at batch 512 is ~8 steps/epoch and never converges). Only used when `finetune_deeplc`.", + "gates": [], + "source_line": 523 + }, + { + "path": "rt_im_train.adaptive_rt_window", + "name": "adaptive_rt_window", + "section": "rt_im_train", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": false, + "default_text": "false", + "help": "Adaptive RT window (sensitivity_plan spec 03 §3.5, backlog P3.2/P3.3): instead of one global residual-percentile half-width for every candidate, bin the calibration anchors by calibrated RT and give each candidate the LOCAL residual percentile of its RT region, clamped to `[rt_window_min_s, fallback_rt_window_s]` and scaled by `rt_window_multiplier`. A fixed window is simultaneously too wide for well-calibrated regions and too narrow for poorly-calibrated ones; this tightens clean regions (less interference) and widens noisy ones (more recall). Empty/sparse bins fall back to the global width. Default false.", + "gates": [], + "source_line": 533 + }, + { + "path": "rt_im_train.adaptive_rt_bins", + "name": "adaptive_rt_bins", + "section": "rt_im_train", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 12, + "default_text": "12", + "help": "Number of equal-width calibrated-RT bins for the adaptive window.", + "gates": [], + "source_line": 535 + }, + { + "path": "rt_im_train.rt_window_min_s", + "name": "rt_window_min_s", + "section": "rt_im_train", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 1.0, + "default_text": "1.0", + "help": "Lower clamp (seconds) for any RT half-window (the existing 1 s floor).", + "gates": [], + "source_line": 537 + }, + { + "path": "rt_im_train.window_holdout_frac", + "name": "window_holdout_frac", + "section": "rt_im_train", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.0, + "default_text": "0.0", + "help": "Size `w_rt` from HELD-OUT residuals instead of in-sample ones. A fraction of anchor peptides (`base_peptide_id % 1000 < round(frac*1000)`, so the split is deterministic and shared with `deeplc_finetune.py`) is excluded from the sizing fit and, when `finetune_deeplc` runs, from the fine-tune reference; `w_rt` is then the residual percentile of those held-out anchors against the fit they never entered. The final calibration curve still uses every anchor. In-sample sizing underestimates the tail and rewards a memorizing RT model with a window it does not deserve (measured: it inverted the 4.0.0a2/4.1.0 ranking); held-out sizing measured +0.9% peptides with DeepLC 4.1.0 and -1.5% with 4.0.0a2 on the AIF benchmark, both at 0.98% decoy, so enable it only with a generalizing RT model. 0.0 (default) keeps in-sample sizing. Mutually exclusive with `adaptive_rt_window`. Benchmark-gated; do not default on.", + "gates": [ + "benchmark-gated", + "do not default" + ], + "source_line": 550 + }, + { + "path": "extract.fixed_scan_window", + "name": "fixed_scan_window", + "section": "extract", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 3, + "default_text": "3", + "help": "", + "gates": [], + "source_line": 577 + }, + { + "path": "extract.frag_tol_ppm", + "name": "frag_tol_ppm", + "section": "extract", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 20.0, + "default_text": "20.0", + "help": "", + "gates": [], + "source_line": 578 + }, + { + "path": "extract.prec_tol_ppm", + "name": "prec_tol_ppm", + "section": "extract", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 20.0, + "default_text": "20.0", + "help": "", + "gates": [], + "source_line": 579 + }, + { + "path": "extract.presence_min_matched", + "name": "presence_min_matched", + "section": "extract", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 3, + "default_text": "3", + "help": "tier-(b) minimum matched fragment count.", + "gates": [], + "source_line": 581 + }, + { + "path": "extract.presence_min_fragments", + "name": "presence_min_fragments", + "section": "extract", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 3, + "default_text": "3", + "help": "minimum distinct fragments for acceptance.", + "gates": [], + "source_line": 583 + }, + { + "path": "extract.presence_min_coelution", + "name": "presence_min_coelution", + "section": "extract", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 2, + "default_text": "2", + "help": "minimum simultaneously-present fragments over the consecutive-scan run.", + "gates": [], + "source_line": 585 + }, + { + "path": "extract.gate_min_score", + "name": "gate_min_score", + "section": "extract", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.2, + "default_text": "0.2", + "help": "tier-(d) spectral-agreement gate: reject a candidate whose observed fragment intensities agree with the predicted pattern below this score. Renamed from `min_frag_corr`, which was accurate for none of the four `gate_mode` values: under the default `apex_pearson` it is an intensity correlation at ONE apex scan rather than a chromatographic co-elution correlation, and under `spectral_entropy` it is not a correlation at all. The old name is not accepted (`deny_unknown_fields`), so an old config fails loudly with the offending key named rather than silently reverting to a default. Applied symmetrically to targets and decoys, but that alone does not prove null exchangeability in chimeric DIA; validate every threshold with an independent entrapment. 0 disables.", + "gates": [], + "source_line": 598 + }, + { + "path": "extract.min_matched_fraction", + "name": "min_matched_fraction", + "section": "extract", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.0, + "default_text": "0.0", + "help": "tier-(c) minimum fraction of the candidate's predicted fragments that must be observed. With enough predicted fragments (top_n>=~10) this is a strong, symmetric discriminator: real peptides match a large fraction, chimeric false matches and decoys match a small fraction alike, so the target-decoy null stays valid.", + "gates": [], + "source_line": 604 + }, + { + "path": "extract.apex_top_fragments", + "name": "apex_top_fragments", + "section": "extract", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 0, + "default_text": "0", + "help": "Shape-aware apex selection: choose the apex scan group by the summed observed intensity of only the top-K predicted (signature) fragments, rather than all matched fragments. In chimeric DIA a bright co-eluting interferent contributing to arbitrary channels wins a max-over-all-fragments apex; restricting to the peptide's strongest predicted ions locks onto its true elution instead. 0 selects the implementation default of the top 3 predicted fragments.", + "gates": [], + "source_line": 612 + }, + { + "path": "extract.apex_rt_prior_s", + "name": "apex_rt_prior_s", + "section": "extract", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.0, + "default_text": "0.0", + "help": "Optional Gaussian RT prior on apex selection: weight each scan group by exp(-0.5*((rt - rt_cal)/sigma)^2) with sigma = this value in seconds, so a distant interferent inside a wide RT window cannot define the apex. 0 = off.", + "gates": [], + "source_line": 616 + }, + { + "path": "extract.apex_count_tol", + "name": "apex_count_tol", + "section": "extract", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 1, + "default_text": "1", + "help": "Fragment-count apex: pick the scan with the most distinct matched fragments, allowing scans within `apex_count_tol` of that maximum (so a slightly-lower- count but much more intense scan can still win), then the max summed-top-3 intensity among them. Supersedes the summed-intensity apex when set.", + "gates": [], + "source_line": 621 + }, + { + "path": "extract.apex_count_window", + "name": "apex_count_window", + "section": "extract", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 1, + "default_text": "1", + "help": "Rolling-window width (in scan groups, centered, odd) for the distinct- fragment count that drives apex selection. Low-intensity fragments flicker in and out scan-to-scan; a single-scan count then spikes at noise scans and misplaces the apex. This sums the per-scan distinct-fragment count over a centered window so the apex lands in the region of *sustained* fragment presence, not an isolated flicker. A sum (not a mean) is used deliberately: edge truncation makes interior positions accumulate more, center-weighting the apex toward the RT-window centre (~= predicted RT) as a mild RT-prior; measured to beat a mean by ~+300 IDs on AIF. 1 = no smoothing (per-scan).", + "gates": [], + "source_line": 631 + }, + { + "path": "extract.apex_gaussian_sigma_scans", + "name": "apex_gaussian_sigma_scans", + "section": "extract", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.0, + "default_text": "0.0", + "help": "Gaussian matched-filter smoothing of the per-scan fragment-count series before apex selection, as a sigma in scan units. 0.0 (default) keeps the `apex_count_window` rolling-sum smoother unchanged. When > 0, the count series is convolved with a Gaussian kernel (radius = 3*sigma) instead, which localizes the apex more robustly than a uniform window against scan-to-scan flicker. Opt-in and benchmark-gated: it changes apex selection and therefore identifications.", + "gates": [ + "benchmark-gated" + ], + "source_line": 639 + }, + { + "path": "extract.emit_window_grid", + "name": "emit_window_grid", + "section": "extract", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": true, + "default_text": "true", + "help": "Emit per-fragment chromatograms on the FULL isolation-window scan grid with 0.0 where a fragment is absent (aggregating scans of the same isolation window), so the elution profile drops to zero between peaks and the features-stage boundary calling is not misled by interpolated gaps.", + "gates": [], + "source_line": 644 + }, + { + "path": "extract.bucket_size", + "name": "bucket_size", + "section": "extract", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 8192, + "default_text": "8192", + "help": "m/z bucket size (power of two).", + "gates": [], + "source_line": 646 + }, + { + "path": "extract.peak_claim", + "name": "peak_claim", + "section": "extract", + "rust_type": "PeakClaim", + "kind": "enum", + "optional": false, + "default": "none", + "default_text": "`none`", + "help": "How a shared observed peak's intensity is apportioned among co-isolated, co-eluting candidates that all match it (see `PeakClaim`).", + "gates": [], + "source_line": 649, + "choices": [ + "none", + "winner_predicted_intensity", + "proportional", + "coelution_winner", + "coelution_proportional", + "coelution_winner_margin", + "coelution_multi_cue", + "coelution_demix", + "coelution_shadow" + ] + }, + { + "path": "extract.emit_demix_features", + "name": "emit_demix_features", + "section": "extract", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": false, + "default_text": "false", + "help": "Spectrum-centric NNLS demixing (D2, fragment-competition report). When true, at each accepted candidate's apex scan, assemble the co-isolated candidate x fragment design matrix, solve non-negative least squares (deterministic ridge-regularized), and emit non-destructive demix features (deconv_explained_frac, deconv_active, deconv_share) so the rescorer sees each candidate's interference-corrected abundance. Default false; changes no extracted intensity.", + "gates": [], + "source_line": 659 + }, + { + "path": "extract.demix_lambda", + "name": "demix_lambda", + "section": "extract", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 1.0, + "default_text": "1.0", + "help": "Ridge for the demix NNLS passive solve (keeps it PD/deterministic under the ~98% wide-window column collinearity). Default 1.0.", + "gates": [], + "source_line": 662 + }, + { + "path": "extract.demix_max_candidates", + "name": "demix_max_candidates", + "section": "extract", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 64, + "default_text": "64", + "help": "Cap on the number of co-isolated candidates (design-matrix columns) in a single demix solve, to bound compute on crowded windows. Default 64.", + "gates": [], + "source_line": 665 + }, + { + "path": "extract.demix_scan_stride", + "name": "demix_scan_stride", + "section": "extract", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 1, + "default_text": "1", + "help": "Scan stride for the DESTRUCTIVE `CoelutionDemix` redistribution: solve the per-scan NNLS every Nth scan and reuse the resulting candidate abundances to apportion the intervening scans (a re-solve is forced whenever a new candidate enters the co-isolated set, so accuracy is preserved where the population changes). This is the practicality lever - a full per-scan solve over the ~465k scans of a wide-window run is impractical. 1 (default) solves at every scan. Only affects `CoelutionDemix`; the non-destructive demix FEATURES are unaffected.", + "gates": [], + "source_line": 673 + }, + { + "path": "extract.emit_contested_features", + "name": "emit_contested_features", + "section": "extract", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": false, + "default_text": "false", + "help": "Emit a non-destructive `contested_frac` per PSM: the fraction of a candidate's matched intensity that a co-eluting competitor claims more strongly (by the two-pass elution-profile arbitration). Does not alter the extracted intensities; feeds a rescorer feature. Forces the two-pass path.", + "gates": [], + "source_line": 678 + }, + { + "path": "extract.peak_claim_margin", + "name": "peak_claim_margin", + "section": "extract", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 2.0, + "default_text": "2.0", + "help": "Dominance factor for `CoelutionWinnerMargin`: a shared peak is claimed winner-take-all only if the top eluter's profile height is at least this multiple of the runner-up's; otherwise the peak stays shared.", + "gates": [], + "source_line": 682 + }, + { + "path": "extract.matcher", + "name": "matcher", + "section": "extract", + "rust_type": "MatcherKind", + "kind": "enum", + "optional": false, + "default": "fragindex", + "default_text": "`fragindex`", + "help": "Fragment-matcher backend (docs/06_predict_frag_index_matchers.md). Default `Fragindex`.", + "gates": [], + "source_line": 685, + "choices": [ + "bucketed", + "fragindex" + ] + }, + { + "path": "extract.min_coelution_run", + "name": "min_coelution_run", + "section": "extract", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 0, + "default_text": "0", + "help": "Minimum-PSMs-per-peptide evidence filter: reject a candidate whose fragments co-elute over fewer than this many consecutive scan groups (`coelution_run`). A single/double-scan spike is a transient (likely-interferent) match; a real peptide persists across its elution. 0 disables (the `scan_window` floor still applies). This is the DIA analog of a \"seen in >= N PSMs\" requirement.", + "gates": [], + "source_line": 691 + }, + { + "path": "extract.ms1_rescue", + "name": "ms1_rescue", + "section": "extract", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": false, + "default_text": "false", + "help": "Rescue a candidate that fails the single-scan fragment-Pearson gate when it has adequate matched fragments AND MS1 isotope-pattern support (mono + a plausible +1/mono ratio). Off by default: it relaxes acceptance, so enable it only with target-decoy/entrapment FDR validation. MS1 evidence is now computed before the gate so this can take effect.", + "gates": [], + "source_line": 697 + }, + { + "path": "extract.retain_top_peaks", + "name": "retain_top_peaks", + "section": "extract", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 1, + "default_text": "1", + "help": "Number of chromatographic peak hypotheses to enumerate per candidate. `K>1` writes up to K local maxima to the diagnostic `.peaks.parquet` sidecar. The primary PSM still contains only the selected apex, so these extra hypotheses are not currently rescored or used to improve identifications. K=1 preserves the single-apex behaviour.", + "gates": [ + "diagnostic", + "not currently" + ], + "source_line": 703 + }, + { + "path": "extract.promote_top_peaks", + "name": "promote_top_peaks", + "section": "extract", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 1, + "default_text": "1", + "help": "Number of chromatographic peaks PROMOTED to real feature/rescore rows per candidate (AlphaDIA plan #7, top-K). `1` (default) emits only the selected apex, so the pipeline is byte-identical. `>1` additionally emits the next strongest non-overlapping `enumerate_peaks` groups (each a full re-sliced PSM record carrying `peak_rank`), so the rescorer can pick the correct-but-not-apex peak; the selected apex stays `peak_rank = 0`. Must be `<= retain_top_peaks`. Behaviour-changing and benchmark/entrapment-gated: it changes the extracted row population, and compete/rescore must collapse per candidate so the decoy null is not K-inflated.", + "gates": [ + "gated" + ], + "source_line": 713 + }, + { + "path": "extract.alt_peak_min_area_frac", + "name": "alt_peak_min_area_frac", + "section": "extract", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.1, + "default_text": "0.10", + "help": "Minimum integrated area of a promoted alternate peak (rank >= 1) as a fraction of the rank-0 peak's area. Suppresses noise-level alternates. Only used when `promote_top_peaks > 1`.", + "gates": [], + "source_line": 717 + }, + { + "path": "extract.alt_peak_min_separation_s", + "name": "alt_peak_min_separation_s", + "section": "extract", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 5.0, + "default_text": "5.0", + "help": "Minimum apex-RT separation (seconds) between a promoted alternate peak and the rank-0 apex, so a near-duplicate of the selected peak is not re-emitted. Only used when `promote_top_peaks > 1`.", + "gates": [], + "source_line": 721 + }, + { + "path": "extract.emit_candidate_audit", + "name": "emit_candidate_audit", + "section": "extract", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": false, + "default_text": "false", + "help": "Diagnostic candidate-audit: when true, extraction records, for every probed candidate, either the survivor stage-flags or the earliest `RejectionReason`, and writes `.audit.parquet` (spec 01 §4 / P0.3). Near-zero cost when false (no per-candidate audit allocation). Default false (production).", + "gates": [ + "diagnostic" + ], + "source_line": 726 + }, + { + "path": "extract.apex_evidence_rank", + "name": "apex_evidence_rank", + "section": "extract", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": true, + "default_text": "true", + "help": "Evidence-count apex selection: choose the apex scan by the NUMBER of distinct co-eluting predicted fragments present (breadth of evidence), using observed signature-ion intensity only as a sub-integer tiebreak. In wide-window DIA a single fragment m/z channel is chimeric, so the tallest scan is often a co-isolated interferent; the scan where the most of the peptide's own predicted transitions co-elute is a more reliable apex. Default `true`, on correctness grounds rather than a count: `false` keeps the legacy signature-intensity apex, whose score is 0.0 at every qualifying scan when none of the top-K predicted fragments is observed, so the strict `>` never replaces the first candidate and the apex silently becomes the LOWEST-RT qualifying scan. The rolling distinct-fragment count (`apex_count_window`) still gates which scans qualify in both modes.", + "gates": [], + "source_line": 739 + }, + { + "path": "extract.emit_gate_diagnostics", + "name": "emit_gate_diagnostics", + "section": "extract", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": false, + "default_text": "false", + "help": "Emit the four gate-diagnostic scores (`gate_apex`, `gate_peak_spectral`, `gate_coelution`, `gate_spectral_entropy`) as extra `psms.parquet` columns, for the offline gate-metric comparison. Default `false` (diagnostic sidecar, like `emit_candidate_audit`): when off, neither the columns nor the extra per-candidate score computation happen, so the default chain is byte-identical.", + "gates": [ + "diagnostic" + ], + "source_line": 745 + }, + { + "path": "extract.gate_mode", + "name": "gate_mode", + "section": "extract", + "rust_type": "GateMode", + "kind": "enum", + "optional": false, + "default": "apex_pearson", + "default_text": "`apex_pearson`", + "help": "Which spectral-agreement score the `gate_min_score` gate thresholds (sensitivity program). The legacy gate uses a single apex-scan intensity Pearson, which one chimeric scan can dominate. See `GateMode`.", + "gates": [], + "source_line": 749, + "choices": [ + "apex_pearson", + "peak_spectral", + "spectral_entropy", + "coelution", + "combined" + ] + }, + { + "path": "extract.gate_coelution_min", + "name": "gate_coelution_min", + "section": "extract", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.5, + "default_text": "0.5", + "help": "Second threshold for `GateMode::Combined`: the co-elution score must exceed this while the peak-integrated spectral score exceeds `gate_min_score`. Requiring BOTH is more specific (rejects interferents that pass one axis).", + "gates": [], + "source_line": 753 + }, + { + "path": "extract.claim_cues.mz_close", + "name": "mz_close", + "section": "extract.claim_cues", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": false, + "default_text": "false", + "help": "Sub-tolerance m/z proximity (S3): weight a claimant by `exp(-(ppm_err/sigma)^2)`, where `ppm_err` is the signed ppm offset of the observed peak from this claimant's predicted fragment m/z. Two collided fragments share a peak only because both fall within `frag_tol`, but the observed peak sits at the true owner's m/z; the sub-tolerance offset is a novel apportionment weight (engines use ppm only as a binary gate).", + "gates": [], + "source_line": 200 + }, + { + "path": "extract.claim_cues.mz_close_sigma_ppm", + "name": "mz_close_sigma_ppm", + "section": "extract.claim_cues", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 5.0, + "default_text": "5.0", + "help": "Gaussian sigma (ppm) for the `mz_close` cue. Default 5 ppm.", + "gates": [], + "source_line": 202 + }, + { + "path": "extract.claim_cues.rt_prior", + "name": "rt_prior", + "section": "extract.claim_cues", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": false, + "default_text": "false", + "help": "DeepLC retention-time prior (S3): weight a claimant by `exp(-(rt - rt_pred)^2 / 2 tau^2)`, where `rt_pred` is the candidate's calibrated predicted RT. A co-isolated interferent whose predicted RT is far from the current scan gets a low weight even if it briefly co-elutes, so a shared peak is apportioned toward the candidate the RT model actually places there. No-op where the predicted RT is unset (0).", + "gates": [], + "source_line": 209 + }, + { + "path": "extract.claim_cues.rt_prior_tau_s", + "name": "rt_prior_tau_s", + "section": "extract.claim_cues", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 30.0, + "default_text": "30.0", + "help": "Gaussian sigma (seconds) for the `rt_prior` cue. Default 30 s.", + "gates": [], + "source_line": 211 + }, + { + "path": "extract.claim_cues.ms1_support", + "name": "ms1_support", + "section": "extract.claim_cues", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": false, + "default_text": "false", + "help": "MS1 precursor-envelope support (S4, cross-dimension): weight a claimant by whether its own precursor isotope envelope (mono + a plausible +1/mono ratio) is actually present in the nearest MS1 scan. A shift/reverse decoy has a well-defined precursor m/z but no real co-eluting MS1 precursor, so its support is noise, starving its MS2 claim via an orthogonal dimension that is nearly impossible to fake. No-op when no MS1 is provided. Down-weights (never zeroes) so a genuinely MS1-poor real peptide is not eliminated.", + "gates": [], + "source_line": 219 + }, + { + "path": "extract.claim_cues.reassign", + "name": "reassign", + "section": "extract.claim_cues", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": false, + "default_text": "false", + "help": "DESTRUCTIVE redistribution for `CoelutionMultiCue`. When true, the cue-weighted arbitration rewrites the extracted peak intensities (winner-take-all on the composite weight), instead of only emitting the apportioned/contested features. The competed evidence then feeds EVERY downstream feature (co-elution, spectral, mass-accuracy, ...), so this is the impactful form. Off by default; changes the search/FDR evidence, so it is entrapment-gated per CLAUDE.md.", + "gates": [ + "gated" + ], + "source_line": 226 + }, + { + "path": "extract.claim_cues.apportion_em_iters", + "name": "apportion_em_iters", + "section": "extract.claim_cues", + "rust_type": "u32", + "kind": "integer", + "optional": false, + "default": 0, + "default_text": "0", + "help": "Uniqueness-seeded EM apportionment (S2): number of fixed-point iterations that re-seed each candidate's per-scan elution profile from its APPORTIONED (not full) intensity before the final arbitration. The plain profile is built from full intensities, so a borrowing candidate's profile is inflated by the very peaks it borrows; re-seeding from the cue-weighted share removes that feedback, while uncontested (single-claimant) peaks contribute full intensity every iteration as an immovable anchor. 0 (default) disables EM (single-pass profile). Deterministic (fixed N); applies under `CoelutionMultiCue`.", + "gates": [], + "source_line": 235 + }, + { + "path": "features.set", + "name": "set", + "section": "features", + "rust_type": "FeatureSet", + "kind": "enum", + "optional": false, + "default": "minimal", + "default_text": "`minimal`", + "help": "", + "gates": [], + "source_line": 865, + "choices": [ + "minimal", + "rich", + "extended" + ] + }, + { + "path": "features.emit_pin", + "name": "emit_pin", + "section": "features", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": false, + "default_text": "false", + "help": "Write the Percolator-style `.pin` text file requested by `--out-pin`. No MuMDIA stage consumes it (`rescore` builds its own PIN for the sidecars); it exists for external tooling. At 1.5M rows x 387 features it is a ~5.4 GB text write. Default false: nothing in MuMDIA reads it, which makes the write pure cost unless an external tool wants the file. Set true to get the artifact back.", + "gates": [], + "source_line": 871 + }, + { + "path": "features.coelution_corr_threshold", + "name": "coelution_corr_threshold", + "section": "features", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.9, + "default_text": "0.9", + "help": "", + "gates": [], + "source_line": 872 + }, + { + "path": "features.prec_tol_ppm", + "name": "prec_tol_ppm", + "section": "features", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 20.0, + "default_text": "20.0", + "help": "", + "gates": [], + "source_line": 873 + }, + { + "path": "features.bound_features", + "name": "bound_features", + "section": "features", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": true, + "default_text": "true", + "help": "Restrict trace-based features (co-elution, profile, xcorr, interference, base width) to the elution peak around the apex rather than the whole extracted RT window, so they are not diluted over large RT stretches.", + "gates": [], + "source_line": 877 + }, + { + "path": "features.bound_peak_fraction", + "name": "bound_peak_fraction", + "section": "features", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.333333, + "default_text": "1.0 / 3.0 (0.333333)", + "help": "Peak-boundary threshold as a fraction of apex height (DIA-NN-style: descend to peak*fraction, or stop earlier at a valley below it). 1/3 matched DIA-NN's RT bounds best in the diagnostic-plot benchmark.", + "gates": [ + "diagnostic" + ], + "source_line": 881 + }, + { + "path": "features.bound_peak_grace", + "name": "bound_peak_grace", + "section": "features", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 0, + "default_text": "0", + "help": "Grace when walking the elution-peak boundary: number of consecutive sub-threshold scans to BRIDGE before stopping. 0 (default) stops at the first scan below `bound_peak_fraction` (brittle on jagged/gappy profiles); 1 bridges a single-scan dip (DIA sampling gap / noise), giving steadier boundaries.", + "gates": [], + "source_line": 886 + }, + { + "path": "features.bound_from_confident", + "name": "bound_from_confident", + "section": "features", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": true, + "default_text": "true", + "help": "Elution-peak boundary source. When true (default) a single set of left/right half-widths (seconds) is learned once from the confident seed PSMs (`spectrum_q <= 0.01`, target-only, the same set that anchors RT calibration / DeepLC fine-tune) and applied to EVERY candidate around its own apex. This removes per-candidate boundary manipulation so a decoy is scored over a real- peptide-width window centred on its apex. When false, each candidate detects its own peak boundary from its top-3-predicted-fragment profile (per-candidate, but noisy/manipulable for chimeric decoys; the legacy behaviour). If the seed yields < 20 confident anchors the stage logs a warning and falls back to per-candidate detection for that run.", + "gates": [], + "source_line": 897 + }, + { + "path": "features.bound_confident_pct", + "name": "bound_confident_pct", + "section": "features", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 50.0, + "default_text": "50.0", + "help": "Percentile (0-100) of the confident-set half-widths taken as the global left/ right elution half-width when `bound_from_confident` is true. 50 = median (typical real peak width); higher percentiles widen the shared window.", + "gates": [], + "source_line": 901 + }, + { + "path": "features.ms1_precursor_features", + "name": "ms1_precursor_features", + "section": "features", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": false, + "default_text": "false", + "help": "Emit the MS1 apex-isotope precursor feature `ms1_isotope_height_corr` (Pearson of the observed apex isotope heights `[i0,i1,i2]` against the Poisson-averagine model). Default false (the feature is present in the battery but returns 0.0, so the vector length is unchanged in effect). It overlaps the existing `ms1_isotope_cosine_apex`, so it is opt-in and benchmark-gated rather than default-on (AlphaDIA-plan item 12).", + "gates": [ + "benchmark-gated" + ], + "source_line": 908 + }, + { + "path": "compete.group_by", + "name": "group_by", + "section": "compete", + "rust_type": "CompeteGroupBy", + "kind": "enum", + "optional": false, + "default": "base_peptide", + "default_text": "`base_peptide`", + "help": "Competition grouping: `precursor` collapses charge/modification siblings separately within each target/decoy label; targets and decoys therefore do not compete directly. `apex` also groups by rounded apex RT; `peptidoform_charge` keeps each peptidoform+charge as its own group (precursor-level, as DIA-NN/Spectronaut report), so sibling charges of one peptide are not collapsed.", + "gates": [], + "source_line": 941, + "choices": [ + "base_peptide", + "apex", + "peptidoform_charge" + ] + }, + { + "path": "compete.apex_rt_tolerance_s", + "name": "apex_rt_tolerance_s", + "section": "compete", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 5.0, + "default_text": "5.0", + "help": "", + "gates": [], + "source_line": 942 + }, + { + "path": "compete.mode", + "name": "mode", + "section": "compete", + "rust_type": "CompetitionMode", + "kind": "enum", + "optional": false, + "default": "winner_take_all", + "default_text": "`winner_take_all`", + "help": "How within-group competition resolves (sensitivity program, spec 04 §6 / P2.4). `winner_take_all` = legacy (keep only the top `prelim_score` per group). The other modes preserve more candidate evidence for the rescorer/ FDR to arbitrate. Default `winner_take_all` (unchanged behaviour).", + "gates": [], + "source_line": 947, + "choices": [ + "winner_take_all", + "none", + "features_only", + "unique_evidence", + "margin_gated" + ] + }, + { + "path": "compete.margin", + "name": "margin", + "section": "compete", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.0, + "default_text": "0.0", + "help": "Score margin (in `prelim_score` units) required to remove a loser under `margin_gated`. A loser closer than this to the winner is kept.", + "gates": [ + "gated" + ], + "source_line": 950 + }, + { + "path": "compete.unique_evidence_min_fragments", + "name": "unique_evidence_min_fragments", + "section": "compete", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 2, + "default_text": "2", + "help": "Minimum distinct unique-fragment count a loser must have to survive under `unique_evidence` (needs the `unique_fragment_count` feature; falls back to winner-take-all when the column is absent).", + "gates": [], + "source_line": 954 + }, + { + "path": "compete.emit_competition_audit", + "name": "emit_competition_audit", + "section": "compete", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": false, + "default_text": "false", + "help": "Diagnostic: when true, write `.compete_audit.parquet` recording every removed candidate with its group, winner, scores, and removal reason.", + "gates": [ + "diagnostic" + ], + "source_line": 957 + }, + { + "path": "rescore.classifier", + "name": "classifier", + "section": "rescore", + "rust_type": "RescorerKind", + "kind": "enum", + "optional": false, + "default": "native_tda", + "default_text": "`native_tda`", + "help": "", + "gates": [], + "source_line": 1293, + "choices": [ + "native_tda", + "mokapot", + "nn_torch", + "percolator", + "entrapment" + ] + }, + { + "path": "rescore.folds", + "name": "folds", + "section": "rescore", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 3, + "default_text": "3", + "help": "", + "gates": [], + "source_line": 1294 + }, + { + "path": "rescore.train_fdr", + "name": "train_fdr", + "section": "rescore", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.01, + "default_text": "0.01", + "help": "", + "gates": [], + "source_line": 1295 + }, + { + "path": "rescore.num_iter", + "name": "num_iter", + "section": "rescore", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 10, + "default_text": "10", + "help": "number of semi-supervised iterations for the native rescorer.", + "gates": [], + "source_line": 1297 + }, + { + "path": "rescore.max_feature_matrix_gib", + "name": "max_feature_matrix_gib", + "section": "rescore", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.0, + "default_text": "0.0", + "help": "Refuse a rescore whose in-memory feature matrix would exceed this many GiB. 0 (default) means no ceiling, which is the previous behaviour. The matrix is `Vec>`: eight bytes per value, plus a heap allocation and a 24-byte spine entry per PSM. Nothing in the workspace estimates or checks available memory (there is deliberately no `sysinfo` dependency), so an experiment-wide rescore over enough runs was simply killed by the OS after however long it took to get there. `native_tda` additionally runs all folds in parallel, each holding an owned standardised copy of its training slice, so the true peak is roughly `(1 + folds) x` this figure. Setting a ceiling converts that into an error at startup, naming the estimate and the two ways out. It is not a batching implementation: sub-batching changes which PSMs share a pooled `q_value`, so it is the operator's decision, not a silent one.", + "gates": [], + "source_line": 1312 + }, + { + "path": "rescore.python", + "name": "python", + "section": "rescore", + "rust_type": "Option", + "kind": "other", + "optional": true, + "default": "null", + "default_text": "null", + "help": "", + "gates": [], + "source_line": 1313 + }, + { + "path": "rescore.percolator_bin", + "name": "percolator_bin", + "section": "rescore", + "rust_type": "Option", + "kind": "other", + "optional": true, + "default": "null", + "default_text": "null", + "help": "Path to an external `percolator` executable. Parsed and never read: no stage launches percolator, and `RescorerKind` has no variant that would. It is the only silently inert config field in the tree, since the three MBR ones warn (see `validate`). Kept rather than deleted because the external-percolator path is still intended; `validate` now warns when it is set.", + "gates": [], + "source_line": 1320 + }, + { + "path": "rescore.entrapment_marker", + "name": "entrapment_marker", + "section": "rescore", + "rust_type": "Option", + "kind": "other", + "optional": true, + "default": "null", + "default_text": "null", + "help": "Protein-accession substring marking spike-in (entrapment) negatives, e.g. \"_HUMAN\". Required when `classifier = entrapment`; PSMs whose protein contains it are the empirical false population.", + "gates": [], + "source_line": 1324 + }, + { + "path": "rescore.entrapment_exclude", + "name": "entrapment_exclude", + "section": "rescore", + "rust_type": "Option", + "kind": "other", + "optional": true, + "default": "null", + "default_text": "null", + "help": "If a protein also contains this substring it is NOT counted as entrapment (the sample's own species, e.g. \"_ECOLI\"): shared peptides then count as real targets. `None` = the marker alone decides.", + "gates": [], + "source_line": 1328 + }, + { + "path": "rescore.entrapment_contaminant_markers", + "name": "entrapment_contaminant_markers", + "section": "rescore", + "rust_type": "Vec", + "kind": "other", + "optional": false, + "default": "[]", + "default_text": "[]", + "help": "Protein substrings marking genuine contaminants inside the spike-in proteome (e.g. \"KRT\", \"ALBU\", keratin/albumin entry-name tokens). A PSM matching `entrapment_marker` but also one of these is treated as a REAL target, not an entrapment negative: such peptides are truly present (handling contaminants) so using them as negatives mislabels real signal and inflates the estimated FDR. Empty = every spike-in hit is a negative.", + "gates": [], + "source_line": 1335 + }, + { + "path": "rescore.entrapment_ratio", + "name": "entrapment_ratio", + "section": "rescore", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 1.0, + "default_text": "1.0", + "help": "N_real_lib / N_entrap_lib. Scales the entrapment FDR estimate so it is unbiased when the spike-in library differs in size from the real one.", + "gates": [], + "source_line": 1338 + }, + { + "path": "rescore.strict", + "name": "strict", + "section": "rescore", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": true, + "default_text": "true", + "help": "When true, any sidecar/classifier failure or misconfiguration (Mokapot or entrapment sidecar error, unwired percolator, entrapment mode with no entrapment PSMs) is a hard error instead of a silent fall back to the native rescorer. Default true so a named scientific workflow cannot silently execute a different model; set false only for explicit legacy compatibility.", + "gates": [], + "source_line": 1345 + }, + { + "path": "rescore.handoff", + "name": "handoff", + "section": "rescore", + "rust_type": "Handoff", + "kind": "enum", + "optional": false, + "default": "tsv", + "default_text": "`tsv`", + "help": "How the feature matrix reaches a sidecar rescorer. See `Handoff`. `parquet` is dramatically faster on large pools but applies to nn_torch only.", + "gates": [], + "source_line": 1349, + "choices": [ + "tsv", + "parquet" + ] + }, + { + "path": "quant.q_threshold", + "name": "q_threshold", + "section": "quant", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.01, + "default_text": "0.01", + "help": "Peptide-level q-value cutoff for inclusion.", + "gates": [], + "source_line": 1108 + }, + { + "path": "quant.top_n_fragments", + "name": "top_n_fragments", + "section": "quant", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 3, + "default_text": "3", + "help": "Number of top fragments summed per peptidoform.", + "gates": [], + "source_line": 1110 + }, + { + "path": "quant.top_n_peptides", + "name": "top_n_peptides", + "section": "quant", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 3, + "default_text": "3", + "help": "Number of top peptides summed per protein group (TopNSum).", + "gates": [], + "source_line": 1112 + }, + { + "path": "quant.rollup", + "name": "rollup", + "section": "quant", + "rust_type": "RollupMethod", + "kind": "enum", + "optional": false, + "default": "top_n_sum", + "default_text": "`top_n_sum`", + "help": "", + "gates": [], + "source_line": 1113, + "choices": [ + "top_n_sum", + "sum" + ] + }, + { + "path": "quant.bound_peak", + "name": "bound_peak", + "section": "quant", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": true, + "default_text": "true", + "help": "Integrate each fragment only over the detected elution-peak window rather than the whole chromatogram. The window is found from the summed XIC apex.", + "gates": [], + "source_line": 1116 + }, + { + "path": "quant.peak_fraction", + "name": "peak_fraction", + "section": "quant", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.166667, + "default_text": "1.0 / 6.0 (0.166667)", + "help": "Descent threshold for the peak-window walk: stop where the summed XIC drops below `peak_fraction` * apex height (1/6 expanded from the 1/3 feature bound).", + "gates": [], + "source_line": 1119 + }, + { + "path": "quant.peak_grace", + "name": "peak_grace", + "section": "quant", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 1, + "default_text": "1", + "help": "Zig-zag grace: bridge up to this many consecutive sub-threshold scans during the peak-window walk; the boundary triggers on `peak_grace + 1` consecutive sub-threshold scans (1 = stop on 2 consecutive misses).", + "gates": [], + "source_line": 1123 + }, + { + "path": "quant.peak_window_mode", + "name": "peak_window_mode", + "section": "quant", + "rust_type": "PeakWindowMode", + "kind": "enum", + "optional": false, + "default": "per_candidate", + "default_text": "`per_candidate`", + "help": "Per-candidate window vs a consensus width derived from confident peptides.", + "gates": [], + "source_line": 1125, + "choices": [ + "per_candidate", + "consensus" + ] + }, + { + "path": "quant.reliable_q", + "name": "reliable_q", + "section": "quant", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.001, + "default_text": "0.001", + "help": "Peptide q-value cutoff defining the \"confident\" set that calibrates the consensus half-widths (Consensus mode only). Tighter than `q_threshold`.", + "gates": [], + "source_line": 1128 + }, + { + "path": "quant.q_filter", + "name": "q_filter", + "section": "quant", + "rust_type": "QuantQColumn", + "kind": "enum", + "optional": false, + "default": "peptide_q", + "default_text": "`peptide_q`", + "help": "Which q-value column to filter candidates on (`peptide_q` default; `precursor_q` is single-run only; use `run_psm_q` for per-run slices of an experiment-wide rescore). See `QuantQColumn`.", + "gates": [], + "source_line": 1132, + "choices": [ + "peptide_q", + "precursor_q", + "psm_q", + "run_psm_q" + ] + }, + { + "path": "quant.interference_envelope", + "name": "interference_envelope", + "section": "quant", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": false, + "default_text": "false", + "help": "Apply an apex-outward interference-correction envelope to each fragment trace before integrating its area, stripping co-eluting interference in the peak wings. Off by default (identity on a clean peak). Opt-in and benchmark-gated: it changes reported quantities.", + "gates": [ + "benchmark-gated" + ], + "source_line": 1137 + }, + { + "path": "quant.fragment_selection", + "name": "fragment_selection", + "section": "quant", + "rust_type": "FragmentSelection", + "kind": "enum", + "optional": false, + "default": "observed_area", + "default_text": "`observed_area`", + "help": "Which fragments enter the top-N sum. `observed_area` (default, legacy) ranks by the integrated area itself, which preferentially selects interfered fragments (their areas are inflated) and so varies run to run. `predicted` ranks by the library (predicted or empirical) fragment intensity, a per-precursor constant, so every run sums the same fragments. Astral HYE 2026-08-26: CV 0.163 -> 0.112 on 6/6 ions at top-3. Benchmark-gated.", + "gates": [ + "benchmark-gated" + ], + "source_line": 1144, + "choices": [ + "observed_area", + "predicted" + ] + }, + { + "path": "quant.fixed_scan_halfwidth", + "name": "fixed_scan_halfwidth", + "section": "quant", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 0, + "default_text": "0", + "help": "When > 0, integrate each fragment over the `2k+1` scans centred on the identification apex instead of the descent-walk window (`bound_peak` window ignored; falls back to it when the apex is unknown). A fixed narrow window is far less sensitive to interference in the peak wings than the walked bounds. 0 (default) = off.", + "gates": [], + "source_line": 1150 + }, + { + "path": "quant.baseline_subtract", + "name": "baseline_subtract", + "section": "quant", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": false, + "default_text": "false", + "help": "Subtract a per-fragment local background before integrating (fixed-scan window only). The background is the `baseline_quantile` quantile of the intensities in the two flanks (`baseline_flank_scans` samples on each side of the integration window); window intensities are clipped at zero after subtraction. Targets the additive floor that compresses ratios in the low-abundance condition. Off by default; benchmark-gated.", + "gates": [ + "benchmark-gated" + ], + "source_line": 1157 + }, + { + "path": "quant.baseline_flank_scans", + "name": "baseline_flank_scans", + "section": "quant", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 12, + "default_text": "12", + "help": "Flank length (samples per side) used to estimate the background.", + "gates": [], + "source_line": 1159 + }, + { + "path": "quant.baseline_quantile", + "name": "baseline_quantile", + "section": "quant", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.25, + "default_text": "0.25", + "help": "Quantile of the flank intensities taken as the background level.", + "gates": [], + "source_line": 1161 + }, + { + "path": "quant.fixed_window_s", + "name": "fixed_window_s", + "section": "quant", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.0, + "default_text": "0.0", + "help": "When > 0, integrate each fragment over the samples within `fixed_window_s` seconds of the identification apex (instrument-independent alternative to `fixed_scan_halfwidth`, which it overrides). 0 (default) = off.", + "gates": [], + "source_line": 1165 + }, + { + "path": "mbr.strategy", + "name": "strategy", + "section": "mbr", + "rust_type": "MbrStrategy", + "kind": "enum", + "optional": false, + "default": "none", + "default_text": "`none`", + "help": "", + "gates": [], + "source_line": 1246, + "choices": [ + "none", + "empirical_library", + "rt_transfer", + "full" + ] + }, + { + "path": "mbr.q_anchor", + "name": "q_anchor", + "section": "mbr", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.01, + "default_text": "0.01", + "help": "q-value for a precursor to become a cross-run anchor (validated at 0.01).", + "gates": [], + "source_line": 1248 + }, + { + "path": "mbr.min_anchor_runs", + "name": "min_anchor_runs", + "section": "mbr", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 2, + "default_text": "2", + "help": "Minimum number of OTHER runs a precursor must be confident in to transfer.", + "gates": [], + "source_line": 1250 + }, + { + "path": "mbr.q_transfer", + "name": "q_transfer", + "section": "mbr", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.01, + "default_text": "0.01", + "help": "Accept threshold for a transferred identification's transfer q-value.", + "gates": [], + "source_line": 1252 + }, + { + "path": "mbr.rt_window_s", + "name": "rt_window_s", + "section": "mbr", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 20.0, + "default_text": "20.0", + "help": "NOT YET WIRED. Transfer RT half-window (seconds) around the cross-run-predicted RT. The M2 leave-target-out residual was ~17 s at p95, ~15x tighter than the search window, which is where this default comes from -- but no code reads this field yet, so setting it has no effect. Kept as the recorded design value for the MBR transfer tier; `validate()` warns if it is changed from the default. See CLAUDE.md, \"MBR transfer/re-extraction remains benchmark-gated\".", + "gates": [ + "benchmark-gated", + "not yet wired" + ], + "source_line": 1259 + }, + { + "path": "mbr.decoy_transfer", + "name": "decoy_transfer", + "section": "mbr", + "rust_type": "DecoyTransfer", + "kind": "enum", + "optional": false, + "default": "permuted_rt", + "default_text": "`permuted_rt`", + "help": "NOT YET WIRED. Which decoy-transfer null would estimate the false-transfer rate (M4). No code reads this field yet; `validate()` warns if it is changed.", + "gates": [ + "not yet wired" + ], + "source_line": 1262, + "choices": [ + "permuted_rt", + "reverse_sequence", + "both" + ] + }, + { + "path": "mbr.consensus_corr_min", + "name": "consensus_corr_min", + "section": "mbr", + "rust_type": "f64", + "kind": "float", + "optional": false, + "default": 0.0, + "default_text": "0.0", + "help": "Minimum correlation of the observed fragment pattern to the empirical consensus for a transfer to be accepted (interference guard; 0 disables).", + "gates": [], + "source_line": 1265 + }, + { + "path": "mbr.requant_all", + "name": "requant_all", + "section": "mbr", + "rust_type": "bool", + "kind": "bool", + "optional": false, + "default": false, + "default_text": "false", + "help": "NOT YET WIRED. Would requantify already-identified precursors too (fill the matrix), not only transferred ones, under `strategy = Full`. No code reads this field yet; `validate()` warns if it is changed.", + "gates": [ + "not yet wired" + ], + "source_line": 1269 + }, + { + "path": "mbr.python", + "name": "python", + "section": "mbr", + "rust_type": "Option", + "kind": "other", + "optional": true, + "default": "null", + "default_text": "null", + "help": "Python interpreter for the `mbr_worker.py` sidecar (pandas/pyarrow/numpy; e.g. the `py312_mumdia` env). Required when `strategy != None`.", + "gates": [], + "source_line": 1272 + }, + { + "path": "experiment.parallel_runs", + "name": "parallel_runs", + "section": "experiment", + "rust_type": "usize", + "kind": "integer", + "optional": false, + "default": 1, + "default_text": "1", + "help": "How many per-run search chains to execute concurrently. 1 (default) is strictly sequential, i.e. the historical behaviour. Runs are independent, so raising this scales nearly linearly in wall time, but EACH concurrent run holds its own extraction working set (tens of GB on a large library), so the practical ceiling is memory, not cores. Raise it deliberately after checking peak RSS for a single run; 2-4 is a reasonable start on a large-memory machine. Results are unaffected: chunks are processed in index order and completion order never reaches the output.", + "gates": [], + "source_line": 1440 + }, + { + "path": "experiment.finetune_scope", + "name": "finetune_scope", + "section": "experiment", + "rust_type": "FinetuneScope", + "kind": "enum", + "optional": false, + "default": "first_run_only", + "default_text": "`first_run_only`", + "help": "Whether the DeepLC fine-tune runs once for the experiment or once per run. Only consulted when `rt_im_train.finetune_deeplc` is set.", + "gates": [], + "source_line": 1443, + "choices": [ + "first_run_only", + "per_run" + ] + } + ], + "list_sections": [ + { + "paths": [ + "peptidoforms.fixed_mods", + "peptidoforms.variable_mods" + ], + "item": "ResidueMod", + "fields": [ + { + "path": "residue", + "name": "residue", + "section": "", + "rust_type": "char", + "kind": "other", + "optional": false, + "default": null, + "default_text": null, + "help": "Target residue; `*` for any / terminal handled separately in MVP.", + "gates": [], + "source_line": 390 + }, + { + "path": "name", + "name": "name", + "section": "", + "rust_type": "String", + "kind": "string", + "optional": false, + "default": null, + "default_text": null, + "help": "UniMod name.", + "gates": [], + "source_line": 392 + } + ] + } + ], + "profiles": { + "dia": [ + { + "path": "features.set", + "value": "FeatureSet::Extended" + }, + { + "path": "extract.apex_count_window", + "value": "5" + }, + { + "path": "extract.apex_rt_prior_s", + "value": "120.0" + } + ] + } +} diff --git a/rust/mumdia/crates/mumdia/src/main.rs b/rust/mumdia/crates/mumdia/src/main.rs index 67efdfb6..5b5e2736 100644 --- a/rust/mumdia/crates/mumdia/src/main.rs +++ b/rust/mumdia/crates/mumdia/src/main.rs @@ -432,9 +432,64 @@ enum Cmd { Doctor { #[arg(long)] config: Option, + /// Emit the report as JSON on stdout instead of prose on stdout. + /// + /// For a caller that has to act on the result rather than read it: the + /// desktop application renders one row per role and offers to install what + /// is missing, which means it needs the modules and versions as data, not a + /// paragraph to regex. The exit status is unchanged. + #[arg(long)] + json: bool, }, } +/// One sidecar role, as `doctor` found it. +#[derive(serde::Serialize)] +struct RoleReport { + /// `rescore` | `deeplc` | `ms2pip` | `mbr` + role: String, + /// The configuration field that names this interpreter. + field: String, + /// Does THIS configuration need the role at all. + required: bool, + /// `ok` | `fail` | `skip` | `warn` + status: String, + python: Option, + /// `configured`, an environment variable name, `CONDA_PREFIX`, `PATH`, ... + provenance: String, + /// What the workers for this role import. + modules: Vec, + /// Of those, the ones this interpreter cannot import. + missing: Vec, + /// Versions of the packages whose version changes results. + versions: std::collections::BTreeMap, + /// The environment variable that overrides this role. + env_var: String, + /// Anything worth saying that is not a failure, such as a DeepLC below the floor. + warnings: Vec, +} + +/// The worker-script directory check, which runs before any interpreter. +#[derive(serde::Serialize)] +struct ScriptsReport { + /// `ok` | `fail` | `skip` + status: String, + dir: String, + /// Worker files the directory should contain but does not. + missing: Vec, + /// False when the configuration needs no sidecar, in which case the directory + /// is never opened and its absence is not a problem. + needed: bool, +} + +/// The whole report. `ok` is the same verdict the exit status carries. +#[derive(serde::Serialize)] +struct DoctorReport { + ok: bool, + scripts: ScriptsReport, + roles: Vec, +} + /// Report whether this configuration can actually run: which interpreter each /// sidecar role resolves to, whether it can import what its workers import, which /// versions it has, and whether the worker scripts are where the engine will look @@ -445,7 +500,7 @@ enum Cmd { /// checked that `sidecar_script_dir` existed (the most common misconfiguration, /// and the one baked into the tracked example config), and reported no versions, /// so a DeepLC old enough to change results looked identical to a current one. -fn doctor(cfg: &Config, config_path: Option<&str>) -> Result<()> { +fn doctor_report(cfg: &Config, config_path: Option<&str>) -> DoctorReport { use mumdia::python::{self, Role, ALL_ROLES}; let mut cfg = cfg.clone(); @@ -453,7 +508,7 @@ fn doctor(cfg: &Config, config_path: Option<&str>) -> Result<()> { let dir_moved = script_dir != cfg.predict_frag.sidecar_script_dir; cfg.predict_frag.sidecar_script_dir = script_dir.clone(); - let mut bad = false; + let mut ok = true; let any_sidecar = ALL_ROLES.iter().any(|r| r.required_by(&cfg)); // 1. Worker scripts, checked before the interpreters because a missing script @@ -461,43 +516,50 @@ fn doctor(cfg: &Config, config_path: Option<&str>) -> Result<()> { // configuration needs no sidecar: the native predictors and `native_tda` // rescorer are the default, and that run must not be failed for a directory // it never opens. - println!("worker scripts"); let dir = std::path::Path::new(&script_dir); - if !any_sidecar { - println!(" [skip] no Python sidecar is needed by this configuration"); + let scripts = if !any_sidecar { + ScriptsReport { + status: "skip".into(), + dir: script_dir.clone(), + missing: Vec::new(), + needed: false, + } } else if !dir.is_dir() { - bad = true; - println!( - " [FAIL] predict_frag.sidecar_script_dir: {script_dir} is not a directory.\n\ - \x20 Point it at the `scripts/` directory that ships beside the binary." - ); - } else { - if dir_moved { - println!(" [note] resolved sidecar_script_dir to {script_dir}"); + ok = false; + ScriptsReport { + status: "fail".into(), + dir: script_dir.clone(), + missing: Vec::new(), + needed: true, } - let mut missing: Vec<&str> = Vec::new(); + } else { + let mut missing: Vec = Vec::new(); for role in ALL_ROLES { if !role.required_by(&cfg) { continue; } for worker in role.workers() { if !dir.join(worker).exists() { - missing.push(worker); + missing.push(worker.to_string()); } } } - if missing.is_empty() { - println!(" [ ok ] {script_dir}"); - } else { - bad = true; - missing.sort_unstable(); - missing.dedup(); - println!(" [FAIL] {script_dir}: missing {}", missing.join(", ")); + missing.sort_unstable(); + missing.dedup(); + if !missing.is_empty() { + ok = false; } - } + ScriptsReport { + status: if missing.is_empty() { "ok" } else { "fail" }.into(), + dir: script_dir.clone(), + missing, + needed: true, + } + }; + let _ = dir_moved; - // 2. Interpreters, one line per role, resolving `auto` exactly as a run would. - println!("sidecar interpreters"); + // 2. Interpreters, one entry per role, resolving `auto` exactly as a run would. + let mut roles = Vec::new(); for role in ALL_ROLES { let configured = match role { Role::Rescore => cfg.rescore.python.clone(), @@ -506,19 +568,32 @@ fn doctor(cfg: &Config, config_path: Option<&str>) -> Result<()> { Role::Mbr => cfg.mbr.python.clone(), }; let required = role.required_by(&cfg); - let modules = role.modules(&cfg); + let modules: Vec = role.modules(&cfg).iter().map(|m| m.to_string()).collect(); let explicit = configured .as_deref() .map(|v| !v.eq_ignore_ascii_case(python::AUTO)) .unwrap_or(false); - let label = role.field(); - // Neither needed nor named: say so and probe nothing. Discovery here used - // to run for every role and then report the interpreter it happened to - // find as "configured but not needed", which described neither the config - // nor the outcome. + let mut r = RoleReport { + role: format!("{role:?}").to_lowercase(), + field: role.field().to_string(), + required, + status: "skip".into(), + python: None, + provenance: "not required".into(), + modules, + missing: Vec::new(), + versions: std::collections::BTreeMap::new(), + env_var: role.env_var().to_string(), + warnings: Vec::new(), + }; + + // Neither needed nor named: say so and probe nothing. Discovery here used to + // run for every role and then report the interpreter it happened to find as + // "configured but not needed", which described neither the config nor the + // outcome. if !required && !explicit { - println!(" [skip] {label}: not needed by this config"); + roles.push(r); continue; } @@ -530,89 +605,138 @@ fn doctor(cfg: &Config, config_path: Option<&str>) -> Result<()> { None => (None, "not found"), } }; + r.provenance = provenance.to_string(); + r.python = path.clone(); + match (&path, required) { (None, true) => { - bad = true; - println!( - " [FAIL] {label}: required by this config, and no usable interpreter was \ - found.\n\x20 Set it, set {}, or activate an environment that can \ - import {}.", - role.env_var(), - modules.join(", ") - ); + ok = false; + r.status = "fail".into(); } - (None, false) => println!(" [skip] {label}: not needed by this config"), + (None, false) => r.status = "skip".into(), (Some(p), _) => { let interp = std::path::Path::new(p); - match python::missing_modules(interp, modules) { + let module_refs: Vec<&str> = r.modules.iter().map(|s| s.as_str()).collect(); + match python::missing_modules(interp, &module_refs) { Ok(missing) if missing.is_empty() => { - // Versions of the packages whose version changes results. - let mut notes: Vec = Vec::new(); for m in ["deeplc", "torch", "mokapot", "ms2pip", "numpy"] { - if modules.contains(&m) { + if module_refs.contains(&m) { if let Some(v) = python::module_version(interp, m) { - notes.push(format!("{m} {v}")); + r.versions.insert(m.to_string(), v); } } } - let tag = if required { " ok " } else { "note" }; - println!( - " [{tag}] {label}: {p} ({provenance}){}", - if notes.is_empty() { - String::new() - } else { - format!("\n\x20 {}", notes.join(", ")) - } - ); - if !required { - println!("\x20 (configured but not needed by this config)"); - } + r.status = if required { "ok" } else { "note" }.into(); // DeepLC below 4.1.1 changes results rather than only - // performance: the 4.0.0a2 multitask preview overfits - // per-run fine-tuning badly enough to invert RT-model - // rankings (docs/08_rt_im_train.md). + // performance: the 4.0.0a2 multitask preview overfits per-run + // fine-tuning badly enough to invert RT-model rankings + // (docs/08_rt_im_train.md). if role == Role::DeepLc && required { - if let Some(v) = python::module_version(interp, "deeplc") { - if version_below(&v, &[4, 1, 1]) { - println!( - "\x20 [warn] DeepLC {v} is older than the \ - supported floor 4.1.1; results, not just speed, differ" - ); + if let Some(v) = r.versions.get("deeplc") { + if version_below(v, &[4, 1, 1]) { + r.warnings.push(format!( + "DeepLC {v} is older than the supported floor 4.1.1; \ + results, not just speed, differ" + )); } } } } Ok(missing) => { if required { - bad = true; + ok = false; } - println!( - " [{}] {label}: {p} ({provenance}) cannot import {}", - if required { "FAIL" } else { "warn" }, - missing.join(", ") - ); + r.status = if required { "fail" } else { "warn" }.into(); + r.missing = missing; } Err(e) => { if required { - bad = true; + ok = false; } - println!( - " [{}] {label}: {e}", - if required { "FAIL" } else { "warn" } - ); + r.status = if required { "fail" } else { "warn" }.into(); + r.warnings.push(e.to_string()); } } } } + roles.push(r); } - if bad { - anyhow::bail!( - "mumdia doctor: this configuration cannot run as it stands (see the FAIL lines above)" - ); + DoctorReport { ok, scripts, roles } +} + +/// Render the report as the prose a person reads in a terminal. +fn print_doctor(rep: &DoctorReport) { + println!("worker scripts"); + match rep.scripts.status.as_str() { + "skip" => println!(" [skip] no Python sidecar is needed by this configuration"), + "ok" => println!(" [ ok ] {}", rep.scripts.dir), + _ if !rep.scripts.missing.is_empty() => println!( + " [FAIL] {}: missing {}", + rep.scripts.dir, + rep.scripts.missing.join(", ") + ), + _ => println!( + " [FAIL] predict_frag.sidecar_script_dir: {} is not a directory.\n\ + \x20 Point it at the `scripts/` directory that ships beside the binary.", + rep.scripts.dir + ), + } + + println!("sidecar interpreters"); + for r in &rep.roles { + let label = &r.field; + match (r.status.as_str(), &r.python) { + ("skip", _) => println!(" [skip] {label}: not needed by this config"), + ("fail", None) => println!( + " [FAIL] {label}: required by this config, and no usable interpreter was \ + found.\n\x20 Set it, set {}, or activate an environment that can \ + import {}.", + r.env_var, + r.modules.join(", ") + ), + (status, Some(p)) + if r.missing.is_empty() && r.warnings.iter().all(|w| w.contains("DeepLC")) => + { + let tag = if status == "ok" { " ok " } else { "note" }; + let notes: Vec = + r.versions.iter().map(|(k, v)| format!("{k} {v}")).collect(); + println!( + " [{tag}] {label}: {p} ({}){}", + r.provenance, + if notes.is_empty() { + String::new() + } else { + format!("\n\x20 {}", notes.join(", ")) + } + ); + if !r.required { + println!("\x20 (configured but not needed by this config)"); + } + for w in &r.warnings { + println!("\x20 [warn] {w}"); + } + } + (status, Some(p)) if !r.missing.is_empty() => println!( + " [{}] {label}: {p} ({}) cannot import {}", + if status == "fail" { "FAIL" } else { "warn" }, + r.provenance, + r.missing.join(", ") + ), + (status, _) => { + for w in &r.warnings { + println!( + " [{}] {label}: {w}", + if status == "fail" { "FAIL" } else { "warn" } + ); + } + } + } + } + + if rep.ok { + println!("mumdia doctor: configuration is runnable"); } - println!("mumdia doctor: configuration is runnable"); - Ok(()) } /// True when the dotted version `v` is below `floor`. Unparseable components @@ -1127,12 +1251,20 @@ fn main() -> Result<()> { "MuMDIA: {n_pep} peptides, {n_prot} protein groups at q <= {q}\n {pep}\n {prot}" ); } - Cmd::Doctor { config } => { + Cmd::Doctor { config, json } => { // Deliberately the raw loader: doctor must be able to diagnose a // configuration whose interpreters do not resolve, so it cannot go // through the resolving loader that would bail first. let cfg = load_config_raw(&config)?; - doctor(&cfg, config.as_deref())?; + let report = doctor_report(&cfg, config.as_deref()); + if json { + println!("{}", serde_json::to_string_pretty(&report)?); + } else { + print_doctor(&report); + } + if !report.ok { + anyhow::bail!("this configuration cannot run as it stands; see the report above"); + } } } Ok(()) From 24ae604f1c7f47a99c0e3b8ea6af1cb8bdf148ff Mon Sep 17 00:00:00 2001 From: MuMDIA Date: Sun, 30 Aug 2026 11:35:02 +0200 Subject: [PATCH 03/14] feat(desktop): milestones 2 and 3, component installation and a generated settings editor M2, installing itself. `uv` supplies both the interpreter and the packages, so conda disappears from the user's machine entirely: no "install Miniconda, create two environments, then edit the config to point at the right interpreters", which is the step where an external user gives up. The environment is created under the per-user data directory, not beside the executable, because on Windows that is Program Files and an installer needing administrator rights on first run is not an easy install. Searching without components is refused, as decided. The predicate is "this configuration requires no Python sidecar at all", asked of the engine through `doctor --json` rather than kept as a list here. Refusing anything that mentions `native_tda` would have been wrong: on an imported library with retention-time modelling it measured 10,847 against nn_torch's 10,914, and the 1,213-vs-10,300 gap that motivates the rule is the zero-component path, not the rescorer. **The one-environment plan was wrong, and uv proved it.** The plan claimed a single environment could serve all four sidecar roles, on the strength of their module lists overlapping. That is not the same question. The resolver answer: deeplc==4.1.1 -> psm-utils>=1.5 -> sqlalchemy>=2 ms2pip==4.0.0 -> sqlalchemy>=1.3,<2 Unsatisfiable. `ms2pip>=4.1` does coexist with DeepLC and would restore one environment, but MS2PIP's version changes predicted fragment intensities and env/docker-rescore.yml pins 4.0.0 deliberately as "a separate, testable upgrade"; trading a measured result for packaging convenience is the wrong direction. So there are two: a primary one covering the whole recommended workflow, and MS2PIP on request, needed only for FASTA-mode prediction. A test asserts MS2PIP stays out of the primary set, so a later "fix" that adds it back fails at test time rather than at install time on a user's machine. M3, configuration. The form is generated from configs/config-schema.json, so nothing about a setting -- name, type, default, help, enum choices, gate markers -- is written in the interface. Saving writes only the difference from the defaults, which keeps configurations short and lets a later release improve a default for someone who saved settings today. Every save is validated by the engine before it is offered, so a rejected value is reported while editing rather than an hour into a search. Benchmark-gated parameters are badged with the reason. Two bugs found by the tests, both user-facing: - The installer's terminal status was overwritten by the next status refresh, so "done" was never observable and a caller watching for completion waited for ever. - `uv venv` refuses an existing directory, so Install failed permanently after the first attempt, including after a failed attempt that left a partial environment -- exactly when someone presses it again. `--allow-existing` repairs instead, rather than `--clear` discarding a several-hundred-megabyte download. Verified: the environment really was created from nothing on this machine, and every sidecar role's imports resolve in it. 20 unit tests, 3 end-to-end. fmt and clippy -D warnings clean. Co-Authored-By: Claude Opus 5 (1M context) --- desktop/src-tauri/src/components.rs | 570 ++++++++++++++++++++++++++ desktop/src-tauri/src/lib.rs | 2 + desktop/src-tauri/src/main.rs | 101 ++++- desktop/src-tauri/src/run.rs | 44 ++ desktop/src-tauri/src/settings.rs | 257 ++++++++++++ desktop/src-tauri/tests/end_to_end.rs | 53 +++ desktop/ui/app.css | 42 ++ desktop/ui/app.js | 290 ++++++++++++- desktop/ui/index.html | 81 ++++ env/console-ms2pip-requirements.txt | 17 + env/console-requirements.txt | 36 ++ 11 files changed, 1491 insertions(+), 2 deletions(-) create mode 100644 desktop/src-tauri/src/components.rs create mode 100644 desktop/src-tauri/src/settings.rs create mode 100644 env/console-ms2pip-requirements.txt create mode 100644 env/console-requirements.txt diff --git a/desktop/src-tauri/src/components.rs b/desktop/src-tauri/src/components.rs new file mode 100644 index 00000000..97d1b0ed --- /dev/null +++ b/desktop/src-tauri/src/components.rs @@ -0,0 +1,570 @@ +//! Installing and detecting the Python analysis components. +//! +//! # Why this exists at all +//! +//! MuMDIA runs with no Python: the native predictors and the `native_tda` rescorer +//! are the shipped defaults. It is tempting to build a zero-dependency application +//! on that basis. The recorded numbers say otherwise -- on the same file, the fully +//! native FASTA path returns about 1,213 report rows against about 10,300 for the +//! imported-library workflow with DeepLC and neural rescoring. Two things differ +//! between those, the library source as well as the predictors, so the gap is not +//! attributable to Python alone; it is large enough to settle the design. An +//! application that avoids installing Python ships the weak path, so this one +//! installs it, and `preflight` refuses to search without it. +//! +//! # Why `uv` and not conda +//! +//! Reading `env/mumdia-deeplc.yml`, conda contributes `python=3.11` and `pip`, and +//! every dependency that matters is already pip. `uv` supplies the interpreter as +//! well, as one self-contained binary, so "install Miniconda first, create two +//! environments, then edit the config to point at the right interpreters" -- the +//! step where an external user gives up -- disappears entirely. +//! +//! # One environment, and the role it cannot cover +//! +//! Rescoring, DeepLC and match-between-runs share one interpreter happily, and that +//! is the whole recommended workflow. MS2PIP cannot join them: at the versions this +//! project tests, `deeplc==4.1.1` needs `sqlalchemy>=2` through `psm-utils` and +//! `ms2pip==4.0.0` needs `sqlalchemy<2`, which `uv` reports as unsatisfiable. +//! +//! This was checked rather than assumed, and the assumption was wrong. `ms2pip>=4.1` +//! does resolve alongside DeepLC, but MS2PIP's version changes predicted fragment +//! intensities, so taking that upgrade to simplify packaging would trade a +//! convenience for a results change. MS2PIP therefore gets its own environment, +//! installed only on request, and it is needed only for FASTA-mode library building +//! with predicted intensities. + +use std::io::{BufRead, BufReader}; +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use std::sync::{Arc, Mutex}; + +use serde::Serialize; + +/// Where the managed environment lives, and the state of the last install. +#[derive(Serialize, Clone, Debug, Default)] +pub struct Status { + /// Absolute path to the managed interpreter, if it exists. + pub python: Option, + /// True when that interpreter can import everything every role needs. + pub complete: bool, + /// Modules the managed interpreter cannot import. + pub missing: Vec, + /// Versions of the packages whose version changes results. + pub versions: std::collections::BTreeMap, + /// `idle` | `installing` | `done` | `failed` + pub install_status: String, + pub install_log: Vec, + pub error: Option, + /// Whether a bundled `uv` was found, without which nothing can be installed. + pub uv: Option, +} + +/// Every module the primary environment must provide. +/// +/// This is the union of what the rescore, DeepLC and match-between-runs roles +/// import: the whole recommended workflow. MS2PIP is deliberately absent -- it +/// cannot share an environment with DeepLC (see the module documentation) and lives +/// in its own, which `MS2PIP_MODULES` describes. +/// +/// Kept here rather than asked of the engine because it is needed BEFORE any +/// configuration exists: the setup screen runs on first launch, when there is +/// nothing to point `doctor` at. `doctor --json` remains the authority once a +/// configuration is in play, and the application shows that too. +const REQUIRED_MODULES: &[&str] = &[ + "deeplc", + "torch", + "psm_utils", + "mokapot", + "sklearn", + "numpy", + "pandas", + "pyarrow", +]; + +/// The optional MS2PIP environment's modules. +const MS2PIP_MODULES: &[&str] = &["ms2pip", "numpy", "pandas"]; + +/// Which managed environment a call is about. +/// +/// Two exist because they must: MS2PIP and DeepLC cannot share one. Everything else +/// about them is identical, so the create-and-install path is written once. +#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Env { + /// Rescoring, DeepLC and match-between-runs: the recommended workflow. + Primary, + /// MS2PIP, for FASTA-mode library building with predicted intensities. + Ms2pip, +} + +impl Env { + fn dir_name(self) -> &'static str { + match self { + Env::Primary => "python", + Env::Ms2pip => "python-ms2pip", + } + } + + fn requirements_name(self) -> &'static str { + match self { + Env::Primary => "console-requirements.txt", + Env::Ms2pip => "console-ms2pip-requirements.txt", + } + } + + pub fn modules(self) -> &'static [&'static str] { + match self { + Env::Primary => REQUIRED_MODULES, + Env::Ms2pip => MS2PIP_MODULES, + } + } + + /// Python version for this environment. MS2PIP pulls `pandas<2`, which has no + /// cp312 wheel, which is why both stay on 3.11. + fn python_version(self) -> &'static str { + "3.11" + } +} + +/// Packages whose version is worth reporting, because it changes results. +const REPORT_VERSIONS: &[&str] = &["deeplc", "torch", "mokapot", "ms2pip", "numpy"]; + +/// Per-user application data, where the managed environment is created. +/// +/// Not beside the executable: on Windows that is under Program Files, which a +/// normal user cannot write to, and an installer that needs administrator rights to +/// finish its first run is not the easy installation this exists to provide. +pub fn data_dir() -> PathBuf { + let base = if cfg!(windows) { + std::env::var_os("LOCALAPPDATA").map(PathBuf::from) + } else { + std::env::var_os("XDG_DATA_HOME") + .map(PathBuf::from) + .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".local/share"))) + }; + base.unwrap_or_else(std::env::temp_dir).join("MuMDIA") +} + +pub fn venv_dir(env: Env) -> PathBuf { + data_dir().join(env.dir_name()) +} + +/// The interpreter inside a managed environment, whether or not it exists yet. +pub fn managed_python(env: Env) -> PathBuf { + let v = venv_dir(env); + if cfg!(windows) { + v.join("Scripts").join("python.exe") + } else { + v.join("bin").join("python") + } +} + +/// Locate `uv`: bundled beside the application first, then whatever is on PATH. +/// +/// PATH is accepted because a developer very likely has it already, and refusing to +/// use it would mean nobody could test this without building a bundle. +pub fn find_uv() -> Option { + let exe_name = if cfg!(windows) { "uv.exe" } else { "uv" }; + if let Ok(exe) = std::env::current_exe() { + if let Some(dir) = exe.parent() { + for cand in [dir.join(exe_name), dir.join("binaries").join(exe_name)] { + if cand.is_file() { + return Some(cand); + } + } + } + } + let path = std::env::var_os("PATH")?; + std::env::split_paths(&path) + .map(|d| d.join(exe_name)) + .find(|p| p.is_file()) +} + +/// The requirements file describing the managed environment. +/// +/// Shipped beside the application in `env/`, which is what the release archive +/// already stages; the development fallback reaches into the repository. +pub fn requirements(env: Env) -> Option { + let name = env.requirements_name(); + let mut cands = Vec::new(); + if let Ok(exe) = std::env::current_exe() { + if let Some(dir) = exe.parent() { + cands.push(dir.join("env").join(name)); + cands.push(dir.join("../../../../env").join(name)); + } + } + cands.into_iter().find(|p| p.is_file()) +} + +/// Ask an interpreter which of `modules` it cannot import. +/// +/// One process for the whole list: nine separate probes cost about a second each on +/// Windows, which is long enough to be visible on a screen that exists to feel +/// responsive. +fn missing_modules(python: &Path, modules: &[&str]) -> Result, String> { + let script = format!( + "import importlib.util as u\n\ + print('\\n'.join(m for m in {:?} if u.find_spec(m) is None))", + modules + ); + let out = crate::engine::command(python) + .args(["-c", &script]) + .output() + .map_err(|e| format!("could not run {}: {e}", python.display()))?; + if !out.status.success() { + return Err(String::from_utf8_lossy(&out.stderr).trim().to_string()); + } + Ok(String::from_utf8_lossy(&out.stdout) + .lines() + .map(|l| l.trim().to_string()) + .filter(|l| !l.is_empty()) + .collect()) +} + +fn versions(python: &Path, modules: &[&str]) -> std::collections::BTreeMap { + let script = format!( + "import importlib.metadata as m\n\ + for name in {:?}:\n\ + \x20 try: print(name, m.version(name))\n\ + \x20 except Exception: pass", + modules + ); + let mut out = std::collections::BTreeMap::new(); + if let Ok(o) = crate::engine::command(python) + .args(["-c", &script]) + .output() + { + for line in String::from_utf8_lossy(&o.stdout).lines() { + if let Some((k, v)) = line.trim().split_once(' ') { + out.insert(k.to_string(), v.to_string()); + } + } + } + out +} + +/// Inspect a managed environment as it stands. +pub fn status_of(env: Env) -> Status { + let mut s = Status { + install_status: "idle".into(), + uv: find_uv().map(|p| p.display().to_string()), + ..Default::default() + }; + let py = managed_python(env); + if !py.is_file() { + s.missing = env.modules().iter().map(|m| m.to_string()).collect(); + return s; + } + s.python = Some(py.display().to_string()); + match missing_modules(&py, env.modules()) { + Ok(missing) => { + s.complete = missing.is_empty(); + s.missing = missing; + if s.complete { + // `sklearn` is imported as `sklearn` but distributed as + // `scikit-learn`, so ask metadata for the name pip knows. + s.versions = versions(&py, REPORT_VERSIONS); + } + } + Err(e) => { + s.error = Some(e); + s.missing = env.modules().iter().map(|m| m.to_string()).collect(); + } + } + s +} + +/// The primary environment, which is what "are the components installed" means. +pub fn status() -> Status { + status_of(Env::Primary) +} + +/// Shared, mutable install state so the interface can poll while it runs. +#[derive(Default)] +pub struct Installer { + primary: Mutex, + ms2pip: Mutex, +} + +impl Installer { + fn slot(&self, env: Env) -> &Mutex { + match env { + Env::Primary => &self.primary, + Env::Ms2pip => &self.ms2pip, + } + } + + /// Refresh one environment from disk. Cheap enough to call whenever the screen + /// is shown. + pub fn refresh(&self, env: Env) -> Status { + let fresh = status_of(env); + if let Ok(mut s) = self.slot(env).lock() { + // Anything an installation put there outlives a refresh. Keeping only + // "installing" lost the outcome: the install thread would set "done", + // the next refresh would overwrite it with the "idle" a fresh probe + // carries, and a caller watching for the transition would wait for ever. + // Found by the test that does exactly that. + let keep = s.install_status.clone(); + let log = s.install_log.clone(); + let err = s.error.clone(); + *s = fresh; + if keep != "idle" { + s.install_status = keep; + s.install_log = log; + // A failure message is part of the outcome and must survive too. + if s.error.is_none() { + s.error = err; + } + } + return s.clone(); + } + fresh + } +} + +/// Create the managed environment and install everything into it. +/// +/// Runs on its own thread and streams both streams into the shared log, because a +/// several-hundred-megabyte download with no visible progress is indistinguishable +/// from a hang. +pub fn install(installer: Arc, env: Env) -> Result<(), String> { + let uv = find_uv().ok_or_else(|| { + "the installer component `uv` was not found beside the application or on PATH".to_string() + })?; + let reqs = requirements(env) + .ok_or_else(|| format!("env/{} was not found", env.requirements_name()))?; + let venv = venv_dir(env); + std::fs::create_dir_all(data_dir()) + .map_err(|e| format!("cannot create {}: {e}", data_dir().display()))?; + + { + let mut s = installer + .slot(env) + .lock() + .map_err(|_| "internal state is poisoned".to_string())?; + if s.install_status == "installing" { + return Err("an installation is already running".into()); + } + s.install_status = "installing".into(); + s.install_log.clear(); + s.error = None; + } + + std::thread::spawn(move || { + let log = |installer: &Installer, line: String| { + if let Ok(mut s) = installer.slot(env).lock() { + s.install_log.push(line); + if s.install_log.len() > 2000 { + let drop = s.install_log.len() - 2000; + s.install_log.drain(0..drop); + } + } + }; + + // Two steps: an interpreter, then the packages. `uv venv` downloads a + // standalone CPython if the machine has none, which is the whole point. + let steps: Vec<(&str, Vec)> = vec![ + ( + "creating the Python environment", + vec![ + "venv".into(), + // Without this, `uv venv` refuses a directory that already + // exists, so Install would fail for ever after the first + // attempt -- including after a FAILED attempt that left a + // partial environment behind, which is exactly when someone + // presses it again. `--clear` would also work but throws away + // a several-hundred-megabyte download to repair one package. + "--allow-existing".into(), + "--python".into(), + env.python_version().into(), + venv.display().to_string(), + ], + ), + ( + "installing the analysis packages", + vec![ + "pip".into(), + "install".into(), + "--python".into(), + venv.display().to_string(), + "-r".into(), + reqs.display().to_string(), + ], + ), + ]; + + for (title, args) in steps { + log(&installer, format!("== {title}")); + let mut cmd = crate::engine::command(&uv); + cmd.args(&args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + let mut child = match cmd.spawn() { + Ok(c) => c, + Err(e) => { + if let Ok(mut s) = installer.slot(env).lock() { + s.install_status = "failed".into(); + s.error = Some(format!("could not run uv: {e}")); + } + return; + } + }; + for stream in [ + child + .stdout + .take() + .map(|s| Box::new(s) as Box), + child + .stderr + .take() + .map(|s| Box::new(s) as Box), + ] + .into_iter() + .flatten() + { + let installer = Arc::clone(&installer); + std::thread::spawn(move || { + for line in BufReader::new(stream).lines().map_while(Result::ok) { + if let Ok(mut s) = installer.slot(env).lock() { + s.install_log.push(line); + } + } + }); + } + match child.wait() { + Ok(st) if st.success() => {} + Ok(st) => { + if let Ok(mut s) = installer.slot(env).lock() { + s.install_status = "failed".into(); + s.error = Some(format!( + "{title} failed ({st}). The log above says why; a failed download \ + can simply be retried." + )); + } + return; + } + Err(e) => { + if let Ok(mut s) = installer.slot(env).lock() { + s.install_status = "failed".into(); + s.error = Some(format!("{title} could not be waited for: {e}")); + } + return; + } + } + } + + // Verify rather than assume: uv exiting zero says the packages resolved, not + // that the interpreter can import them. A torch wheel that does not match the + // machine installs perfectly and fails on import. + let fresh = status_of(env); + if let Ok(mut s) = installer.slot(env).lock() { + let log_so_far = s.install_log.clone(); + *s = fresh; + s.install_log = log_so_far; + if s.complete { + s.install_status = "done".into(); + } else { + s.install_status = "failed".into(); + s.error = Some(format!( + "the packages installed but the interpreter still cannot import: {}", + s.missing.join(", ") + )); + } + } + }); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_two_environments_do_not_share_a_directory() { + // They must not: installing one would then half-overwrite the other. + assert_ne!(venv_dir(Env::Primary), venv_dir(Env::Ms2pip)); + assert_ne!( + requirements_name_of(Env::Primary), + requirements_name_of(Env::Ms2pip) + ); + } + + fn requirements_name_of(e: Env) -> &'static str { + e.requirements_name() + } + + #[test] + fn the_managed_paths_are_per_user_and_platform_shaped() { + let py = managed_python(Env::Primary); + let s = py.display().to_string(); + assert!(s.contains("MuMDIA"), "{s}"); + if cfg!(windows) { + assert!(s.ends_with("python.exe"), "{s}"); + assert!(s.contains("Scripts"), "{s}"); + } else { + assert!(s.ends_with("bin/python"), "{s}"); + } + // Never inside the installation directory, which is not user-writable on + // Windows. + let exe_dir = std::env::current_exe() + .ok() + .and_then(|e| e.parent().map(|p| p.to_path_buf())); + if let Some(d) = exe_dir { + assert!( + !py.starts_with(d), + "the environment must not live beside the exe" + ); + } + } + + #[test] + fn status_of_an_absent_environment_lists_everything_as_missing() { + // Nothing is installed under a temp HOME, so this exercises the cold path + // without touching a real installation. + let s = if managed_python(Env::Primary).is_file() { + eprintln!("a managed environment exists on this machine; checking the warm path"); + let s = status(); + assert!(s.python.is_some()); + return; + } else { + status() + }; + assert!(s.python.is_none()); + assert!(!s.complete); + assert_eq!(s.missing.len(), REQUIRED_MODULES.len()); + } + + #[test] + fn every_role_the_engine_defines_is_covered_by_the_required_modules() { + // The engine's own lists, transcribed from python.rs. If a role gains a + // dependency there and not here, the setup screen would report a complete + // installation that a run then fails on. + for role_modules in [ + vec!["torch", "numpy", "pandas", "pyarrow"], + vec!["mokapot", "sklearn", "numpy", "pandas", "pyarrow"], + vec!["deeplc", "numpy", "pandas", "pyarrow", "torch", "psm_utils"], + vec!["numpy", "pyarrow"], + ] { + for m in role_modules { + assert!( + REQUIRED_MODULES.contains(&m), + "{m} is imported by a sidecar role but is not installed" + ); + } + } + } + + /// MS2PIP is excluded on purpose, not by oversight. If someone "fixes" the list + /// by adding it, the environment stops resolving; this says so at test time + /// rather than at install time on a user's machine. + #[test] + fn ms2pip_is_deliberately_not_in_the_primary_environment() { + assert!( + !REQUIRED_MODULES.contains(&"ms2pip"), + "ms2pip==4.0.0 needs sqlalchemy<2 and deeplc==4.1.1 needs sqlalchemy>=2; they cannot share an environment" + ); + assert!(MS2PIP_MODULES.contains(&"ms2pip")); + } +} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 464e2716..876c0fef 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -4,5 +4,7 @@ //! `main.rs` is the Tauri shell over this: it owns the window, the command surface //! and the run registry, and nothing else. +pub mod components; pub mod engine; pub mod run; +pub mod settings; diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs index 3e0af8df..1d05ec35 100644 --- a/desktop/src-tauri/src/main.rs +++ b/desktop/src-tauri/src/main.rs @@ -2,7 +2,7 @@ // builds keep it, because that is where the developer's own `println!` goes. #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use mumdia_console::{engine, run}; +use mumdia_console::{components, engine, run, settings}; use std::collections::HashMap; use std::path::Path; @@ -15,6 +15,7 @@ use tauri::Manager; struct AppState { runs: Mutex>>, next_id: AtomicU64, + installer: Arc, } /// Which engine will be used, and does it execute. @@ -23,6 +24,99 @@ fn engine_info() -> Result { engine::info() } +/// The state of the managed Python environment. +#[tauri::command] +fn components_status(state: tauri::State<'_, AppState>) -> serde_json::Value { + serde_json::json!({ + "primary": state.installer.refresh(components::Env::Primary), + // Reported separately because it is optional and cannot share the primary + // environment: MS2PIP and DeepLC pin incompatible sqlalchemy majors. + "ms2pip": state.installer.refresh(components::Env::Ms2pip), + }) +} + +/// Create the managed environment and install the analysis packages into it. +/// +/// Returns as soon as the work is under way; the interface polls +/// `components_status` for the log and the outcome. +#[tauri::command] +fn components_install( + state: tauri::State<'_, AppState>, + env: Option, +) -> Result<(), String> { + components::install( + Arc::clone(&state.installer), + env.unwrap_or(components::Env::Primary), + ) +} + +/// Everything that must be true before a search can start. +/// +/// Checked here rather than at the moment of starting, so the interface can explain +/// and offer a fix instead of reporting a failure. +#[tauri::command] +fn preflight( + state: tauri::State<'_, AppState>, + req: run::Request, +) -> Result { + let (exe, _) = engine::resolve()?; + let mut blockers: Vec = Vec::new(); + + // The minimal path is not offered. See `run::needs_no_sidecar` for why the + // predicate is "needs no sidecar at all" rather than anything about the + // rescorer. + match run::needs_no_sidecar(&exe, req.config.as_deref()) { + Ok(true) => blockers.push( + "This configuration would run without any of the analysis components, which \ + identifies far fewer peptides. Choose a preset that uses retention-time \ + modelling, or install the components." + .into(), + ), + Ok(false) => {} + // A configuration the engine cannot even read is a real problem, but it is + // the engine's message that says what is wrong with it. + Err(e) => blockers.push(e), + } + + let comp = state.installer.refresh(components::Env::Primary); + if !comp.complete { + blockers.push(format!( + "The analysis components are not installed{}.", + if comp.missing.is_empty() { + String::new() + } else { + format!(" (missing {})", comp.missing.join(", ")) + } + )); + } + + Ok(serde_json::json!({ + "ok": blockers.is_empty(), + "blockers": blockers, + "components_complete": comp.complete, + })) +} + +/// The settings schema the editor renders its form from. +#[tauri::command] +fn config_schema() -> Result { + settings::load_schema() +} + +/// Write an override set, then ask the engine whether it accepts it. +/// +/// Validating here rather than at run time is the point: a value the engine would +/// reject is reported next to the field while it is being edited. +#[tauri::command] +fn save_settings( + name: String, + overrides: std::collections::BTreeMap, +) -> Result { + let path = settings::save(&name, overrides)?; + settings::validate(&path)?; + Ok(path) +} + /// Start a search. Returns the run id used by every subsequent call. #[tauri::command] fn start_run(state: tauri::State<'_, AppState>, req: run::Request) -> Result { @@ -132,6 +226,11 @@ fn main() { .manage(AppState::default()) .invoke_handler(tauri::generate_handler![ engine_info, + components_status, + components_install, + config_schema, + save_settings, + preflight, start_run, run_state, cancel_run, diff --git a/desktop/src-tauri/src/run.rs b/desktop/src-tauri/src/run.rs index 73c1e84b..358c82d6 100644 --- a/desktop/src-tauri/src/run.rs +++ b/desktop/src-tauri/src/run.rs @@ -278,6 +278,50 @@ fn quote(s: &str) -> String { } } +/// Does this request describe a search that needs no Python at all? +/// +/// That is the configuration the application refuses to run. The predicate is +/// deliberately narrow. The measured gap that motivates the refusal is about 1,213 +/// report rows against about 10,300 on the same file, and that is the fully native +/// FASTA path against the imported-library workflow -- but the rescorer is not what +/// separates them. On an imported library with retention-time modelling in place, +/// `native_tda` measured 10,847 against `nn_torch`'s 10,914, a difference of 0.6%. +/// Refusing every configuration that mentions `native_tda` would block one that is +/// within noise of the best. +/// +/// So the rule is "needs no sidecar at all", which is exactly the zero-component +/// path the 1,213 figure describes. +/// +/// The authority for this is the engine, not a list kept here: `mumdia doctor +/// --json` reports `required` per role from the configuration it is given, and if +/// every role is unrequired then the run is the minimal path. +pub fn needs_no_sidecar(engine: &Path, config: Option<&str>) -> Result { + let mut cmd = engine::command(engine); + cmd.arg("doctor").arg("--json"); + if let Some(c) = config { + if !c.trim().is_empty() { + cmd.arg("--config").arg(c); + } + } + let out = cmd + .output() + .map_err(|e| format!("could not ask the engine what this configuration needs: {e}"))?; + // `doctor` exits non-zero when the configuration cannot run, which is exactly + // the case where an interpreter is required and missing. The report on stdout is + // still valid and still says which roles are required, so the exit status is + // deliberately not checked here. + let text = String::from_utf8_lossy(&out.stdout); + let v: serde_json::Value = serde_json::from_str(&text) + .map_err(|e| format!("could not read the engine's configuration report: {e}"))?; + let roles = v + .get("roles") + .and_then(|r| r.as_array()) + .ok_or_else(|| "the engine's configuration report has no roles section".to_string())?; + Ok(!roles + .iter() + .any(|r| r.get("required").and_then(|b| b.as_bool()).unwrap_or(false))) +} + /// Start a search. Returns immediately with a handle; progress arrives by polling. pub fn start(id: String, req: Request) -> Result, String> { let args = argv(&req)?; diff --git a/desktop/src-tauri/src/settings.rs b/desktop/src-tauri/src/settings.rs new file mode 100644 index 00000000..21670fe2 --- /dev/null +++ b/desktop/src-tauri/src/settings.rs @@ -0,0 +1,257 @@ +//! The generated settings editor, and writing a configuration the engine accepts. +//! +//! # Why the form is generated +//! +//! There are 150 settings. An interface that restated their names, types, defaults +//! and help text would be a second copy of `config.rs`, and the copy that drifts is +//! the one a user reads. So the form is rendered from `configs/config-schema.json`, +//! which `ci/gen_config_reference.py` emits from the same parse that produces the +//! reference document, checked for staleness in CI beside it. +//! +//! # Why only overrides are written +//! +//! `Config` is `deny_unknown_fields` with serde defaults, so a valid configuration +//! contains only what differs from the default. Writing the full 150 keeps nothing +//! useful and freezes every default at the moment the file was saved: a later +//! release that improves a default would not reach anyone who had ever opened this +//! screen. Writing the difference keeps saved configurations short, reviewable, and +//! forward-compatible. + +use std::collections::BTreeMap; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +/// One setting, as the schema describes it. +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct Field { + pub path: String, + pub name: String, + pub section: String, + pub kind: String, + pub optional: bool, + #[serde(default)] + pub default: serde_json::Value, + #[serde(default)] + pub help: String, + #[serde(default)] + pub gates: Vec, + #[serde(default)] + pub choices: Option>, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct Schema { + pub sections: Vec, + pub fields: Vec, +} + +/// Find `config-schema.json`: beside the application, then in the repository. +pub fn schema_path() -> Option { + let mut cands = Vec::new(); + if let Ok(exe) = std::env::current_exe() { + if let Some(dir) = exe.parent() { + cands.push(dir.join("configs").join("config-schema.json")); + cands.push(dir.join("../../../../configs/config-schema.json")); + } + } + cands.into_iter().find(|p| p.is_file()) +} + +pub fn load_schema() -> Result { + let path = schema_path().ok_or_else(|| { + "configs/config-schema.json was not found beside the application".to_string() + })?; + let text = std::fs::read_to_string(&path) + .map_err(|e| format!("could not read {}: {e}", path.display()))?; + serde_json::from_str(&text).map_err(|e| format!("could not parse {}: {e}", path.display())) +} + +/// Turn `{"extract.gate_min_score": 0.3}` into the nested JSON the engine reads. +/// +/// Only the paths present are written, so the result is the override set and +/// nothing else. +pub fn nest(flat: &BTreeMap) -> serde_json::Value { + let mut root = serde_json::Map::new(); + for (path, value) in flat { + let mut cursor = &mut root; + let parts: Vec<&str> = path.split('.').collect(); + for part in &parts[..parts.len().saturating_sub(1)] { + cursor = cursor + .entry((*part).to_string()) + .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new())) + .as_object_mut() + .expect("intermediate config nodes are always objects"); + } + if let Some(last) = parts.last() { + cursor.insert((*last).to_string(), value.clone()); + } + } + serde_json::Value::Object(root) +} + +/// Where a configuration built in the interface is written. +/// +/// Under the per-user data directory rather than beside the results, so the same +/// settings can be reused across searches, and so a results folder stays a results +/// folder. +pub fn config_dir() -> PathBuf { + crate::components::data_dir().join("configs") +} + +/// Write an override set and hand back the path. +pub fn save(name: &str, flat: BTreeMap) -> Result { + let dir = config_dir(); + std::fs::create_dir_all(&dir).map_err(|e| format!("cannot create {}: {e}", dir.display()))?; + // A name typed by a person becomes a filename, so keep it to something that is + // one on every platform. + let safe: String = name + .chars() + .map(|c| { + if c.is_alphanumeric() || c == '-' || c == '_' { + c + } else { + '_' + } + }) + .collect(); + let safe = if safe.trim_matches('_').is_empty() { + "settings".to_string() + } else { + safe + }; + let path = dir.join(format!("{safe}.json")); + let text = serde_json::to_string_pretty(&nest(&flat)) + .map_err(|e| format!("could not serialise the settings: {e}"))?; + std::fs::write(&path, text + "\n") + .map_err(|e| format!("could not write {}: {e}", path.display()))?; + Ok(path.display().to_string()) +} + +/// Ask the engine whether a configuration file is acceptable. +/// +/// `doctor` loads the configuration through the same path a run does, so a value the +/// engine would reject is rejected here, while editing, rather than an hour into a +/// search. A missing interpreter is NOT a validation failure: that is what the setup +/// screen and the preflight component check are for, and conflating the two would +/// make every configuration look invalid until the components are installed. +pub fn validate(config_path: &str) -> Result<(), String> { + let (exe, _) = crate::engine::resolve()?; + let out = crate::engine::command(&exe) + .args(["doctor", "--config", config_path, "--json"]) + .output() + .map_err(|e| format!("could not run the engine: {e}"))?; + // A configuration the engine cannot even parse produces no JSON at all; that is + // the case worth reporting, and stderr carries the reason. + if serde_json::from_slice::(&out.stdout).is_err() { + let err = String::from_utf8_lossy(&out.stderr); + let line = err + .lines() + .rev() + .find(|l| !l.trim().is_empty() && !l.contains("INFO") && !l.contains("WARN")) + .unwrap_or("the engine rejected this configuration"); + return Err(line.trim().to_string()); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn flat(pairs: &[(&str, serde_json::Value)]) -> BTreeMap { + pairs + .iter() + .map(|(k, v)| (k.to_string(), v.clone())) + .collect() + } + + #[test] + fn nesting_builds_the_shape_the_engine_reads() { + let v = nest(&flat(&[ + ("extract.gate_min_score", json!(0.3)), + ("extract.frag_tol_ppm", json!(20.0)), + ("rescore.classifier", json!("nn_torch")), + ("threads", json!(8)), + ])); + assert_eq!(v["extract"]["gate_min_score"], json!(0.3)); + assert_eq!(v["extract"]["frag_tol_ppm"], json!(20.0)); + assert_eq!(v["rescore"]["classifier"], json!("nn_torch")); + assert_eq!(v["threads"], json!(8)); + } + + #[test] + fn only_the_given_paths_appear() { + // The whole point: a saved configuration is the difference from the + // defaults, so a later release that improves a default still reaches a user + // who saved settings today. + let v = nest(&flat(&[("extract.gate_min_score", json!(0.3))])); + let obj = v.as_object().unwrap(); + assert_eq!(obj.len(), 1); + assert_eq!(obj["extract"].as_object().unwrap().len(), 1); + } + + #[test] + fn an_empty_override_set_is_an_empty_object() { + // Which is a valid configuration meaning "every default", not an error. + assert_eq!(nest(&BTreeMap::new()), json!({})); + } + + #[test] + fn a_hostile_name_cannot_escape_the_configuration_directory() { + let dir = config_dir(); + for name in ["../../evil", "a/b", "c:\\d", "..", ""] { + let p = save(name, BTreeMap::new()).expect("save should succeed"); + let p = PathBuf::from(p); + assert_eq!( + p.parent().map(|x| x.to_path_buf()), + Some(dir.clone()), + "{name:?} escaped to {}", + p.display() + ); + let _ = std::fs::remove_file(&p); + } + } + + /// The schema ships with the repository, so this runs everywhere the tests do. + #[test] + fn the_shipped_schema_parses_and_describes_real_settings() { + let Ok(s) = load_schema() else { + eprintln!("config-schema.json not found from this build; skipping"); + return; + }; + assert!( + s.fields.len() > 100, + "expected the full settings set, got {}", + s.fields.len() + ); + let gate = s + .fields + .iter() + .find(|f| f.path == "extract.gate_min_score") + .expect("a known setting should be present"); + assert_eq!(gate.kind, "float"); + assert_eq!(gate.default, json!(0.2)); + assert!( + !gate.help.is_empty(), + "help text should come from the doc comment" + ); + + let group_by = s + .fields + .iter() + .find(|f| f.path == "compete.group_by") + .expect("an enum setting should be present"); + assert_eq!(group_by.kind, "enum"); + let choices = group_by.choices.as_ref().expect("an enum has choices"); + assert!(choices.contains(&"base_peptide".to_string()), "{choices:?}"); + + // Gate markers are what stop a benchmark-gated parameter being changed as if + // it were ordinary. + assert!( + s.fields.iter().any(|f| !f.gates.is_empty()), + "some settings are documented as gated and should be marked" + ); + } +} diff --git a/desktop/src-tauri/tests/end_to_end.rs b/desktop/src-tauri/tests/end_to_end.rs index a46582da..c939a3fe 100644 --- a/desktop/src-tauri/tests/end_to_end.rs +++ b/desktop/src-tauri/tests/end_to_end.rs @@ -180,3 +180,56 @@ fn stopping_a_run_kills_it_and_leaves_no_temp_files() { let _ = std::fs::remove_dir_all(&out); } + +/// Create the primary environment for real and check every role can import. +/// +/// This is the milestone-2 acceptance criterion: no conda, no pre-existing Python, +/// just the bundled installer and the pinned requirements. Opt in with +/// MUMDIA_TEST_INSTALL=1, because it downloads several hundred megabytes. +#[test] +fn the_primary_environment_installs_and_imports() { + if std::env::var("MUMDIA_TEST_INSTALL").ok().as_deref() != Some("1") { + eprintln!("MUMDIA_TEST_INSTALL=1 not set; skipping the real installation"); + return; + } + use mumdia_console::components::{self, Env}; + + assert!( + components::find_uv().is_some(), + "uv must be bundled or on PATH for the installer to work" + ); + assert!( + components::requirements(Env::Primary).is_some(), + "env/console-requirements.txt must ship beside the application" + ); + + let installer = std::sync::Arc::new(components::Installer::default()); + components::install(std::sync::Arc::clone(&installer), Env::Primary) + .expect("the installation should start"); + + let start = Instant::now(); + loop { + let s = installer.refresh(Env::Primary); + if s.install_status == "done" { + assert!(s.complete, "installed but not importable: {:?}", s.missing); + assert!( + s.versions.contains_key("torch") && s.versions.contains_key("deeplc"), + "the versions that change results should be reported: {:?}", + s.versions + ); + break; + } + if s.install_status == "failed" { + panic!( + "installation failed: {}\n{}", + s.error.unwrap_or_default(), + s.install_log.join("\n") + ); + } + assert!( + start.elapsed() < Duration::from_secs(1800), + "installation did not finish within 30 minutes" + ); + std::thread::sleep(Duration::from_secs(2)); + } +} diff --git a/desktop/ui/app.css b/desktop/ui/app.css index 10d99e9c..08261b41 100644 --- a/desktop/ui/app.css +++ b/desktop/ui/app.css @@ -170,6 +170,48 @@ select { min-width: 220px; } font-family: ui-monospace, "Cascadia Mono", monospace; font-size: 10.5px; color: var(--ink-faint); margin-top: 5px; } +.card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; flex-wrap: wrap; } +.card-sub { font-size: 12px; color: var(--ink-faint); margin-top: 4px; line-height: 1.5; max-width: 62ch; } +.pill { display: inline-flex; align-items: center; font-size: 11px; font-weight: 600; + padding: 1px 9px; border-radius: 20px; vertical-align: 1px; } +.pill.ok { background: var(--ok-soft); color: var(--ok); } +.pill.warn { background: var(--warn-soft); color: var(--warn); } +.pill.bad { background: var(--danger-soft); color: var(--danger); } +.pill.mute { background: var(--sunk); color: var(--ink-faint); } +.bar { height: 4px; border-radius: 3px; background: var(--sunk); overflow: hidden; margin-top: 12px; } +.bar > i { display: block; height: 100%; width: 40%; background: var(--accent); + animation: slide 1.4s ease-in-out infinite; } +@keyframes slide { 0% { margin-left: -40%; } 100% { margin-left: 100%; } } +@media (prefers-reduced-motion: reduce) { .bar > i { animation: none; width: 100%; } } + +/* ── settings editor ──────────────────────────────────────────────────── */ +input[type="search"] { + font: inherit; font-size: 12.5px; padding: 6px 10px; + border: 1px solid var(--rule); border-radius: 5px; + background: var(--panel); color: var(--ink); +} +.toggle-label { display: flex; align-items: center; gap: 6px; font-size: 12.5px; + color: var(--ink-soft); white-space: nowrap; } +.sec-title { font-size: 12px; font-weight: 600; letter-spacing: .06em; + text-transform: uppercase; color: var(--ink-faint); + margin: 18px 0 6px; font-family: ui-monospace, monospace; } +.setting { display: grid; grid-template-columns: 1fr auto; gap: 3px 14px; + padding: 10px 0; border-bottom: 1px solid var(--rule); align-items: start; } +.setting:last-child { border-bottom: none; } +.setting .sname { font-family: ui-monospace, "Cascadia Mono", monospace; + font-size: 12.5px; font-weight: 500; } +.setting .shelp { grid-column: 1; font-size: 11.5px; color: var(--ink-faint); + line-height: 1.45; max-width: 62ch; } +.setting .sctl { grid-column: 2; grid-row: 1 / span 2; display: flex; + align-items: center; gap: 8px; } +.setting .sctl input[type="text"], .setting .sctl select { + font-family: ui-monospace, monospace; font-size: 12.5px; + border: 1px solid var(--rule); border-radius: 5px; padding: 4px 8px; + background: var(--panel); color: var(--ink); min-width: 130px; +} +.setting.changed .sname { color: var(--accent); } +.banner.ok { background: var(--ok-soft); border-color: var(--ok); color: var(--ink); } + .files { display: flex; flex-direction: column; gap: 5px; font-size: 12.5px; } .files code { font-family: ui-monospace, "Cascadia Mono", monospace; font-size: 11.5px; diff --git a/desktop/ui/app.js b/desktop/ui/app.js index 111c855d..a93a3f5f 100644 --- a/desktop/ui/app.js +++ b/desktop/ui/app.js @@ -40,6 +40,11 @@ const state = { timer: null, lastStatus: null, outDir: "", + componentsReady: false, + setupTimer: null, + schema: null, + overrides: {}, + savedConfig: null, }; // ── small helpers ─────────────────────────────────────────────────────────── @@ -112,7 +117,12 @@ async function init() { } for (const b of document.querySelectorAll(".nav")) { - b.addEventListener("click", () => !b.disabled && screen(b.dataset.screen)); + b.addEventListener("click", () => { + if (b.disabled) return; + screen(b.dataset.screen); + if (b.dataset.screen === "setup") refreshComponents(); + if (b.dataset.screen === "settings") loadSettings(); + }); } for (const t of document.querySelectorAll(".tab")) { t.addEventListener("click", () => setMode(t.dataset.mode)); @@ -121,6 +131,9 @@ async function init() { b.addEventListener("click", () => pick(b.dataset.pick)); } $("start").addEventListener("click", start); + $("install-primary").addEventListener("click", () => installComponents("primary")); + $("install-ms2pip").addEventListener("click", () => installComponents("ms2pip")); + refreshComponents(); $("stop").addEventListener("click", stop); $("another").addEventListener("click", () => screen("input")); $("open-folder").addEventListener("click", () => invoke("reveal", { path: state.outDir })); @@ -131,6 +144,261 @@ async function init() { }); } + +// ── settings editor ───────────────────────────────────────────────────────── +// The form is generated from configs/config-schema.json, which the engine's own +// documentation generator emits from config.rs. Nothing about a setting -- its +// name, type, default or help text -- is written here, so this cannot describe a +// parameter the engine does not have. + +async function loadSettings() { + if (state.schema) return; + try { + state.schema = await invoke("config_schema"); + } catch (e) { + banner($("settings-error"), String(e)); + return; + } + $("settings-search").addEventListener("input", renderSettings); + $("only-changed").addEventListener("change", renderSettings); + $("save-settings").addEventListener("click", saveSettings); + renderSettings(); +} + +/// The value currently shown for a setting: an override if one was typed, else the +/// engine's default. +function currentValue(f) { + return f.path in state.overrides ? state.overrides[f.path] : f.default; +} + +function renderSettings() { + const q = $("settings-search").value.trim().toLowerCase(); + const onlyChanged = $("only-changed").checked; + const bySection = new Map(); + + for (const f of state.schema.fields) { + const changed = f.path in state.overrides; + if (onlyChanged && !changed) continue; + if (q && !(f.path.toLowerCase().includes(q) || f.help.toLowerCase().includes(q))) continue; + const sec = f.section || "(top level)"; + if (!bySection.has(sec)) bySection.set(sec, []); + bySection.get(sec).push(f); + } + + const esc = (t) => + String(t).replace(/&/g, "&").replace(//g, ">") + .replace(/"/g, """); + + const parts = []; + for (const [sec, fields] of bySection) { + parts.push(`
${esc(sec)}
`); + for (const f of fields) { + const changed = f.path in state.overrides; + const v = currentValue(f); + let control; + if (f.kind === "bool") { + control = + ``; + } else if (f.kind === "enum" && f.choices) { + control = + ``; + } else { + control = ``; + } + // A gated parameter is one the project documents as not to be changed from a + // single benchmark count. Saying so where the decision is made is the whole + // reason the schema carries the marker. + const gate = f.gates.length + ? `gated` + : ""; + const badge = changed ? `changed` : ""; + parts.push( + `
` + + `
${esc(f.path)} ${gate} ${badge}
` + + `
${esc(f.help || "No description in the engine source.")}` + + (f.default !== null && f.default !== undefined + ? ` Default: ${esc(f.default)}` + : "") + + `
${control}
` + ); + } + parts.push(`
`); + } + + const list = $("settings-list"); + list.innerHTML = + parts.join("") || `

Nothing matches that search.

`; + + for (const el of list.querySelectorAll("[data-path]")) { + el.addEventListener("change", () => onSettingChanged(el)); + } + + const n = Object.keys(state.overrides).length; + $("settings-sub").textContent = + `${state.schema.fields.length} settings. ` + + (n ? `${n} changed from the defaults; only those are saved.` : "None changed."); +} + +/// Record a change, or drop it when the value returns to the default. +/// +/// Dropping matters: a saved configuration is meant to be the difference from the +/// defaults, and a value typed and then typed back should leave nothing behind. +function onSettingChanged(el) { + const path = el.dataset.path; + const f = state.schema.fields.find((x) => x.path === path); + if (!f) return; + let raw = el.value; + let value = raw; + if (f.kind === "bool") value = raw === "true"; + else if (f.kind === "integer" || f.kind === "float") { + const n = Number(raw); + if (raw.trim() === "" || Number.isNaN(n)) { + banner($("settings-error"), `${path} must be a number.`); + return; + } + value = n; + } + banner($("settings-error"), ""); + if (JSON.stringify(value) === JSON.stringify(f.default)) delete state.overrides[path]; + else state.overrides[path] = value; + renderSettings(); +} + +async function saveSettings() { + banner($("settings-error"), ""); + banner($("settings-saved"), ""); + try { + const path = await invoke("save_settings", { + name: "console", + overrides: state.overrides, + }); + state.savedConfig = path; + // The saved file becomes the preset the next search uses, so the settings on + // this screen and the settings a run uses cannot diverge. + const sel = $("preset"); + let opt = [...sel.options].find((o) => o.value === path); + if (!opt) { + opt = document.createElement("option"); + opt.value = path; + opt.textContent = "My settings"; + sel.appendChild(opt); + } + sel.value = path; + banner( + $("settings-saved"), + `Saved and accepted by the engine. The next search will use these settings.` + ); + } catch (e) { + banner($("settings-error"), String(e)); + } +} + +// ── components ────────────────────────────────────────────────────────────── +// Polled while an installation runs, then left alone. The backend owns the state, +// so closing and reopening this screen shows the truth rather than a stale copy. +async function refreshComponents() { + let c; + try { + c = await invoke("components_status"); + } catch (e) { + banner($("setup-error"), String(e)); + return; + } + const p = c.primary; + state.componentsReady = !!p.complete; + + const pill = $("primary-pill"); + const btn = $("install-primary"); + const installing = p.install_status === "installing"; + show($("primary-bar"), installing); + show($("install-log-card"), installing || p.install_status === "failed"); + + if (installing) { + pill.textContent = "installing…"; + pill.className = "pill warn"; + btn.disabled = true; + } else if (p.complete) { + pill.textContent = "installed"; + pill.className = "pill ok"; + btn.disabled = true; + btn.textContent = "Installed"; + } else { + pill.textContent = "not installed"; + pill.className = "pill bad"; + btn.disabled = !c.primary.uv; + btn.textContent = "Install"; + } + + if (!c.primary.uv && !p.complete) { + banner( + $("setup-error"), + "The installer component `uv` was not found beside the application or on PATH, " + + "so components cannot be installed automatically." + ); + } else if (p.error) { + banner($("setup-error"), p.error); + } else { + banner($("setup-error"), ""); + } + + const m = c.ms2pip; + const mpill = $("ms2pip-pill"); + const mbtn = $("install-ms2pip"); + if (m.install_status === "installing") { + mpill.textContent = "installing…"; + mpill.className = "pill warn"; + mbtn.disabled = true; + } else if (m.complete) { + mpill.textContent = "installed"; + mpill.className = "pill ok"; + mbtn.disabled = true; + mbtn.textContent = "Installed"; + } else { + mpill.textContent = "optional"; + mpill.className = "pill mute"; + mbtn.disabled = false; + } + + // One log pane, showing whichever installation is talking. + const active = m.install_status === "installing" ? m : p; + const logEl = $("install-log"); + const text = (active.install_log || []).join("\n"); + if (logEl.textContent !== text) { + logEl.textContent = text; + logEl.scrollTop = logEl.scrollHeight; + } + + const versions = Object.entries(p.versions || {}); + $("setup-versions").textContent = versions.length + ? versions.map(([k, v]) => `${k} ${v}`).join(" · ") + : ""; + + const busy = installing || m.install_status === "installing"; + if (busy && !state.setupTimer) { + state.setupTimer = setInterval(refreshComponents, 900); + } else if (!busy && state.setupTimer) { + clearInterval(state.setupTimer); + state.setupTimer = null; + } +} + +async function installComponents(env) { + banner($("setup-error"), ""); + try { + await invoke("components_install", { env }); + } catch (e) { + banner($("setup-error"), String(e)); + return; + } + refreshComponents(); +} + function setMode(mode) { state.mode = mode; for (const t of document.querySelectorAll(".tab")) t.classList.toggle("on", t.dataset.mode === mode); @@ -185,6 +453,26 @@ async function start() { threads: Number.isFinite(threads) && threads > 0 ? threads : null, }; + // Ask the backend whether this is runnable before starting it, so a missing + // component or a configuration that needs no components at all is explained here + // rather than failing once the engine is under way. + banner($("preflight-block"), ""); + try { + const pf = await invoke("preflight", { req }); + if (!pf.ok) { + banner( + $("preflight-block"), + pf.blockers.join("\n\n") + + (pf.components_complete ? "" : "\n\nOpen Setup to install the components.") + ); + return; + } + } catch (e) { + // A preflight that cannot run is not a reason to refuse: say so and let the + // engine be the judge, since it reports its own errors perfectly well. + banner($("preflight-block"), `Could not check before starting: ${e}`); + } + try { state.runId = await invoke("start_run", { req }); } catch (e) { diff --git a/desktop/ui/index.html b/desktop/ui/index.html index d780a3f1..bfc2e66b 100644 --- a/desktop/ui/index.html +++ b/desktop/ui/index.html @@ -11,7 +11,9 @@