fix(deps): fall through to hash check when providers have no outputs#9622
fix(deps): fall through to hash check when providers have no outputs#9622
Conversation
Built-in providers whose install command writes outside the project tree (bundler, pip, go, poetry, uv) previously hardcoded an in-project path as their output. For projects that don't use that path (system gem path, no virtualenv, no go vendor/) the output never existed, so check_freshness short-circuited on `OutputsMissing` and re-ran the install on every invocation — the source-hash mechanism was never reached. The engine now skips the output existence/session checks when a provider returns no outputs and lets the source-hash check decide freshness. The affected providers return `vec![]` when their conventional output isn't present, and keep the existence check when it is. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request modifies the dependency engine to support hash-based freshness for providers that do not produce local output files, preventing them from being perpetually stale. It removes the NoOutputs freshness state and updates the Bundler, Go, Pip, Poetry, and UV providers to return empty outputs when local directories like vendor or .venv are absent. Review feedback identifies a regression where providers with neither sources nor outputs would never run, suggesting they should default to a stale state. It was also recommended to add go.sum to the Go provider's sources to ensure lockfile updates trigger re-runs.
| if !outputs.is_empty() { | ||
| // Check if any output was created this session (before deps ran) | ||
| for output in &outputs { | ||
| if super::is_output_stale(output) { | ||
| return Ok(FreshnessResult::Stale( | ||
| "output created this session".to_string(), | ||
| )); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Check if any output is missing | ||
| for output in &outputs { | ||
| if !output.exists() { | ||
| return Ok(FreshnessResult::OutputsMissing); | ||
| // Check if any output is missing | ||
| for output in &outputs { | ||
| if !output.exists() { | ||
| return Ok(FreshnessResult::OutputsMissing); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
When both outputs and sources are empty, the provider will now be considered fresh (via FreshnessResult::NoSources on line 686), meaning it will never run. Previously, providers with no outputs were always considered stale. To maintain the "always run" behavior for providers with no declared state (common for custom providers used as hooks), we should explicitly return a stale result when both lists are empty.
if !outputs.is_empty() {
// Check if any output was created this session (before deps ran)
for output in &outputs {
if super::is_output_stale(output) {
return Ok(FreshnessResult::Stale(
"output created this session".to_string(),
));
}
}
// Check if any output is missing
for output in &outputs {
if !output.exists() {
return Ok(FreshnessResult::OutputsMissing);
}
}
} else if sources.is_empty() {
// No outputs and no sources: always run to be safe (previous behavior)
return Ok(FreshnessResult::Stale("no outputs or sources defined".to_string()));
}| fn outputs(&self) -> Vec<PathBuf> { | ||
| let root = self.base.config_root(); | ||
| // Go downloads modules to GOPATH/pkg/mod, but we can check vendor/ if used | ||
| let vendor = root.join("vendor"); | ||
| // Go downloads modules to GOPATH/pkg/mod by default, leaving nothing in | ||
| // the project tree to check. Only treat `vendor/` as an output when the | ||
| // project is vendored; otherwise fall back to source-hash freshness. | ||
| let vendor = self.base.config_root().join("vendor"); | ||
| if vendor.exists() { | ||
| vec![vendor] | ||
| } else { | ||
| // go.sum gets updated after go mod download completes | ||
| vec![root.join("go.sum")] | ||
| vec![] | ||
| } | ||
| } |
There was a problem hiding this comment.
Since go.sum is no longer treated as an output, it should be added to the sources() method. Otherwise, changes to the lockfile (like checksum updates) won't trigger a re-run if go.mod remains unchanged. This is consistent with other providers like bundler or poetry which include their lockfiles in sources.
Greptile Summary
Confidence Score: 5/5Safe to merge; no P0 or P1 findings — the freshness semantics change is well-scoped and covered by new e2e tests. All five built-in providers have lockfile-based sources, so the one identified edge case (optional_outputs-only provider reaching NoSources before ever running) is unreachable with current code. The seen_outputs mechanism correctly arms the deletion check only after a successful run, and the hash comparison already detects go.sum removal. No data-loss, security, or correctness issues found. No files require special attention beyond normal review. Reviews (2): Last reviewed commit: "fix(deps): track optional outputs to det..." | Re-trigger Greptile |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 4b9e460. Configure here.
Hyperfine Performance
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.5.0 x -- echo |
24.3 ± 2.7 | 20.3 | 38.2 | 1.00 |
mise x -- echo |
24.7 ± 1.6 | 20.6 | 33.6 | 1.02 ± 0.13 |
mise env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.5.0 env |
23.9 ± 1.7 | 19.9 | 32.0 | 1.00 |
mise env |
24.8 ± 1.6 | 20.0 | 31.0 | 1.03 ± 0.10 |
mise hook-env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.5.0 hook-env |
24.6 ± 1.4 | 21.1 | 29.3 | 1.00 |
mise hook-env |
25.1 ± 1.6 | 20.6 | 31.1 | 1.02 ± 0.09 |
mise ls
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.5.0 ls |
22.4 ± 1.5 | 18.0 | 28.2 | 1.10 ± 0.10 |
mise ls |
20.3 ± 1.2 | 17.3 | 27.0 | 1.00 |
xtasks/test/perf
| Command | mise-2026.5.0 | mise | Variance |
|---|---|---|---|
| install (cached) | 138ms | 138ms | +0% |
| ls (cached) | 72ms | 71ms | +1% |
| bin-paths (cached) | 73ms | 79ms | -7% |
| task-ls (cached) | 575ms | 582ms | -1% |
PR feedback addressed three concerns with the original fix: 1. Deleted-output regression (Greptile P1, Cursor): if a user ran `uv sync` to create `.venv` and then `rm -rf .venv`, the previous approach silently kept the provider fresh because the missing path was no longer returned from `outputs()`. Introduce `DepsProvider::optional_outputs()`: providers declare canonical paths that may not exist on first run but should be tracked once observed. The engine records which optional outputs existed after a successful run (`DepsState.seen_outputs`) and enforces them on subsequent freshness checks. 2. Empty-sources-empty-outputs custom hook (Gemini): a provider with no sources and no outputs has no freshness signal — return `Stale` so it always runs, matching pre-PR behavior. 3. Go sources (Gemini): include `go.sum` alongside `go.mod` so that a `go mod tidy` that updates only checksums still triggers a re-run (matches how bundler/poetry treat their lockfiles). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Addressed all three pieces of review feedback in 14f1876: 1. Deleted-output regression (Greptile P1, Cursor Bugbot) — introduced 2. Empty-sources-empty-outputs (Gemini) — 3. Add E2E coverage added in This comment was generated by an AI coding assistant. |
|
cheers, thanks @jdx. I kinda figured there was "a 10 line version and a 200 line version" and you've gone and done the hard work! |
### 🚀 Features - **(aqua)** support registry libc variants by @jdx in [#9652](#9652) - **(bin-paths)** add executable names output by @risu729 in [#9617](#9617) ### 🐛 Bug Fixes - **(aqua)** preserve configured file extensions by @risu729 in [#9611](#9611) - **(aqua)** support registry file links by @risu729 in [#9610](#9610) - **(backend)** reject bare package backend names by @risu729 in [#9608](#9608) - **(backend)** apply inline tool option overrides by @risu729 in [#9306](#9306) - **(backend)** skip versions host for local tool opts by @risu729 in [#9568](#9568) - **(github)** chmod explicit archive bin by @risu729 in [#9609](#9609) - **(install)** skip remote-versions refresh in prefer-offline mode by @jdx in [#9627](#9627) - **(lock)** scope targets to active project root by @risu729 in [#9319](#9319) - **(lockfile)** respect existing platforms during auto-lock by @jdx in [#9621](#9621) - **(pipx)** filter yanked pypi releases by @risu729 in [#9607](#9607) - **(pipx)** declare python as a backend dependency by @jdx in [#9678](#9678) - **(schema)** update refs to $defs in mise-registry-tool.json by @risu729 in [#9671](#9671) - **(task)** terminate parallel siblings on failure via process groups by @jdx in [#9655](#9655) - **(task)** stable MISE_PROJECT_ROOT for monorepo tasks, add MISE_MONOREPO_ROOT by @jdx in [#9657](#9657) - **(trust)** run enter hooks after trusting config by @risu729 in [#9634](#9634) - **(ui)** stop clearing screen for prompts by @jdx in [#9619](#9619) - use /bin/cp on macos by @pdehlke in [#9656](#9656) ### 🚜 Refactor - **(aqua)** store aqua var defaults as strings by @risu729 in [#9645](#9645) - **(config)** support structured TOML values in registry backend options by @risu729 in [#9584](#9584) - **(deps)** remove serde_derive dependency by @risu729 in [#9670](#9670) - **(deps)** remove anyhow dependency by @risu729 in [#9661](#9661) - **(deps)** use std::sync::LazyLock instead of once_cell::Lazy by @risu729 in [#9668](#9668) - **(schema)** generate task schema from mise schema by @risu729 in [#9581](#9581) - **(schema)** reuse task props with unevaluatedProperties by @risu729 in [#9582](#9582) - **(schema)** reuse registry common types by @risu729 in [#9648](#9648) - **(schema)** reuse plugin script config by @risu729 in [#9647](#9647) - **(schema)** use $defs in schema files by @risu729 in [#9646](#9646) ### 📚 Documentation - **(node)** add tips for enabling node idiomatic by @fu050409 in [#9675](#9675) ### 🧪 Testing - **(cli)** remove nondeterministic tool depends assertion by @risu729 in [#9633](#9633) - **(e2e)** pin uv to 0.11.8 around astral-sh/uv#19278 by @jdx in [#9618](#9618) - **(e2e)** wait for docker env cleanup by @risu729 in [#9631](#9631) - **(zig)** use official zig instead of mach mirror by @jdx in [#9659](#9659) ### 📦️ Dependency Updates - fall through to hash check when providers have no outputs by @jdx in [#9622](#9622) - bump Cargo.lock by @jdx in [#9625](#9625) ### 📦 Registry - remove registry depends by @risu729 in [#9571](#9571) - add code-review-graph (pipx:code-review-graph) by @chautruonglong in [#9673](#9673) ### Chore - **(ci)** split large registry test-tool changes by @risu729 in [#9628](#9628) - **(ci)** make perf script robust to runner noise by @jdx in [#9635](#9635) - **(ci)** skip hyperfine comments without permission by @risu729 in [#9629](#9629) ### New Contributors - @chautruonglong made their first contribution in [#9673](#9673) - @pdehlke made their first contribution in [#9656](#9656) ## 📦 Aqua Registry Updates ### New Packages (5) - [`anthropics/anthropic-cli`](https://github.com/anthropics/anthropic-cli) - [`crates.io/wasmi_cli`](https://github.com/wasmi-labs/wasmi) - [`openclaw/gogcli`](https://github.com/openclaw/gogcli) - `racket-lang.org/racket-minimal` - [`runs-on/cli`](https://github.com/runs-on/cli) ### Updated Packages (13) - [`UpCloudLtd/upcloud-cli`](https://github.com/UpCloudLtd/upcloud-cli) - [`aristocratos/btop`](https://github.com/aristocratos/btop) - [`dprint/dprint`](https://github.com/dprint/dprint) - [`j178/prek`](https://github.com/j178/prek) - [`jdx/hk`](https://github.com/jdx/hk) - [`jdx/mise`](https://github.com/jdx/mise) - [`jdx/usage`](https://github.com/jdx/usage) - [`jreleaser/jreleaser`](https://github.com/jreleaser/jreleaser) - [`jreleaser/jreleaser/standalone`](https://github.com/jreleaser/jreleaser) - [`pnpm/pnpm`](https://github.com/pnpm/pnpm) - [`suzuki-shunsuke/cmdx`](https://github.com/suzuki-shunsuke/cmdx) - [`suzuki-shunsuke/ghir`](https://github.com/suzuki-shunsuke/ghir) - [`twpayne/chezmoi`](https://github.com/twpayne/chezmoi)

Summary
check_freshnessinsrc/deps/engine.rsnow skips the output existence / session-stale checks when a provider returns no outputs and lets the source-hash check decide freshness on its own. First run is still stale (no stored hashes); subsequent runs with unchanged sources are fresh.bundler,pip,go,poetry, anduvproviders now return their conventional in-project output (vendor/bundle,.venv,vendor/) only when it actually exists; otherwise they returnvec![]and rely on hashing. This stopsbundle install/pip install/go mod download/poetry install/uv syncfrom re-running on every invocation for projects that don't keep deps in the project tree.FreshnessResult::NoOutputsis removed since it's no longer reachable.Background
The Greptile bot flagged this on #9585 — verified by tracing the flow:
engine.rs::check_freshnesschecked output existence before the hash check, returningOutputsMissing(not fresh) whenever any declared output didn't exist.bundle installto the system gem path,pip installwithout a venv,go mod downloadwithoutvendor/, etc. all left nothing in the project tree, so the providers were perpetually stale and the source-hash mechanism was never reached.Test plan
mise run lintcleanmise run test:unit— 827 passedmise run test:e2e cli/test_deps cli/test_deps_depends cli/test_deps_tool_install— all greene2e/cli/test_depscovers the empty-outputs path: first run stale → install runs, second run fresh, source change → stale again🤖 Generated with Claude Code
Note
Medium Risk
Changes deps freshness evaluation and persisted state, which can alter when dependency install steps run or are skipped across multiple built-in providers.
Overview
Fixes deps providers that don’t write outputs inside the project tree from being perpetually stale by letting
check_freshnessfall back to source hashing when required outputs are empty.Introduces
DepsProvider::optional_outputs()and persists seen optional outputs inDepsState, so built-in providers can detect deletion of canonical directories (e.g..venv,vendor/,vendor/bundle) only after they’ve been observed, without forcing reruns for projects that never create them. Updatesbundler,pip,go,poetry, anduvto use optional outputs, extends--list/--explainto display them, and adds e2e coverage for empty-output providers and the “always run when no sources/outputs” case.Reviewed by Cursor Bugbot for commit 14f1876. Bugbot is set up for automated code reviews on this repo. Configure here.