diff --git a/.bivvy/workflows/release.yml b/.bivvy/workflows/release.yml index e5f26c2..82a5415 100644 --- a/.bivvy/workflows/release.yml +++ b/.bivvy/workflows/release.yml @@ -1,13 +1,8 @@ description: "Tag and push a release (replaces bin/release)" -vars: - version: - command: "grep '^version = ' Cargo.toml | head -1 | sed 's/version = \"\\(.*\\)\"/\\1/'" - steps: version-bump: template: rust/version-bump - force: true depends_on: [release-preflight] version-commit: @@ -19,7 +14,8 @@ steps: validation: success command: | VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') - git add Cargo.toml Cargo.lock && git commit -m "Bump version to $${VERSION}" + git add Cargo.toml Cargo.lock + git diff --cached --quiet || git commit -m "Bump version to $${VERSION}" release-preflight: title: "Release preflight" @@ -33,9 +29,6 @@ steps: - type: execution command: "git diff --quiet && git diff --cached --quiet" validation: success - - type: execution - command: "! git rev-parse --verify --quiet \"refs/tags/$${VERSION}\"" - validation: success command: | VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') echo "Ready to release $${VERSION}" @@ -43,11 +36,21 @@ steps: release-tag: title: "Tag and push release" depends_on: [version-commit] + precondition: + type: all + checks: + - type: execution + command: "git diff --quiet && git diff --cached --quiet" + validation: success + - type: execution + command: "! git rev-parse --verify --quiet \"refs/tags/$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = \"\\(.*\\)\"/\\1/')\" >/dev/null" + validation: success command: | VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') git tag "$${VERSION}" && git push origin main && git push origin "$${VERSION}" workflow: + force_all: true steps: - rust - clippy diff --git a/CHANGELOG.md b/CHANGELOG.md index fff8c7f..672c4fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,9 @@ Pre-release versions are < 2.0.0. - `--clear` flag added to `bivvy history` command to clear run history for a project ### Changed +- A step whose `check`/`checks` fails is now unsatisfied outright instead of falling through to the rerun window. Previously a successful run inside the window could mark a step satisfied even though its own check had just reported work still to do. Checks that cannot reach a verdict still fall through to execution history +- `change` checks with `on_change: proceed` had inverted polarity: a changed target skipped the step and an unchanged target ran it. An unchanged target now satisfies the step and a changed target runs it, matching the documented meaning of `proceed` +- `change` checks with `on_change: require` now report no verdict instead of always passing, so they no longer silently skip the step they are attached to. `require_step` is still recorded in the check result but does not yet influence which steps run - `bivvy run ` now performs a two-phase load: phase 1 reads only `.bivvy/config.yml` to resolve the workflow name; phase 2 deep-merges with only the named workflow file in the chain. Sibling workflow files are not parsed, so a malformed neighbor cannot break a run of an unrelated workflow - `bivvy lint` now requires a target. Bare `bivvy lint` lints `.bivvy/config.yml` only. `bivvy lint ` resolves to `.bivvy/workflows/.yml` first, then `.bivvy/steps/.yml`. `--workflow ` and `--step ` are explicit disambiguators. `--all` opts into the legacy full-merge behavior - `bivvy list` now uses cheap discovery by default — workflow file bodies are not parsed, only headers (description + step ordering). Pass a positional workflow name to load that single file in detail, or `--all` to restore the legacy full-merge behavior @@ -61,6 +64,12 @@ Pre-release versions are < 2.0.0. - Legacy code removed as part of system redesign ### Fixed +- `change` check baselines are now recorded after a step runs successfully, so change detection works without capturing a snapshot by hand. Previously no baseline was ever written during a run, leaving every change check using the default baseline permanently without a verdict +- A `change` check whose target is missing, unreadable, or larger than its `size_limit` now reports no verdict instead of a failure. A `checks:` list naming several possible filenames no longer forces the step to run every time because the alternatives are absent +- An `any` check whose sub-checks all reach no verdict now reports no verdict instead of a failure, so it falls through to the rerun window rather than forcing a re-run +- Bundled install templates for Docker, PostgreSQL, and Redis no longer use liveness probes as completion checks. They now test for the installed binary, so an installed-but-stopped service no longer re-runs the install step or fails the workflow +- Bundled templates for yarn, pip, and Lerna no longer use completion checks that are removed, unrelated, or network-dependent +- Unterminated quotes in the completion checks for the fnm and .NET restore templates made those checks fail with a shell syntax error on every run - Docker daemon connection-refused errors now produce actionable recovery suggestions instead of a generic menu - Bundler recovery bugs and version resolver - Process drop in zsh diff --git a/Cargo.lock b/Cargo.lock index 6da1e6d..4225427 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,18 +4,18 @@ version = 4 [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] @@ -72,9 +72,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arraydeque" @@ -84,9 +84,9 @@ checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "assert-json-diff" @@ -100,9 +100,9 @@ dependencies = [ [[package]] name = "assert_cmd" -version = "2.2.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39bae1d3fa576f7c6519514180a72559268dd7d1fe104070956cb687bc6673bd" +checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6" dependencies = [ "anstyle", "bstr", @@ -136,13 +136,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] @@ -153,15 +153,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.16.3" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" dependencies = [ "aws-lc-sys", "zeroize", @@ -169,14 +169,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.40.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] @@ -193,13 +194,13 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "bivvy" -version = "1.10.0" +version = "1.11.0" dependencies = [ "anyhow", "assert_cmd", @@ -223,7 +224,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "sha2 0.11.0", + "sha2", "tempfile", "thiserror", "tracing", @@ -242,44 +243,44 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", ] [[package]] name = "bstr" -version = "1.12.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" dependencies = [ "memchr", "regex-automata", - "serde", + "serde_core", ] [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" dependencies = [ "serde", ] [[package]] name = "cc" -version = "1.2.61" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" dependencies = [ "find-msvc-tools", "jobserver", @@ -295,15 +296,26 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core", +] [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -315,9 +327,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.1" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", "clap_derive", @@ -325,9 +337,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ "anstream", "anstyle", @@ -337,23 +349,23 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.6.3" +version = "4.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "660c0520455b1013b9bcb0393d5f643d7e4454fb69c915b8d6d2aa0e9a45acc3" +checksum = "3be2ad0423bdbbb0e25bc89add796f3559706d4a95e1bc98e4d9662a957b6a19" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] @@ -379,23 +391,14 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" -version = "4.6.7" +version = "4.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" dependencies = [ "bytes", "memchr", ] -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "conpty" version = "0.5.1" @@ -407,9 +410,9 @@ dependencies = [ [[package]] name = "console" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" dependencies = [ "encode_unicode", "libc", @@ -450,18 +453,18 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crypto-common" @@ -475,9 +478,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "hybrid-array", ] @@ -512,24 +515,24 @@ dependencies = [ [[package]] name = "digest" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "const-oid", - "crypto-common 0.2.1", + "crypto-common 0.2.2", ] [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] @@ -583,11 +586,10 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] @@ -616,15 +618,15 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] name = "float-cmp" @@ -664,9 +666,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -674,50 +676,50 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-timer" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", "futures-io", @@ -754,36 +756,23 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.4" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi 5.3.0", - "wasip2", + "r-efi", + "rand_core", "wasm-bindgen", ] -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", -] - [[package]] name = "h2" -version = "0.4.13" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" dependencies = [ "atomic-waker", "bytes", @@ -809,9 +798,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hashlink" @@ -860,9 +849,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "http" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -870,9 +859,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", "http", @@ -880,9 +869,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", @@ -939,18 +928,18 @@ dependencies = [ [[package]] name = "hybrid-array" -version = "0.4.11" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ "typenum", ] [[package]] name = "hyper" -version = "1.9.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" dependencies = [ "atomic-waker", "bytes", @@ -1032,9 +1021,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", @@ -1046,9 +1035,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -1059,9 +1048,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1073,16 +1062,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -1093,15 +1083,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", @@ -1112,12 +1102,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "idna" version = "1.1.0" @@ -1160,21 +1144,19 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" dependencies = [ "equivalent", - "hashbrown 0.17.0", - "serde", - "serde_core", + "hashbrown 0.17.1", ] [[package]] name = "indicatif" -version = "0.18.4" +version = "0.18.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" dependencies = [ "console", "portable-atomic", @@ -1185,9 +1167,9 @@ dependencies = [ [[package]] name = "insta" -version = "1.47.2" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" +checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82" dependencies = [ "console", "once_cell", @@ -1200,19 +1182,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "is_terminal_polyfill" @@ -1253,7 +1225,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn", + "syn 2.0.119", ] [[package]] @@ -1272,28 +1244,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.97" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -1303,17 +1274,11 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "linux-raw-sys" @@ -1323,15 +1288,15 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "log" -version = "0.4.29" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lru-slab" @@ -1361,9 +1326,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memoffset" @@ -1382,9 +1347,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mio" -version = "1.2.0" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "wasi", @@ -1469,9 +1434,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.6" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +checksum = "5a07a60cc7a4d00c91f95c685609d1d2f79050e6804b70ebedd7650f0b839bcf" dependencies = [ "memchr", "ucd-trie", @@ -1479,9 +1444,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.6" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +checksum = "b3a83744a5c8455b8b3e0dc5031362780a347c878bdd11584d1a8984228cc88d" dependencies = [ "pest", "pest_generator", @@ -1489,25 +1454,24 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.6" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +checksum = "e0cd3451aa3de60d4b9a1e736885e4dea6b31617598026f12256ad566d63304a" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "pest_meta" -version = "2.8.6" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +checksum = "e04d3a0849e241d7dfce834c83b1c5edc8622009e8dd51a12ba1927c32f05496" dependencies = [ "pest", - "sha2 0.10.9", ] [[package]] @@ -1523,27 +1487,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "portable-atomic" -version = "1.13.1" +name = "pkg-config" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] -name = "potential_utf" -version = "0.1.5" +name = "portable-atomic" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] -name = "ppv-lite86" -version = "0.2.21" +name = "potential_utf" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ - "zerocopy", + "zerovec", ] [[package]] @@ -1576,21 +1537,11 @@ dependencies = [ "termtree", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -1606,9 +1557,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", @@ -1626,15 +1577,16 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" dependencies = [ "aws-lc-rs", "bytes", - "getrandom 0.3.4", + "getrandom 0.4.3", "lru-slab", "rand", + "rand_pcg", "ring", "rustc-hash", "rustls", @@ -1648,33 +1600,27 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +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" @@ -1683,58 +1629,55 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.9.4" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "rand_chacha", + "chacha20", + "getrandom 0.4.3", "rand_core", ] [[package]] -name = "rand_chacha" -version = "0.9.0" +name = "rand_core" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core", -] +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] -name = "rand_core" -version = "0.9.5" +name = "rand_pcg" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" dependencies = [ - "getrandom 0.3.4", + "rand_core", ] [[package]] name = "ref-cast" -version = "1.0.25" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.25" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -1744,9 +1687,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -1755,15 +1698,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64", "bytes", @@ -1814,9 +1757,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -1833,7 +1776,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys", @@ -1842,9 +1785,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "aws-lc-rs", "once_cell", @@ -1856,9 +1799,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -1868,9 +1811,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "web-time", "zeroize", @@ -1905,9 +1848,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "aws-lc-rs", "ring", @@ -1917,9 +1860,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" @@ -1947,9 +1890,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" dependencies = [ "dyn-clone", "ref-cast", @@ -1960,14 +1903,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 3.0.4", ] [[package]] @@ -1976,7 +1919,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "core-foundation", "core-foundation-sys", "libc", @@ -2001,9 +1944,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -2011,40 +1954,40 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] name = "serde_derive_internals" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -2055,9 +1998,9 @@ dependencies = [ [[package]] name = "serde_regex" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" +checksum = "bafc8d0c5330cecff10f16b459b479fd9acaa5b4acd7167301414e21b0057012" dependencies = [ "regex", "serde", @@ -2078,20 +2021,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.10.9" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" dependencies = [ "cfg-if", "cpufeatures 0.2.17", @@ -2105,8 +2037,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.2", + "cpufeatures 0.3.1", + "digest 0.11.3", ] [[package]] @@ -2126,9 +2058,9 @@ checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook-registry" @@ -2142,9 +2074,9 @@ dependencies = [ [[package]] name = "simd_cesu8" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" dependencies = [ "rustc_version", "simdutf8", @@ -2170,15 +2102,15 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -2210,9 +2142,20 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +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", @@ -2236,7 +2179,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2255,7 +2198,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", @@ -2269,38 +2212,38 @@ checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ "cfg-if", ] [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -2308,9 +2251,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -2323,9 +2266,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.1" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -2339,13 +2282,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] @@ -2360,13 +2303,14 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-sink", + "libc", "pin-project-lite", "tokio", ] @@ -2388,20 +2332,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -2436,7 +2380,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2486,9 +2430,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "ucd-trie" @@ -2514,12 +2458,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "unit-prefix" version = "0.5.2" @@ -2641,29 +2579,11 @@ 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.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - [[package]] name = "wasm-bindgen" -version = "0.2.120" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ "cfg-if", "once_cell", @@ -2674,9 +2594,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.70" +version = "0.4.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" dependencies = [ "js-sys", "wasm-bindgen", @@ -2684,9 +2604,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.120" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2694,65 +2614,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.120" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.120" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "web-sys" -version = "0.3.97" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" dependencies = [ "js-sys", "wasm-bindgen", @@ -2770,9 +2656,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" dependencies = [ "rustls-pki-types", ] @@ -2816,7 +2702,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2827,7 +2713,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2863,15 +2749,6 @@ 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" @@ -2905,30 +2782,13 @@ 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_gnullvm", "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", - "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_aarch64_gnullvm" version = "0.42.2" @@ -2941,12 +2801,6 @@ 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" @@ -2959,12 +2813,6 @@ 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" @@ -2977,24 +2825,12 @@ 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" @@ -3007,12 +2843,6 @@ 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" @@ -3025,12 +2855,6 @@ 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" @@ -3043,12 +2867,6 @@ 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" @@ -3061,111 +2879,11 @@ 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 = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.1", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "yaml-rust2" @@ -3180,9 +2898,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -3197,35 +2915,15 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] -[[package]] -name = "zerocopy" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] @@ -3238,21 +2936,21 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -3261,9 +2959,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "yoke", "zerofrom", @@ -3272,17 +2970,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 1e0724b..8091a31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bivvy" -version = "1.10.0" +version = "1.11.0" edition = "2021" rust-version = "1.93" description = "Interactive development environment setup automation" diff --git a/docs/commands/run.md b/docs/commands/run.md index 486563a..245bc45 100644 --- a/docs/commands/run.md +++ b/docs/commands/run.md @@ -30,7 +30,7 @@ bivvy run -w ci | `--skip` | | Skip specified steps (comma-separated) | | `--skip-behavior` | | How to handle skipped dependencies | | `--force` | `-f` | Force re-run of specified steps (comma-separated) | -| `--force-all` | | Force re-run of every step, bypassing all checks and step-level configuration | +| `--force-all` | | Force re-run of every step, bypassing `satisfied_when`, `check`/`checks`, the rerun window, and step-level behavior configuration. Preconditions are still enforced | | `--fresh` | | Discard all persisted satisfaction records and evaluate every step from scratch | | `--resume` | | Resume interrupted run | | `--save-preferences` | | Save prompt answers | @@ -69,8 +69,9 @@ Force re-run of node_deps: bivvy run --force=node_deps ``` -Force re-run of every step in the workflow, bypassing checks and any -step-level configuration: +Force re-run of every step in the workflow, bypassing completed checks, +`satisfied_when`, the rerun window, and step-level behavior configuration +(preconditions are still enforced): ```bash bivvy run --force-all diff --git a/docs/commands/snapshot.md b/docs/commands/snapshot.md index c8b1dbf..d0a89e3 100644 --- a/docs/commands/snapshot.md +++ b/docs/commands/snapshot.md @@ -31,7 +31,7 @@ bivvy snapshot delete ## What It Does -When you configure a step with a `change` check, Bivvy compares the current hash of a target (file, directory, or glob) against a stored baseline to decide whether the step needs to re-run. By default, baselines are updated automatically after each successful run. +When you configure a step with a `change` check, Bivvy compares the current hash of a target (file, directory, or glob) against a stored baseline to decide whether the step needs to re-run. The first successful run of the step establishes that baseline automatically, and by default it is re-recorded after each subsequent successful run - so you do not have to capture anything by hand for ordinary change detection. The `snapshot` command lets you capture a **named** baseline at a specific point in time. You can then reference that snapshot in your config so a step re-runs only when its target has changed relative to that fixed point -- not relative to the last run. @@ -83,11 +83,11 @@ steps: check: type: change target: Gemfile.lock - on_change: require + on_change: proceed baseline_snapshot: v1.0 ``` -With this config, `bundle_install` re-runs only when `Gemfile.lock` differs from the hash captured in the `v1.0` snapshot -- regardless of how many times `bivvy run` has executed since. +With this config, `bundle_install` re-runs only when `Gemfile.lock` differs from the hash captured in the `v1.0` snapshot -- regardless of how many times `bivvy run` has executed since. Use `on_change: proceed` here: it is the mode that means "a change is work that still needs doing". ## Examples diff --git a/docs/configuration/completed-checks.md b/docs/configuration/completed-checks.md index 429dd79..b2b7dc8 100644 --- a/docs/configuration/completed-checks.md +++ b/docs/configuration/completed-checks.md @@ -1,6 +1,6 @@ # Checks -Checks report facts about the external world: "does this file exist?", "does this command succeed?", "has this target changed?" When a check passes, Bivvy skips the step (unless `--force` is used). Use `check:` for a single check or `checks:` for multiple (treated as an implicit `all`). +Checks report facts about the external world: "does this file exist?", "does this command succeed?", "has this target changed?" When a check passes, Bivvy skips the step - unless `--force` is used, a `satisfied_when` condition on the step fails, or the step's precondition fails and blocks it outright. Use `check:` for a single check or `checks:` for multiple (treated as an implicit `all`). Checks can also be used in [`satisfied_when`](#satisfied_when) conditions, which declare when a step's purpose is already fulfilled. @@ -92,14 +92,24 @@ steps: The `on_change` field controls what happens when a change is detected: -- `proceed` (default) -- the step runs (change means "work needed") -- `fail` -- the step fails immediately (change means "unexpected drift") -- `require` -- flags another step as required via `require_step` +- `proceed` (default) -- the step runs (change means "work needed"); when the target is unchanged, the step is skipped +- `fail` -- the step fails immediately (change means "unexpected drift"); when the target is unchanged, the step is skipped +- `require` -- records another step name via `require_step` + +`require` names a *different* step, so the check is written on the step that +observes the change and points at the step that has to do something about it: ```yaml steps: deps: command: "yarn install" + check: + type: presence + target: "node_modules" + + build: + command: "yarn build" + depends_on: [deps] check: type: change target: "yarn.lock" @@ -107,6 +117,25 @@ steps: require_step: deps ``` +`require` records that the named step should run, but it does not currently +change the decision Bivvy makes about either step: the check reports no +verdict either way, so the step falls through to its rerun window. Use +`proceed` when you want a change to actually re-run the step, and `fail` when +a change should stop the workflow. + +### When a change check has no answer + +A change check needs two things to reach a verdict: the target has to exist, +and a baseline has to have been recorded for it. If either is missing, the +check reports no verdict rather than passing or failing, and Bivvy moves on +to the next satisfaction signal (the rerun window). The same is true when the +target cannot be read or is larger than its `size_limit`. + +This is what makes it safe to list several possible filenames under +`checks:` - the ones that are absent stay quiet instead of forcing the step +to run every time. If you need "this file must exist" to be an actual +requirement, use a `presence` check instead. + Change checks support different target kinds: - `file` (default) -- hash a single file @@ -126,7 +155,16 @@ steps: ### Baselines -Change checks compare the current hash against a stored baseline. By default the baseline updates after each successful run (`each_run`). Use `first_run` to freeze the baseline after the first evaluation: +Change checks compare the current hash of the target against a stored baseline. +The first time a step with a change check runs, there is no baseline yet, so +the check reports no verdict and the run establishes the baseline. From the +second run onward the comparison is meaningful. + +By default the baseline is re-recorded after each successful run (`each_run`), +so the check answers "has this changed since the last time this step +succeeded?". Use `first_run` to freeze the baseline at the value captured the +first time, so the check answers "has this changed since we started watching +it?": ```yaml steps: @@ -229,10 +267,21 @@ steps: target: "vendor/bundle" ``` +### Sub-checks with no verdict + +A sub-check that cannot reach a verdict - a `change` check on a file that +does not exist, for example - is neither a pass nor a failure. + +`all` still fails as soon as one sub-check definitely fails, but if none fail +and any were undecided, the combinator reports no verdict. `any` still passes +as soon as one sub-check passes, but if every branch was undecided it reports +no verdict rather than a failure. In both cases Bivvy moves on to the next +satisfaction signal instead of forcing the step to run. + ## Forcing Re-run -Bypass `check:` and `satisfied_when` evaluation for one or more steps. -Preconditions are not bypassed by either flag. +Bypass `check:`, `satisfied_when`, and rerun window evaluation for one or +more steps. Preconditions are not bypassed by either flag. Force specific steps (comma-separated): @@ -329,11 +378,16 @@ satisfied_when: Bivvy evaluates satisfaction in priority order (first match wins): 1. **`satisfied_when`** -- if present and all conditions pass, the step is satisfied. If present and any condition fails, the step is **not** satisfied (no fallthrough to `check` or history). -2. **`check`/`checks`** -- if the step's check passes, the step is satisfied. +2. **`check`/`checks`** -- if the step's check passes, the step is satisfied. If it fails, the step is **not** satisfied (no fallthrough to history). A check that cannot reach a verdict does fall through. A `change` check reaches no verdict when it has no recorded baseline yet, when its target is missing or unreadable, when the target exceeds its `size_limit`, or when `on_change: require` is set. 3. **Execution history** -- if the step ran successfully within its rerun window, the step is satisfied. If none of these apply, the step needs to run. +This hierarchy is only consulted after the step's dependencies and its +`precondition` have passed. A failing precondition blocks the step outright, +so a passing `check` never turns a blocked step into a skipped one. See +[Preconditions](steps.md#preconditions). + ## Behavior Options ```yaml @@ -343,6 +397,6 @@ steps: check: type: presence target: "node_modules" - prompt_on_rerun: false # Skip silently if satisfied (default: false) + prompt_on_rerun: false # Skip silently after a recent run (default: false) skippable: true # Allow skipping (default: true) ``` diff --git a/docs/configuration/settings.md b/docs/configuration/settings.md index 9692d82..e75f6a6 100644 --- a/docs/configuration/settings.md +++ b/docs/configuration/settings.md @@ -110,8 +110,13 @@ Accepted values: | Value | Meaning | |---|---| | `"4h"`, `"30m"`, `"7d"` | Duration string | -| `"0"` or `"never"` | Always treat the step as if it had never run | -| `"forever"` | Never re-prompt; the step stays satisfied indefinitely | +| `"0"` or `"never"` | A previous successful run never counts toward satisfaction | +| `"forever"` | A previous successful run counts toward satisfaction with no expiry | + +This window only controls the execution-history signal. A step's own +`satisfied_when` conditions and `check`/`checks` are evaluated first, and +whenever they reach a verdict that verdict wins - so neither value can make +a step skip work that its own check says is outstanding. ## Diagnostic Funnel @@ -181,7 +186,7 @@ Set default behavior for all steps: settings: defaults: auto_run: true # Auto-run unsatisfied steps (default: true) - prompt_on_rerun: false # Skip satisfied steps silently (default: false) + prompt_on_rerun: false # Skip recently-run steps silently (default: false) rerun_window: "4h" # How long a successful run counts as satisfied (default: "4h") ``` diff --git a/docs/configuration/state.md b/docs/configuration/state.md index 93734a5..835c339 100644 --- a/docs/configuration/state.md +++ b/docs/configuration/state.md @@ -72,7 +72,7 @@ steps: on_change: proceed ``` -Change detection computes a SHA-256 hash of the target and compares it to the stored baseline. If the hashes differ, the step re-runs. Baselines are stored in `~/.bivvy/projects/{hash}/snapshots/` and updated after each successful execution. +Change detection computes a SHA-256 hash of the target and compares it to the stored baseline. If the hashes differ, the step is not satisfied and re-runs. Baselines are stored in `~/.bivvy/projects/{hash}/snapshots/`. With the default `baseline: each_run`, the baseline is re-recorded after each successful execution; with `baseline: first_run` it is captured once and then frozen. Until a baseline exists - or if the target file is missing - the check reports no verdict and Bivvy falls back to the step's rerun window. > Still using the legacy `watches:` field? See the [Migrate to the New Check Schema](../guides/migrate-to-checks.md) guide to convert it (along with `completed_check:` and `prompt_if_complete:`) to the modern check fields. diff --git a/docs/configuration/steps.md b/docs/configuration/steps.md index cffb764..6125d19 100644 --- a/docs/configuration/steps.md +++ b/docs/configuration/steps.md @@ -50,9 +50,11 @@ Sensitive steps receive special treatment: ## Auto-Run Behavior -By default, steps that need to run execute automatically. Steps that are -already satisfied (via checks, `satisfied_when`, or recent execution -history) prompt you before re-running. You can control this per-step: +By default, steps that need to run execute automatically, and steps that +are already satisfied (via `satisfied_when`, `check`/`checks`, or a recent +successful run) are skipped silently. Set `prompt_on_rerun: true` to be +asked before re-running a step that was satisfied only because it ran +recently. You can control all of this per-step: ```yaml steps: @@ -60,7 +62,7 @@ steps: command: npm install auto_run: true # run without asking (default) confirm: false # always prompt before running (default: false) - prompt_on_rerun: false # skip silently if already satisfied + prompt_on_rerun: false # skip silently after a recent run rerun_window: "24h" # trust a successful run for 24 hours ``` @@ -207,9 +209,14 @@ opposite semantics: ### Combining with Completed Checks -When a step has both `check` and `precondition`, the -completed check is evaluated first. If the step is already complete, -it is skipped and the precondition is never evaluated. +When a step has both `check` and `precondition`, the precondition is +evaluated first. It is a gate on whether running the step is safe at all, +not on whether the step still has work to do, so it runs before Bivvy asks +whether the work is already done. If the precondition fails, the step is +blocked - even if its completed check passes, and even if the step was +forced with `--force` or `--force-all`. Only once the precondition passes +does Bivvy evaluate `satisfied_when`, then `check`/`checks`, then execution +history to decide whether to run or skip. ```yaml steps: diff --git a/docs/guides/auto-run.md b/docs/guides/auto-run.md index 622cd07..ded9233 100644 --- a/docs/guides/auto-run.md +++ b/docs/guides/auto-run.md @@ -12,7 +12,7 @@ outcomes: | Decision | What happens | |----------|-------------| | **Auto-run** | The step executes immediately, no prompt | -| **Skip** | The step is skipped (already satisfied) | +| **Skip** | The step is skipped (already satisfied, or a system requirement is still unmet) | | **Prompt** | You are asked whether to run or skip | | **Block** | The step cannot proceed (dependency failed or precondition failed) | @@ -38,7 +38,13 @@ first match wins: and the remaining signals are not checked. 2. **`check` / `checks`** -- If the step has a completed check and it passes, - the step is satisfied. + the step is satisfied. If it fails, the step is _not_ satisfied and the + rerun window is not consulted, so a stale successful run cannot overrule a + check that just reported work still to do. A check that cannot reach a + verdict gives no answer either way, so it falls through to the next + signal. A `change` check reaches no verdict when it has no recorded + baseline yet, when its target is missing, unreadable, or larger than its + size limit, or when `on_change: require` is set. 3. **Rerun window** -- If the step ran successfully within its rerun window, the step is satisfied based on execution history. @@ -47,17 +53,30 @@ If none of these signals match, the step is not satisfied and needs to run. ## The decision flow -Once satisfaction is determined, the decision engine follows this logic: +The decision engine evaluates each step in this order and stops at the first +rule that matches. Satisfaction is computed near the end, not at the start: 1. **Dependencies** -- If a dependency failed or was skipped (and is not satisfied), the step is **blocked**. 2. **Precondition** -- If the step has a precondition and it fails, the step - is **blocked**. -3. **Satisfied** -- If the step is satisfied: - - With `prompt_on_rerun: false` (default): the step is **skipped** silently. - - With `prompt_on_rerun: true`: you are **prompted** to re-run - or skip. -4. **Not satisfied** -- If the step needs to run: + is **blocked**. This gate comes first, so a failing precondition blocks a + step even when the step is satisfied and even when it was forced. +3. **Requirements** -- If the step declares a system requirement that is + still unmet after Bivvy has offered to install it, the step is + **skipped**. +4. **Force** -- If the step was forced (`--force`, `--force-all`, a workflow + `force` / `force_all` entry, or step-level `force: true`), it **runs**. + Satisfaction is not evaluated at all. +5. **Satisfied** -- If the step is satisfied: + - Satisfied by `satisfied_when` or by a passing `check`: the step is + **skipped** silently. + - Satisfied only by the rerun window, with `prompt_on_rerun: false` + (default): the step is **skipped** silently. + - Satisfied only by the rerun window, with `prompt_on_rerun: true`: you + are **prompted** to re-run or skip. +6. **Not satisfied** -- If the step needs to run: + - With `sensitive: true` or `confirm: true`: you are **prompted** before + execution, regardless of `auto_run`. - With `auto_run: true` (default): the step **auto-runs**. - With `auto_run: false`: you are **prompted** before execution. @@ -91,9 +110,13 @@ settings: ### `prompt_on_rerun` -Controls what happens when a step is already satisfied. When `false` (the -default), satisfied steps are silently skipped. When `true`, Bivvy asks -if you want to re-run it. +Controls what happens when a step is satisfied only because it ran +successfully inside its rerun window. When `false` (the default), the step is +silently skipped. When `true`, Bivvy asks whether you want to re-run it. + +This flag does not apply to steps satisfied by `satisfied_when` or by a +passing `check`. Those have positive evidence that the work is already done, +so they are always skipped silently. ```yaml steps: @@ -133,7 +156,9 @@ confirmation only fires when Bivvy would actually execute the command. How long a previous successful run counts as "recent enough" to consider a step satisfied by execution history alone. This is the third signal in the satisfaction hierarchy -- it only applies when the step has no -`satisfied_when` conditions and no passing `check`. +`satisfied_when` conditions and no `check`, or when its check could not reach +a verdict. A check that explicitly fails ends evaluation there, so the rerun +window never gets the chance to override it. The default is `4h` (4 hours). @@ -145,11 +170,11 @@ steps: compile_assets: command: npm run build - rerun_window: "never" # always re-run, ignore history + rerun_window: "never" # ignore history entirely one_time_setup: command: ./scripts/initial-setup.sh - rerun_window: "forever" # once it succeeds, never re-run + rerun_window: "forever" # a past success never expires ``` Accepted values: @@ -160,7 +185,12 @@ Accepted values: | `"4h"` | 4 hours (default) | | `"7d"` | 7 days | | `"never"` or `"0"` | Execution history never satisfies the step | -| `"forever"` | A previous success always satisfies the step | +| `"forever"` | A previous success satisfies the step with no expiry | + +Both values only affect the third signal. If the step has `satisfied_when` +conditions, or a `check` that reaches a verdict, that verdict decides the +outcome and the rerun window is never consulted - so `"forever"` will not +keep a step skipped when its own check says there is work to do. Set the global default under `settings.defaults`: @@ -245,8 +275,8 @@ decision engine thinks. Bivvy exposes three CLI flags for this: | Flag | Effect | |------|--------| -| `--force [,...]` | Force-run the named steps, bypassing satisfaction checks. Other steps follow the normal decision flow. | -| `--force-all` | Force-run every step in the workflow, bypassing all checks and step-level configuration. | +| `--force [,...]` | Force-run the named steps, bypassing `satisfied_when`, `check`/`checks`, and the rerun window. Other steps follow the normal decision flow. | +| `--force-all` | Force-run every step in the workflow, bypassing `satisfied_when`, `check`/`checks`, the rerun window, and step-level behavior configuration. Preconditions are still enforced. | | `--fresh` | Discard all persisted satisfaction records and re-evaluate every step from scratch. | Examples: @@ -258,17 +288,20 @@ bivvy run --force install_deps # Re-run several steps in one go bivvy run --force install_deps,build -# Re-run the whole workflow, ignoring checks and step config +# Re-run the whole workflow, ignoring completed checks and step config bivvy run --force-all # Throw away cached satisfaction state, then evaluate normally bivvy run --fresh ``` -`--force` / `--force-all` always run the listed steps. `--fresh` clears -prior state but still respects `check`, `satisfied_when`, and `auto_run` -once it re-evaluates -- so a step whose `check` passes will still be -considered satisfied after `--fresh`. +`--force` / `--force-all` run the listed steps regardless of +`satisfied_when`, `check`/`checks`, or the rerun window. They do not override +a failing precondition or a failed dependency - those still block the step, +because they describe conditions under which running the step would be +unsafe rather than unnecessary. `--fresh` clears prior state but still +respects `check`, `satisfied_when`, and `auto_run` once it re-evaluates, so a +step whose `check` passes will still be considered satisfied after `--fresh`. ## Examples diff --git a/docs/guides/migrate-to-checks.md b/docs/guides/migrate-to-checks.md index 2bfb406..03dcbbb 100644 --- a/docs/guides/migrate-to-checks.md +++ b/docs/guides/migrate-to-checks.md @@ -113,12 +113,20 @@ For a single watched path, use `check:` instead of `checks:`. See [Change checks](../configuration/completed-checks.md#change) for `on_change`, `baseline`, and glob/command target kinds. +The first `bivvy run` after converting establishes the baseline for each new +`change` check rather than comparing against one, so change detection starts +working from the second run onward. If a converted step has no other check and +no recent successful run, it will simply run once to set the baseline. + ### `prompt_if_complete:` → `prompt_on_rerun:` Pure rename. The default also changed: the old `prompt_if_complete` defaulted to `true` (always ask before re-running a satisfied step), while -`prompt_on_rerun` defaults to `false` (silently skip satisfied steps). If you -relied on the old default, set `prompt_on_rerun: true` explicitly. +`prompt_on_rerun` defaults to `false` (silently skip a step that ran recently). +If you relied on the old default, set `prompt_on_rerun: true` explicitly. Note +that `prompt_on_rerun` only applies to steps satisfied by a recent successful +run; a step satisfied by `satisfied_when` or a passing `check` is always +skipped silently. **Before:** diff --git a/docs/index.md b/docs/index.md index 2692142..0211d1f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -87,7 +87,7 @@ Bivvy remembers what's done. Steps: ✓ brew 2h ago - ⚠ ruby stale + ✓ ruby 2h ago ✓ node 2h ago ✓ docker 2h ago ◌ db never run @@ -105,7 +105,7 @@ ruby: target: "Gemfile.lock" ``` -Changed `Gemfile.lock`? Ruby step is stale. Didn't change it? Skip. +Changed `Gemfile.lock`? The ruby step runs. Didn't change it? It's skipped. No more "just run the whole script to be safe." diff --git a/docs/reference/config-reference.md b/docs/reference/config-reference.md index 8a1182a..a3be5f7 100644 --- a/docs/reference/config-reference.md +++ b/docs/reference/config-reference.md @@ -77,7 +77,7 @@ At minimum, a step needs either `command` or `template`. | `required` | bool | `false` | Cannot be skipped | | `auto_run` | bool | — | Auto-run when pipeline says step needs to run. `None` = use global default. | | `confirm` | bool | `false` | Always prompt user before running (never auto-runs) | -| `prompt_on_rerun` | bool | `false` | Ask before re-running | +| `prompt_on_rerun` | bool | `false` | Ask before re-running a step satisfied only by a recent successful run | | `allow_failure` | bool | `false` | Continue workflow on failure | | `retry` | int | `0` | Retry attempts on failure | | `env` | map | `{}` | Step-specific env vars | @@ -127,8 +127,8 @@ Tagged union — the `type` field determines which other fields apply. All check | `name` | string | — | Optional name for `satisfied_when` refs | | `target` | string | — | **Required.** File path, glob pattern, or command to hash | | `kind` | `file` \| `glob` \| `command` | `file` | Target type | -| `on_change` | `proceed` \| `fail` \| `require` | `proceed` | What a detected change means: `proceed` = step should run; `fail` = unexpected drift; `require` = flags `require_step` as needed | -| `require_step` | string | — | Step to flag when `on_change: require` and change is detected | +| `on_change` | `proceed` \| `fail` \| `require` | `proceed` | What a detected change means: `proceed` = step should run; `fail` = unexpected drift; `require` = records `require_step` in the check result. `require` does not currently change what Bivvy runs - the check reaches no verdict either way | +| `require_step` | string | - | Step named in the check result when `on_change: require` and a change is detected | | `baseline` | `each_run` \| `first_run` | `each_run` | When the baseline hash is updated | | `baseline_snapshot` | string | — | Compare against a named snapshot instead of run-based baseline | | `baseline_git` | string | — | Compare against content at a git ref | @@ -145,6 +145,13 @@ Tagged union — the `type` field determines which other fields apply. All check `all`: every sub-check must pass. `any`: at least one sub-check must pass. +Sub-checks that cannot reach a verdict (for example a `change` check on a file +that does not exist) are neither passes nor failures. `all` still fails as soon +as one sub-check definitely fails, but if none fail and any were undecided the +combinator reports no verdict. `any` still passes as soon as one sub-check +passes, but if every branch was undecided it reports no verdict rather than a +failure. In both cases Bivvy moves on to the next satisfaction signal. + ### Satisfaction Condition Used in the step's `satisfied_when` list. Each entry is one of: diff --git a/docs/templates/builtin.md b/docs/templates/builtin.md index 4af5cee..69aed85 100644 --- a/docs/templates/builtin.md +++ b/docs/templates/builtin.md @@ -213,10 +213,10 @@ steps: Each template provides: -- **Command** — the shell command to run -- **Completed checks** — how Bivvy decides the step is already done (it skips when checks pass) -- **Watches** — files whose changes mark the step dirty and force a re-run -- **Environment impact** — `PATH` or shell config changes the step makes +- **Command** - the shell command to run +- **Completed checks** - how Bivvy decides the step is already done (it skips when checks pass) +- **Watches** - files whose changes make the step unsatisfied, so it runs again on the next `bivvy run` +- **Environment impact** - `PATH` or shell config changes the step makes ### brew-bundle @@ -245,7 +245,7 @@ Installs Node.js dependencies using Yarn. - **Platforms**: macOS, Linux, Windows - **Detects**: `yarn.lock` - **Command**: `yarn install` -- **Completion check**: `yarn check --verify-tree` +- **Completion check**: `node_modules` directory exists - **Watches**: `yarn.lock`, `package.json` - **Environment**: sets `NODE_ENV=development` @@ -318,7 +318,7 @@ Installs Python packages from `requirements.txt`. - **Platforms**: macOS, Linux, Windows - **Detects**: `requirements.txt`, `pyproject.toml` - **Command**: `pip install -r requirements.txt` -- **Completion check**: `pip check` +- **Completion check**: change detection on `requirements.txt` (no separate completion command) - **Watches**: `requirements.txt` ### poetry-install @@ -407,7 +407,7 @@ Installs and activates the Node.js version pinned by `.nvmrc` or `.node-version` - **Platforms**: macOS, Linux, Windows - **Detects**: `.nvmrc`, `.node-version`, or `fnm` available - **Command**: `fnm install && fnm use` -- **Completion check**: `fnm current` +- **Completion check**: `fnm current` reports a version other than `none` - **Watches**: `.nvmrc`, `.node-version` ### rbenv-ruby @@ -477,7 +477,7 @@ Restores .NET project dependencies. - **Platforms**: macOS, Linux, Windows - **Detects**: `*.sln`, `*.csproj` - **Command**: `dotnet restore` -- **Completion check**: `dotnet restore --no-restore` reports nothing to do +- **Completion check**: change detection on `*.sln`, `*.csproj`, and `Directory.Build.props` (no separate completion command) - **Watches**: `*.sln`, `*.csproj`, `Directory.Build.props` ### dart-pub-get @@ -693,6 +693,7 @@ Bootstraps a Lerna monorepo workspace. - **Platforms**: macOS, Linux, Windows - **Detects**: `lerna.json` - **Command**: `npx lerna bootstrap` +- **Completion check**: `node_modules` directory exists - **Watches**: `lerna.json`, `packages/*/package.json` ## version-bump templates @@ -787,7 +788,7 @@ Installs Docker Desktop (macOS) or Docker Engine (Linux). On macOS, the template - **Platforms**: macOS, Linux, Windows - **Command**: Platform-specific (manual on macOS, `curl -fsSL https://get.docker.com | sh` on Debian/Ubuntu) -- **Completion check**: `docker info` +- **Completion check**: the `docker` binary is on `PATH` (installation only - the step does not require the daemon to be running) ### mise-install @@ -830,6 +831,7 @@ Installs the project's Node.js version using fnm. - **Platforms**: macOS, Linux, Windows - **Requires**: `fnm` - **Command**: `fnm install && fnm use` +- **Completion check**: `fnm current` reports a version other than `none` ### nvm-node @@ -869,7 +871,7 @@ Installs PostgreSQL database server and client tools. - **Platforms**: macOS, Linux - **Command**: Platform-specific (Homebrew on macOS, `apt-get` on Debian/Ubuntu) -- **Completion check**: `pg_isready -q` +- **Completion check**: the `psql` binary is on `PATH` (installation only - the step does not require the server to be running) ### redis-install @@ -877,7 +879,7 @@ Installs Redis in-memory data store. - **Platforms**: macOS, Linux - **Command**: Platform-specific (Homebrew on macOS, `apt-get` on Debian/Ubuntu) -- **Completion check**: `redis-cli ping` +- **Completion check**: the `redis-server` binary is on `PATH` (installation only - the step does not require the server to be running) ### rust-install diff --git a/src/checks/change.rs b/src/checks/change.rs index 5d1e9a0..d5f9555 100644 --- a/src/checks/change.rs +++ b/src/checks/change.rs @@ -3,7 +3,7 @@ //! Detects whether a specific target has changed from a known baseline //! by computing a SHA-256 hash and comparing it to a stored value. -use super::{ChangeKind, CheckOutcome, CheckResult, OnChange, SizeLimit}; +use super::{ChangeKind, CheckResult, OnChange, SizeLimit}; use sha2::{Digest, Sha256}; use std::path::Path; @@ -177,10 +177,26 @@ pub fn check_size_limit(path: &Path, size_limit: &SizeLimit) -> Option<(u64, u64 /// This is the core logic that interprets the hash comparison result /// according to the `on_change` semantics. /// -/// `require_step` is the step name to flag when `on_change` is `Require` -/// and a change is detected. It's passed separately because the `OnChange` -/// enum is a unit enum — the step name lives on the Change check's -/// `require_step` field. +/// A check answers "is this step's work already done?", so `Passed` means +/// satisfied (the runner skips the step) and `Failed` means not satisfied +/// (the runner runs it). The two `on_change` modes read that question +/// differently: +/// +/// - [`OnChange::Proceed`] treats the target as an input the step consumes, +/// so an unchanged target means the work is current (`Passed`) and a changed +/// target means it is stale (`Failed`). +/// - [`OnChange::Fail`] treats the target as something the step keeps stable, +/// so an unchanged target means the invariant holds (`Passed`) and a changed +/// target is unexpected drift (`Failed`). +/// - [`OnChange::Require`] reports on the step named by `require_step`, not on +/// the step hosting the check, so it is always `Indeterminate`. +/// +/// With no baseline the comparison never happens, so the outcome is +/// `Indeterminate` and satisfaction falls through to execution history. +/// +/// `require_step` is the step name reported when `on_change` is `Require`. +/// It's passed separately because the `OnChange` enum is a unit enum - the +/// step name lives on the Change check's `require_step` field. pub fn evaluate_change_result( target: &str, current_hash: &str, @@ -188,55 +204,52 @@ pub fn evaluate_change_result( on_change: &OnChange, require_step: Option<&str>, ) -> CheckResult { - match baseline_hash { - None => { - // No baseline exists — indeterminate - CheckResult::indeterminate( - format!( - "\u{25C7} No baseline for {} \u{2014} this run will establish it", - target - ), - format!( - "No baseline exists for {}. This run will establish the baseline.", - target - ), - ) - } - Some(baseline) => { - let changed = current_hash != baseline; - match (changed, on_change) { - (true, OnChange::Proceed) => CheckResult::passed(format!( - "\u{25B3} {} changed \u{2014} step will run", - target - )), - (false, OnChange::Proceed) => CheckResult::failed( - format!("\u{2713} {} unchanged", target), - "No changes detected since last run", + let Some(baseline) = baseline_hash else { + return CheckResult::indeterminate( + format!( + "\u{25C7} No baseline for {} - this run will establish it", + target + ), + format!( + "No baseline exists for {}. This run will establish the baseline.", + target + ), + ); + }; + + let changed = current_hash != baseline; + match (changed, on_change) { + (true, OnChange::Proceed) => CheckResult::failed( + format!("\u{25B3} {} changed - step will run", target), + format!("{} changed since the last baseline", target), + ), + (false, OnChange::Proceed) => CheckResult::passed(format!("\u{2713} {} unchanged", target)), + (true, OnChange::Fail) => CheckResult::failed( + format!("\u{2717} {} changed unexpectedly", target), + "File was expected to remain stable", + ), + (false, OnChange::Fail) => CheckResult::passed(format!("\u{2713} {} unchanged", target)), + (_, OnChange::Require) => { + let state = if changed { "changed" } else { "unchanged" }; + match require_step { + Some(step_name) => CheckResult::indeterminate_with_details( + format!( + "\u{25C7} {} {} - {} is the required step", + target, state, step_name + ), + format!( + "on_change: require reports on '{}' and gives no verdict for this step", + step_name + ), + format!("Change in {} is tracked for step {}", target, step_name), ), - (true, OnChange::Fail) => CheckResult::failed( - format!("\u{2717} {} changed unexpectedly", target), - "File was expected to remain stable", + None => CheckResult::indeterminate( + format!("\u{25C7} {} {} - no require_step set", target, state), + format!( + "on_change: require needs a require_step to name; {} gives no verdict for this step", + target + ), ), - (false, OnChange::Fail) => { - CheckResult::passed(format!("\u{2713} {} unchanged", target)) - } - (true, OnChange::Require) => { - let step_name = require_step.unwrap_or(""); - CheckResult { - outcome: CheckOutcome::Passed, - description: format!( - "\u{25B3} {} changed \u{2014} {} is now required", - target, step_name - ), - details: Some(format!( - "Step {} must run due to change in {}", - step_name, target - )), - } - } - (false, OnChange::Require) => { - CheckResult::passed(format!("\u{2713} {} unchanged", target)) - } } } } @@ -272,6 +285,7 @@ pub fn compute_target_hash( #[cfg(test)] mod tests { use super::*; + use crate::checks::CheckOutcome; use std::fs; use tempfile::TempDir; @@ -430,7 +444,7 @@ mod tests { // --- Change result evaluation tests --- #[test] - fn proceed_passes_when_changed() { + fn proceed_fails_when_changed() { let result = evaluate_change_result( "Gemfile.lock", "hash_new", @@ -438,12 +452,12 @@ mod tests { &OnChange::Proceed, None, ); - assert!(result.passed_check()); + assert!(!result.passed_check()); assert!(result.description.contains("changed")); } #[test] - fn proceed_fails_when_unchanged() { + fn proceed_passes_when_unchanged() { let result = evaluate_change_result( "Gemfile.lock", "hash_same", @@ -451,7 +465,7 @@ mod tests { &OnChange::Proceed, None, ); - assert!(!result.passed_check()); + assert!(result.passed_check()); assert!(result.description.contains("unchanged")); } @@ -481,7 +495,7 @@ mod tests { } #[test] - fn require_passes_when_changed() { + fn require_is_indeterminate_when_changed() { let result = evaluate_change_result( "Gemfile.lock", "hash_new", @@ -489,13 +503,12 @@ mod tests { &OnChange::Require, Some("bundle_install"), ); - assert!(result.passed_check()); + assert!(matches!(result.outcome, CheckOutcome::Indeterminate(_))); assert!(result.description.contains("bundle_install")); - assert!(result.description.contains("required")); } #[test] - fn require_passes_when_unchanged() { + fn require_is_indeterminate_when_unchanged() { let result = evaluate_change_result( "Gemfile.lock", "hash_same", @@ -503,7 +516,7 @@ mod tests { &OnChange::Require, Some("bundle_install"), ); - assert!(result.passed_check()); + assert!(matches!(result.outcome, CheckOutcome::Indeterminate(_))); } #[test] diff --git a/src/checks/evaluator.rs b/src/checks/evaluator.rs index c66b1f6..f0971e8 100644 --- a/src/checks/evaluator.rs +++ b/src/checks/evaluator.rs @@ -6,7 +6,7 @@ //! The evaluator has access to: //! - `project_root` — for resolving relative paths //! - `interpolation` — for variable resolution in commands -//! - `snapshots` — for change check baseline reads (read-only during evaluation) +//! - `snapshots` — for change check baseline reads during evaluation and baseline writes after execution //! //! It does NOT access the state store, UI, or gap checker. @@ -14,7 +14,7 @@ use super::change::{compute_target_hash, evaluate_change_result, HashResult}; use super::execution::evaluate_execution; use super::presence::evaluate_presence; use super::{BaselineConfig, Check, CheckOutcome, CheckResult}; -use crate::config::interpolation::{has_interpolation, resolve_string, InterpolationContext}; +use crate::config::interpolation::{interpolate_if_needed, InterpolationContext}; use crate::snapshots::{SnapshotKey, SnapshotStore}; use std::path::Path; @@ -53,8 +53,6 @@ pub struct CheckEvaluator<'a> { snapshots: &'a mut SnapshotStore, /// Step name for snapshot key construction. Set via [`with_step`](Self::with_step). step_name: Option, - /// Config hash for snapshot key construction. Set via [`with_step`](Self::with_step). - config_hash: Option, /// Workflow name for workflow-scoped snapshot keys. Set via [`with_workflow`](Self::with_workflow). workflow_name: Option, } @@ -71,7 +69,6 @@ impl<'a> CheckEvaluator<'a> { interpolation, snapshots, step_name: None, - config_hash: None, workflow_name: None, } } @@ -79,14 +76,13 @@ impl<'a> CheckEvaluator<'a> { /// Set the step context for change check baseline lookups. /// /// Must be called before evaluating change checks that need snapshot - /// store access. Without this, change checks will return an error. - pub fn with_step( - mut self, - step_name: impl Into, - config_hash: impl Into, - ) -> Self { + /// store access. Without it, change checks are indeterminate because no + /// baseline key can be constructed. + /// + /// The rest of the key comes from each change leaf's own + /// [`Check::config_hash`], so the step name is the only context required. + pub fn with_step(mut self, step_name: impl Into) -> Self { self.step_name = Some(step_name.into()); - self.config_hash = Some(config_hash.into()); self } @@ -139,16 +135,92 @@ impl<'a> CheckEvaluator<'a> { self.evaluate_all(checks) } + /// Record change-check baselines for a step after successful execution. + /// + /// Walks the check tree and records the current target hash for all + /// `Change` leaves that use automatic baselines (`_last_run` or + /// `_first_run`). Skips named snapshots and git baselines. + /// + /// For `baseline: first_run`, the baseline is recorded only once — when no + /// prior baseline exists for that key. + /// + /// Call this after a step has run and its result is [`StepStatus::Completed`]. + pub fn record_run_baselines(&mut self, check: &Check) { + self.record_baselines_recursive(check); + } + + fn record_baselines_recursive(&mut self, check: &Check) { + match check { + Check::Change { + target, + kind, + baseline, + baseline_snapshot, + baseline_git, + size_limit, + scope, + .. + } => { + if baseline_snapshot.is_some() || baseline_git.is_some() { + return; + } + + let resolved_target = if *kind == super::ChangeKind::Command { + self.interpolate(target) + } else { + target.to_string() + }; + + let hash = match super::change::compute_target_hash( + &resolved_target, + *kind, + self.project_root, + size_limit, + ) { + super::change::HashResult::Ok(h) => h, + _ => return, + }; + + let config_hash = check.config_hash(); + let baseline_name = self.resolve_baseline_name(baseline, None); + + let Some(key) = self.make_snapshot_key(scope, &config_hash) else { + return; + }; + + if *baseline == super::BaselineConfig::FirstRun + && self.snapshots.get_baseline(&key, &baseline_name).is_some() + { + return; + } + + self.snapshots + .record_baseline(&key, &baseline_name, hash, resolved_target); + } + Check::All { checks, .. } | Check::Any { checks, .. } => { + for c in checks { + self.record_baselines_recursive(c); + } + } + _ => {} + } + } + /// Interpolate a string if it contains variable references. fn interpolate(&self, input: &str) -> String { - if has_interpolation(input) { - resolve_string(input, self.interpolation).unwrap_or_else(|_| input.to_string()) - } else { - input.to_string() - } + interpolate_if_needed(input, self.interpolation) } /// Evaluate a change check. Expects `check` to be `Check::Change`. + /// + /// When the target cannot be hashed - it does not exist, hashing errored, + /// or it exceeds `size_limit` - the check has no evidence either way and + /// returns [`CheckOutcome::Indeterminate`] rather than a failure. A missing + /// target is not drift. + /// + /// The baseline is keyed by this leaf's own [`Check::config_hash`], so + /// sibling change checks in the same step keep independent baselines and + /// agree with the keys written by `bivvy snapshot capture`. fn evaluate_change(&mut self, check: &Check) -> CheckResult { let Check::Change { target, @@ -180,19 +252,34 @@ impl<'a> CheckEvaluator<'a> { HashResult::SizeLimitExceeded { actual, limit } => { let actual_mb = actual as f64 / (1024.0 * 1024.0); let limit_mb = limit as f64 / (1024.0 * 1024.0); - return CheckResult::failed( + return CheckResult::indeterminate_with_details( format!( "Change target '{}' exceeds size limit ({:.1} MB > {:.1} MB)", target, actual_mb, limit_mb ), + format!( + "{} could not be hashed, so no change verdict is available", + target + ), "Narrow the target or increase size_limit", ); } HashResult::NotFound(msg) => { - return CheckResult::failed(format!("{} not found", target), msg); + return CheckResult::indeterminate_with_details( + format!("{} not found", target), + format!("{} does not exist, so there is nothing to compare", target), + msg, + ); } HashResult::Error(msg) => { - return CheckResult::failed(format!("Error hashing {}", target), msg); + return CheckResult::indeterminate_with_details( + format!("Error hashing {}", target), + format!( + "{} could not be hashed, so no change verdict is available", + target + ), + msg, + ); } }; @@ -201,7 +288,7 @@ impl<'a> CheckEvaluator<'a> { let baseline_hash = if let Some(git_ref) = baseline_git { self.get_git_baseline(target, git_ref) } else { - self.get_snapshot_baseline(&baseline_name, scope) + self.get_snapshot_baseline(&baseline_name, scope, &check.config_hash()) }; evaluate_change_result( @@ -234,8 +321,9 @@ impl<'a> CheckEvaluator<'a> { &mut self, baseline_name: &str, scope: &super::SnapshotScope, + config_hash: &str, ) -> Option { - let key = self.make_snapshot_key(scope)?; + let key = self.make_snapshot_key(scope, config_hash)?; self.snapshots.get_baseline(&key, baseline_name) } @@ -260,9 +348,14 @@ impl<'a> CheckEvaluator<'a> { } /// Construct a snapshot key from the current step context. - fn make_snapshot_key(&self, scope: &super::SnapshotScope) -> Option { + /// + /// `config_hash` is the change leaf's own [`Check::config_hash`]. + fn make_snapshot_key( + &self, + scope: &super::SnapshotScope, + config_hash: &str, + ) -> Option { let step_name = self.step_name.as_ref()?; - let config_hash = self.config_hash.as_deref().unwrap_or("default"); Some(match scope { super::SnapshotScope::Project => SnapshotKey::project(step_name.clone(), config_hash), @@ -325,7 +418,11 @@ impl<'a> CheckEvaluator<'a> { /// Evaluate an `Any` combinator. /// /// At least one check must pass. Short-circuits on first pass. - /// If all checks fail (or are indeterminate), the result is failed. + /// + /// Disjunction precedence is `Passed > Indeterminate > Failed`: the result + /// is only `Failed` when every branch actually failed. If nothing passed but + /// at least one branch was indeterminate, the combinator is indeterminate, + /// because a branch that gave no answer could have been the one that passed. fn evaluate_any(&mut self, checks: &[Check]) -> CheckResult { if checks.is_empty() { return CheckResult::failed( @@ -336,21 +433,34 @@ impl<'a> CheckEvaluator<'a> { let mut descriptions = Vec::new(); let mut last_details = None; + let mut indeterminate_reason: Option = None; for check in checks { let result = self.evaluate(check); descriptions.push(result.description.clone()); - if result.passed_check() { - return CheckResult::passed(format!("Any: {} passed", result.description)); + match &result.outcome { + CheckOutcome::Passed => { + return CheckResult::passed(format!("Any: {} passed", result.description)); + } + CheckOutcome::Indeterminate(reason) => { + indeterminate_reason = Some(reason.clone()); + } + CheckOutcome::Failed => {} } last_details = result.details; } - CheckResult { - outcome: CheckOutcome::Failed, - description: format!("Any: none passed ({})", descriptions.join(", ")), - details: last_details, + match indeterminate_reason { + Some(reason) => CheckResult::indeterminate( + format!("Any: indeterminate ({})", descriptions.join(", ")), + reason, + ), + None => CheckResult { + outcome: CheckOutcome::Failed, + description: format!("Any: none passed ({})", descriptions.join(", ")), + details: last_details, + }, } } } @@ -603,8 +713,8 @@ mod tests { let ctx = make_context(); let mut store = make_store(snap_dir.path()); - let mut eval = CheckEvaluator::new(temp.path(), &ctx, &mut store) - .with_step("bundle_install", "cfg123"); + let mut eval = + CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("bundle_install"); let check = Check::Change { name: None, @@ -625,7 +735,7 @@ mod tests { } #[test] - fn evaluator_change_with_matching_baseline_proceed_fails() { + fn evaluator_change_with_matching_baseline_proceed_passes() { let temp = TempDir::new().unwrap(); let snap_dir = TempDir::new().unwrap(); fs::write(temp.path().join("Gemfile.lock"), "gem contents").unwrap(); @@ -633,26 +743,6 @@ mod tests { let ctx = make_context(); let mut store = make_store(snap_dir.path()); - // First compute and store the baseline - let current_hash = { - let hash_result = compute_target_hash( - "Gemfile.lock", - ChangeKind::File, - temp.path(), - &SizeLimit::default(), - ); - match hash_result { - HashResult::Ok(h) => h, - _ => panic!("Expected hash"), - } - }; - - let key = SnapshotKey::project("bundle_install", "cfg123"); - store.record_baseline(&key, "_last_run", current_hash, "Gemfile.lock".to_string()); - - let mut eval = CheckEvaluator::new(temp.path(), &ctx, &mut store) - .with_step("bundle_install", "cfg123"); - let check = Check::Change { name: None, target: "Gemfile.lock".to_string(), @@ -666,14 +756,30 @@ mod tests { scope: SnapshotScope::Project, }; - // File hasn't changed → on_change: proceed → fails (no reason to run) + let current_hash = match compute_target_hash( + "Gemfile.lock", + ChangeKind::File, + temp.path(), + &SizeLimit::default(), + ) { + HashResult::Ok(h) => h, + _ => panic!("Expected hash"), + }; + + let key = SnapshotKey::project("bundle_install", check.config_hash()); + store.record_baseline(&key, "_last_run", current_hash, "Gemfile.lock".to_string()); + + let mut eval = + CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("bundle_install"); + + // File unchanged since baseline - proceed means "skip when nothing to do" let result = eval.evaluate(&check); - assert!(!result.passed_check()); + assert!(result.passed_check()); assert!(result.description.contains("unchanged")); } #[test] - fn evaluator_change_with_different_baseline_proceed_passes() { + fn evaluator_change_with_different_baseline_proceed_fails() { let temp = TempDir::new().unwrap(); let snap_dir = TempDir::new().unwrap(); fs::write(temp.path().join("Gemfile.lock"), "new contents").unwrap(); @@ -681,18 +787,6 @@ mod tests { let ctx = make_context(); let mut store = make_store(snap_dir.path()); - // Store a different baseline hash - let key = SnapshotKey::project("bundle_install", "cfg123"); - store.record_baseline( - &key, - "_last_run", - "sha256:old_hash_value".to_string(), - "Gemfile.lock".to_string(), - ); - - let mut eval = CheckEvaluator::new(temp.path(), &ctx, &mut store) - .with_step("bundle_install", "cfg123"); - let check = Check::Change { name: None, target: "Gemfile.lock".to_string(), @@ -706,9 +800,20 @@ mod tests { scope: SnapshotScope::Project, }; - // File changed → on_change: proceed → passes (step should run) + let key = SnapshotKey::project("bundle_install", check.config_hash()); + store.record_baseline( + &key, + "_last_run", + "sha256:old_hash_value".to_string(), + "Gemfile.lock".to_string(), + ); + + let mut eval = + CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("bundle_install"); + + // File changed since baseline - proceed means "run when something changed" let result = eval.evaluate(&check); - assert!(result.passed_check()); + assert!(!result.passed_check()); assert!(result.description.contains("changed")); } @@ -721,23 +826,6 @@ mod tests { let ctx = make_context(); let mut store = make_store(snap_dir.path()); - // Store matching baseline - let current_hash = match compute_target_hash( - ".env.example", - ChangeKind::File, - temp.path(), - &SizeLimit::default(), - ) { - HashResult::Ok(h) => h, - _ => panic!("Expected hash"), - }; - - let key = SnapshotKey::project("validate_env", "cfg456"); - store.record_baseline(&key, "_last_run", current_hash, ".env.example".to_string()); - - let mut eval = - CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("validate_env", "cfg456"); - let check = Check::Change { name: None, target: ".env.example".to_string(), @@ -751,6 +839,21 @@ mod tests { scope: SnapshotScope::Project, }; + let current_hash = match compute_target_hash( + ".env.example", + ChangeKind::File, + temp.path(), + &SizeLimit::default(), + ) { + HashResult::Ok(h) => h, + _ => panic!("Expected hash"), + }; + + let key = SnapshotKey::project("validate_env", check.config_hash()); + store.record_baseline(&key, "_last_run", current_hash, ".env.example".to_string()); + + let mut eval = CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("validate_env"); + let result = eval.evaluate(&check); assert!(result.passed_check()); } @@ -764,17 +867,6 @@ mod tests { let ctx = make_context(); let mut store = make_store(snap_dir.path()); - let key = SnapshotKey::project("validate_env", "cfg456"); - store.record_baseline( - &key, - "_last_run", - "sha256:old_hash".to_string(), - ".env.example".to_string(), - ); - - let mut eval = - CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("validate_env", "cfg456"); - let check = Check::Change { name: None, target: ".env.example".to_string(), @@ -788,13 +880,23 @@ mod tests { scope: SnapshotScope::Project, }; + let key = SnapshotKey::project("validate_env", check.config_hash()); + store.record_baseline( + &key, + "_last_run", + "sha256:old_hash".to_string(), + ".env.example".to_string(), + ); + + let mut eval = CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("validate_env"); + let result = eval.evaluate(&check); assert!(!result.passed_check()); assert!(result.description.contains("changed unexpectedly")); } #[test] - fn evaluator_change_on_change_require_flags_step() { + fn evaluator_change_on_change_require_is_indeterminate() { let temp = TempDir::new().unwrap(); let snap_dir = TempDir::new().unwrap(); fs::write(temp.path().join("Gemfile.lock"), "new gem contents").unwrap(); @@ -802,17 +904,6 @@ mod tests { let ctx = make_context(); let mut store = make_store(snap_dir.path()); - let key = SnapshotKey::project("check_gemfile", "cfg789"); - store.record_baseline( - &key, - "_last_run", - "sha256:old_hash".to_string(), - "Gemfile.lock".to_string(), - ); - - let mut eval = - CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("check_gemfile", "cfg789"); - let check = Check::Change { name: None, target: "Gemfile.lock".to_string(), @@ -826,10 +917,20 @@ mod tests { scope: SnapshotScope::Project, }; + let key = SnapshotKey::project("check_gemfile", check.config_hash()); + store.record_baseline( + &key, + "_last_run", + "sha256:old_hash".to_string(), + "Gemfile.lock".to_string(), + ); + + let mut eval = + CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("check_gemfile"); + let result = eval.evaluate(&check); - assert!(result.passed_check()); + assert!(matches!(result.outcome, CheckOutcome::Indeterminate(_))); assert!(result.description.contains("bundle_install")); - assert!(result.description.contains("required")); } #[test] @@ -839,7 +940,7 @@ mod tests { let ctx = make_context(); let mut store = make_store(snap_dir.path()); - let mut eval = CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("step", "cfg"); + let mut eval = CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("step"); let check = Check::Change { name: None, @@ -857,6 +958,10 @@ mod tests { let result = eval.evaluate(&check); assert!(!result.passed_check()); assert!(result.description.contains("not found")); + assert!( + matches!(result.outcome, CheckOutcome::Indeterminate(_)), + "missing target must be Indeterminate, not Failed" + ); } #[test] @@ -867,7 +972,7 @@ mod tests { let ctx = make_context(); let mut store = make_store(snap_dir.path()); - let mut eval = CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("step", "cfg"); + let mut eval = CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("step"); let check = Check::Change { name: None, @@ -885,6 +990,10 @@ mod tests { let result = eval.evaluate(&check); assert!(!result.passed_check()); assert!(result.description.contains("exceeds size limit")); + assert!( + matches!(result.outcome, CheckOutcome::Indeterminate(_)), + "size limit exceeded must be Indeterminate, not Failed" + ); } #[test] @@ -896,18 +1005,6 @@ mod tests { let ctx = make_context(); let mut store = make_store(snap_dir.path()); - // Store a named snapshot baseline - let key = SnapshotKey::project("bundle_install", "cfg123"); - store.capture_named( - &key, - "v1.0", - "sha256:old_release_hash".to_string(), - "Gemfile.lock".to_string(), - ); - - let mut eval = CheckEvaluator::new(temp.path(), &ctx, &mut store) - .with_step("bundle_install", "cfg123"); - let check = Check::Change { name: None, target: "Gemfile.lock".to_string(), @@ -921,9 +1018,20 @@ mod tests { scope: SnapshotScope::Project, }; - // File differs from named snapshot → on_change: proceed → passes + let key = SnapshotKey::project("bundle_install", check.config_hash()); + store.capture_named( + &key, + "v1.0", + "sha256:old_release_hash".to_string(), + "Gemfile.lock".to_string(), + ); + + let mut eval = + CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("bundle_install"); + + // File differs from named snapshot - proceed means not satisfied (step must run) let result = eval.evaluate(&check); - assert!(result.passed_check()); + assert!(!result.passed_check()); } #[test] @@ -1241,6 +1349,132 @@ mod tests { assert!(!result.passed_check()); } + #[test] + fn evaluator_any_indeterminate_when_all_indeterminate() { + let temp = TempDir::new().unwrap(); + let snap_dir = TempDir::new().unwrap(); + let ctx = make_context(); + let mut store = make_store(snap_dir.path()); + let mut eval = CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("step"); + + let check = Check::Any { + name: None, + checks: vec![ + Check::Change { + name: None, + target: "nonexistent1.lock".to_string(), + kind: ChangeKind::File, + on_change: OnChange::Proceed, + require_step: None, + baseline: BaselineConfig::EachRun, + baseline_snapshot: None, + baseline_git: None, + size_limit: SizeLimit::default(), + scope: SnapshotScope::Project, + }, + Check::Change { + name: None, + target: "nonexistent2.lock".to_string(), + kind: ChangeKind::File, + on_change: OnChange::Proceed, + require_step: None, + baseline: BaselineConfig::EachRun, + baseline_snapshot: None, + baseline_git: None, + size_limit: SizeLimit::default(), + scope: SnapshotScope::Project, + }, + ], + }; + + let result = eval.evaluate(&check); + assert!(!result.passed_check()); + assert!( + matches!(result.outcome, CheckOutcome::Indeterminate(_)), + "all-Indeterminate Any must be Indeterminate, not Failed" + ); + } + + #[test] + fn evaluator_any_indeterminate_beats_failed() { + let temp = TempDir::new().unwrap(); + let snap_dir = TempDir::new().unwrap(); + let ctx = make_context(); + let mut store = make_store(snap_dir.path()); + let mut eval = CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("step"); + + let check = Check::Any { + name: None, + checks: vec![ + Check::Presence { + name: None, + target: Some("missing.txt".to_string()), + kind: Some(PresenceKind::File), + command: None, + }, + Check::Change { + name: None, + target: "nonexistent.lock".to_string(), + kind: ChangeKind::File, + on_change: OnChange::Proceed, + require_step: None, + baseline: BaselineConfig::EachRun, + baseline_snapshot: None, + baseline_git: None, + size_limit: SizeLimit::default(), + scope: SnapshotScope::Project, + }, + ], + }; + + let result = eval.evaluate(&check); + assert!(!result.passed_check()); + assert!( + matches!(result.outcome, CheckOutcome::Indeterminate(_)), + "[Failed, Indeterminate] Any must be Indeterminate, not Failed" + ); + } + + #[test] + fn evaluator_any_indeterminate_beats_failed_reversed_order() { + let temp = TempDir::new().unwrap(); + let snap_dir = TempDir::new().unwrap(); + let ctx = make_context(); + let mut store = make_store(snap_dir.path()); + let mut eval = CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("step"); + + let check = Check::Any { + name: None, + checks: vec![ + Check::Change { + name: None, + target: "nonexistent.lock".to_string(), + kind: ChangeKind::File, + on_change: OnChange::Proceed, + require_step: None, + baseline: BaselineConfig::EachRun, + baseline_snapshot: None, + baseline_git: None, + size_limit: SizeLimit::default(), + scope: SnapshotScope::Project, + }, + Check::Presence { + name: None, + target: Some("missing.txt".to_string()), + kind: Some(PresenceKind::File), + command: None, + }, + ], + }; + + let result = eval.evaluate(&check); + assert!(!result.passed_check()); + assert!( + matches!(result.outcome, CheckOutcome::Indeterminate(_)), + "[Indeterminate, Failed] Any must be Indeterminate, not Failed" + ); + } + // --- evaluate_all_checks helper --- #[test] @@ -1282,17 +1516,6 @@ mod tests { let ctx = make_context(); let mut store = make_store(snap_dir.path()); - let key = SnapshotKey::project("db_migrate", "cfgglob"); - store.record_baseline( - &key, - "_last_run", - "sha256:old_glob_hash".to_string(), - "*.rb".to_string(), - ); - - let mut eval = - CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("db_migrate", "cfgglob"); - let check = Check::Change { name: None, target: "*.rb".to_string(), @@ -1306,9 +1529,19 @@ mod tests { scope: SnapshotScope::Project, }; - // Current glob hash will differ from stored → passes + let key = SnapshotKey::project("db_migrate", check.config_hash()); + store.record_baseline( + &key, + "_last_run", + "sha256:old_glob_hash".to_string(), + "*.rb".to_string(), + ); + + let mut eval = CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("db_migrate"); + + // Current glob hash differs from stored - proceed means not satisfied let result = eval.evaluate(&check); - assert!(result.passed_check()); + assert!(!result.passed_check()); } // --- Change check with command through evaluator --- @@ -1321,17 +1554,6 @@ mod tests { let ctx = make_context(); let mut store = make_store(snap_dir.path()); - let key = SnapshotKey::project("check_version", "cfgcmd"); - store.record_baseline( - &key, - "_last_run", - "sha256:old_output_hash".to_string(), - "echo hello".to_string(), - ); - - let mut eval = - CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("check_version", "cfgcmd"); - let check = Check::Change { name: None, target: "echo hello".to_string(), @@ -1345,8 +1567,19 @@ mod tests { scope: SnapshotScope::Project, }; - // Note: baseline is FirstRun but we stored as "_last_run" - // FirstRun → looks for "_first_run" baseline → won't find it → indeterminate + // Store under _last_run but baseline: first_run looks for _first_run - no match + let key = SnapshotKey::project("check_version", check.config_hash()); + store.record_baseline( + &key, + "_last_run", + "sha256:old_output_hash".to_string(), + "echo hello".to_string(), + ); + + let mut eval = + CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("check_version"); + + // FirstRun looks for "_first_run" baseline, only "_last_run" exists - indeterminate let result = eval.evaluate(&check); assert!(matches!(result.outcome, CheckOutcome::Indeterminate(_))); } @@ -1370,12 +1603,6 @@ mod tests { format!("sha256:{}", hex::encode(Sha256::digest(&output.stdout))) }; - let key = SnapshotKey::project("check_version", "cfgcmd2"); - store.record_baseline(&key, "_first_run", actual_hash, "echo stable".to_string()); - - let mut eval = CheckEvaluator::new(temp.path(), &ctx, &mut store) - .with_step("check_version", "cfgcmd2"); - let check = Check::Change { name: None, target: "echo stable".to_string(), @@ -1389,6 +1616,12 @@ mod tests { scope: SnapshotScope::Project, }; + let key = SnapshotKey::project("check_version", check.config_hash()); + store.record_baseline(&key, "_first_run", actual_hash, "echo stable".to_string()); + + let mut eval = + CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("check_version"); + // Output hasn't changed from first run → on_change: fail → passes let result = eval.evaluate(&check); assert!(result.passed_check()); @@ -1461,8 +1694,19 @@ mod tests { let ctx = make_context(); let mut store = make_store(snap_dir.path()); - // Store baseline under workflow-scoped key - let wf_key = SnapshotKey::workflow("bundle_install", "ci", "cfg123"); + let check = Check::Change { + name: None, + target: "Gemfile.lock".to_string(), + kind: ChangeKind::File, + on_change: OnChange::Proceed, + require_step: None, + baseline: BaselineConfig::EachRun, + baseline_snapshot: None, + baseline_git: None, + size_limit: SizeLimit::default(), + scope: SnapshotScope::Workflow, + }; + let current_hash = match compute_target_hash( "Gemfile.lock", ChangeKind::File, @@ -1472,6 +1716,8 @@ mod tests { HashResult::Ok(h) => h, _ => panic!("Expected hash"), }; + + let wf_key = SnapshotKey::workflow("bundle_install", "ci", check.config_hash()); store.record_baseline( &wf_key, "_last_run", @@ -1480,25 +1726,12 @@ mod tests { ); let mut eval = CheckEvaluator::new(temp.path(), &ctx, &mut store) - .with_step("bundle_install", "cfg123") + .with_step("bundle_install") .with_workflow("ci"); - let check = Check::Change { - name: None, - target: "Gemfile.lock".to_string(), - kind: ChangeKind::File, - on_change: OnChange::Proceed, - require_step: None, - baseline: BaselineConfig::EachRun, - baseline_snapshot: None, - baseline_git: None, - size_limit: SizeLimit::default(), - scope: SnapshotScope::Workflow, - }; - - // File unchanged → on_change: proceed → fails (no reason to run) + // File unchanged since baseline - proceed means satisfied (skip) let result = eval.evaluate(&check); - assert!(!result.passed_check()); + assert!(result.passed_check()); assert!(result.description.contains("unchanged")); } @@ -1511,20 +1744,6 @@ mod tests { let ctx = make_context(); let mut store = make_store(snap_dir.path()); - // Store baseline under project-scoped key only - let proj_key = SnapshotKey::project("bundle_install", "cfg123"); - store.record_baseline( - &proj_key, - "_last_run", - "sha256:some_hash".to_string(), - "Gemfile.lock".to_string(), - ); - - // Evaluate with workflow scope — no workflow baseline exists - let mut eval = CheckEvaluator::new(temp.path(), &ctx, &mut store) - .with_step("bundle_install", "cfg123") - .with_workflow("ci"); - let check = Check::Change { name: None, target: "Gemfile.lock".to_string(), @@ -1538,7 +1757,21 @@ mod tests { scope: SnapshotScope::Workflow, }; - // Workflow key has no baseline → indeterminate + // Store baseline under project-scoped key only (not workflow-scoped) + let proj_key = SnapshotKey::project("bundle_install", check.config_hash()); + store.record_baseline( + &proj_key, + "_last_run", + "sha256:some_hash".to_string(), + "Gemfile.lock".to_string(), + ); + + // Evaluate with workflow scope — no workflow baseline exists + let mut eval = CheckEvaluator::new(temp.path(), &ctx, &mut store) + .with_step("bundle_install") + .with_workflow("ci"); + + // Workflow key has no baseline - indeterminate let result = eval.evaluate(&check); assert!(matches!(result.outcome, CheckOutcome::Indeterminate(_))); } @@ -1552,22 +1785,6 @@ mod tests { let ctx = make_context(); let mut store = make_store(snap_dir.path()); - // Store baseline under project key - let key = SnapshotKey::project("step", "cfg"); - let current_hash = match compute_target_hash( - "file.txt", - ChangeKind::File, - temp.path(), - &SizeLimit::default(), - ) { - HashResult::Ok(h) => h, - _ => panic!("Expected hash"), - }; - store.record_baseline(&key, "_last_run", current_hash, "file.txt".to_string()); - - // No with_workflow call — falls back to project scope - let mut eval = CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("step", "cfg"); - let check = Check::Change { name: None, target: "file.txt".to_string(), @@ -1581,8 +1798,25 @@ mod tests { scope: SnapshotScope::Workflow, }; - // Falls back to project scope, finds baseline → unchanged → fails + let current_hash = match compute_target_hash( + "file.txt", + ChangeKind::File, + temp.path(), + &SizeLimit::default(), + ) { + HashResult::Ok(h) => h, + _ => panic!("Expected hash"), + }; + + // Store baseline under project key + let key = SnapshotKey::project("step", check.config_hash()); + store.record_baseline(&key, "_last_run", current_hash, "file.txt".to_string()); + + // No with_workflow call - falls back to project scope + let mut eval = CheckEvaluator::new(temp.path(), &ctx, &mut store).with_step("step"); + + // Falls back to project scope, finds baseline, file unchanged - satisfied (skip) let result = eval.evaluate(&check); - assert!(!result.passed_check()); + assert!(result.passed_check()); } } diff --git a/src/checks/mod.rs b/src/checks/mod.rs index 1234152..0da20d3 100644 --- a/src/checks/mod.rs +++ b/src/checks/mod.rs @@ -189,6 +189,46 @@ impl Check { let hash = Sha256::digest(yaml.as_bytes()); hex::encode(hash)[..8].to_string() } + + /// Collect every [`Check::Change`] leaf in this check tree, in declaration order. + /// + /// Combinators are walked recursively; non-change leaves are ignored. Each + /// returned leaf owns the `config_hash` that keys its baseline, so callers + /// that read and write baselines agree on the key. + /// + /// ``` + /// use bivvy::checks::Check; + /// + /// let check: Check = serde_yaml::from_str(r#" + /// type: all + /// checks: + /// - type: presence + /// target: node_modules + /// - type: change + /// target: package-lock.json + /// "#).unwrap(); + /// + /// let leaves = check.change_checks(); + /// assert_eq!(leaves.len(), 1); + /// assert_eq!(leaves[0].type_name(), "change"); + /// ``` + pub fn change_checks(&self) -> Vec<&Check> { + let mut out = Vec::new(); + self.collect_change_checks(&mut out); + out + } + + fn collect_change_checks<'c>(&'c self, out: &mut Vec<&'c Check>) { + match self { + Check::Change { .. } => out.push(self), + Check::All { checks, .. } | Check::Any { checks, .. } => { + for check in checks { + check.collect_change_checks(out); + } + } + _ => {} + } + } } /// Subtype for presence checks. @@ -231,27 +271,35 @@ pub enum ChangeKind { /// What a detected change means for a change check. /// +/// A check answers "is this step's work already done?", so [`CheckOutcome::Passed`] +/// means satisfied (skip) and [`CheckOutcome::Failed`] means not satisfied (run). +/// /// In YAML: /// ```yaml -/// on_change: proceed # step should run when target changed -/// on_change: fail # check fails when target changed -/// on_change: require # flags require_step as needed when target changed +/// on_change: proceed # step's work is stale when the target changed +/// on_change: fail # target is expected to stay stable +/// on_change: require # informational: names another step in require_step /// ``` -/// -/// When `on_change: require`, the `require_step` field on the Change check -/// specifies which step to flag. The check itself always passes. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum OnChange { - /// Change detected = check passes (step should run). - /// No change = check fails (step has no reason to run). + /// The step consumes the target, so a change means its work is out of date. + /// + /// Change detected = check fails (the step must run). + /// No change = check passes (the step's work is still current). #[default] Proceed, - /// Change detected = check fails (something unstable changed). + /// The step keeps the target stable, so a change is unexpected drift. + /// + /// Change detected = check fails (drift). /// No change = check passes (stability maintained). Fail, - /// Change detected = the step named in `require_step` is flagged as required. - /// The check itself always passes regardless of change. + /// The change is reported against the step named in `require_step`. + /// + /// This makes a statement about a different step, so it contributes no + /// evidence about the step that hosts it. The outcome is always + /// [`CheckOutcome::Indeterminate`] and satisfaction falls through to + /// execution history. Require, } @@ -271,7 +319,7 @@ pub enum BaselineConfig { /// Baseline updated after each successful run (default). #[default] EachRun, - /// Baseline established on first evaluation, never updated. + /// Baseline established after the first successful run, never updated. FirstRun, } @@ -387,6 +435,23 @@ impl CheckResult { } } + /// Create an indeterminate result that carries actionable details. + /// + /// Use this when the check declined to produce a verdict but still has + /// something the user can act on, such as the underlying IO error or a + /// remediation hint. + pub fn indeterminate_with_details( + description: impl Into, + reason: impl Into, + details: impl Into, + ) -> Self { + Self { + outcome: CheckOutcome::Indeterminate(reason.into()), + description: description.into(), + details: Some(details.into()), + } + } + /// Whether the check passed. pub fn passed_check(&self) -> bool { matches!(self.outcome, CheckOutcome::Passed) diff --git a/src/config/interpolation.rs b/src/config/interpolation.rs index 06ae827..01ac8f0 100644 --- a/src/config/interpolation.rs +++ b/src/config/interpolation.rs @@ -249,6 +249,31 @@ pub fn resolve_string(input: &str, context: &InterpolationContext) -> Result String { + if has_interpolation(input) { + resolve_string(input, context).unwrap_or_else(|_| input.to_string()) + } else { + input.to_string() + } +} + /// Resolve string with optional default value for missing variables. /// /// Unlike `resolve_string`, this never fails - missing variables diff --git a/src/runner/engine.rs b/src/runner/engine.rs index 3d6ac95..5bbd61f 100644 --- a/src/runner/engine.rs +++ b/src/runner/engine.rs @@ -859,6 +859,57 @@ mod tests { ); } + #[test] + fn failing_check_overrides_recent_success_and_runs() { + let mut step = make_step("build", vec![]); + step.behavior.prompt_on_rerun = false; + step.execution.check = Some(crate::checks::Check::Execution { + name: None, + command: "exit 1".to_string(), + validation: Default::default(), + }); + let mut steps = HashMap::new(); + steps.insert("build".to_string(), step); + + let temp = tempfile::TempDir::new().unwrap(); + let mut snapshots = SnapshotStore::new(temp.path().to_path_buf()); + let context = InterpolationContext::new(); + let mut cache = SatisfactionCache::empty(temp.path().join("satisfaction.json")); + let failed = HashSet::new(); + let skipped = HashSet::new(); + let satisfied = HashSet::new(); + + let mut state = crate::state::StateStore::new( + &crate::state::ProjectId::from_path(temp.path()).unwrap(), + ); + state.record_step_result( + "build", + crate::state::StepStatus::Success, + std::time::Duration::from_secs(1), + ); + + let mut ctx = make_context( + &steps, + &mut snapshots, + &context, + &mut cache, + &failed, + &skipped, + &satisfied, + ); + ctx.state = Some(&state); + + // Without the check the recent success would auto-skip this step, exactly as + // satisfied_via_history_without_prompt_on_rerun_auto_skips asserts. + let result = evaluate_step("build", &mut ctx); + assert_eq!( + result.decision, + StepDecision::AutoRun, + "a failing check must override execution history, got {:?}", + result.decision + ); + } + #[test] fn satisfied_via_history_without_prompt_on_rerun_auto_skips() { let mut step = make_step("build", vec![]); diff --git a/src/runner/orchestrate.rs b/src/runner/orchestrate.rs index ef9363b..bbcc828 100644 --- a/src/runner/orchestrate.rs +++ b/src/runner/orchestrate.rs @@ -10,6 +10,7 @@ use std::collections::{HashMap, HashSet}; use std::time::Instant; +use crate::checks::evaluator::CheckEvaluator; use crate::checks::CheckResult; use crate::config::schema::StepOverride; use crate::error::{BivvyError, Result}; @@ -204,6 +205,21 @@ impl<'a> WorkflowRunner<'a> { }; satisfaction_cache.store(step_name, record); + // Record change-check baselines so the next run can compare. + // Skip in dry-run mode since no commands actually ran. + if !options.dry_run { + if let Some(check) = step.execution.effective_check() { + let mut evaluator = CheckEvaluator::new( + project_root, + &context, + &mut self.snapshot_store, + ) + .with_step(step_name) + .with_workflow(workflow_name); + evaluator.record_run_baselines(&check); + } + } + results.push(result); } diff --git a/src/runner/satisfaction.rs b/src/runner/satisfaction.rs index 991c640..e3ae9ea 100644 --- a/src/runner/satisfaction.rs +++ b/src/runner/satisfaction.rs @@ -14,7 +14,7 @@ use std::collections::HashMap; use crate::checks::evaluator::CheckEvaluator; -use crate::checks::{Check, CheckResult, SatisfactionCondition}; +use crate::checks::{Check, CheckOutcome, CheckResult, SatisfactionCondition}; use crate::runner::RerunWindow; use crate::state::satisfaction::{ PresenceKind as EvidencePresenceKind, SatisfactionEvidence, SatisfactionSource, @@ -156,6 +156,12 @@ pub struct ComputedSatisfaction { /// 2. Step's check (presence, execution, change) passes /// 3. Within rerun window and last run succeeded /// +/// Signals 1 and 2 are the step's own definition of "already done", so when either +/// is present and definitely fails, the step is unsatisfied and evaluation stops +/// there. Otherwise a stale successful run could overrule a check that just reported +/// work still to do. A check that is `Indeterminate` has given no answer, so it falls +/// through to signal 3, as does a step that declares neither signal. +/// /// Returns `ComputedSatisfaction` with the result and evidence. /// /// # Arguments @@ -231,7 +237,18 @@ pub fn compute_satisfaction( description: check_result.description.clone(), }; } - // Check exists but failed — not satisfied, but we still check rerun window + // A definite failure is the step's own answer to "am I already done?", so it + // must not be overruled by execution history. An `Indeterminate` outcome is + // not an answer at all (a change check with no baseline, say), so that case + // still falls through. + if matches!(check_result.outcome, CheckOutcome::Failed) { + return ComputedSatisfaction { + satisfied: false, + source: check_to_source(&check), + evidence: SatisfactionEvidence::None, + description: format!("check failed: {}", check_result.description), + }; + } } // 3. Within rerun window + last run succeeded @@ -1012,6 +1029,130 @@ mod compute_satisfaction_tests { assert_eq!(result.source, SatisfactionSource::ExecutionHistory); } + #[test] + fn failing_check_blocks_fallthrough_to_rerun_window() { + let dir = tempfile::TempDir::new().unwrap(); + // "output" does NOT exist, so the check fails + + let step = make_step( + vec![], + Some(Check::Presence { + name: None, + target: Some("output".to_string()), + kind: Some(crate::checks::PresenceKind::File), + command: None, + }), + ); + + let mut snapshots = crate::snapshots::SnapshotStore::new(std::env::temp_dir()); + let context = crate::config::interpolation::InterpolationContext::new(); + let mut evaluator = CheckEvaluator::new(dir.path(), &context, &mut snapshots); + let named = HashMap::new(); + + // A recent successful run would satisfy the rerun window on its own + let last_success = Some(chrono::Utc::now() - chrono::Duration::hours(1)); + let window = RerunWindow::Duration(std::time::Duration::from_secs(4 * 3600)); + + let result = compute_satisfaction( + &step, + &mut evaluator, + &named, + "test_step", + &window, + last_success, + ); + + assert!(!result.satisfied); + assert_eq!(result.source, SatisfactionSource::PresenceCheck); + assert!( + result.description.starts_with("check failed: "), + "expected a check-failure description, got {:?}", + result.description + ); + } + + #[test] + fn indeterminate_check_falls_through_to_rerun_window() { + let dir = tempfile::TempDir::new().unwrap(); + std::fs::write(dir.path().join("Gemfile.lock"), "deps").unwrap(); + + // A change check with no stored baseline is Indeterminate rather than Failed: + // it has given no answer, so execution history still applies. + let step = make_step( + vec![], + Some(Check::Change { + name: None, + target: "Gemfile.lock".to_string(), + kind: crate::checks::ChangeKind::File, + on_change: crate::checks::OnChange::Proceed, + require_step: None, + baseline: crate::checks::BaselineConfig::EachRun, + baseline_snapshot: None, + baseline_git: None, + size_limit: crate::checks::SizeLimit::default(), + scope: crate::checks::SnapshotScope::Project, + }), + ); + + let snap_dir = tempfile::TempDir::new().unwrap(); + let mut snapshots = crate::snapshots::SnapshotStore::new(snap_dir.path().to_path_buf()); + let context = crate::config::interpolation::InterpolationContext::new(); + let mut evaluator = + CheckEvaluator::new(dir.path(), &context, &mut snapshots).with_step("test_step"); + let named = HashMap::new(); + + let last_success = Some(chrono::Utc::now() - chrono::Duration::hours(1)); + let window = RerunWindow::Duration(std::time::Duration::from_secs(4 * 3600)); + + let result = compute_satisfaction( + &step, + &mut evaluator, + &named, + "test_step", + &window, + last_success, + ); + + assert!( + result.satisfied, + "an indeterminate check must not suppress execution history: {:?}", + result.description + ); + assert_eq!(result.source, SatisfactionSource::ExecutionHistory); + } + + #[test] + fn failing_check_blocks_fallthrough_with_forever_window() { + let dir = tempfile::TempDir::new().unwrap(); + let step = make_step( + vec![], + Some(Check::Execution { + name: None, + command: "exit 1".to_string(), + validation: Default::default(), + }), + ); + + let mut snapshots = crate::snapshots::SnapshotStore::new(std::env::temp_dir()); + let context = crate::config::interpolation::InterpolationContext::new(); + let mut evaluator = CheckEvaluator::new(dir.path(), &context, &mut snapshots); + let named = HashMap::new(); + + let last_success = Some(chrono::Utc::now() - chrono::Duration::days(365)); + + let result = compute_satisfaction( + &step, + &mut evaluator, + &named, + "test_step", + &RerunWindow::Forever, + last_success, + ); + + assert!(!result.satisfied); + assert_eq!(result.source, SatisfactionSource::ExecutionCheck); + } + #[test] fn no_last_success_not_satisfied_by_history() { let step = make_step(vec![], None); diff --git a/src/runner/workflow.rs b/src/runner/workflow.rs index 17568ae..8b0560b 100644 --- a/src/runner/workflow.rs +++ b/src/runner/workflow.rs @@ -262,10 +262,9 @@ impl<'a> WorkflowRunner<'a> { // Evaluate completed check using the new CheckEvaluator if !options.should_force(step_name) && !step.behavior.force { if let Some(check) = step.execution.effective_check() { - let config_hash = check.config_hash(); let mut evaluator = CheckEvaluator::new(project_root, context, &mut self.snapshot_store) - .with_step(step_name, &config_hash) + .with_step(step_name) .with_workflow(workflow_name); let check_result = evaluator.evaluate(&check); if check_result.passed_check() { @@ -322,6 +321,19 @@ impl<'a> WorkflowRunner<'a> { let status = result.status(); + // Record change-check baselines after successful execution so that + // the next run can compare against the current state. Skip in dry-run + // mode since no commands actually ran. + if status == StepStatus::Completed && !options.dry_run { + if let Some(check) = step.execution.effective_check() { + let mut evaluator = + CheckEvaluator::new(project_root, context, &mut self.snapshot_store) + .with_step(step_name) + .with_workflow(workflow_name); + evaluator.record_run_baselines(&check); + } + } + results.push(result); if status == StepStatus::Failed { diff --git a/src/runner/workflow_tests.rs b/src/runner/workflow_tests.rs index 6dff2ee..ae03314 100644 --- a/src/runner/workflow_tests.rs +++ b/src/runner/workflow_tests.rs @@ -4187,3 +4187,134 @@ fn runner_env_override_depends_on_cycle_detected() { let cycle = graph.find_cycle(); assert!(cycle.is_some()); } + +#[test] +fn run_records_change_check_baseline_after_successful_step() { + use crate::checks::SnapshotScope; + use crate::checks::{BaselineConfig, ChangeKind, Check, OnChange, SizeLimit}; + use crate::snapshots::{SnapshotKey, SnapshotStore}; + + let project_dir = TempDir::new().unwrap(); + let snap_dir = TempDir::new().unwrap(); + let lockfile = project_dir.path().join("package.lock"); + fs::write(&lockfile, "version=1").unwrap(); + + let config: BivvyConfig = serde_yaml::from_str( + r#" + workflows: + default: + steps: [install] + "#, + ) + .unwrap(); + + let check = Check::Change { + name: None, + target: "package.lock".to_string(), + kind: ChangeKind::File, + on_change: OnChange::Proceed, + require_step: None, + baseline: BaselineConfig::EachRun, + baseline_snapshot: None, + baseline_git: None, + size_limit: SizeLimit::default(), + scope: SnapshotScope::Project, + }; + + let mut step = make_step("install", "exit 0", vec![]); + step.execution.check = Some(check.clone()); + + let mut steps = HashMap::new(); + steps.insert("install".to_string(), step); + + let snapshot_store = SnapshotStore::new(snap_dir.path().to_path_buf()); + let mut runner = WorkflowRunner::with_snapshot_store(&config, steps, snapshot_store); + + let options = RunOptions::default(); + let ctx = InterpolationContext::new(); + + let result = runner + .run(&RunContext { + options: &options, + interpolation: &ctx, + project_root: project_dir.path(), + base_env: &HashMap::new(), + process_env: &HashMap::new(), + }) + .unwrap(); + + assert!(result.success); + + let config_hash = check.config_hash(); + let key = SnapshotKey::project("install", config_hash); + let baseline = runner.snapshot_store_mut().get_baseline(&key, "_last_run"); + assert!( + baseline.is_some(), + "baseline must be recorded after successful step execution" + ); +} + +#[test] +fn dry_run_does_not_record_change_check_baseline() { + use crate::checks::SnapshotScope; + use crate::checks::{BaselineConfig, ChangeKind, Check, OnChange, SizeLimit}; + use crate::snapshots::{SnapshotKey, SnapshotStore}; + + let project_dir = TempDir::new().unwrap(); + let snap_dir = TempDir::new().unwrap(); + fs::write(project_dir.path().join("package.lock"), "version=1").unwrap(); + + let config: BivvyConfig = serde_yaml::from_str( + r#" + workflows: + default: + steps: [install] + "#, + ) + .unwrap(); + + let check = Check::Change { + name: None, + target: "package.lock".to_string(), + kind: ChangeKind::File, + on_change: OnChange::Proceed, + require_step: None, + baseline: BaselineConfig::EachRun, + baseline_snapshot: None, + baseline_git: None, + size_limit: SizeLimit::default(), + scope: SnapshotScope::Project, + }; + + let mut step = make_step("install", "exit 0", vec![]); + step.execution.check = Some(check.clone()); + + let mut steps = HashMap::new(); + steps.insert("install".to_string(), step); + + let snapshot_store = SnapshotStore::new(snap_dir.path().to_path_buf()); + let mut runner = WorkflowRunner::with_snapshot_store(&config, steps, snapshot_store); + + let options = RunOptions { + dry_run: true, + ..RunOptions::default() + }; + let ctx = InterpolationContext::new(); + + let result = runner + .run(&RunContext { + options: &options, + interpolation: &ctx, + project_root: project_dir.path(), + base_env: &HashMap::new(), + process_env: &HashMap::new(), + }) + .unwrap(); + + assert!(result.success); + + let config_hash = check.config_hash(); + let key = SnapshotKey::project("install", config_hash); + let baseline = runner.snapshot_store_mut().get_baseline(&key, "_last_run"); + assert!(baseline.is_none(), "dry-run must not record baselines"); +} diff --git a/src/state/satisfaction.rs b/src/state/satisfaction.rs index 75d4909..262f796 100644 --- a/src/state/satisfaction.rs +++ b/src/state/satisfaction.rs @@ -96,7 +96,9 @@ pub enum SatisfactionEvidence { /// Multiple pieces of evidence (for `satisfied_when` with multiple conditions). Composite(Vec), - /// No evidence (step has never been evaluated). + /// No evidence to record. Either the step has never been evaluated, or the + /// verdict came from something that carries no evidence of its own, such as + /// a `check` that failed. None, }