1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
use std::collections::HashMap;
use chrono::DateTime;
use chrono::FixedOffset;
use super::run::LintRun;
use super::run::LintRunStatus;
use super::status;
use super::status::LintStatus;
/// Per-project lint state: run history and current status.
///
/// Private fields with methods maintain internal consistency.
/// The name matches the UI terminology for a unified domain model.
#[derive(Clone, Debug, Default)]
pub struct LintRuns {
runs: Vec<LintRun>,
status: LintStatus,
/// Archive byte size per run, keyed by `run_id`. Populated eagerly on
/// `set_runs` so UI reads never touch the filesystem.
archive_bytes: HashMap<String, u64>,
}
impl LintRuns {
pub fn runs(&self) -> &[LintRun] { &self.runs }
pub const fn status(&self) -> &LintStatus { &self.status }
/// `started_at` of the most recent terminal run (newest run that is not
/// `Running`), parsed to a timestamp. `None` when there is no such run or
/// it fails to parse. The startup staleness check compares this against
/// the newest source-file mtime to decide whether an edit post-dates the
/// last lint.
pub fn last_started_at(&self) -> Option<DateTime<FixedOffset>> {
self.runs
.iter()
.find(|run| !matches!(run.status, LintRunStatus::Running))
.and_then(|run| status::parse_timestamp(&run.started_at))
}
/// Archive byte size for a single run. `None` means we have no entry
/// for this `run_id` (run not yet seen by `set_runs`); `Some(0)` means
/// the entry exists and the archive directory is empty. Callers must
/// distinguish these — rendering "—" vs "0 B" — because they signal
/// different states (missing vs known-empty) and conflating them
/// hides bugs in the watcher/archive pipeline.
pub fn archive_bytes(&self, run_id: &str) -> Option<u64> {
self.archive_bytes.get(run_id).copied()
}
/// Replace run history and derive status from the latest run. Archive
/// sizes are read straight off each run (persisted when the run was
/// archived), so this does no disk I/O and `archive_bytes` lookups stay
/// O(1).
pub fn set_runs(&mut self, runs: Vec<LintRun>) {
self.status = runs.first().map_or(LintStatus::NoLog, status::parse_run);
self.archive_bytes = runs
.iter()
.map(|run| (run.run_id.clone(), run.archive_bytes))
.collect();
self.runs = runs;
}
/// Replace run history from a cache load without replacing a live status
/// that the on-disk history can't represent: a worker's `Running` run, or
/// the `Stale` marker a paused/killed run leaves pending re-lint. Both are
/// in-memory states, not terminal runs, so a history hydrate must not
/// overwrite them with the prior terminal result.
pub fn set_hydrated_runs(&mut self, runs: Vec<LintRun>) {
let live_status = matches!(self.status, LintStatus::Running(_) | LintStatus::Stale)
.then(|| self.status.clone());
self.set_runs(runs);
if let Some(status) = live_status {
self.status = status;
}
}
pub const fn set_status(&mut self, status: LintStatus) { self.status = status; }
pub fn clear_runs(&mut self) {
self.runs.clear();
self.archive_bytes.clear();
self.status = LintStatus::NoLog;
}
/// True iff `run_id` has an entry in the archive-size map. Distinct from
/// `archive_bytes(run_id) == 0`, which could mean either "no entry" or
/// "entry, zero bytes." Test-only — production code always asks for the
/// size, never whether we know it.
#[cfg(test)]
pub fn has_archive_entry(&self, run_id: &str) -> bool {
self.archive_bytes.contains_key(run_id)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lint::LintRunStatus;
fn make_run(run_id: &str) -> LintRun {
LintRun {
run_id: run_id.to_string(),
started_at: "2026-04-24T12:00:00Z".to_string(),
finished_at: Some("2026-04-24T12:00:01Z".to_string()),
duration_ms: Some(1000),
status: LintRunStatus::Passed,
commands: Vec::new(),
archive_bytes: 0,
}
}
#[test]
fn set_runs_populates_archive_entry_per_run() {
let mut lr = LintRuns::default();
lr.set_runs(vec![make_run("a"), make_run("b")]);
assert!(lr.has_archive_entry("a"));
assert!(lr.has_archive_entry("b"));
assert!(!lr.has_archive_entry("c"));
}
#[test]
fn archive_bytes_returns_none_for_unknown_run_id() {
let lr = LintRuns::default();
assert_eq!(lr.archive_bytes("nonexistent"), None);
}
#[test]
fn archive_bytes_returns_the_size_persisted_on_the_run() {
// The entry is present for every run in the set; its value is the
// `archive_bytes` field carried on the run. Distinguishing a present
// entry from an unknown run is the whole point of the `Option` API.
let mut lr = LintRuns::default();
lr.set_runs(vec![make_run("a")]);
assert_eq!(lr.archive_bytes("a"), Some(0));
assert_eq!(lr.archive_bytes("not-a-real-run"), None);
}
#[test]
fn clear_runs_empties_archive_entries() {
let mut lr = LintRuns::default();
lr.set_runs(vec![make_run("a")]);
assert!(lr.has_archive_entry("a"));
lr.clear_runs();
assert!(!lr.has_archive_entry("a"));
assert!(lr.runs().is_empty());
}
#[test]
fn set_runs_replaces_previous_archive_entries() {
let mut lr = LintRuns::default();
lr.set_runs(vec![make_run("a")]);
lr.set_runs(vec![make_run("b")]);
assert!(!lr.has_archive_entry("a"), "old run's entry should be gone");
assert!(lr.has_archive_entry("b"));
}
#[test]
fn set_hydrated_runs_preserves_live_stale_over_prior_terminal() {
// A paused/killed run publishes `Stale`; the subsequent history hydrate
// must not overwrite it with the prior passed run, or the killed lint
// would read as settled-green instead of pending re-lint.
let mut lr = LintRuns::default();
lr.set_status(LintStatus::Stale);
lr.set_hydrated_runs(vec![make_run("prior-pass")]);
assert_eq!(lr.status(), &LintStatus::Stale);
}
#[test]
fn last_started_at_skips_running_and_parses_newest_terminal() {
fn run_at(run_id: &str, started_at: &str, status: LintRunStatus) -> LintRun {
LintRun {
run_id: run_id.to_string(),
started_at: started_at.to_string(),
finished_at: Some(started_at.to_string()),
duration_ms: Some(1),
status,
commands: Vec::new(),
archive_bytes: 0,
}
}
let mut lr = LintRuns::default();
assert_eq!(lr.last_started_at(), None);
// Stored newest-first: the leading Running run is skipped; the next
// terminal run supplies the start timestamp.
lr.set_runs(vec![
run_at("c", "2026-04-24T12:00:05Z", LintRunStatus::Running),
run_at("b", "2026-04-24T12:00:03Z", LintRunStatus::Passed),
run_at("a", "2026-04-24T12:00:00Z", LintRunStatus::Failed),
]);
let expected = DateTime::parse_from_rfc3339("2026-04-24T12:00:03Z")
.ok()
.map(|ts| ts.timestamp());
assert_eq!(lr.last_started_at().map(|ts| ts.timestamp()), expected);
}
}