Skip to content

Commit 8552ecf

Browse files
committed
feat: improve cache miss messages with specific input change kinds
Unify GlobbedInput and PostRunFingerprint mismatch variants into a single InputChanged { kind, path } with Added/Removed/ContentModified discrimination. Messages now show '{path} modified', '{file} added in {dir}', or '{file} removed from {dir}' instead of generic 'content of input changed'. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> # Conflicts: # crates/vite_task_bin/tests/e2e_snapshots/fixtures/inputs-negative-glob-subpackage/snapshots/dotdot auto negative - miss on non-excluded sibling inferred file.snap
1 parent 09f1343 commit 8552ecf

31 files changed

Lines changed: 244 additions & 112 deletions

File tree

crates/vite_task/src/session/cache/display.rs

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
88
use vite_str::Str;
99
use vite_task_plan::cache_metadata::SpawnFingerprint;
1010

11-
use super::{CacheMiss, FingerprintMismatch};
11+
use super::{CacheMiss, FingerprintMismatch, InputChangeKind, split_path};
1212
use crate::session::event::CacheStatus;
1313

1414
/// Describes a single atomic change between two spawn fingerprints.
@@ -174,24 +174,28 @@ pub fn format_cache_status_inline(cache_status: &CacheStatus) -> Option<Str> {
174174
}
175175
}
176176
FingerprintMismatch::InputConfig => "inputs configuration changed",
177-
FingerprintMismatch::GlobbedInput { path } => {
178-
return Some(vite_str::format!(
179-
"✗ cache miss: content of input '{path}' changed, executing"
180-
));
181-
}
182-
FingerprintMismatch::PostRunFingerprint(diff) => {
183-
use crate::session::execute::fingerprint::PostRunFingerprintMismatch;
184-
match diff {
185-
PostRunFingerprintMismatch::InputContentChanged { path } => {
186-
return Some(vite_str::format!(
187-
"✗ cache miss: content of input '{path}' changed, executing"
188-
));
189-
}
190-
}
177+
FingerprintMismatch::InputChanged { kind, path } => {
178+
let desc = format_input_change_str(*kind, path.as_str());
179+
return Some(vite_str::format!("✗ cache miss: {desc}, executing"));
191180
}
192181
};
193182
Some(vite_str::format!("✗ cache miss: {reason}, executing"))
194183
}
195184
CacheStatus::Disabled(_) => Some(Str::from("⊘ cache disabled")),
196185
}
197186
}
187+
188+
/// Format an input change as a [`Str`] for inline display.
189+
pub fn format_input_change_str(kind: InputChangeKind, path: &str) -> Str {
190+
match kind {
191+
InputChangeKind::ContentModified => vite_str::format!("'{path}' modified"),
192+
InputChangeKind::Added => {
193+
let (dir, filename) = split_path(path);
194+
vite_str::format!("'{filename}' added in {dir}")
195+
}
196+
InputChangeKind::Removed => {
197+
let (dir, filename) = split_path(path);
198+
vite_str::format!("'{filename}' removed from {dir}")
199+
}
200+
}
201+
}

crates/vite_task/src/session/cache/mod.rs

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@ use std::{collections::BTreeMap, fmt::Display, fs::File, io::Write, sync::Arc, t
77
use bincode::{Decode, Encode, decode_from_slice, encode_to_vec};
88
// Re-export display functions for convenience
99
pub use display::format_cache_status_inline;
10-
pub use display::{SpawnFingerprintChange, detect_spawn_fingerprint_changes, format_spawn_change};
10+
pub use display::{
11+
SpawnFingerprintChange, detect_spawn_fingerprint_changes, format_input_change_str,
12+
format_spawn_change,
13+
};
1114
use rusqlite::{Connection, OptionalExtension as _, config::DbConfig};
1215
use serde::{Deserialize, Serialize};
1316
use tokio::sync::Mutex;
1417
use vite_path::{AbsolutePath, RelativePathBuf};
1518
use vite_task_graph::config::ResolvedInputConfig;
1619
use vite_task_plan::cache_metadata::{CacheMetadata, ExecutionCacheKey, SpawnFingerprint};
1720

18-
use super::execute::{
19-
fingerprint::{PostRunFingerprint, PostRunFingerprintMismatch},
20-
spawn::StdOutput,
21-
};
21+
use super::execute::{fingerprint::PostRunFingerprint, spawn::StdOutput};
2222

2323
/// Cache lookup key identifying a task's execution configuration.
2424
///
@@ -83,6 +83,16 @@ pub enum CacheMiss {
8383
FingerprintMismatch(FingerprintMismatch),
8484
}
8585

86+
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
87+
pub enum InputChangeKind {
88+
/// File content changed but path is the same
89+
ContentModified,
90+
/// New file or folder added
91+
Added,
92+
/// Existing file or folder removed
93+
Removed,
94+
}
95+
8696
#[derive(Debug, Serialize, Deserialize)]
8797
pub enum FingerprintMismatch {
8898
/// Found a previous cache entry key for the same task, but the spawn fingerprint differs.
@@ -95,10 +105,11 @@ pub enum FingerprintMismatch {
95105
},
96106
/// Found a previous cache entry key for the same task, but `input_config` differs.
97107
InputConfig,
98-
/// Found the cache entry with the same spawn fingerprint, but an explicit globbed input changed
99-
GlobbedInput { path: RelativePathBuf },
100-
/// Found the cache entry with the same spawn fingerprint, but the post-run fingerprint mismatches
101-
PostRunFingerprint(PostRunFingerprintMismatch),
108+
109+
InputChanged {
110+
kind: InputChangeKind,
111+
path: RelativePathBuf,
112+
},
102113
}
103114

104115
impl Display for FingerprintMismatch {
@@ -110,14 +121,22 @@ impl Display for FingerprintMismatch {
110121
Self::InputConfig => {
111122
write!(f, "inputs configuration changed")
112123
}
113-
Self::GlobbedInput { path } => {
114-
write!(f, "content of input '{path}' changed")
124+
Self::InputChanged { kind, path } => {
125+
write!(f, "{}", display::format_input_change_str(*kind, path.as_str()))
115126
}
116-
Self::PostRunFingerprint(diff) => Display::fmt(diff, f),
117127
}
118128
}
119129
}
120130

131+
/// Split a relative path into `(parent_dir, filename)`.
132+
/// Returns `("workspace root", path)` if there is no parent directory.
133+
pub fn split_path(path: &str) -> (&str, &str) {
134+
match path.rsplit_once('/') {
135+
Some((parent, filename)) => (parent, filename),
136+
None => ("workspace root", path),
137+
}
138+
}
139+
121140
impl ExecutionCache {
122141
#[tracing::instrument(level = "debug", skip_all)]
123142
pub fn load_from_path(path: &AbsolutePath) -> anyhow::Result<Self> {
@@ -197,11 +216,9 @@ impl ExecutionCache {
197216
}
198217

199218
// Validate post-run fingerprint (inferred inputs from fspy)
200-
if let Some(post_run_fingerprint_mismatch) =
201-
cache_value.post_run_fingerprint.validate(workspace_root)?
202-
{
219+
if let Some((kind, path)) = cache_value.post_run_fingerprint.validate(workspace_root)? {
203220
return Ok(Err(CacheMiss::FingerprintMismatch(
204-
FingerprintMismatch::PostRunFingerprint(post_run_fingerprint_mismatch),
221+
FingerprintMismatch::InputChanged { kind, path },
205222
)));
206223
}
207224
// Associate the execution key to the cache entry key if not already,
@@ -264,22 +281,40 @@ fn detect_globbed_input_change(
264281
loop {
265282
match (s, c) {
266283
(None, None) => return None,
267-
(Some((path, _)), None) | (None, Some((path, _))) => {
268-
return Some(FingerprintMismatch::GlobbedInput { path: path.clone() });
284+
(Some((sp, _)), None) => {
285+
return Some(FingerprintMismatch::InputChanged {
286+
kind: InputChangeKind::Removed,
287+
path: sp.clone(),
288+
});
289+
}
290+
(None, Some((cp, _))) => {
291+
return Some(FingerprintMismatch::InputChanged {
292+
kind: InputChangeKind::Added,
293+
path: cp.clone(),
294+
});
269295
}
270296
(Some((sp, sh)), Some((cp, ch))) => match sp.cmp(cp) {
271297
std::cmp::Ordering::Equal => {
272298
if sh != ch {
273-
return Some(FingerprintMismatch::GlobbedInput { path: sp.clone() });
299+
return Some(FingerprintMismatch::InputChanged {
300+
kind: InputChangeKind::ContentModified,
301+
path: sp.clone(),
302+
});
274303
}
275304
s = stored_iter.next();
276305
c = current_iter.next();
277306
}
278307
std::cmp::Ordering::Less => {
279-
return Some(FingerprintMismatch::GlobbedInput { path: sp.clone() });
308+
return Some(FingerprintMismatch::InputChanged {
309+
kind: InputChangeKind::Removed,
310+
path: sp.clone(),
311+
});
280312
}
281313
std::cmp::Ordering::Greater => {
282-
return Some(FingerprintMismatch::GlobbedInput { path: cp.clone() });
314+
return Some(FingerprintMismatch::InputChanged {
315+
kind: InputChangeKind::Added,
316+
path: cp.clone(),
317+
});
283318
}
284319
},
285320
}

crates/vite_task/src/session/execute/fingerprint.rs

Lines changed: 64 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use vite_path::{AbsolutePath, RelativePathBuf};
1717
use vite_str::Str;
1818

1919
use super::spawn::PathRead;
20-
use crate::collections::HashMap;
20+
use crate::{collections::HashMap, session::cache::InputChangeKind};
2121

2222
/// Post-run fingerprint capturing file state after execution.
2323
/// Used to validate whether cached outputs are still valid.
@@ -50,22 +50,6 @@ pub enum DirEntryKind {
5050
Symlink,
5151
}
5252

53-
/// Describes why the post-run fingerprint validation failed
54-
#[derive(Debug, Serialize, Deserialize, Clone)]
55-
pub enum PostRunFingerprintMismatch {
56-
InputContentChanged { path: RelativePathBuf },
57-
}
58-
59-
impl std::fmt::Display for PostRunFingerprintMismatch {
60-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61-
match self {
62-
Self::InputContentChanged { path } => {
63-
write!(f, "{path} content changed")
64-
}
65-
}
66-
}
67-
}
68-
6953
impl PostRunFingerprint {
7054
/// Creates a new fingerprint from path accesses after task execution.
7155
///
@@ -94,12 +78,12 @@ impl PostRunFingerprint {
9478
}
9579

9680
/// Validates the fingerprint against current filesystem state.
97-
/// Returns `Some(mismatch)` if validation fails, `None` if valid.
81+
/// Returns `Some((kind, path))` if an input changed, `None` if all valid.
9882
#[tracing::instrument(level = "debug", skip_all, name = "validate_post_run_fingerprint")]
9983
pub fn validate(
10084
&self,
10185
base_dir: &AbsolutePath,
102-
) -> anyhow::Result<Option<PostRunFingerprintMismatch>> {
86+
) -> anyhow::Result<Option<(InputChangeKind, RelativePathBuf)>> {
10387
let input_mismatch = self.inferred_inputs.par_iter().find_map_any(
10488
|(input_relative_path, path_fingerprint)| {
10589
let input_full_path = Arc::<AbsolutePath>::from(base_dir.join(input_relative_path));
@@ -113,16 +97,74 @@ impl PostRunFingerprint {
11397
if path_fingerprint == &current_path_fingerprint {
11498
None
11599
} else {
116-
Some(Ok(PostRunFingerprintMismatch::InputContentChanged {
117-
path: input_relative_path.clone(),
118-
}))
100+
let (kind, entry_name) =
101+
determine_change_kind(path_fingerprint, &current_path_fingerprint);
102+
let path = if let Some(name) = entry_name {
103+
// For folder changes, build `dir/entry` path
104+
let entry = match RelativePathBuf::new(name.as_str()) {
105+
Ok(p) => p,
106+
Err(e) => return Some(Err(e.into())),
107+
};
108+
input_relative_path.as_relative_path().join(entry)
109+
} else {
110+
input_relative_path.clone()
111+
};
112+
Some(Ok((kind, path)))
119113
}
120114
},
121115
);
122116
input_mismatch.transpose()
123117
}
124118
}
125119

120+
/// Determine the kind of change between two differing path fingerprints.
121+
/// Caller guarantees `stored != current`.
122+
///
123+
/// Returns `(kind, entry_name)` where `entry_name` is `Some` for folder changes
124+
/// when a specific added/removed entry can be identified.
125+
fn determine_change_kind<'a>(
126+
stored: &'a PathFingerprint,
127+
current: &'a PathFingerprint,
128+
) -> (InputChangeKind, Option<&'a Str>) {
129+
match (stored, current) {
130+
(PathFingerprint::NotFound, _) => (InputChangeKind::Added, None),
131+
(_, PathFingerprint::NotFound) => (InputChangeKind::Removed, None),
132+
(PathFingerprint::FileContentHash(_), PathFingerprint::FileContentHash(_)) => {
133+
(InputChangeKind::ContentModified, None)
134+
}
135+
(PathFingerprint::Folder(old), PathFingerprint::Folder(new)) => {
136+
determine_folder_change_kind(old.as_ref(), new.as_ref())
137+
}
138+
// Type changed (file ↔ folder)
139+
_ => (InputChangeKind::Added, None),
140+
}
141+
}
142+
143+
/// Determine whether a folder change is an addition or removal by comparing entries.
144+
/// Returns the specific entry name that was added or removed, if identifiable.
145+
fn determine_folder_change_kind<'a>(
146+
old: Option<&'a HashMap<Str, DirEntryKind>>,
147+
new: Option<&'a HashMap<Str, DirEntryKind>>,
148+
) -> (InputChangeKind, Option<&'a Str>) {
149+
match (old, new) {
150+
(Some(old_entries), Some(new_entries)) => {
151+
for key in old_entries.keys() {
152+
if !new_entries.contains_key(key) {
153+
return (InputChangeKind::Removed, Some(key));
154+
}
155+
}
156+
for key in new_entries.keys() {
157+
if !old_entries.contains_key(key) {
158+
return (InputChangeKind::Added, Some(key));
159+
}
160+
}
161+
// Same keys but different entry kinds — default to Added
162+
(InputChangeKind::Added, None)
163+
}
164+
_ => (InputChangeKind::Added, None),
165+
}
166+
}
167+
126168
/// Hash file content using `xxHash3_64`
127169
fn hash_content(mut stream: impl Read) -> io::Result<u64> {
128170
let mut hasher = twox_hash::XxHash3_64::default();

crates/vite_task/src/session/reporter/summary.rs

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ use vite_str::Str;
1717
use super::{CACHE_MISS_STYLE, COMMAND_STYLE, ColorizeExt};
1818
use crate::session::{
1919
cache::{
20-
CacheMiss, FingerprintMismatch, SpawnFingerprintChange, detect_spawn_fingerprint_changes,
21-
format_spawn_change,
20+
CacheMiss, FingerprintMismatch, InputChangeKind, SpawnFingerprintChange,
21+
detect_spawn_fingerprint_changes, format_input_change_str, format_spawn_change,
2222
},
2323
event::{CacheDisabledReason, CacheErrorKind, CacheStatus, ExecutionError},
2424
};
@@ -114,8 +114,8 @@ pub enum SavedCacheMissReason {
114114
SpawnFingerprintChanged(Vec<SpawnFingerprintChange>),
115115
/// Task configuration changed (`input_config` or `glob_base`).
116116
ConfigChanged,
117-
/// Content of an input file changed.
118-
InputContentChanged { path: Str },
117+
/// An input file or folder changed.
118+
InputChanged { kind: InputChangeKind, path: Str },
119119
}
120120

121121
/// An execution error, serializable for persistence.
@@ -241,16 +241,8 @@ impl SavedCacheMissReason {
241241
Self::SpawnFingerprintChanged(detect_spawn_fingerprint_changes(old, new))
242242
}
243243
FingerprintMismatch::InputConfig => Self::ConfigChanged,
244-
FingerprintMismatch::GlobbedInput { path } => {
245-
Self::InputContentChanged { path: Str::from(path.as_str()) }
246-
}
247-
FingerprintMismatch::PostRunFingerprint(diff) => {
248-
use crate::session::execute::fingerprint::PostRunFingerprintMismatch;
249-
match diff {
250-
PostRunFingerprintMismatch::InputContentChanged { path } => {
251-
Self::InputContentChanged { path: Str::from(path.as_str()) }
252-
}
253-
}
244+
FingerprintMismatch::InputChanged { kind, path } => {
245+
Self::InputChanged { kind: *kind, path: Str::from(path.as_str()) }
254246
}
255247
},
256248
}
@@ -434,8 +426,9 @@ impl TaskResult {
434426
SavedCacheMissReason::ConfigChanged => {
435427
Str::from("→ Cache miss: inputs configuration changed")
436428
}
437-
SavedCacheMissReason::InputContentChanged { path } => {
438-
vite_str::format!("→ Cache miss: content of input '{path}' changed")
429+
SavedCacheMissReason::InputChanged { kind, path } => {
430+
let desc = format_input_change_str(*kind, path.as_str());
431+
vite_str::format!("→ Cache miss: {desc}")
439432
}
440433
},
441434
},

crates/vite_task_bin/tests/e2e_snapshots/fixtures/builtin-different-cwd/snapshots/builtin different cwd.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Finished in <duration> on 2 files with 90 rules using <n> threads.
4646
> echo 'console.log(1);' > folder2/a.js # modify folder2
4747

4848
> cd folder1 && vp run lint # cache hit
49-
$ vp lint ✗ cache miss: content of input 'folder2/a.js' changed, executing
49+
$ vp lint ✗ cache miss: 'folder2/a.js' modified, executing
5050

5151
! eslint-plugin-unicorn(no-empty-file): Empty files are not allowed.
5252
,-[folder1/a.js:1:1]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
extra content

0 commit comments

Comments
 (0)